<?php
/**
 * Provides the offer class, an interface to the offer table in the database
 *
 * @author Camil Staps
 *
 * BusinessAdmin: administrative software for small companies
 * Copyright (C) 2015 Camil Staps (ViviSoft)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * An interface to the offer table in the database
 */
class Offer extends Model{
	/** {@inheritDoc} */
	public static
		$table = 'offer',
		$fillable_columns = ['contactId', 'start_date', 'end_date', 'invoice_date', 'accepted', 'invoice_fileId', 'payment_key'],
		$dates = ['start_date', 'end_date', 'invoice_date'],
		$booleans = ['accepted'];

	/**
	 * A random max-63-char string that can be used as payment_key
	 *
	 * @return string                       The random string
	 */
	public static function getRandomPaymentKey() {
		return preg_replace('/[^\w]+/', '',
			base64_encode(openssl_random_pseudo_bytes(45)));
	}

	/**
	 * Get whether the offer is eligible for online payment or not
	 *
	 * @return bool                         True iff it is eligible
	 */
	public function getPaymentEligibility() {
		return $this->payment_key != '';
	}

	/**
	 * Get the URL on which the offer can be paid
	 *
	 * @return string                       The URL
	 */
	public function getPaymentUrl() {
		return Constants::url_external . "pay?id={$this->id}&key={$this->payment_key}";
	}

	/**
	 * Get the contact that this offer is linked to
	 *
	 * @return contact                      The contact
	 */
	public function getContact() {
		return new Contact($this->pdo, $this->contactId);
	}

	/**
	 * Get all assignment ids for this offer
	 *
	 * @see offer::getAssignments()         This funtion returns instances of the assignment class instead of just the ids
	 *
	 * @throws PDOException                 Is something went wrong with the database
	 *
	 * @return int[]                        The ids
	 */
	public function getAssignmentIds() {
		return Assignment::searchIds($this->pdo, ['`offerId`=?'], [$this->id]);
	}

	/**
	 * Get all assignments for this offer
	 *
	 * @see offer::getAssignmentIds()       This function returns just the ids of the assignments, and not instances of the assignment class
	 *
	 * @throws PDOException                 If something went wrong with the database
	 *
	 * @return assignment[]                 An array indexed by id of instances of the assignment class
	 */
	public function getAssignments() {
		return Assignment::search($this->pdo, ['`offerId`=?'], [$this->id]);
	}

	/**
	 * Get all discount ids for this offer
	 *
	 * @see offer::getDiscounts()           This funtion returns instances of the discount class instead of just the ids
	 *
	 * @throws PDOException                 Is something went wrong with the database
	 *
	 * @return int[]                        The ids
	 */
	public function getDiscountIds() {
		return Discount::searchIds($this->pdo, ['`offerId`=?'], [$this->id]);
	}

	/**
	 * Get all discounts for this offer
	 *
	 * @see offer::getDiscountIds()         This function returns just the ids of the discounts, and not instances of the discount class
	 *
	 * @throws PDOException                 If something went wrong with the database
	 *
	 * @return discount[]                   An array indexed by id of instances of the discount class
	 */
	public function getDiscounts() {
		return Discount::search($this->pdo, ['`offerId`=?'], [$this->id]);
	}

	/**
	 * Get all assignments and discounts for this offer
	 *
	 * @throws PDOException                 If something went wrong with the database
	 *
	 * @return mixed[]                      An array of assignments and discounts
	 */
	public function getItems() {
		return array_merge($this->getAssignments(), $this->getDiscounts());
	}

	/**
	 * Get the payment id for this offer
	 *
	 * @see offer::getPayment()             This funtion returns an instance of the payment class instead of just the id
	 *
	 * @throws PDOException                 Is something went wrong with the database
	 *
	 * @return int|null                     The id, or null if no payment exists
	 */
	public function getPaymentId() {
		$ids = Payment::searchIds($this->pdo, ['`offerId`=?'], [$this->id]);
		if (count($ids) == 0) {
			return null;
		} else {
			return $ids[0];
		}
	}

	/**
	 * Get the payment for this offer
	 *
	 * @see offer::getPaymentId()           This function returns just the id of the payment, and not an instance of the payment class
	 *
	 * @throws PDOException                 If something went wrong with the database
	 *
	 * @return payment|null                 The payment, or null if it does not exist
	 */
	public function getPayment() {
		$id = $this->getPaymentId();
		return is_null($id) ? null : new Payment($this->pdo, $id);
	}

	/**
	 * Get the date the payment was received
	 *
	 * @return int|null                     The date as a UNIX timestamp, or null if it wasn't received yet
	 */
	public function getPaymentReceived() {
		$payment = $this->getPayment();
		return is_null($payment) ? null : $payment->date;
	}

	/**
	* Get the file that the invoice this offer is linked to
	*
	* @see offer::getInvoiceId()           This function returns just the id
	*
	* @return file|null                    The file, or null if it doesn't exist
	*/
	public function getInvoiceFile() {
		return is_null($this->invoice_fileId) ? null : new File($this->pdo, $this->invoice_fileId);
	}

	/**
	 * Calculate a handy number about the invoice
	 *
	 * Subtotal: the sum of the prices of the assignments excl. VAT
	 *
	 * VAT: the sum of all the VAT from all the assignments
	 *
	 * Total: the sum of subtotal and total
	 *
	 * @param int $what                     Any of Calculatable::SUBTOTAL, Calculatable::VAT and Calculatable::TOTAL
	 * @param int $round                    How many decimals to round the result on
	 * @param bool $format                  Whether to format the number nicely (for output) or not (for calculations)
	 *
	 * @throws PDOException                 If something went wrong with the database
	 *
	 * @return float|bool                   The calculated value rounded to $round decimals, or false on incorrect input
	 */
	public function calculate($what = Calculatable::TOTAL, $round = 2, $format = true) {
		$return = 0;
		switch ($what) {
			case Calculatable::SUBTOTAL:
				foreach ($this->getAssignments() as $assignment) {
					$return += $assignment->calculate(Calculatable::SUBTOTAL, $round + 1, false);
				}
				foreach ($this->getDiscounts() as $discount) {
					$return += $discount->calculate(Calculatable::SUBTOTAL, $round + 1, false);
				}
				break;
			case Calculatable::VAT:
				$assignments = $this->getAssignments();
				foreach ($assignments as $assignment) {
					$return += $assignment->calculate(Calculatable::VAT, $round + 1, false);
				}
				foreach ($this->getDiscounts() as $discount) {
					$return += $discount->calculate(Calculatable::VAT, $round + 1, false);
				}
				break;
			case Calculatable::TOTAL:
				$return = $this->calculate(Calculatable::SUBTOTAL, $round + 1, false) + $this->calculate(Calculatable::VAT, $round + 1, false);
				break;
			default:
				return false;
		}
		if ($format) {
			return number_format($return, 2);
		} else {
			return round($return, 2);
		}
	}

	/**
	 * Make a mailer to send about this offer
	 *
	 * @return Mailer                   The mailer
	 */
	public function mailer() {
		$file = $this->getInvoiceFile();
		if (is_null($file)) {
			throw new Exception("The invoice for this offer has not been generated yet.");
		}

		$mailer = new Mailer($this->pdo);
		$mailer->setOffer($this);

		$lang = $this->getContact()->language;
		$mailer->addAttachment($file->getFilenamePath());
		$mailer->Subject = Correspondence::__('mail-offer-subject', $lang);
		$mailer->Body = Correspondence::__r('mail-offer', $lang, $this);

		return $mailer;
	}

	/**
	 * Send the invoice to the contact
	 *
	 * @return bool                         The result of Mailer::send
	 */
	public function send() {
		return $this->mailer()->send();
	}

	/**
	 * Make a new assignment linked to this order
	 *
	 * @param string $title                 The title for this assignment
	 * @param string $description           The description for this assignment
	 * @param int $hours                    The amount of hours to work on this assignment
	 * @param float $price_per_hour         The price per hour on this assignment
	 * @param float $vat                    The VAT percentage (so, 21 for 21%, not 0.21!)
	 *
	 * @throws PDOException                 If something went wrong with the database
	 * @throws Exception                    If there was a problem with the input
	 *
	 * @return assignment                   A new instance of the assignment class containing the new assignment
	 */
	public function createAssignment($title, $description, $hours, $price_per_hour, $vat) {
		$stmt = $this->pdo->prepare("INSERT INTO `".Constants::db_prefix."assignment` (`offerId`,`title`,`description`,`hours`,`price_per_hour`,`VAT_percentage`) VALUES (?,?,?,?,?,?)");
		$stmt->execute([
			$this->id,
			$title,
			$description,
			$hours,
			$price_per_hour,
			$vat
		]);
		if ($stmt->rowCount() == 1) {
			return new Assignment($this->pdo, $this->pdo->lastInsertId());
		} else {
			$error = $stmt->errorInfo();
			throw new Exception($error[2]);
		}
		}

	/**
	 * Make a new discount linked to this order
	 *
	 * @param string $title                 The title for this discount
	 * @param string $description           The description for this discount
	 * @param float $value                  The value for this discount
	 * @param float $vat                    The VAT percentage (so, 21 for 21%, not 0.21!)
	 *
	 * @throws PDOException                 If something went wrong with the database
	 * @throws Exception                    If there was a problem with the input
	 *
	 * @return discount                     A new instance of the discount class containing the new discount
	 */
	public function createDiscount($title, $description, $value, $vat) {
		$stmt = $this->pdo->prepare("INSERT INTO `".Constants::db_prefix."discount` (`offerId`,`title`,`description`,`value`,`VAT_percentage`) VALUES (?,?,?,?,?)");
		$stmt->execute([
			$this->id,
			$title,
			$description,
			$value,
			$vat
		]);
		if ($stmt->rowCount() == 1) {
			return new Discount($this->pdo, $this->pdo->lastInsertId());
		} else {
			$error = $stmt->errorInfo();
			throw new Exception($error[2]);
		}
	}

	/**
	 * Add a payment for this order
	 *
	 * @param string $date                  Optional: the date for the payment
	 *
	 * @throws PDOException                 If something went wrong with the database
	 * @throws Exception                    If there was a problem with the input
	 *
	 * @return payment                      A new instance of the payment class containing the new payment
	 */
	public function createPayment($date=null) {
		$date = is_null($date) ? time() : $date;
		$stmt = $this->pdo->prepare("INSERT INTO `".Constants::db_prefix."payment` (`offerId`,`date`) VALUES (?,?)");
		$stmt->execute([$this->id, date('Y-m-d H:i:s', $date)]);
		if ($stmt->rowCount() == 1) {
			return new Payment($this->pdo, $this->pdo->lastInsertId());
		} else {
			$error = $stmt->errorInfo();
			throw new Exception($error[2]);
		}
	}

	/**
	 * Generate a PDF invoice
	 *
	 * @throws PDOException                 If something went wrong with the database
	 * @throws Exception                    If the file could not be written or an other error occured
	 *
	 * @return file                         An instance of the file class with information on the invoice file generated
	 */
	public function generateInvoice() {
		// Check if we already have a file
		$file = $this->getInvoiceFile();
		if (!($file instanceof file)) {
			// If not, create a new file
			$i = 1;
			do {
				$invoice_nr = date('Y',$this->invoice_date) . str_pad($i++, 2, '0', STR_PAD_LEFT);
				$filename = 'invoice-' . $invoice_nr . '.pdf';
			} while (file_exists(Constants::files_folder . $filename));
			$file = File::create($this->pdo, [$filename]);

			$this->invoice_fileId = $file->id;
		} else {
			$invoice_nr = str_replace(['invoice-','.pdf'], ['', ''], $file->filename);
		}

		$list = [];
		foreach ($this->getAssignments() as $assignment)
			$list[] = [
				$assignment->title,
				$assignment->price_per_hour * $assignment->hours,
				$assignment->VAT_percentage . "%",
				$assignment->price_per_hour * $assignment->hours * (1 + $assignment->VAT_percentage / 100)
			];
		foreach ($this->getDiscounts() as $discount)
			$list[] = [
				$discount->title,
				$discount->calculate(Calculatable::SUBTOTAL),
				$discount->VAT_percentage . "%",
				$discount->calculate(Calculatable::TOTAL)
			];

		$pdf = new Correspondence();
		$pdf->SetContact($this->getContact());
		$pdf->SetTitle($pdf->_('invoice') . ' ' . $invoice_nr);
		$pdf->AddPage();
		$pdf->CorrespondenceHeader();

		$pdf->SetY(100);
		$pdf->SetFont('','B',14);
		$pdf->SetTextColor(Correspondence::HEAD_RED, Correspondence::HEAD_GREEN, Correspondence::HEAD_BLUE);
		$pdf->Cell(60,6, $pdf->_('invoice'),'B');
		$pdf->SetTextColor(0);
		$pdf->Ln();

		$width = [90, 25, 20, 25];
		$subtotal = 0;
		$btw = [];
		$total = 0;

		// Header

		$pdf->SetFont('','',9);
		$pdf->Cell(60,4);
		$pdf->Ln();
		$pdf->SetFont('','B');
		$pdf->Cell(30,4.5,$pdf->_('invoice-date'));
		$pdf->SetFont('','');
		$pdf->Cell(50,4.5,date("d-m-Y", $this->invoice_date));
		$pdf->Ln();
		$pdf->SetFont('','B');
		$pdf->Cell(30,4.5,$pdf->_('invoice-nr'));
		$pdf->SetFont('','');
		$pdf->Cell(50,4.5,$invoice_nr);
		$pdf->Ln();
		$pdf->SetFont('','B');
		$pdf->Cell(30,4.5,$pdf->_('due-date'));
		$pdf->SetFont('','');
		$pdf->Cell(50,4.5,date("d-m-Y",$this->invoice_date+3600*24*30));
		$pdf->Ln();
		$pdf->Cell(60,4.5,'','B');
		$pdf->Ln();

		$pdf->SetY(140);

		// Table

		$pdf->SetFont('','B',11);
		$pdf->SetTextColor(Correspondence::HEAD_RED, Correspondence::HEAD_GREEN, Correspondence::HEAD_BLUE);
		$pdf->Cell($width[0],7,$pdf->_('description'),'B');
		$pdf->Cell($width[1],7,$pdf->_('price-excl'),'B',0,'R');
		$pdf->Cell($width[2],7,$pdf->_('vat'),'B',0,'R');
		$pdf->Cell($width[3],7,$pdf->_('price-incl'),'B',0,'R');
		$pdf->SetTextColor(0);
		$pdf->Ln();

		$pdf->SetFont('','');
		foreach ($list as $row) {
			$x = $pdf->getX();
			$y = $pdf->getY();
			$pdf->MultiCell($width[0],6,iconv('utf-8', 'iso-8859-1', $row[0]),0,'L');
			$newy = $pdf->getY();
			$pdf->SetXY($x + $width[0], $y);
			$pdf->Cell($width[1],6,Correspondence::valuta().number_format($row[1],2),'',0,'R');
			$pdf->Cell($width[2],6,round($row[2],0) . '%','',0,'R');
			$pdf->Cell($width[3],6,Correspondence::valuta().number_format($row[3],2),'',0,'R');
			$pdf->Ln();
			$pdf->SetY($newy);
			$pdf->addPageIfOnEnd();
			$subtotal += $row[1];
			if (!isset($btw[$row[2]])) $btw[$row[2]] = 0;
			$btw[$row[2]] += $row[3] - $row[1];
		}
		$total = $subtotal;
		foreach ($btw as $m) {
			$total += $m;
		}

		$pdf->Cell(array_sum($width),5,'','T');
		$pdf->Ln();
		$pdf->Cell(array_sum($width),5);
		$pdf->Ln();

		$pdf->Cell($width[0],7);
		$pdf->SetFont('','B');
		$pdf->Cell($width[1] + $width[2],7,$pdf->_('amount'));
		$pdf->SetFont('','');
		$pdf->Cell($width[3],7,Correspondence::valuta() . $this->calculate(Calculatable::SUBTOTAL),'',0,'R');
		$pdf->Ln();

		foreach ($btw as $p => $m) {
			$pdf->Cell($width[0],7);
			$pdf->Cell($width[1] + $width[2],7,$pdf->_('vat') . ' '.round($p,0).'%');
			$pdf->Cell($width[3],7,Correspondence::valuta() . number_format($m,2),'',0,'R');
			$pdf->Ln();
		}

		$pdf->Cell(array_sum($width),5);
		$pdf->Ln();

		$pdf->Cell($width[0],7);
		$pdf->SetFont('','B');
		$pdf->Cell($width[1] + $width[2],7,$pdf->_('total'));
		$pdf->SetFont('','');
		$pdf->Cell($width[3],7,Correspondence::valuta() . $this->calculate(Calculatable::TOTAL),'T',0,'R');
		$pdf->Ln();

		// Footer

		$pdf->Ln();
		$pdf->addPageIfOnEnd();
		if ($pdf->GetY() < 230) {
			$pdf->SetY(230);
		}
		$oldY = $pdf->GetY();
		$pdf->Cell(45,20,'',1);
		$pdf->Cell(12.5,20);
		$pdf->Cell(45,20,'',1);
		$pdf->Cell(12.5,20);
		$pdf->Cell(45,20,'',1);

		$pdf->SetFont('','B',10);
		$pdf->SetTextColor(Correspondence::HEAD_RED, Correspondence::HEAD_GREEN, Correspondence::HEAD_BLUE);
		$pdf->SetY($oldY + 3);
		$pdf->Cell(5,5);
		$pdf->Cell(40,5,$pdf->_('iban'));
		$pdf->Cell(17.5,5);
		$pdf->Cell(40,5,$pdf->_('invoice-nr'));
		$pdf->Cell(17.5,5);
		$pdf->Cell(40,5,$pdf->_('amount-due'));
		$pdf->SetTextColor(0);

		$pdf->SetFont('','',8);
		$pdf->Ln();
		$pdf->Ln();
		$oldY = $pdf->GetY();

		$pdf->Cell(5,5);
		$pdf->Cell(40,5, Constants::invoice_iban);
		$pdf->Cell(17.5,5);
		$pdf->Cell(40,5,$invoice_nr);
		$pdf->Cell(17.5,5);
		$pdf->Cell(40,5,Correspondence::valuta() . $this->calculate(Calculatable::TOTAL));

		$pdf->SetY($oldY + 14);

		$pdf->SetFontSize(7);
		$pdf->Cell(160,5,$pdf->_('request'),0,0,'C');
		$pdf->Ln();
		$pdf->Cell(160,5,str_replace('%%', Constants::invoice_bic, $pdf->_('biccode')),0,0,'C');

		if (file_exists($file->getFilenamePath())) {
			unlink($file->getFilenamePath());
		}
		$pdf->Output($file->getFilenamePath(),'F');
		chmod($file->getFilenamePath(),0644);

		return $file;
	}
}