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

/**
 * The base class for a block parser
 * @author Lucian Sirbu
 */
class BlockAbstract
{
	/**
	 * Various settings for this particular block (type, length for example)
	 * @var array
	 */
	protected $options = [];

	/**
	 * The children structure
	 * If this is filled in, that this block is assumed to be a "complex" block, and that it contains sub-childs
	 * @var array
	 */
	protected $children = null;

	/**
	 * Actual data associated for this block.
	 * May be an array for complex types(simple types may return arrays too), or a scalar value, for simple types
	 * @var mixed
	 */
	protected $extractedData = null;

	/**
	 * The string from where we need to extract data
	 * @var string
	 */
	protected $rawData = null;

	/**
	 * The parent object, if any
	 */
	protected $parent = null;

	/**
	 * The helper object, that handles various date & time & array processing
	 * @var \App\Utils\BaseParser\TextParser\Helper
	 */
	protected $helper = null;

	/**
	 * Store internal data
	 * @var array
	 */
	protected $storedData = [];

	/**
	 * Local extractor
	 * @var \App\Utils\BaseParser\Utils\Extractor\Single
	 */
	private $extractor = null;


	/**
	 * Initializes the basic block element
	 */
	public function  __construct($options, $parent = null)
	{
		$this->options = $options;
		$this->parent = $parent;
	}

	/**
	 * Set an elemente to be stored in this data-block
	 * @param string $key
	 * @param mixed $value
	 * @return \App\Utils\BaseParser\TextParser\BlockAbstract
	 */
	public function setStoredData($key, $value)
	{
		$this->storedData[$key] = $value;
		return $this;
	}

	/**
	 * Get a stored key
	 * @param string $key
	 * @return mixed
	 */
	public function getStoredData($key)
	{
		return $this->storedData[$key];
	}

	/**
	 * Get the raw data behind the storedData attribtue
	 * @return array
	 */
	public function getStoredDataKeys()
	{
		return array_keys($this->storedData);
	}

	/**
	 * Attach a helper object. The helper object is supposed to help cleaning up arrays, date matching, time matching, etc
	 * @param \App\Utils\BaseParser\TextParser\Helper $helper
	 */
	public function setHelper(\App\Utils\BaseParser\TextParser\Helper $helper)
	{
		$this->helper = $helper;
	}

	/**
	 * Create a new object for the required type
	 * This function can be overidden to be able to overwrite some of the basic types
	 * (Ex: to load ParseBlock_XYZ_float instead of the default ParseBlock_float)
	 * @param string $type
	 * @param array $params
	 * @return \App\Utils\BaseParser\TextParser\BlockAbstract
	 */
	public function createObject($type, $options, $self)
	{
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__, func_get_args());

		$class = sprintf("\App\Utils\BaseParser\TextParser\Types\%s", $type);
		if (class_exists($class)) {
			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logOutput(['found' => true]);
			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
			return new $class($options, $self);
		}

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logOutput(['found' => false]);
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
		throw new \Exception("Class not found({$class})");
	}

	/**
	 * Get the parent block
	 * @return BlockAbstract
	 */
	public function getParent()
	{
		return $this->parent;
	}

	/**
	 * Get the base parent
	 */
	public function getBaseParent()
	{
		$p = $this->parent;
		while ($p) {
			if ($p->parent) {
				$p = $p->parent;
			}
			else {
				return $p;
			}
		}
		return null;
	}

	/**
	 * Get the internal raw data used by the parser
	 * @return string
	 */
	public function getRawData()
	{
		return $this->rawData;
	}

	/**
	 * Set the internal raw data that gets parsed
	 * @param string $data
	 */
	public function setRawData($rawData)
	{
		$this->rawData = $rawData;
		return $this;
	}

	/**
	 * Get the internal raw data used by the parser
	 * @return mixed
	 */
	public function getExtractedData()
	{
		return $this->extractedData;
	}

	/**
	 * Get the internal defined children structure
	 * @return array
	 */
	public function getStructure()
	{
		return $this->children;
	}

	/**
	 * Called before extracting a block of data
	 */
	protected function preExtract(\App\Utils\BaseParser\TextParser\ExtractedBlock $ret)
	{
		return $ret;
	}

	/**
	 * Called after data extraction is done
	 */
	protected function postExtract(\App\Utils\BaseParser\TextParser\ExtractedBlock $ret)
	{
		return $ret;
	}

	/**
	 * Check if this should generate a data member in the output list
	 * By default, it returns true, but it may return false for some constants
	 * 	- by implemnting this, we will be able to skip reporting constant values in $extractedData
	 * @return boolean
	 */
	public function isMemberOfExtractedData()
	{
		return true;
	}

	/**
	 * Check if this block is required in the final structure
	 * @return boolean
	 */
	public function isRequired()
	{
		return (bool)$this->options['req'];
	}


	/**
	 * Get the internal options raw array
	 * @return array
	 */
	public function getOptions()
	{
		return $this->options;
	}


	/**
	 * Check if the block contents is set to the expected format
	 * @return bool
	 */
	public function isValid()
	{
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__, func_get_args());
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
			'key' => $this->options['key'],
		]);

		$ret = (bool)$this->rawData;

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logOutput([
			'return' => $ret,
		]);

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

	/**
	 * Internal callback that does all the heavy-lifting
	 * @return \App\Utils\BaseParser\TextParser\ExtractedBlock
	 */
	protected function _extract(\App\Utils\BaseParser\TextParser\ExtractedBlock $ret, $originalRawData)
	{
		// process a block with children
		if (is_array($this->children)) {
			$ret = $this->_extract_children($ret, $this->children, $originalRawData, [$this, "_extract_children_callback_associativeArray"]);
		}
		// process a single value
		else {
			$ret = $this->_extract_value($ret, $originalRawData);
		}
		return $ret;
	}

	/**
	 * Callback that should insert extracted data into the tree structure
	 * @param array $extractedData
	 * @param array $child
	 * @param mixed $value
	 * @param bool $is_array
	 * @return array
	 */
	protected function _extract_children_callback_associativeArray($extractedData, $child, $value, $is_array)
	{
		if ($is_array) {
			$extractedData[$child['key']][] = $value;
		}
		else {
			$extractedData[$child['key']] = $value;
		}

		// $extractedData, $child, $ret2->extractedData, true
		return $extractedData;
	}

	/**
	 * Extract children from the $children structure
	 * @param \App\Utils\BaseParser\TextParser\ExtractedBlock $ret
	 * @throws \App\Utils\BaseParser\TextParser\Exception\MissingRequiredElementException
	 * @return \App\Utils\BaseParser\TextParser\ExtractedBlock
	 */
	protected function _extract_children(\App\Utils\BaseParser\TextParser\ExtractedBlock $ret, $children, $originalRawData, $insertCallback)
	{
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__, []);
		// $rawData will contain "data to be parsed". the parser will start with the full string and will "consume" chunks from it
		$rawData = $originalRawData;

		/**
		 * TODO: this whole function seems to be more convoluted thaq needed.
		 * We should do a refactoring/rewrite.
		 * The sample in ParserForBlocks.php seems cleaner, but it might not cover all cases as used by all current implemented systems (Galileo/CSOB/Sabre)
		 */

		$extractedData = [];
		foreach ($children as $child) {
			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__."::in_loop", []);

			$reps = $child['rep'];
			if (!$reps) {
				$reps = [1, 1];
				if ($child['repCallback']) {
					// this is a dynamically repeatable element. We need to create sub-array's and handle it as it will always contain multiple childs
					$reps = [2, 2];
				}
			}

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

			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
				'child[key]' => $child['key'],
				'reps' => 		$reps,
				'greedy'=>  	$child['greedy'],
			]);

			$repIndex = 0;					// repetition-index
			$repLoop = true;				// repeat-loop
//			$repLoop_foundElement = true;	// not used, it could be useful for the AMA-style parser below
			while ($repLoop || $child['greedy']) {
				$repLoop = false;
				if ($repIndex<$reps[0]) {
					$repLoop = true;
				}

				if ($child['greedy'] && $repIndex>$reps[1]) {
					$child['greedy'] = false;
				}

				\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
					'repIndex' => 	$repIndex,
					'repLoop' => 	$repLoop,
				]);

				/*
				// handle multiple elements, where not all of them are mandatory, AMA-style, not tested
				if (!$repLoop && ($repIndex<$reps[1] || $reps[1]===null)) {
					if ($repLoop_foundElement) {
						$repLoop = true;

						if ($required && $repIndex>0) {
							$required = false;
						}
					}
				}
				*/

				if ($repLoop || $child['greedy']) {
					// this is a child that has other childs, but no type set. We should parse it with $this(recurse), instead of creating a new parser object
					if ($child['children'] && !$child['type']) {
						\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logStep("this is a child that has other childs, but no type set");

						try {
							$ret = $this->_extract_children($ret, $child['children'], $rawData, $insertCallback);

							if ($child['merge']) {
								foreach($ret->extractedData as $k=>$v) {
									$extractedData[$k] = $v;
								}
							}
							else {
								// if repetitions is set to 1, then this element may only appear once, we dont need to create a sub-array for him
								if ($reps[1]>1) {
									$extractedData = call_user_func_array($insertCallback, [$extractedData, $child, $ret->extractedData, true]);
								}
								else {
									$extractedData = call_user_func_array($insertCallback, [$extractedData, $child, $ret->extractedData, false]);
								}
							}

							$rawData = $ret->remainingGlobalData;
						}
						catch (\App\Utils\BaseParser\TextParser\Exception\MissingRequiredElementException $ex) {
							if ($child['req']) {
								if ($child['greedy'] && $repIndex>$reps[0]) {
									// break the loop without errors
									break;
								}
								else {
									\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
									throw new \App\Utils\BaseParser\TextParser\Exception\MissingRequiredElementException($ex->getMessage(), $ex->getCode(), $ex, $this);
								}
							}
							// else ignore missing elements in this sub-block, they are not mandatory
						}
					}
					else {
						$block = static::createObject($child['type'], $child, $this);
						$block->setRawData($rawData);

						$isRequired = $block->isRequired();
						$isValid = $block->isValid();
						$isMemberOfExtractedData = $block->isMemberOfExtractedData();

						\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInputWithNull([
							'isRequired()' =>              $isRequired,
							'isValid()' =>                 $isValid,
							'isMemberOfExtractedData()' => $isMemberOfExtractedData,
							#'$rawData' =>                  $rawData,
						]);
						// only handle data this way for required elements
						if ($isRequired) {		// check if this is a required element
							if (!$isValid) {	// check if element actually exists (was marked as mandatory)
								if(is_subclass_of($this->getBaseParent(), "\App\Utils\BaseParser\TextParser\FileParser\ParserForIndividualLines")) {
									\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
									$message = trim(sprintf(	"Required element is missing or it isn't valid[1].\n".
														"Key: %s. %s.\n".
														"Line index: %d, offset in line: %d. \n\n".
														"Extracted data: %s\n",
											$child['key'],
											$this->debug_getCallStack(),
											$this->getBaseParent()->getCurrentIndex(),
											strlen($originalRawData) - strlen($rawData),
											implode("\n     ", explode("\n", var_export($extractedData, true)))
									));

									throw new \App\Utils\BaseParser\TextParser\Exception\MissingRequiredElementException("<pre>{$message}</pre>", 0, null, $this);
								}
								else {
									\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
									$message = trim(sprintf(	"Required element is missing or it isn't valid[2].\n".
														"Key: %s. %s.\n".
														"Offset in line: %d. <em style='color: #400;'>(unknown line though)</em> \n\n".
														"Extracted data: %s\n",
											$child['key'],
											$this->debug_getCallStack(),
											strlen($originalRawData) - strlen($rawData),
											implode("\n     ", explode("\n", var_export($extractedData, true)))
									));

									throw new \App\Utils\BaseParser\TextParser\Exception\MissingRequiredElementException("<pre>{$message}</pre>", 0, null, $this);
								}
							}

							// this block uses extract() to read data from the child block. This means that a "recursive" call could be generated here, for the same function, but with different child in mind
							$ret2 = $block->extract();

							// remaining raw data to process in this block
							$rawData = $ret2->remainingGlobalData;
//							$repLoop_foundElement = true;

							// only add this to the internal data list if he actually generates a data-member(constants could be skipped by using this method)
							if ($isMemberOfExtractedData) {
								if ($child['merge']) {
									foreach($ret2->extractedData as $k=>$v) {
										$extractedData[$k] = $v;
									}
								}
								else {
									// if repetitions is set to 1, the this element may only appear once, we dont need to create a sub-array for him
									if ($reps[1]==1) {
										$extractedData = call_user_func_array($insertCallback, [$extractedData, $child, $ret2->extractedData, false]);
									}
									else {
										// else, when we got more than one possible repetition, the data should be a list
										$extractedData = call_user_func_array($insertCallback, [$extractedData, $child, $ret2->extractedData, true]);
									}
								}
							}

						}
						elseif ($isValid) {	// optional element, has content

							// this block uses extract() to read data from the child block. This means that a "recursive" call could be generated here, for the same function, but with different child in mind
							$ret2 = $block->extract();

							// remaining raw data to process in this block
							$rawData = $ret2->remainingGlobalData;
//							$repLoop_foundElement = true;

							// only add this to the internal data list if he actually generates a data-member(constants could be skipped by using this method)
							if ($isMemberOfExtractedData) {
								if ($child['merge']) {
									foreach($ret2->extractedData as $k=>$v) {
										$extractedData[$k] = $v;
									}
								}
								else {
									// if repetitions is set to 1, the this element may only appear once, we dont need to create a sub-array for him
									if ($reps[1]==1) {
										$extractedData = call_user_func_array($insertCallback, [$extractedData, $child, $ret2->extractedData, false]);
									}
									else {
										// else, when we got more than one possible repetition, the data should be a list
										$extractedData = call_user_func_array($insertCallback, [$extractedData, $child, $ret2->extractedData, true]);
									}
								}
							}
						}
						else {
							// missing optional element, or invalid element. we dont really care about this case
//							$repLoop_foundElement = false;
						}
					}

				}

				// increment the current repetitions index, to determine how many times we repeated this block
				$repIndex++;

				if ($child['repCallback'] && !isset($child['rep'])) {
					$shouldRepeat = call_user_func_array([$this, $child['repCallback']], [$child, $ret, $rawData, $repIndex]);

					$repLoop = false;
					if ($shouldRepeat) {
						$reps[0] = $repIndex+1;
						$reps[1] = $repIndex+1;
						$repLoop = true;
					}
				}
			}

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

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

	/**
	 * Extract a single value
	 * @param \App\Utils\BaseParser\TextParser\ExtractedBlock $ret
	 * @return \App\Utils\BaseParser\TextParser\ExtractedBlock
	 */
	protected function _extract_value(\App\Utils\BaseParser\TextParser\ExtractedBlock $ret, $originalRawData)
	{
		$ret->extractedData = $originalRawData;
		return $ret;
	}

	/**
	 * Public function, expected to extract everything it can from the input data and return remaining data
	 * @return \App\Utils\BaseParser\TextParser\ExtractedBlock
	 */
	public function extract()
	{
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__, func_get_args());
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
			'key' => $this->options['key'],
			'rawData' => $this->rawData,
		]);

		$ret = new \App\Utils\BaseParser\TextParser\ExtractedBlock();
		$ret = $this->preExtract($ret);

		$ret = $this->_extract($ret, $this->rawData);

		$this->extractedData = $ret->extractedData;

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logOutput([
			'remainingBlockData' => $ret->remainingBlockData,
			'remainingGlobalData' => $ret->remainingGlobalData,
		]);

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logOutput([
			'extractedData' => $this->extractedData,
		], 'highlight');

		// we still have data to parse in this block. Throw an exception
		if ($ret->remainingBlockData) {
			$message = trim(sprintf(
				"There are still elements in this block unprocessed[1]: '%s' <em>(%d bytes)</em>.\n".
				"%s\n\n".
				"Extracted data: %s\n",

				$ret->remainingGlobalData, strlen($ret->remainingGlobalData),
				$this->debug_getCallStack(),
				implode("\n     ", explode("\n", var_export($this->extractedData, true)))
			));

			// this exception wasn't always here. Be careful about side-effects. Added@2017-10-19
			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
			throw new \App\Utils\BaseParser\TextParser\Exception\UnprocessedElementsRemaining("<pre>{$message}</pre>", null, null, $ret);
		}

		// we're on the root level, and we still have data to parse
		if ($this->parent==null && strlen((string)$ret->remainingGlobalData)) {
			$message = trim(sprintf(
				"There are still elements in this block unprocessed[2]: '%s' <em>(%d bytes)</em>.\n".
				"%s\n\n".
				"Extracted data: %s\n",

				$ret->remainingGlobalData, strlen($ret->remainingGlobalData),
				$this->debug_getCallStack(),
				implode("\n     ", explode("\n", var_export($this->extractedData, true)))
			));

			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
			throw new \App\Utils\BaseParser\TextParser\Exception\UnprocessedElementsRemaining("<pre>{$message}</pre>", null, null, $ret);
		}

		$ret = $this->postExtract($ret);
		$this->extractedData = $ret->extractedData;

		if ($this->getBaseParent() && $this->options['store']) {
			$this->getBaseParent()->setStoredData($this->options['store'], $ret->extractedData);
		}

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

	/**
	 * Get a debug callstack to be printed in the error messages
	 */
	protected function debug_getCallStack()
	{
		$excDetails = ["\n"];
		$classParents = array_merge([get_class($this)], class_parents($this));
		$excDetails[] = sprintf("callstack(with extended class parents): %s", implode(" <- ", $classParents));

		$debugCallstack = array_reverse(debug_backtrace());
		$callStack = [];
		foreach($debugCallstack as $call){
			$callStack[] = sprintf("%s%s%s()", ($call['object']?get_class($call['object']):'(non object)'), $call['type'], $call['function']);
		}
		$excDetails[] = sprintf("callstack: %s", "\n\t".implode("\n\t", array_reverse($callStack)));

		return implode("\n", $excDetails);
	}

	/**
	 * Return a list of duplicate keys. Useful when building parser trees
	 */
	protected function internalCheckKeyUniqueness($tree)
	{
		$childs = $this->internalLinearizeChilds($tree, [
			'depth' => 0,
			'key' => '/',
		]);

		uasort($childs, function($a, $b) {
			return strcasecmp($a['key'], $b['key']);
		});

		$childs = array_filter($childs, function($c) {
			return !($c['key']=='' || preg_match("/(^#.*)/", $c['key']) || in_array($c['type'], ['Constant', 'Separator',]));
		});

		// return a mapping of all duplicate keys
		$return = [];
		foreach ($childs as $k1=>$v1) {
			foreach ($childs as $k2=>$v2) {
				if ($k1!=$k2 && $v1['key']==$v2['key']) {
					$return[$k1] = $k2;
				}
			}
		}

		return $return;
	}

	/**
	 * Walk the $tree and return a linear list, with keys meaning something
	 */
	protected function internalLinearizeChilds($tree, $options)
	{
		$return = [];
		foreach ($tree as $idx=>$child) {
			if ($child['children']) {
				$o = $options;
				$o['depth']++;
				$o['key'].= $child['key'].'/';

				$ret = $this->internalLinearizeChilds($child['children'], $o);
				foreach ($ret as $k=>$v) {
					$return[$k] = $v;
				}
			}
			else {
				$key = sprintf("%s[%s]/%s", $options['key'], $idx, $child['key']);
				$return[$key] = $child;
			}
		}

		return $return;
	}

	/**
	 * Build an 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;
	}


}
