<?php
namespace App\Utils\BaseParser\TextParser\FileParser;
// TODO: remove commented-out debug code after 2015-01-01

// NOTE: this is the base parser for Sabre files. Not sure it can be reused. It should be duplicated as needed

class ParserForBlocks extends \App\Utils\BaseParser\TextParser\BlockRegexpAbstract
{
	protected $extractRegexp = "/^(?P<rawData>.+)$/m";

	/**
	 * The line structure
	 */
	protected $children = [
	];

	/**
	 * Byte index in the file
	 * @var int
	 */
	protected $rawIndex = 	null;

	protected $rawDataRemaining = null;

	/**
	 * Line helper (date parser, formatter, array filtering, etc)
	 * @var \App\Utils\BaseParser\TextParser\Helper
	 */
	protected $lineHelper = 	null;

	/**
	 * The internal messages list
	 * @var \App\MessagesCollector
	 */
	protected $messages = null;

	/**
	 * Get the current line number
	 * @return number
	 */
	public function getCurrentIndex()
	{
		return $this->rawIndex;
	}

	/**
	 * Get remaining data
	 * @return string
	 */
	public function getRawDataRemaining()
	{
		return $this->rawDataRemaining;
	}

	/**
	 * Set the remainder data
	 */
	public function setRawDataRemaining($rawData)
	{
		$this->rawDataRemaining = $rawData;
	}

	/**
	 * Set the message container
	 * @param \App\MessagesCollector $messageContainer
	 */
	public function setMessagesCollector(\App\MessagesCollector $messageContainer)
	{
		$this->messages = $messageContainer;
		return $this;
	}

	/**
	 * Get the message container
	 * @return \App\MessagesCollector
	 */
	public function getMessagesCollector()
	{
		return ($this->messages?:$this->messages = new \App\MessagesCollector());
	}

	/**
	 * Extract from a list of children
	 * @param array $lineCfg
	 * @param array $callCfg
	 * @throws \App\Utils\BaseParser\TextParser\FileParser\Exception\General
	 * @return array|NULL
	 */
	protected function extractFromChildrenList($lineCfg, $callCfg)
	{
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__, func_get_args());

		$extractedBlockData = [];

		// TODO: implement support for startsWith, endsWith

		// Loop through all children and extract data from them
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
			'msg' => "Loop over children",
			'key' => $lineCfg['key'],
			'line'=> $this->getCurrentIndex(),
		]);

		foreach ($lineCfg['children'] as $child) {
			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
				'key'=>$child['key'],
				'children'=>($child['children']?'yes':'no'),
			]);

			$child['req'] = (isset($child['req'])?$child['req']:true);

			$continueExtract = true;

			if ($child['children']) {
				$expectSiblings = false;
				$repetitionCurrentIndex = 0;
				if ($child['rep']) {
					if ($child['rep'][1]>1) {
						$repetitionsExpectedMaximum = $child['rep'][1];	// can be PHP_INT_MAX for "unbounded" repetitions
						$expectSiblings = true;
					}
				}
				else {
					$repetitionsExpectedMaximum = 1;
				}

				// if this line is expected to reapeat, loop through all repetitions
				while ($repetitionCurrentIndex<$repetitionsExpectedMaximum && $continueExtract) {
					// check if we still have lines to parse
					if ($this->getCurrentIndex() >= strlen($this->rawData)) {
						$continueExtract = false;
						break;
					}

					$isRequired = $child['req'];

					if ($repetitionCurrentIndex>=$child['rep'][0]) {
						$isRequired = false;
					}

					// call itself
					$ccfg = $callCfg;
					$ccfg['depth']++;

					$extractedLineData = null;

					if ($isRequired) {
						// this is a required element, let the BlockIsRequired exception bubble up, if they occur
						$extractedLineData = $this->extractFromChildrenList($child, $ccfg);
					}
					else {
						// this is an optional element, its ok not to find it
						try {
							$extractedLineData = $this->extractFromChildrenList($child, $ccfg);
						}
						catch (\App\Utils\BaseParser\TextParser\FileParser\Exception\BlockIsRequired $ex) {
							// do nothing, eat the exception
						}
					}

					if ($extractedLineData!==null) {
						if ($expectSiblings) {
							if ($child['merge']) {
								if ($extractedLineData[$child['key']]) {
									foreach ($extractedLineData[$child['key']] as $k=>$v) {
										$extractedBlockData[$child['key']][] = $v;
									}
								}
								elseif($extractedLineData) {
									\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
										'msg' => "Merged block should return a key, but it returned value in some other form",
									]);
									\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
									throw new \App\Utils\BaseParser\TextParser\FileParser\Exception\InvalidFileStructure("Merged block should return a key, but it returned value in some other form. Line: {$child['key']}, lineIndex #{$this->getCurrentIndex()}");
								}
							}
							else {
								$extractedBlockData[$child['key']][] = $extractedLineData;
							}
						}
						else {
							if ($child['merge']) {
								foreach ($extractedLineData as $k=>$v) {
									$extractedBlockData[$k] = $v;
								}
							}
							else {
								$extractedBlockData[$child['key']] = $extractedLineData;
							}
						}
					}
					elseif ($isRequired) {	// throw exceptions for required lines
						\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
							'msg' => "Block is required, but it wasn't found[2]",
						]);
						\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
						throw new \App\Utils\BaseParser\TextParser\FileParser\Exception\BlockIsRequired("Block is required, but it wasn't found[2]. Line: {$child['key']}, lineIndex #{$this->getCurrentIndex()}");
					}
					else {
						$continueExtract = false;
					}

					$repetitionCurrentIndex++;
				}
			}
			else {
				// this tree has no children, so we should treat it as a leaf
				$ccfg = $callCfg;
				$ccfg['depth']++;
				$extractedLineData = $this->extractFromSingleChild($child, $ccfg);
				if ($extractedLineData!==null) {
					$extractedBlockData[$child['key']] = $extractedLineData;
				}
			}
		}
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
			'msg' => "End loop",
		]);

		// return the block of extracted data
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
			'msg' => "Normal return loop",
		]);

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

	/**
	 * Extract data from a single leaf node
	 * @param array $lineCfg
	 * @param array $callCfg
	 * @throws \App\Utils\BaseParser\TextParser\FileParser\Exception\General
	 * @return array|NULL
	 */
	protected function extractFromSingleChild($lineCfg, $callCfg)
	{
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__, func_get_args());
		$extractedBlockData = [];

		$continueExtract = true;

		$expectSiblings = false;
		$repetitionCurrentIndex = 0;
		if ($lineCfg['rep']) {
			if ($lineCfg['rep'][1]>1) {
				$repetitionsExpectedMaximum = $lineCfg['rep'][1];	// can be PHP_INT_MAX for "unbounded" repetitions
				$expectSiblings = true;
			}
		}
		else {
			$repetitionsExpectedMaximum = 1;
		}

		while ($repetitionCurrentIndex<$repetitionsExpectedMaximum && $continueExtract) {
			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->dlog("Extract {%key}, rep: {%rep}/{%repmax}", [
				'key' => 	$lineCfg['key'],
				'rep' => 	$repetitionCurrentIndex,
				'repmax' => $repetitionsExpectedMaximum,
			]);

			// check if we still have lines to parse
			if ($this->getCurrentIndex() >= strlen($this->rawData)) {
				\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->dlog("No more blocks to parse in the input data. Break loop");
				$continueExtract = false;
				break;
			}

			$lineStr = $this->getRawDataRemaining();
			$obj = $this->createObject($lineCfg['type'], $lineCfg, $this);
			$obj->setRawData($lineStr);

			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
				'key' => 	$lineCfg['key'],
				'lineStr' => $lineStr,
				'obj' => $obj,
			]);

			if ($obj->isValid()) {
				$lineRet = $obj->extract();
				$this->setRawDataRemaining($lineRet->remainingGlobalData);

				$extractedLineData = $lineRet->extractedData;

				if ($obj->isMemberOfExtractedData()) {
					if ($expectSiblings) {
						// when we have siblings, the 'merge' flag is ignored
						$extractedBlockData[] = $extractedLineData;
					}
					else {
						if ($lineCfg['merge']) {
							foreach ($extractedLineData as $k=>$v) {
								$extractedBlockData[$k] = $v;
							}
						}
						else {
							$extractedBlockData = $extractedLineData;
						}
					}
				}
			}
			elseif ($lineCfg['req']) {	// throw exceptions for required lines
				if ($expectSiblings && $extractedBlockData) {
					$continueExtract = false;
				}
				else {
					\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
					throw new \App\Utils\BaseParser\TextParser\FileParser\Exception\BlockIsRequired("Block is required, but it wasn't found[1]. Block: {$lineCfg['key']}, lineIndex #{$this->getCurrentIndex()}");
				}
			}
			else {
				$continueExtract = false;
			}

			$repetitionCurrentIndex++;
		}

		if ($extractedBlockData) {
			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->dlog("Normal return");
			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
			return $extractedBlockData;
		}
		// else

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->dlog("NULL return");
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
		return null;
	}

	/**
	 * Preprocess data
	 * @param string $originalRawData
	 */
	protected function preProcessRawData($originalRawData)
	{
		$this->rawData = str_replace(["\r\n", "\r", "\n"], "\n", $originalRawData);
		$this->setRawDataRemaining($this->rawData);
		$this->rawIndex = 0;
	}

	/**
	 * (non-PHPdoc)
	 * @see \App\Utils\BaseParser\TextParser\BlockRegexpAbstract::_extract()
	 */
	protected function _extract(\App\Utils\BaseParser\TextParser\ExtractedBlock $ret, $originalRawData)
	{
		$this->preProcessRawData($this->rawData);

		if (!$this->isValid()) {
			throw new \App\Utils\BaseParser\TextParser\FileParser\Exception\General(sprintf("Invalid file detected. The header and the finish lines might not be correct."));
		}

		$this->lineHelper = new \App\Utils\BaseParser\TextParser\Helper();

		$ret->extractedData = $this->extractFromChildrenList(['children'=>$this->children, 'key'=>'root'], ['depth'=>0]);

		if ($this->getRawDataRemaining()) {
			$totalBytes = strlen($this->getRawData());
			$totalLines = substr_count($this->getRawData(), "\n");
			$processedBytes = $totalBytes - strlen($this->getRawDataRemaining());
			$processedLines = substr_count(substr($this->getRawData(), 0, $processedBytes), "\n");

			throw new \App\Utils\BaseParser\TextParser\FileParser\Exception\UnprocessedLinesRemaining(
				sprintf("Not all bytes were parsed in this file. We parsed %d bytes (%d lines) out of %d bytes (%d lines). \nExtracted data: %s",
					$processedBytes, $processedLines,
					$totalBytes, $totalLines,
					var_export($ret->extractedData, true)
			));
		}

		return $ret;
	}

	/**
	 * Get the unique file id
	 * @return string
	 */
	public function getFileId()
	{
		return '';
	}

	/**
	 * Collect various possible file dates
	 * @param string $path
	 * @return string[]
	 */
	public function collectFileDatesFromPath($path)
	{
		/**
		 * Try to extract the file date, from any possible source. The basic algorighm goes like this:
		 * 	- get the current date
		 * 	- get the file date (file ctime)
		 * 	- get the file date (regexp parse the filename)
		 * 	- get various data from within the file, without having access to parsed data (if possible, it depends on each GDS)
		 * 	- get the 'FluX' date (if possible. Not implemented for now, it probably is the current date anyway)
		 *
		 *  * the caller manually picks one of the above dates (by using the minimum date from all of the collected dates usually)
		 */

		$collectedDates = [];
		#$collectedDates['current'] = 	\Engine\DateTime::create()->formatDateIso();
		$collectedDates['filectime'] = 	\Engine\DateTime::createFromTimestamp(filectime($path))->formatDateIso();

		if ($m = $this->getExtractor()->findAndStore("/(.+)?(?P<dY>20[0-9]{2})(?P<dm>(01|02|03|04|05|06|07|08|09|10|11|12))(?P<dd>((0|1|2)([0-9]{1}))|30|31) [0-9]{6} ([^\/].+)\.FluX\.([^\/].+)$/", $path)) {
			// 20160826115310.0004.fluX.AIR00001.AIR.txt
			// 20171221 154541 31 01.FluX.AIRB06360.AIR
			// 20161213 000523 45 01.FluX.AIR00027.AIR.txt
			// 20190720 115159 33 02.FluX.SJPM20190720100612634.txt
			$collectedDates['FluXFilenameTime'] = sprintf("%04d-%02d-%02d", $m->dY, $m->dm, $m->dd);
		}
		elseif ($m = $this->getExtractor()->findAndStore("/(.+)?(?P<dY>20[0-9]{2})(?P<dm>(01|02|03|04|05|06|07|08|09|10|11|12))(?P<dd>((0|1|2)([0-9]{1}))|30|31)[^\/].+$/", $path)) {
			// SJPM20190716200141649.txt
			$collectedDates['FluXFilenameTime'] = sprintf("%04d-%02d-%02d", $m->dY, $m->dm, $m->dd);
		}


		// 2008-11-27_123949_86.127.90.91_1_@2700027.NOV
		// 2013-02-08_150735_89.149.8.211__@0800035.FEB.txt
		$m = $this->getExtractor()->find("/(?P<dY>20[0-9]{2})-(?P<dm>(01|02|03|04|05|06|07|08|09|10|11|12))-(?P<dd>((0|1|2)([0-9]{1}))|30|31).+$/", $path);
		if ($m->hasValue()) {
			$collectedDates['FluXFilenameTime2'] = \Engine\DateTime::createFromIsoDate(sprintf("%04d-%02d-%02d", $m->dY, $m->dm, ((int)$m->dd?$m->dd:1)))->format("Y-m-t");
		}

		return array_filter($collectedDates);
	}
}
