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

class BlockOptimizedComplexField extends \App\Utils\BaseParser\TextParser\BlockAbstract
{
	/**
	 * The final composed regexp expression
	 * @var string
	 */
	protected $extractRegexp = null;

	/**
	 * Regex extractor
	 * @var \App\Utils\BaseParser\Utils\Extractor\Single
	 */
	protected $extractor = null;

	protected $objectCache = [];

	protected $matchRenames = [];

	protected $alias = null;

	public function  __construct($options = [], $parent = null)
	{
		$this->extractor = new \App\Utils\BaseParser\Utils\Extractor\Single();

		$this->extractRegexp = $this->createRegexp($this->children, 'Standard');

		parent::__construct($options, $parent);
	}

	/**
	 * regex helper: convert lengths array to a regexp expression
	 * @param array $lengths
	 * @param bool $required
	 * @return string
	 */
	protected function regexp_convertLengthsToGreedyRegexpMultiplier($lengths, $required)
	{
		if (is_array($lengths) && ($lengths[0] || $lengths[1])) {
			return sprintf("{%d,%d}", (int)$lengths[0], (int)$lengths[1]);
		}
		elseif ($lengths) {
			return sprintf("{%d}", (int)$lengths);
		}
		else {
			if ($required) {
				return "+";
			}
			else {
				return "*";
			}
		}

		return '';
	}

	/**
	 * (non-PHPdoc)
	 * @see \App\Utils\BaseParser\TextParser\BlockAbstract::isValid()
	 */
	public function isValid()
	{
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__, func_get_args());
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
			'alias'=> $this->alias,
			'key' => $this->options['key'],
			'rawData' => $this->rawData,
		]);

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
			'extractRegexp' => $this->extractRegexp,
		], 'regexp');

		$ret = $this->extractor->contains($this->extractRegexp, $this->rawData);

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

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

	/**
	 * (non-PHPdoc)
	 * @see \App\Utils\BaseParser\TextParser\BlockAbstract::_extract()
	 */
	protected function _extract(\App\Utils\BaseParser\TextParser\ExtractedBlock $ret, $originalRawData)
	{
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
			'regexp' => $this->extractRegexp,
		], 'regexp');

		$m = $this->extractor->find($this->extractRegexp, $originalRawData);
		$this->rawData = $this->extractValueFromMatch($m, $this->children);
		$ret->extractedData = $this->rawData;

		if ($this->options['lookahead']) {
			$ret->remainingGlobalData = $originalRawData;
		}
		else {
			$ret->remainingGlobalData = substr($originalRawData, $m->getMatchedLength());
		}

		return $ret;
	}

	/**
	 *
	 * @param string $namedPatternsStrategy = Standard, None
	 */
	protected function regexpBuilder_generatePatternName($child, $type, $namedPatternsStrategy='Standard')
	{
		$ret = '';
		switch ($namedPatternsStrategy) {
			case 'Standard':
				if ($type=='root') {
					$ret = sprintf('?P<%s>', ($child['key']?:"autoKey".substr(md5(serialize($child['children'])), 0, 4)));
				}
				else {
					if ($child['key']) {
						$ret = sprintf('?P<%s>', $child['key']);
					}
					else {
						// do nothing, this child is nameless
					}
				}
			break;

			case 'None':
				// do nothing
			break;
		}

		return $ret;
	}

	public function createRegexp($children, $namedPatternsStrategy='Standard')
	{
		return sprintf("/^%s$/", $this->convertChildListToRegexp($children, $namedPatternsStrategy));
	}


	/**
	 * Called from the constructor, convert the children list to a big regexp
	 * @throws \App\Utils\BaseParser\TextParser\Exception\LineParser
	 * @return string
	 */
	public function convertChildListToRegexp($children, $namedPatternsStrategy='Standard')
	{
		$extractRegexp = [];
		if ($children) {
			$patternNames = [
				'name' => [],
			];

			foreach ($children as $child) {
				if ($child['children']) {
					$key = $this->regexpBuilder_generatePatternName($child, 'root', $namedPatternsStrategy);
					$extractRegexp[] = sprintf("(%s%s)%s", $key, $this->convertChildListToRegexp($child['children'], $namedPatternsStrategy), ($child['req']?'':'?'));
				}
				else {
					$rxs = [	// regexp settings
						'childRegexp' => null,
						'namedPattern' => true,
						'isOptional' => !$child['req'],
					];

					$handled = $this->convertChildListToRegexp_custom($rxs, $child);

					if (!$handled) {
						switch ($child['type']) {
							case 'InternalMarker':
								$rxs['childRegexp'] = '';
								$rxs['namedPattern'] = false;

								$this->alias = $child['val'];
							break;

							case 'Constant':
								$rxs['childRegexp'] = preg_quote($child['val'], "/");
								$rxs['namedPattern'] = false;
							break;
							case 'Set':
								$rxs['childRegexp'] = implode("|", array_map(function($v){
									return preg_quote($v, "/");
								}, $child['val']));
								$rxs['namedPattern'] = true;
							break;

							case 'StringRaw':
							case 'StringTrim':
							case 'StringRTrim':
								if ($child['len']) {
									$rxs['childRegexp'] = sprintf(".%s", $this->regexp_convertLengthsToGreedyRegexpMultiplier($child['len'], true));
								}
								else {
									$rxs['childRegexp'] = sprintf(".*");
								}
							break;

							case 'Alphanumeric':
								$rxs['childRegexp'] = sprintf("[a-zA-Z0-9]%s", $this->regexp_convertLengthsToGreedyRegexpMultiplier($child['len'], true));
							break;

							case 'Letters':
								$rxs['childRegexp'] = sprintf("[a-zA-Z]%s", $this->regexp_convertLengthsToGreedyRegexpMultiplier($child['len'], true));
							break;

							case 'StringDelimitedByChar':
							case 'StringTrimmedDelimitedByChar':
								if (strlen((string)$child['val'])==1) {
									$rxs['childRegexp'] = sprintf("[^%s]*", preg_quote((string)$child['val'], "/"));
								}
								else {
									// lookahead
									$rxs['childRegexp'] = sprintf(".*?(?=%s)", preg_quote((string)$child['val'], "/"));
								}
							break;

							case 'Digits':
							case 'Integer':
								$rxs['childRegexp'] = sprintf("[0-9]%s", $this->regexp_convertLengthsToGreedyRegexpMultiplier($child['len'], true));
							break;

							case 'FloatRaw':
								$rxs['childRegexp'] = sprintf("[0-9\.]%s", $this->regexp_convertLengthsToGreedyRegexpMultiplier($child['len'], true));
							break;

							case 'Currency':
								$rxs['childRegexp'] = sprintf("[A-Z]{3}");
							break;

							case 'Date':
								$obj = $this->objectCache['Date'];
								if (!$obj) {
									$obj = self::createObject('Date', $child, $this);
									$this->objectCache['Date'] = $obj;
								}
								$skey = sprintf("%s:%s", $child['key'], $child['type']);

								$rxs['childRegexp'] = $obj->getSubregexp($child);

								if (preg_match_all('/\(\?P\<(?P<name>[^>]+)\>/', $rxs['childRegexp'], $patternNames)) {
									if (!$this->matchRenames[$skey]) {
										$this->matchRenames[$skey] = [];
									}
									list($rxs['childRegexp'], $this->matchRenames[$skey]) = $this->extractor->renameRegexpPatternsInRegexp($rxs['childRegexp'], $this->matchRenames[$skey], $patternNames['name'], $skey);
								}
							break;

							case 'Time':
								$obj = $this->objectCache['Time'];
								if (!$obj) {
									$obj = self::createObject('Time', $child, $this);
									$this->objectCache['Time'] = $obj;
								}
								$skey = sprintf("%s:%s", $child['key'], $child['type']);

								$rxs['childRegexp'] = $obj->getSubregexp($child);

								if (preg_match_all('/\(\?P\<(?P<name>[^>]+)\>/', $rxs['childRegexp'], $patternNames)) {
									if (!$this->matchRenames[$skey]) {
										$this->matchRenames[$skey] = [];
									}
									list($rxs['childRegexp'], $this->matchRenames[$skey]) = $this->extractor->renameRegexpPatternsInRegexp($rxs['childRegexp'], $this->matchRenames[$skey], $patternNames['name'], $skey);
								}
							break;

							default:
								throw new \App\Utils\BaseParser\TextParser\Exception\LineParser(sprintf("[%s] child '%s' is of type '%s'. This extractor doesn't handle it. Cannot use %s to extract it",
								__METHOD__,
								$child['key'],
								$child['type'],
								__CLASS__
							));
						}
					}

					$key = '';
					if ($rxs['namedPattern']) {
						$key = $this->regexpBuilder_generatePatternName($child, 'child', $namedPatternsStrategy);
					}

					if ($rxs['childRegexp'] && $child['lookahead']) {
						$rxs['childRegexp'] = sprintf("(?=%s)", $rxs['childRegexp']);
					}

					if ($rxs['childRegexp']) {
						$extractRegexp[]= sprintf("(%s%s)%s", $key, $rxs['childRegexp'], ($rxs['isOptional']?'?':''));
					}
				}
			}
		}

		return implode("", $extractRegexp);
	}


	/**
	 * Called after extract
	 * @param \App\Utils\BaseParser\Utils\Extractor\SingleFindResponse $m
	 */
	protected function extractValueFromMatch(\App\Utils\BaseParser\Utils\Extractor\SingleFindResponse $m, $children)
	{
		$rawData = [];
		foreach ($children as $child) {
			$vxs = [
				'doAssign' => true,
			];
			$value = $m->get($child['key']);
			$key = ($child['key']?:"autoKey".substr(md5(serialize($child['children'])), 0, 4));

			if ($child['children']) {
				$value = $this->extractValueFromMatch($m, $child['children']);

				if (count(array_filter($value, function($v) { return $v!==null;}))!=0) {
					if ($child['merge']) {
						foreach ($value as $k=>$v) {
							$rawData[$k] = $v;
						}

						$vxs['doAssign'] = false;
					}
				}
			}
			else {
				$handled = $this->extractValueFromMatch_custom($value, $vxs, $child);

				if (!$handled) {
					switch ($child['type']) {
						case 'InternalMarker':
							$value = $child['val'];
						break;

						case 'Constant':
							$vxs['doAssign'] = false;
						break;

						case 'StringTrimmedDelimitedByChar':
						case 'StringTrim':
							$value = trim($value);
						break;
						case 'StringRTrim':
							$value = rtrim($value);
						break;

						case 'Integer':
							$value = (int)$value;
						break;

						case 'FloatRaw':
							$value = (float)$value;
						break;

						case 'Date':
							$obj = $this->objectCache['Date'];

							$skey = sprintf("%s:%s", $child['key'], $child['type']);

							$matches = [];
							$matches[0] = $value;
							foreach ($this->matchRenames[$skey] as $ren=>$orig) {
								$matches[$orig] = $m->{$ren};
							}
							$value = $obj->getDate($matches, $child, null);
						break;

						case 'Time':
							$obj = $this->objectCache['Time'];

							$skey = sprintf("%s:%s", $child['key'], $child['type']);

							$matches = [];
							$matches[0] = $value;
							foreach ($this->matchRenames[$skey] as $ren=>$orig) {
								$matches[$orig] = $m->{$ren};
							}
							$value = $obj->getTime($matches, $child['fmt']);
							break;

					}
				}
			}

			if ($vxs['doAssign']) {
				$rawData[$key] = $value;
			}
		}

		return $rawData;
	}

	protected function convertChildListToRegexp_custom(&$rxs, $child)
	{
		return false;
	}

	protected function extractValueFromMatch_custom(&$value, &$vxs, $child)
	{
		return false;
	}
}
