<?php
namespace App\Utils\BaseParser\Utils;

/**
 * Extract form of payments and related helpers
 *
 * @author Lucian Sirbu
 */
class FormOfPaymentExtractor
{

	/**
	 * Notations for the check FOP
	 * @var array
	 */
	protected $checkNotations = [
		[ 'code' => 'CHECK',  'valid' => true, ],
		[ 'code' => 'CK',     'valid' => true, ],
		[ 'code' => 'CKD',    'valid' => false, ],
		[ 'code' => 'CHEQUE', 'valid' => false, ],
		[ 'code' => 'BANK',   'valid' => false, ],
	];

	/**
	 * Notations for the cash FOP
	 * @var array
	 */
	protected $cashNotations = [
		[ 'code' => 'CASH',  'valid' => true, ],
		[ 'code' => 'CA',    'valid' => true, ],
		[ 'code' => 'CACH',  'valid' => false, ],
		[ 'code' => 'CAH',   'valid' => false, ],
		[ 'code' => 'CAHS',  'valid' => false, ],
		[ 'code' => 'CAS',   'valid' => false, ],
		[ 'code' => 'C',     'valid' => false, ],
	];

	protected $invoiceNotations = [
		[ 'code' => 'INVOICE',  'valid' => true, ],
		[ 'code' => 'INV',      'valid' => true, ],
		[ 'code' => 'INVK',     'valid' => false, ],
		[ 'code' => 'INOICE',   'valid' => false, ],
		[ 'code' => 'IVNOIVE',  'valid' => false, ],
	];

	/**
	 * CC companies
	 * @see https://support.travelport.com/webhelp/uapi/Content/Air/ACH_CarrierFunctionality/ACH_Carriers_Functionality_Table.htm
	 * @see https://www2.arccorp.com/support-training/airlines/payment-acceptance/
	 * @see https://pro.delta.com/content/agency/us/en/policy-library/reservations-and-ticketing/paying-for-a-ticket.html
	 * @var array
	 */
	protected $CCCompanies = [
		[ 'code' => 'DL', 'isCreditCard' => false, 'label' => 'Visa Delta Debit (debit)', ],
		[ 'code' => 'VE', 'isCreditCard' => false, 'label' => 'Visa Electron Debit (debit)', ],
		[ 'code' => 'VI', 'isCreditCard' => true,  'label' => 'Visa/Visa Connect Debit/Visa Credit Card', ],
		[ 'code' => 'CA', 'isCreditCard' => true,  'label' => 'Mastercard', ],
		[ 'code' => 'MA', 'isCreditCard' => true,  'label' => 'Mastercard Maestro', ],
		[ 'code' => 'MC', 'isCreditCard' => true,  'label' => 'Mastercard/Visa Connect Mastercard', ],
		[ 'code' => 'DM', 'isCreditCard' => false, 'label' => 'Mastercard Debit (debit)', ],
		[ 'code' => 'TP', 'isCreditCard' => true,  'label' => 'Universal Air Travel Plan Corporate Card/UATP', ],
		[ 'code' => 'AX', 'isCreditCard' => true,  'label' => 'American Express', ],
		[ 'code' => 'CB', 'isCreditCard' => true,  'label' => 'Carte Blanche', ],
		[ 'code' => 'DN', 'isCreditCard' => true,  'label' => 'Diners Club', ],
		[ 'code' => 'DS', 'isCreditCard' => true,  'label' => 'Discover', ],
		[ 'code' => 'EL', 'isCreditCard' => true,  'label' => 'easyJet Electron', ],
		[ 'code' => 'JC', 'isCreditCard' => true,  'label' => 'Japan Credit Bureau Intl. Credit Card/JCB', ],
		[ 'code' => 'SO', 'isCreditCard' => true,  'label' => 'Solo', ],
		[ 'code' => 'SW', 'isCreditCard' => true,  'label' => 'Switch/Maestro', ],
		[ 'code' => 'DC', 'isCreditCard' => true,  'label' => 'Diners Club Int\'l', ],
		[ 'code' => 'BB', 'isCreditCard' => true,  'label' => 'Barclaycard/Visa (Barclays Bank)', ],
		[ 'code' => 'BF', 'isCreditCard' => true,  'label' => 'Carte Bleue - Bank Francaises Nationalisees', ],
		[ 'code' => 'CG', 'isCreditCard' => true,  'label' => 'Chargex', ],
		[ 'code' => 'EC', 'isCreditCard' => true,  'label' => 'Eurocard', ],
		[ 'code' => 'MD', 'isCreditCard' => true,  'label' => 'DC Card (formerly Diamond Credit)', ],
	];

	/**
	 * Generate the CC with CC number regexp
	 */
	public function generateCCRegexp()
	{
		$CCUniversalRegexp = [
			$this->generateCCCompanyRegexp(),
			"(?P<ccNumber>[0-9X]{11,16})",
		];

		return implode("", $CCUniversalRegexp);
	}

	/**
	 * Generate the CC company regexp
	 */
	public function generateCCCompanyRegexp()
	{
		$CCUniversalRegexp = [
			sprintf("(?P<ccCompany>%s)", implode("|", array_map(function($ccc) {
				return $ccc['code'];
			}, $this->CCCompanies))),
		];

		return implode("", $CCUniversalRegexp);
	}

	/**
	 * Generate the CC expiry date regexp
	 */
	public function generateCCExpiryDateRegexp()
	{
		$CCUniversalRegexp = [
			"(?P<ccExpiryDate_m>(0|1)[0-9])",	// month: 01..12
			"(\/)?",							// delimiter, optional
			"(?P<ccExpiryDate_Y>(1|2|3)[0-9])",	// year: 10..39, means 2010-2039
		];

		return implode("", $CCUniversalRegexp);
	}

	/**
	 * Generate the cash regexp
	 * @param string $validity
	 */
	public function generateCashRegexp($validity='all')
	{
		return implode("|",
			array_map(function($cn) {
				return $cn['code'];
			},
				array_filter($this->cashNotations, function($cn) use ($validity) {
					switch ($validity) {
						case 'all':
							return true;
						break;
						case 'valid':
							return $cn['valid'];
						break;
						case 'invalid':
							return !$cn['valid'];
						break;
					}
					return false;
				})
			)
		);
	}

	/**
	 * Generate the invoice regexp
	 * @param string $validity
	 */
	public function generateInvoiceRegexp($validity='all')
	{
		return implode("|",
			array_map(function($cn) {
				return $cn['code'];
			},
				array_filter($this->invoiceNotations, function($cn) use ($validity) {
					switch ($validity) {
						case 'all':
							return true;
						break;
						case 'valid':
							return $cn['valid'];
						break;
						case 'invalid':
							return !$cn['valid'];
						break;
					}
					return false;
				})
			)
		);
	}

	/**
	 * Generate the check regexp
	 * @param string $validity
	 */
	public function generateCheckRegexp($validity='all')
	{
		return implode("|",
			array_map(function($cn) {
				return $cn['code'];
			},
				array_filter($this->checkNotations, function($cn) use ($validity) {
					switch ($validity) {
						case 'all':
							return true;
						break;
						case 'valid':
							return $cn['valid'];
						break;
						case 'invalid':
							return !$cn['valid'];
						break;
					}
					return false;
				})
			)
		);
	}

	/**
	 * Generate an amount regexp
	 * @return string
	 */
	public function generateAmountRegexp()
	{
		return "[0-9]+\.[0-9]{1,3}";
	}

	/**
	 * Extract te form of payment, from a simplified format.
	 * @return Array
	 */
	public function extractSimpleFormats($text, $unknownFopHandler='return:skip')
	{
		$extractor = new \App\Utils\BaseParser\Utils\Extractor\Single();

		$ret = [];

		$section = 'new';
		while ($text) {
			$handled = false;

			// handle cash
			if (!$handled && $matches = $extractor->findAndStore(sprintf("/^"
				."(?P<raw>{$this->generateCashRegexp('valid')})"
				."((?P<totalFare>{$this->generateAmountRegexp()}))?"
				."(?=[^A-Z0-9]|$)"	// the "CA" text is not followed by "SH"
				."/"), $text)) {
				$ret[]= [
					// 'raw' => 	$matches->getRawMatchedStrings(),
					'type' => 	'cash', 	'section' => $section,	'subtype' => 'cash:v1',
					'amount' => 	$matches->getFloat('amount'),
					'totalFare' => 	$matches->getFloat('totalFare'),
				];

				$handled = true;
				$text = substr($text, $matches->getMatchedLength());
			}

			// handle CC: CCVI12345678901234512EXP0124
			if (!$handled && $matches = $extractor->findAndStore(sprintf("/^"
				."(?P<pre3>CC)"
				."{$this->generateCCRegexp()}"
				."(EXP{$this->generateCCExpiryDateRegexp()})"
				."/"), $text)) {
				$ret[]= [
					// 'raw' => 		$matches->getRawMatchedStrings(),
					'type' => 		'CC', 	'section' => $section,	'subtype' => 'cc:v2',
					'company' => 	$matches->ccCompany,
					'number' => 	$matches->ccNumber,
					'expiryDate' => ($matches->ccExpiryDate_Y && $matches->ccExpiryDate_m ? sprintf("%04d-%02d-01", 2000+$matches->ccExpiryDate_Y , $matches->ccExpiryDate_m):null),
				];

				$handled = true;
				$text = substr($text, $matches->getMatchedLength());
			}

			// handle CC: CCVI12345678901234512/12/20/123.45
			if (!$handled && $matches = $extractor->findAndStore(sprintf("/^"
				."(?P<pre3>CC[\s]*)?"
				."{$this->generateCCRegexp()}"
				."((\/|#){$this->generateCCExpiryDateRegexp()})?"
				."((\/|#)(?P<totalFare>{$this->generateAmountRegexp()}))?"
				."/"), $text)) {
				$ret[]= [
					// 'raw' => 		$matches->getRawMatchedStrings(),
					'type' => 		'CC', 	'section' => $section,	'subtype' => 'cc:v1',
					'company' => 	$matches->ccCompany,
					'number' => 	$matches->ccNumber,
					'expiryDate' => ($matches->ccExpiryDate_Y && $matches->ccExpiryDate_m ? sprintf("%04d-%02d-01", 2000+$matches->ccExpiryDate_Y , $matches->ccExpiryDate_m):null),
					'totalFare' => 	$matches->getFloat('totalFare'),
				];

				$handled = true;
				$text = substr($text, $matches->getMatchedLength());
			}

			// handle CC: CCVI, at the end of the text
			if (!$handled && $matches = $extractor->findAndStore(sprintf("/^"
				."(?P<pre3>CC[\s]*)?"
				."{$this->generateCCCompanyRegexp()}"
				."$/"), $text)) {
				$ret[]= [
					// 'raw' => 		$matches->getRawMatchedStrings(),
					'type' => 		'CC', 	'section' => $section,	'subtype' => 'cc:v1',
					'company' => 	$matches->ccCompany,
				];

				$handled = true;
				$text = substr($text, $matches->getMatchedLength());
			}

			// handle simplified CC, for old tickets, at the end of the text
			if (!$handled && $matches = $extractor->findAndStore(sprintf("/^"
				."CC"
				."$/"), $text)) {
				$ret[]= [
					// 'raw' => 		$matches->getRawMatchedStrings(),
					'type' => 		'CC', 	'section' => $section,	'subtype' => 'cc:v2',
				];

				$handled = true;
				$text = substr($text, $matches->getMatchedLength());
			}

			// handle check
			if (!$handled && $matches = $extractor->findAndStore(sprintf("/^"
				."(?P<raw>{$this->generateCheckRegexp('valid')})"
				."(\/(?P<amount>{$this->generateAmountRegexp()}))?"
				."( TF[\s]*(?P<totalFare>{$this->generateAmountRegexp()})TAX[\s]*(?P<tax>{$this->generateAmountRegexp()}))?"
				."/"), $text)) {
				$ret[]= [
					// 'raw' => 	$matches->getRawMatchedStrings(),
					'type' => 	'check', 	'section' => $section,	'subtype' => 'check:v1',
					'amount' => 	$matches->getFloat('amount'),
					'totalFare' => 	$matches->getFloat('totalFare'),
					'tax' => 		$matches->getFloat('tax'),
				];

				$handled = true;
				$text = substr($text, $matches->getMatchedLength());
			}

			// handle invoice
			if (!$handled && $matches = $extractor->findAndStore(sprintf("/^"
				."(?P<raw>{$this->generateInvoiceRegexp('valid')})"
				."(\/(?P<amount>{$this->generateAmountRegexp()}))?"
				."((\s|\/|@|)(?P<number>[0-9]+))?"
				."/"), $text)) {
				$ret[]= [
					// 'raw' => 	$matches->getRawMatchedStrings(),
					'type' => 	'invoice', 	'section' => $section,	'subtype' => 'invoice:v1',
					'number' => 	(trim($matches->number)?:null),
					'amount' => 	$matches->getFloat('amount'),
				];

				$handled = true;
				$text = substr($text, $matches->getMatchedLength());
			}


			// misspellings
			// cash
			if (!$handled && $matches = $extractor->findAndStore(sprintf("/^"
				."(?P<raw>{$this->generateCashRegexp('invalid')})"
				."(?=[^A-Z0-9]|$)"	// the "CA" text is not followed by "SH"
				."/"), $text)) {
				$ret[]= [
					// 'raw' => 	$matches->getRawMatchedStrings(),
					'type' => 	'cash', 	'section' => $section,	'subtype' => 'cash:misspell:v1',
					'amount' => 	$matches->getFloat('amount'),
					'totalFare' => 	$matches->getFloat('totalFare'),
					'tax' => 		$matches->getFloat('tax'),
				];

				$handled = true;
				$text = substr($text, $matches->getMatchedLength());
			}

			// separators
			// A/C is used by US/Downtown, doesn't seem to mean anything
			if (!$handled && $matches = $extractor->findAndStore(sprintf("/^(?P<pre1>(A\/C))/"), $text)) {
				$handled = true;
				$text = substr($text, $matches->getMatchedLength());
			}

			// means 'another FOP'
			if (!$handled && $text[0]=='/') {
				$text = substr($text, 1);	// ignore / characters at the beginning, they usually separate form of payments
				$handled = true;
			}

			// '/' handles new->old FOP boundary
			if (!$handled && $text[0]==' ') {
				$text = substr($text, 1);	// ignore ' ' characters at the beginning, they usually separate nwe/old sections
				$handled = true;
				$section = 'old';
			}

			if (!$handled) {
				switch ($unknownFopHandler) {
					case 'throw':
						// stop with error
						throw new \Exception("Unknown form of payment: '{$text}'");
					break;

					case 'return:partial':
						// return what we have until now, assuming the rest of the line format is in an invalid format/state
						return $ret;
					break;

					case 'return:stop':
						// unknwon form of payment, mark the rest of the list as "unknown", return what we have until now
						$ret[]= [
							'type' => 'unknown', 	'section' => $section,
							'raw' => [
								$text
							],
						];
						$handled = true;
						$text = '';
					break;

					case 'return:skip':
						// unknwon form of payment, try to ignore unknown sections and parse only known sections
						$lastFopIndex = count($ret)-1;
						if ($ret[$lastFopIndex]['type']=='unknown') {
							$ret[$lastFopIndex]['raw'].= $text[0];
						}
						else {
							$lastFopIndex++;
							$ret[$lastFopIndex]= [
								'type' => 'unknown', 	'section' => $section,
							];
							$ret[$lastFopIndex]['raw'] = $text[0];
						}

						$handled = true;
						$text = substr($text, 1);
					break;

					default:
						throw new \Exception("Unknown form of payment handling: '{$unknownFopHandler}'");
				}
			}
		}

		return $ret;
	}
}