<?php
namespace App\Bsp\Parser\Parsers;

// TODO: conjunction tickets are not reported???
// TODO: period end is not reported?

#class MIAGTXNDET extends \App\Bsp\Parser\BspTraverserAbstractWithDebug
class MIAGTXNDET extends \App\Bsp\Parser\BspTraverserAbstract
{
	protected $DETRowSlicer = null;
	protected $TAXRowSlicer = null;
	protected $PNRRowSlicer = null;
	protected $CCDRowSlicer = null;
	protected $RTDRowSlicer = null;

	protected $fileHandle = null;
	protected $fileLineOffsets = [];

	protected $extractor;

	public function __construct()
	{
		parent::__construct();
		$this->options['allowInvalidHeader'] = false;

		$this->options['currencySubdivisionMap'] = [
			/**
			 * according to JIRA: TD-10465(Interpret correctly the values coming in Bosnian BSP )
			 * Bosninan market seems to report BSP's with no decimals (BAM currency)
			 * */
			'BAM' => 1,

			/**
			 * according to TD-6006 - RSD currency is reported as-is in the BSP file, so we shouldn't divide by 100, as usual
			 * this seems to be the only case until now where we shouldn't divide by 100, and the "hint" seems to be the currency
			 * also see JIRA: TD-10502(Values are not interpreted correctly from the BSP file # 81156)
			 * */
			'RSD' => 1,

			/**
			 * according to TD-7828, HUF currency is reported without decimals in the BSP file, so we shouldn't divide by 100
			 * */
			'HUF' => 1,
		];

		$this->DETRowSlicer = $this->getExtractor()->buildPartsSlicer([
			"DET" => 3,
			"Ticketing Airline Code" => 3,
			"Agent Code" => 8,
			"System Provider" => 4,
			"Billing Category" => 4,
			"Billing Class" => 20,
			"Transaction Code" => 4,
			"Document Number" => 10,
			"Check Digit" => 1,

			"Date of Issue" => 6,
			"Coupon Usage Indicator" => 4,
			"Currency Code" => 3,
			"Currency Code2" => 1,
			"Commissionable Amount" => 12,
			"Standard Commission Rate" => 5,
			"Standard Commission Amount" => 11,
			"Supplementary Commission Rate" => 5,
			"Supplementary Commission Amount" => 11,
			"Effective Commission Rate" => 5,
			"Effective Commission Amount" => 11,
			"Remittance Amount" => 12,
			"Tax on Commission" => 11,
			"TAX" => 11,
			"Fees" => 11,
			"Penalties" => 11,
			"Net Remit Indicator" => 2,
			"Net Remit" => 3,
			"Stat Code" => 3,
			"Tour Code" => 15,
			"Waiver Code" => 14,
			"Cash Amount" => 11,
			"Credit Amount" => 12,
			"Related Ticket1 Document Type" => 2,
			"Related Ticket1 Document Number" => 13,
			"Related Ticket1 Document COUPONS" => 4,
			"Related Ticket2 Document Type" => 2,
			"Related Ticket2 Document Number" => 13,
			"Related Ticket2 Document COUPONS" => 4,
			"In Connection with Ticket" => 13,
			"Electronic Settlement Authorisation Code" => 14,
			"Tax on Commission Calculation Method" => 1,
			"Payment Card Information" => 2,
			"Payment Card Information - ccCompany" => 2,
			"Payment Card Information - ccNumber" => 4,
			"Payment Card Transaction ID" => 25,
			"Payment Card CCVR" => 1,
			"extra" => PHP_INT_MAX
		]);

		$this->RTDRowSlicer = $this->getExtractor()->buildPartsSlicer([
			"RTD" => 3,
			"Ticketing Airline Code" => 3,
			"Agent Code" => 8,
			"System Provider" => 4,
			"Billing Category" => 4,
			"Billing Class" => 20,
			"Transaction Code" => 4,
			"Document Number" => 10,
			"Check Digit" => 1,

			"extra" => 			PHP_INT_MAX
		]);

		$this->TAXRowSlicer = $this->getExtractor()->buildPartsSlicer([
			"TAX" => 3,
			"Ticketing Airline Code" => 3,
			"Agent Code" => 8,
			"System Provider" => 4,
			"Billing Category" => 4,
			"Billing Class" => 20,
			"Transaction Code" => 4,
			"Document Number" => 10,
			"Check Digit" => 1,

			"TMFT00" => 8,	"TMFA00" => 11,
			"TMFT01" => 8,	"TMFA01" => 11,
			"TMFT02" => 8,	"TMFA02" => 11,
			"TMFT03" => 8,	"TMFA03" => 11,
			"TMFT04" => 8,	"TMFA04" => 11,
			"TMFT05" => 8,	"TMFA05" => 11,
			"TMFT06" => 8,	"TMFA06" => 11,
			"TMFT07" => 8,	"TMFA07" => 11,
			"TMFT08" => 8,	"TMFA08" => 11,
			"TMFT09" => 8,	"TMFA09" => 11,
			"TMFT10" => 8,	"TMFA10" => 11,
			"TMFT11" => 8,	"TMFA11" => 11,
			"TMFT12" => 8,	"TMFA12" => 11,
			"TMFT13" => 8,	"TMFA13" => 11,
			"TMFT14" => 8,	"TMFA14" => 11,
			"extra" => PHP_INT_MAX,
		]);

		$this->PNRRowSlicer = $this->getExtractor()->buildPartsSlicer([
			"PNR" => 3,
			"Ticketing Airline Code" => 3,
			"Agent Code" => 8,
			"System Provider" => 4,
			"Billing Category" => 4,
			"Billing Class" => 20,
			"Transaction Code" => 4,
			"Document Number" => 10,
			"Check Digit" => 1,

			"paxName" =>	78,
			"RECLOC" => 	6,
			"delim" =>		1, // always '/'
			"GDS" =>		2, // 1G stands for Amadeus
			"extra" => 		PHP_INT_MAX,
		]);

		$this->CCDRowSlicer = $this->getExtractor()->buildPartsSlicer([
			"CCD" => 3,
			"Ticketing Airline Code" => 3,
			"Agent Code" => 8,
			"System Provider" => 4,
			"Billing Category" => 4,
			"Billing Class" => 20,
			"Transaction Code" => 4,
			"Document Number" => 10,
			"Check Digit" => 1,

			"fop" =>		300,
			"extra" => 		PHP_INT_MAX,
		]);
	}

	/**
	 * Get the next line in file
	 * @return string|NULL
	 */
	protected function getNextLine()
	{
		$this->state['lineIdx']++;
		$fileLine = fgets($this->fileHandle);

		$this->fileLineOffsets[]= ftell($this->fileHandle);
		$this->fileLineOffsets = array_slice($this->fileLineOffsets, -100);

		if (feof($this->fileHandle)) {
			return null;
		}

		return $fileLine;
	}

	/**
	 * Go to previous line in the file
	 * @return NULL
	 */
	protected function gotoPrevLineInFile()
	{
		$this->state['lineIdx']--;

		$this->fileLineOffsets = array_slice($this->fileLineOffsets, 0, -1);

		$newOffset = end($this->fileLineOffsets);

		fseek($this->fileHandle, $newOffset);

		return null;
	}

	/**
	 *
	 * {@inheritDoc}
	 * @see \App\Bsp\Parser\BspTraverserAbstract::initialize()
	 */
	public function initialize()
	{
		$this->stats['fileDetails']['size'] = filesize($this->path);

		if ($this->path) {
			$this->fileHandle = fopen($this->path, 'rt');
			if (!$this->fileHandle) {
				throw new \App\Bsp\BspRowException(sprintf("Invalid BSP file specified[2]"));
			}
		}
		else {
			throw new \App\Bsp\BspRowException(sprintf("Invalid BSP file specified[1]"));
		}

		// reset the internal parser pointers
		$this->state['lineIdx'] = -1;
	}

	/**
	 * (non-PHPdoc)
	 * @see \App\Bsp\Parser\BspTraverserAbstract::destroy()
	 */
	public function destroy()
	{
		$this->extractor = null;

		if ($this->fileHandle) {
			fclose($this->fileHandle);
			$this->fileHandle = null;
		}
	}

	/**
	 * (non-PHPdoc)
	 * @see \App\Bsp\Parser\BspTraverserAbstract::hasContent()
	 */
	public function hasContent()
	{
		if (!$this->initialized) {
			$this->initialize();
			$this->initialized = true;
		}
		return $this->stats['fileDetails']['size']>0;
	}

	protected function _next()
	{
		$fileLine = $this->getNextLine();
		if (!$fileLine) {	// all lines finished parsing
			// stop the parser, no more data to parse
			return null;
		}

		if ($this->state['lineIdx']===0) {
			$this->extractHeaderData($fileLine);
			return "intern:skip";	// skip header
		}

		return $this->extractTicketRows($fileLine);
	}

	protected function extractTicketRows($fileLine)
	{
		// parse the 1'st line, DET

		$DET = $this->extractDETRow($fileLine);
		$currency = $DET['Currency Code'];

		// parse TAXes
		$TAXLines = [];
		$fileLine = $this->getNextLine();
		$lineHandled = false;
		if ($fileLine && $this->getExtractor()->startsWith("TAX", $fileLine)) {	// we didn't reach the EOF after reading the previous row
			while ($this->getExtractor()->startsWith("TAX", $fileLine)) {
				$TAXLines[] = $this->extractTAXRow($fileLine, $currency);
				$fileLine = $this->getNextLine();
				$lineHandled = true;
			}
			$this->gotoPrevLineInFile();	// back off 1 line, to allow _next() to pick it up next time it runs
		}
		if (!$lineHandled) {
			$this->gotoPrevLineInFile();
		}

		// parse CCD - CC data
		$CCDLines = [];
		$fileLine = $this->getNextLine();
		$lineHandled = false;
		if ($fileLine && $this->getExtractor()->startsWith("CCD", $fileLine)) {	// we didn't reach the EOF after reading the previous row
			while ($this->getExtractor()->startsWith("CCD", $fileLine)) {
				$CCDLines[] = $this->extractCCDRow($fileLine, $currency);
				$fileLine = $this->getNextLine();
				$lineHandled = true;
			}
			$this->gotoPrevLineInFile();	// back off 1 line, to allow _next() to pick it up next time it runs
		}
		if (!$lineHandled) {
			$this->gotoPrevLineInFile();
		}

		// parse RTD, used for DEBIT MEMOS it seems
		$RTDLines = [];
		$fileLine = $this->getNextLine();
		$lineHandled = false;
		if ($fileLine && $this->getExtractor()->startsWith("RTD", $fileLine)) {	// we didn't reach the EOF after reading the previous row
			while ($this->getExtractor()->startsWith("RTD", $fileLine)) {
				$RTDLines[] = $this->extractRTDRow($fileLine, $currency);
				$fileLine = $this->getNextLine();
				$lineHandled = true;
			}
			$this->gotoPrevLineInFile();	// back off 1 line, to allow _next() to pick it up next time it runs
		}
		if (!$lineHandled) {
			$this->gotoPrevLineInFile();
		}

		// parse PNR, contains passenger data
		$PNRLines = [];
		$fileLine = $this->getNextLine();
		$lineHandled = false;
		if ($fileLine && $this->getExtractor()->startsWith("PNR", $fileLine)) {	// we didn't reach the EOF after reading the previous row
			while ($this->getExtractor()->startsWith("PNR", $fileLine)) {
				$PNRLines[] = $this->extractPNRRow($fileLine, $currency);
				$fileLine = $this->getNextLine();
				$lineHandled = true;
			}
			$this->gotoPrevLineInFile();	// back off 1 line, to allow _next() to pick it up next time it runs
		}
		if (!$lineHandled) {
			$this->gotoPrevLineInFile();
		}

		// do the actual row processing, taking into account collected data
		return $this->processRecords([
			'DET' => $DET,
			'TAXLines' => $TAXLines,
			'PNRLines' => $PNRLines,
			'CCDLines' => $CCDLines,
			'RTDLines' => $RTDLines,
		]);	// should return "intern:skip" for non-interesting rows

	}

	/***************************************************************************
	 * LOCAL FUNCTIONS *********************************************************
	 * *************************************************************************/

	/**
	 * The internal regexp extractor
	 * @return \App\Utils\BaseParser\Utils\Extractor\Single
	 */
	protected function getExtractor()
	{
		if ($this->extractor) {
			return $this->extractor;
		}

		$this->extractor = new \App\Utils\BaseParser\Utils\Extractor\Single();
		return $this->extractor;
	}

	/**
	 * Process a single row
	 * @throws \App\Bsp\BspRowException
	 */
	protected function extractHeaderData($line)
	{
		$match = $this->getExtractor()->find("/^"
			."HDR"
			."(?P<Agent Numeric Code>[0-9 ]{8})"
			."(?P<Agent Trading Name>.{64})"
			."(?P<Agent Type>.{2})" // HO, BR, SA etc
			."(?P<Billing Period>[0-9]{6})"
			."\s+"
			."/"
		, $line)->cleanup();

		if (!$this->options['allowInvalidHeader'] && !$match->hasValue()) {
			throw new \App\Bsp\BspRowException(sprintf("Invalid BSP header record: %s", $line));
		}

		$this->filesDetails[0]->periodStart = \Engine\DateTime::createFromFormat("ymd", $match->{'Billing Period'})->formatDateIso();
		$this->filesDetails[0]->agencyIataCode = $match->{'Agent Numeric Code'};
		$this->filesDetails[0]->agency = rtrim($match->{'Agent Trading Name'});
	}

	protected function extractDETRow($line)
	{
		$fcn = $this->DETRowSlicer;
		$match = $fcn($line);

		if (!$match->hasValue()) {
			throw new \App\Bsp\BspRowException(sprintf("Invalid BSP record: '%s'", $line));
		}

		if ($match->DET!='DET') {
			throw new \App\Bsp\BspRowException(sprintf("Invalid BSP header record, the DET line header is invalid"));
		}

		$ret = $match->getMatches();
		$currency = $ret['Currency Code'];

		$ret['Ticketing Airline Code'] =                        rtrim($ret['Ticketing Airline Code']);
		$ret['Agent Code'] =                                    rtrim($ret['Agent Code']);
		$ret['System Provider'] =                               rtrim($ret['System Provider']);
		$ret['Billing Category'] =                              rtrim($ret['Billing Category']);
		$ret['Billing Class'] =                                 rtrim($ret['Billing Class']);
		$ret['Transaction Code'] =                              rtrim($ret['Transaction Code']);
		$ret['Document Number'] =                               rtrim($ret['Document Number']);
		$ret['Check Digit'] =                                   rtrim($ret['Check Digit']);
		$ret['Date of Issue'] = 	                            \Engine\DateTime::createFromFormat("ymd", $ret['Date of Issue'])->formatDateIso();
		$ret['Coupon Usage Indicator'] =                        rtrim($ret['Coupon Usage Indicator']);
		$ret['Commissionable Amount'] =                         $this->convert_strToPrice($ret['Commissionable Amount'], $currency);
		$ret['Standard Commission Rate'] =                      $this->convert_strToPercent($ret['Standard Commission Rate'], $currency);
		$ret['Standard Commission Amount'] =                    $this->convert_strToPrice($ret['Standard Commission Amount'], $currency);
		$ret['Supplementary Commission Rate'] =                 $this->convert_strToPercent($ret['Supplementary Commission Rate'], $currency);
		$ret['Supplementary Commission Amount'] =               $this->convert_strToPrice($ret['Supplementary Commission Amount'], $currency);
		$ret['Effective Commission Rate'] =                     $this->convert_strToPercent($ret['Effective Commission Rate'], $currency);
		$ret['Effective Commission Amount'] =                   $this->convert_strToPrice($ret['Effective Commission Amount'], $currency);
		$ret['Remittance Amount'] =                             $this->convert_strToPrice($ret['Remittance Amount'], $currency);
		$ret['Tax on Commission'] =                             $this->convert_strToPrice($ret['Tax on Commission'], $currency);
		$ret['TAX'] =                                           $this->convert_strToPrice($ret['TAX'], $currency);
		$ret['Fees'] =                                          $this->convert_strToPrice($ret['Fees'], $currency);
		$ret['Penalties'] =                                     $this->convert_strToPrice($ret['Penalties'], $currency);
		$ret['Net Remit Indicator'] =                           rtrim($ret['Net Remit Indicator']);
		$ret['Net Remit'] =                                     rtrim($ret['Net Remit']);
		$ret['Stat Code'] =                                     rtrim($ret['Stat Code']);
		$ret['Tour Code'] =                                     rtrim($ret['Tour Code']);
		$ret['Waiver Code'] =                                   rtrim($ret['Waiver Code']);
		$ret['Cash Amount'] =                                   $this->convert_strToPrice($ret['Cash Amount'], $currency);
		$ret['Credit Amount'] =                                 $this->convert_strToPrice($ret['Credit Amount'], $currency);
		$ret['Related Ticket1 Document Type'] =                 rtrim($ret['Related Ticket1 Document Type']);
		$ret['Related Ticket1 Document Number'] =               rtrim($ret['Related Ticket1 Document Number']);
		$ret['Related Ticket1 Document COUPONS'] =              rtrim($ret['Related Ticket1 Document COUPONS']);
		$ret['Related Ticket2 Document Type'] =                 rtrim($ret['Related Ticket2 Document Type']);
		$ret['Related Ticket2 Document Number'] =               rtrim($ret['Related Ticket2 Document Number']);
		$ret['Related Ticket2 Document COUPONS'] =              rtrim($ret['Related Ticket2 Document COUPONS']);
		$ret['In Connection with Ticket'] =     				rtrim($ret['In Connection with Ticket']);
		$ret['Electronic Settlement Authorisation Code'] =      rtrim($ret['Electronic Settlement Authorisation Code']);
		$ret['Tax on Commission Calculation Method'] =          rtrim($ret['Tax on Commission Calculation Method']);
		$ret['Payment Card Information'] =                      rtrim($ret['Payment Card Information']);
		$ret['Payment Card Information - ccCompany'] =          rtrim($ret['Payment Card Information - ccCompany']);
		$ret['Payment Card Information - ccNumber'] =           rtrim($ret['Payment Card Information - ccNumber']);
		$ret['Payment Card Transaction ID'] =                   rtrim($ret['Payment Card Transaction ID']);
		$ret['Payment Card CCVR'] =                             rtrim($ret['Payment Card CCVR']);
		return $ret;
	}

	protected function extractRTDRow($line)
	{
		$fcn = $this->RTDRowSlicer;
		$match = $fcn($line);

		if (!$match->hasValue()) {
			throw new \App\Bsp\BspRowException(sprintf("Invalid BSP record: '%s'", $line));
		}

		if ($match->RTD!='RTD') {
			throw new \App\Bsp\BspRowException(sprintf("Invalid BSP header record, the RTD line header is invalid"));
		}

		$ret = $match->getMatches();

		$ret['Payment Card CCVR'] =                             rtrim($ret['Payment Card CCVR']??'');
		return $ret;
	}

	protected function extractTAXRow($line, $currency)
	{
		$fcn = $this->TAXRowSlicer;
		$match = $fcn($line);

		if (!$match->hasValue()) {
			throw new \App\Bsp\BspRowException(sprintf("Invalid BSP record: '%s'", $line));
		}

		if ($match->TAX!='TAX') {
			throw new \App\Bsp\BspRowException(sprintf("Invalid BSP header record, the TAX line header is invalid"));
		}

		$ret = $match->getMatches();
		$ret['Ticketing Airline Code'] =    rtrim($ret['Ticketing Airline Code']);
		$ret['Agent Code'] =                rtrim($ret['Agent Code']);
		$ret['System Provider'] =           rtrim($ret['System Provider']);
		$ret['Billing Category'] =          rtrim($ret['Billing Category']);
		$ret['Billing Class'] =             rtrim($ret['Billing Class']);
		$ret['Transaction Code'] =          rtrim($ret['Transaction Code']);
		$ret['Document Number'] =           rtrim($ret['Document Number']);
		$ret['Check Digit'] =               rtrim($ret['Check Digit']);

		$shouldCutOff = false;
		for ($i=0; $i<=14; $i++) {
			$idx = sprintf("%02d", $i);

			if ($shouldCutOff) {
				unset($ret["TMFA{$idx}"]);
				unset($ret["TMFT{$idx}"]);
			}
			else {
				$ret["TMFT{$idx}"] =   rtrim($ret["TMFT{$idx}"]);
				$ret["TMFA{$idx}"] =   $this->convert_strToPrice($ret["TMFA{$idx}"], $currency);

				if ($ret["TMFA{$idx}"]===0.0 && $ret["TMFT{$idx}"]==='') {
					$shouldCutOff = true;

					unset($ret["TMFA{$idx}"]);
					unset($ret["TMFT{$idx}"]);
				}
			}
		}

		return $ret;
	}

	protected function extractPNRRow($line, $currency)
	{
		$fcn = $this->PNRRowSlicer;
		$match = $fcn($line);

		if (!$match->hasValue()) {
			throw new \App\Bsp\BspRowException(sprintf("Invalid BSP record: '%s'", $line));
		}

		if ($match->PNR!='PNR') {
			throw new \App\Bsp\BspRowException(sprintf("Invalid BSP header record, the PNR line header is invalid"));
		}

		$ret = $match->getMatches();
		$ret['paxName'] =    		rtrim($ret['paxName']);

		return $ret;
	}

	protected function extractCCDRow($line, $currency)
	{
		$fcn = $this->CCDRowSlicer;
		$match = $fcn($line);

		if (!$match->hasValue()) {
			throw new \App\Bsp\BspRowException(sprintf("Invalid BSP record: '%s'", $line));
		}

		if ($match->CCD!='CCD') {
			throw new \App\Bsp\BspRowException(sprintf("Invalid BSP header record, the CCD line header is invalid"));
		}

		$ret = $match->getMatches();
		$ret['fop'] =    		rtrim($ret['fop']);

		return $ret;
	}

	/**
	 * Process a single row
	 */
	protected function processRecords(array $extractedLines)
	{
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__, func_get_args());

		$DET = $extractedLines['DET'];

		$ret = new \App\Bsp\BspRow();

		// validations
		foreach ($extractedLines['TAXLines'] as $TAX) {
			if (!(
				   $DET['Ticketing Airline Code']==$TAX['Ticketing Airline Code']
				&& $DET['Agent Code']==$TAX['Agent Code']
				&& $DET['System Provider']==$TAX['System Provider']
				&& $DET['Billing Category']==$TAX['Billing Category']
				&& $DET['Billing Class']==$TAX['Billing Class']
				&& $DET['Transaction Code']==$TAX['Transaction Code']
				&& $DET['Document Number']==$TAX['Document Number']
				)) {
				\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
				throw new \App\Bsp\BspRowException(sprintf("Invalid BSP DET & TAX line found. They don't match the same ticket, at transaction %d (check around line %d)", $this->state['rawTransaction'], ($this->state['rawTransaction']*2)+1));
			}
		}

		$ret->supplierCode3 =  $DET['Ticketing Airline Code'];
		$ret->ticketNumber =   $DET['Document Number'];
		$ret->ticketDate =     $DET['Date of Issue'];

		if ($DET['Stat Code']==='') {
			$ret->locationType =   '';
		}
		elseif (in_array($DET['Stat Code'], ['I', 'INT', ])) {
			$ret->locationType =   'I';
		}
		else {
			$ret->locationType =   'D';
		}

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
			'ticketNumber' => sprintf("%s %s %s", $ret->supplierCode3, $ret->ticketNumber, $ret->ticketDate),
		]);

		$ret->extraData->CPUI = $DET['Coupon Usage Indicator'];

		$currency =	$DET['Currency Code'];
		$currencyDecimals = \App\Config\CurrencyDecimals::getCurrencyDecimals($currency);

		$paidWithCc = (bool)($DET['Payment Card Information']);	// CC usually, EP (TD-5981)
		$isRefund = (in_array($DET['Transaction Code'], ['RFND', 'ACMA', ]));
		#$isRefund = (($DET['Cash Amount'] + $DET['Credit Amount'])<0);
		$taxSign = ($isRefund?-1:+1);

		$taxes = new \App\Bsp\BspRowPriceTax();
		foreach ($extractedLines['TAXLines'] as $TAX) {
			for ($i=0; $i<=14; $i++) {
				$idx = sprintf("%02d", $i);

				if (!$TAX["TMFT{$idx}"]) {
					break;
				}

				if ($DET['Billing Class']=='CREDIT MEMOS') {
					$taxes->addTax($TAX["TMFT{$idx}"], $taxSign*$TAX["TMFA{$idx}"], $currency);
				}
				else {
					if ($TAX["TMFT{$idx}"]=='CP') {	// cancellation tax. dont't change signs
						$taxes->addTax($TAX["TMFT{$idx}"], $TAX["TMFA{$idx}"], $currency);
					}
					else {
						$taxes->addTax($TAX["TMFT{$idx}"], $taxSign*$TAX["TMFA{$idx}"], $currency);
					}
				}
			}
		}

		$predeterminedTotalTaxes = $DET['TAX'] + $DET['Fees'] + $DET['Penalties'];

		if ($this->floatsAreEqual($predeterminedTotalTaxes, $taxes->amount)) {
			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logStep("predeterminedTotalTaxes is equal with taxes->amount");
			if ($paidWithCc) {
				$ret->priceTaxCc = $taxes;
			}
			else {
				$ret->priceTaxCash = $taxes;
			}
		}
		else {
			/*
			 * in case the tax values don't match, use the predetermined values
			 * 	- if the file contains more than 14 taxes, the tax line will get truncated, so we'll miss some tax codes
			 * 	- if this is a refund, the penalty might(or might not be, not sure why) negative. In these cases the $predeterminedTotalTaxes will contain the correct value, so we should use that one
			 */
			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logStep(sprintf("predeterminedTotalTaxes is NOT equal with taxes->amount (%.2f vs %.2f)", $predeterminedTotalTaxes, $taxes->amount));
			$overrideTaxes = false;
			if ($taxes->amount<$predeterminedTotalTaxes) {	// this is a refund, ignore the detailed tax
				$overrideTaxes = true;
			}
			elseif (in_array($DET['Transaction Code'], ['RFND', ])) {	// this is a refund, ignore the detailed tax
				$overrideTaxes = true;
			}
			elseif (in_array($DET['Transaction Code'], ['SPCR', ])) {	// this is a credit memo
				$overrideTaxes = true;
			}
			else {
				if ($extractedLines['TAXLines']) {
					\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
					throw new \Exception(sprintf('Invalid tax row. Detailed sum from taxes->amount is different than predeterminedTotalTaxes. (ticketNumber %s, %.2f vs %.2f)', $ret->ticketNumber, $taxes->amount, $predeterminedTotalTaxes));
				}
				else {
					$overrideTaxes = true;
				}
			}

			if ($overrideTaxes) {
				if ($paidWithCc) {
					$ret->priceTaxCc->addTax('custom', $predeterminedTotalTaxes, $currency);
				}
				else {
					$ret->priceTaxCash->addTax('custom', $predeterminedTotalTaxes, $currency);
				}
			}
		}

		$ret->priceCommission->amount = 	$DET['Effective Commission Amount'];
		$ret->priceCommission->currency = 	$currency;
		$ret->priceCommissionPercent = 		$DET['Standard Commission Rate'];
		$ret->priceCommissionVat->amount = 	$DET['Tax on Commission'];
		$ret->priceCommissionVat->currency= $currency;

		$ret->priceBalancePayable->amount = $DET['Remittance Amount'];
		$ret->priceBalancePayable->currency = $currency;

		if ($paidWithCc) {
			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logStep("ticket paid with cc");
			$fop = $ret->addFormOfPayment('cc');
			$fop->companyIndicator = $DET['Payment Card Information - ccCompany'];
			$fop->number = $DET['Payment Card Information - ccNumber'];
		}

		if ($DET['Transaction Code'] == 'CANN' ) {
			// @see https://servicehub.amadeus.com/c/portal/view-solution/834731/ticketing-sales-report-troubleshooting-guidelines
			return "intern:skip";	// skip "technical/system voids"
		}


		$ret->extraData->rowType = 		$DET['Transaction Code'];
		$ret->extraData->isVoid = 		in_array($DET['Transaction Code'], ['CANN', 'CANX', ]);
		$ret->extraData->isReissue = 	$this->getExtractor()->startsWith("EX", $DET['Related Ticket Document Type']);	// TODO: is this correct?
		$ret->extraData->isIssue = 		in_array($DET['Transaction Code'], ['TKTT', 'EMDS', 'EMDA', ]);
		$ret->extraData->isRefund = 	in_array($DET['Transaction Code'], ['RFND', ]);
		$ret->extraData->isAdm = 		in_array($DET['Transaction Code'], ['ADMA', 'ACMA', 'SPDR', 'SPCR', ]);
		$ret->extraData->isEMD = 		in_array($DET['Transaction Code'], ['EMDS', 'EMDA', ]);
		$ret->extraData->isTicketing = 	$ret->extraData->isIssue && !($ret->extraData->isAdm || $ret->extraData->isEMD);

		if ($DET['Related Ticket1 Document Type'] && $DET['Related Ticket1 Document Number']) {
			switch ($DET['Related Ticket1 Document Type']) {
				case 'RF':
					$rel = new \App\Bsp\BspRowRelation('refund',
						substr($DET['Related Ticket1 Document Number'], -10),
						substr($DET['Related Ticket1 Document Number'], 0, 3)
					);
					$ret->relations->append($rel);
				break;

				case 'EX':
					$ret->relations->append(new \App\Bsp\BspRowRelation('reissue',
						substr($DET['Related Ticket1 Document Number'], -10),
						substr($DET['Related Ticket1 Document Number'], 0, 3)
					));
				break;

				default:
					// ignore unknown relations
			}
		}

		if ($DET['Related Ticket2 Document Type'] && $DET['Related Ticket2 Document Number']) {
			switch ($DET['Related Ticket2 Document Type']) {
				case 'RF':
					$rel = new \App\Bsp\BspRowRelation('refund',
						substr($DET['Related Ticket2 Document Number'], -10),
						substr($DET['Related Ticket2 Document Number'], 0, 3)
					);
					$ret->relations->append($rel);
				break;

				case 'EX':
					$ret->relations->append(new \App\Bsp\BspRowRelation('reissue',
						substr($DET['Related Ticket2 Document Number'], -10),
						substr($DET['Related Ticket2 Document Number'], 0, 3)
					));
				break;

				default:
					// ignore unknown relations
			}
		}

		// Normalize refund rows so the original ticket becomes the primary identity.
		if ($isRefund && $ret->relations->relations) {
			/**
			 * Detect if we're refunding the second part of a conjunction.
			 * Ex: TICK has ticker numbers (conjunctions) 1001,1002. RFND is issued for 1002 (instead of 1001).
			 * But in IO we only reference 1001, and have no ideea 1002 even exists (it's stored in a separate, non-indexed array)
			 * WORKAROUND for IO: detect this case and report 1001 as the ticket number, not 1002
			 *
			 * In order to detect this, we look for relations that are consecutive to the main ticket, and is 1 number lower.
			 * This means the user refunded the relation, not the main ticket, when refunding a conjunction.
			 */
			$foundConjunctionRefundReference = false;		// mark refunds for conjunctions (even correct ones)
			$foundBrokenConjunctionRefundReference = false; // mark broken refunds
			foreach ($ret->relations->relations as $rel) {
				// convert to int, as string comparison would be wrong (ex: "999" > "000")
				// this WILL break if PHP is running on 32-bit systems and the ticket number is too large
				$relTicketNumberInt = (int)$rel->ticketNumber;
				$retTicketNumberInt = (int)$ret->ticketNumber;

				// relation is consecutive to the main ticket, and is 1 number difference. This means we're looking at a conjunction
				if (abs($retTicketNumberInt - $relTicketNumberInt)==1) {
					$foundConjunctionRefundReference = true;
				}

				// relation is consecutive to the main ticket, and is 1 number lower. This means the user refunded the relation, not the main ticket, when refunding a conjunction
				if (($retTicketNumberInt - $relTicketNumberInt)==1) {
					$foundBrokenConjunctionRefundReference = true;
				}
			}

			foreach ($ret->relations->relations as $rel) {
				$swapRelationWithMain = false;
				$swapRelationType = '';

				// skip self-references; only act when the relation points to a different ticket
				$relTicketNumberInt = (int)$rel->ticketNumber;
				$retTicketNumberInt = (int)$ret->ticketNumber;

				if ($foundConjunctionRefundReference && $foundBrokenConjunctionRefundReference && $rel->relationType=='refund' && ($retTicketNumberInt - $relTicketNumberInt)==1) {	// relation is consecutive to the main ticket, and is 1 number lower. This means the user refunded the relation, not the main ticket, when refunding a conjunction
					$swapRelationWithMain = true;
					$swapRelationType = 'swapConjunctionRefund';
				}
				elseif (!$foundConjunctionRefundReference && $rel->relationType=='refund' && ($retTicketNumberInt - $relTicketNumberInt)!=0) {
					// we're not looking at a conjunction, just a regular refund
					$swapRelationWithMain = true;
					$swapRelationType = 'swap';
				}

				if ($swapRelationWithMain) {	// keep only the smallest ticket number, numerically it could be $rel->ticketNumber > $ret->ticketNumber
					$this->logger->warning(sprintf("Replacing ticket number %s with it's relation ticket %s, reason: %s", $ret->ticketNumber, $rel->ticketNumber, $swapRelationType));
					// 1) Preserve the current (refund) document as a relation so we don't loose it after swapping
					$rel2 = new \App\Bsp\BspRowRelation($swapRelationType,
						$ret->ticketNumber,
						$ret->supplierCode3
					);
					$ret->relations->append($rel2);

					// 2) Switch the primary identity to the original ticket referenced by the relation
					$ret->ticketNumber = 	$rel->ticketNumber;
					$ret->supplierCode3 = 	$rel->supplierCode3;
				}

			}

		}


		if ($paidWithCc) {
			$ret->priceSupplierCc->amount = 	round(($DET['Cash Amount'] + $DET['Credit Amount']) - ($DET['TAX'] + $DET['Fees'] + $DET['Penalties']), $currencyDecimals);
			$ret->priceSupplierCc->currency = 	$currency;
		}
		else {
			$ret->priceSupplierCash->amount = 	round(($DET['Cash Amount'] + $DET['Credit Amount']) - ($DET['TAX'] + $DET['Fees'] + $DET['Penalties']), $currencyDecimals);
			$ret->priceSupplierCash->currency = $currency;
		}

		if ($paidWithCc && $DET['Credit Amount'] && $DET['Cash Amount']) {
			// when we have a ticket paid with both fop's, distribuite the loaded cc amount according to the ratio from credit/total
			// ticketTotal = $DET['Cash Amount']+$DET['Credit Amount']; // Please note that the price balance payable only contans the cash amount
			$ratio = $DET['Credit Amount']/($DET['Cash Amount']+$DET['Credit Amount']);

			// handle supplier price
			$amountOriginal = 		$ret->priceSupplierCc->amount;
			$amountRemaining = 		round($ret->priceSupplierCc->amount * $ratio, $currencyDecimals);
			$ret->priceSupplierCc->amount = $amountRemaining;
			$ret->priceSupplierCash->amount = $amountOriginal - $amountRemaining;

			// handle taxes
			$amountOriginal = 		$ret->priceTaxCc->amount;
			$amountRemaining = 		round($ret->priceTaxCc->amount * $ratio, $currencyDecimals);
			$ret->priceTaxCc->addTax('mixedPaymentOverflow:resetTax', -1*$ret->priceTaxCc->amount, $currency)
							->addTax('mixedPaymentOverflow', $amountRemaining, $currency);
			$ret->priceTaxCash->addTax('mixedPaymentOverflow', $amountOriginal - $amountRemaining, $currency);
		}


		if ($ret->priceSupplierCash->amount>0 || $ret->priceTaxCash->amount>0) {
			$fop = $ret->addFormOfPayment('cash');
		}

		$ret->rawData = $extractedLines;

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logOutput([
			'priceBalancePayable' => $ret->priceBalancePayable->amount,
			'priceSupplierCash' => $ret->priceSupplierCash->amount,
			'priceSupplierCc' => $ret->priceSupplierCc->amount,
			'priceTaxCash' => $ret->priceTaxCash->amount,
			'priceTaxCc' => $ret->priceTaxCc->amount,
		]);

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
		return $ret;
	}

	/**
	 * Convert a text specific to this filetype to price
	 * @return number
	 */
	protected function convert_strToPrice($str, $currency)
	{
		$ret = null;

		/*
		 * '    ' -> 0.00
		 * 000000 -> 0.00
		 * 000001 -> 0.01
		 * 000100 -> 1.00
		 * -00100 -> -1.00
		 */
		if ($str) {
			$sign = +1;
			$divider = null;
			if (array_key_exists($currency, $this->options['currencySubdivisionMap'])) {
				$divider = $this->options['currencySubdivisionMap'][$currency];
			}
			else {
				$divider = pow(10, \App\Config\CurrencyDecimals::getCurrencyDecimals($currency));
			}
			if ($str[0]==='-') {
				$str = substr($str, 1);
				$sign = -1;
			}
			$ret = $sign*((int)ltrim($str, ' 0'))/$divider;
		}

		return $ret;
	}

	/**
	 * Convert a text specific to this filetype to price
	 * Similar to convert_strToPrice(), but fith a fixed divider
	 * @return number
	 */
	protected function convert_strToPercent($str, $currency)
	{
		$ret = null;

		/*
		 * '    ' -> 0.00
		 * 000000 -> 0.00
		 * 000001 -> 0.01
		 * 000100 -> 1.00
		 * -00100 -> -1.00
		 */
		if ($str) {
			$sign = +1;
			$divider = 100;
			if ($str[0]==='-') {
				$str = substr($str, 1);
				$sign = -1;
			}
			$ret = $sign*((int)ltrim($str, ' 0'))/$divider;
		}
		return $ret;
	}


	/**
	 * Compare float numbers, return true if equal
	 * @see _classes\App\Utils\BaseParser\Structure\StructureParser\Structures\Base\Price.php
	 * @param float $price1
	 * @param float $price2
	 * @return boolean
	 */
	protected function floatsAreEqual($price1, $price2)
	{
		#return (sprintf("%.2f", $price1)===sprintf("%.2f", $price2));
		return abs($price1-$price2)<0.01;
	}
}
