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

/**
 *
 * @author Lucian Sirbu
 *
 */
class Date extends \App\Utils\BaseParser\TextParser\BlockRegexpAbstract
{
	protected $matchedData = 			[];
	protected $subregexp = 				null;
	protected $renamedPatternNames = 	[];

	protected $referenceDate = 			null;

	protected $monthsAssoc = [
		"JAN"=>'01', "FEB"=>'02', "MAR"=>'03',
		"APR"=>'04', "MAY"=>'05', "JUN"=>'06',
		"JUL"=>'07', "AUG"=>'08', "SEP"=>'09',
		"OCT"=>'10', "NOV"=>'11', "DEC"=>'12',
	];

	/**
	 * Build the object
	 * @param string $parent
	 * @throws \Exception
	 */
	public function  __construct($options, $parent = null)
	{
		/*
		 * subregexp will contain a raw regexp, with the pattern names possibly renamed
		 * renamedPatternNames will contain the reverse mapping from the renamed pattern to the original name
		 * this "hack" will allow us to run a regexp that might contain multiple subregexps with repeating pattern names (this is not allowed by pcre)
		 */
		list($this->subregexp, $this->renamedPatternNames) = $this->getExtractor()->renameDuplicateRegexpPatternsInRegexp($this->getSubregexp($options), $this->renamedPatternNames, [
			'YY', 'YYYY',
			'M', 'MM', 'MMM',
			'D', 'DD',
		], 'dt');

		$this->extractRegexp = "/^(?P<rawData>{$this->subregexp})/sim";

		$this->referenceDate = \Engine\DateTime::create();

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

		// load the element from a global container (stored via 'store'=>'XYZ')
		if ($this->getBaseParent() && $this->getBaseParent()->getStoredData($options['ref'])) {
			$this->referenceDate = \Engine\DateTime::createFromIsoDate($this->getBaseParent()->getStoredData($options['ref']));
		}

		if ($options['referenceDate']) {
			$this->referenceDate = \Engine\DateTime::createFromIsoDate($options['referenceDate']);
		}
	}

	public function getSubregexp($options)
	{
		$subregexps = [];

		$formats = [];
		if ($options['fmt']) {
			$formats[] = $options['fmt'];
		}
		if ($options['fmts']) {
			$formats = array_merge($formats, $options['fmts']);
		}
		foreach ($formats as $fmt) {
			$subregexps[]= $this->getSubregexpForSpecificFmt($fmt, $options);
		}

		if ($options['allowSpaces']) {
			$subregexps[]= "[\s]{$this->regexp_convertLengthsToGreedyRegexpMultiplier($options['len'], $options['req'])}";
		}

		return implode("|", $subregexps);
	}

	public function getSubregexpForSpecificFmt($fmt, $options)
	{
		$subregexp = null;
		switch ($fmt) {
			case 'DDMMMYY':
				$subregexp = sprintf("(?P<DD>[0-9]{2})(?P<MMM>%s)(?P<YY>[0-9]{2})", implode("|", array_keys($this->monthsAssoc)));
			break;
			case 'DDMMM':
				$subregexp = sprintf("(?P<DD>[0-9]{2})(?P<MMM>%s)", implode("|", array_keys($this->monthsAssoc)));
			break;
			case 'MMMDD':
				$subregexp = sprintf("(?P<MMM>%s)(?P<DD>[0-9]{2})", implode("|", array_keys($this->monthsAssoc)));
			break;
			case 'YYMMDD':
				$subregexp = sprintf("(?P<YY>[0-9]{2})(?P<MM>[0-9]{2})(?P<DD>[0-9]{2})");
			break;
			case 'YYYYMMDD':
				$subregexp = sprintf("(?P<YYYY>[0-9]{4})(?P<MM>[0-9]{2})(?P<DD>[0-9]{2})");
			break;
			case 'YYYYMMMDD':
				$subregexp = sprintf("(?P<YYYY>[0-9]{4})(?P<MMM>%s)(?P<DD>[0-9]{2})", implode("|", array_keys($this->monthsAssoc)));
			break;
			case 'YYYYDDMMM':
				$subregexp = sprintf("(?P<YYYY>[0-9]{4})(?P<DD>[0-9]{2})(?P<MMM>%s)", implode("|", array_keys($this->monthsAssoc)));
			break;
			case 'DDMMMYYYY':
				$subregexp = sprintf("(?P<DD>[0-9]{2})(?P<MMM>%s)(?P<YYYY>[0-9]{4})", implode("|", array_keys($this->monthsAssoc)));
			break;
			case 'DDMMYYYY':
				$subregexp = sprintf("(?P<DD>[0-9]{2})(?P<MM>[0-9]{2})(?P<YYYY>[0-9]{4})");
			break;
			case 'MMDD':
				$subregexp = sprintf("(?P<MM>[0-9]{2})(?P<DD>[0-9]{2})");
			break;
			case 'DDMM':
				$subregexp = sprintf("(?P<DD>[0-9]{2})(?P<MM>[0-9]{2})");
			break;
			case 'MMYYYY':
				$subregexp = sprintf("(?P<MM>[0-9]{2})(?P<YYYY>[0-9]{4})");
			break;
			case 'MMYY':
				$subregexp = sprintf("(?P<MM>[0-9]{2})(?P<YY>[0-9]{2})");
			break;
			case 'MYY':
				$subregexp = sprintf("(?P<M>[0-9]{1})(?P<YY>[0-9]{2})");
			break;
			default:
				throw new \Exception("Unknown date format '{$fmt}'");
		}

		return $subregexp;
	}
	/**
	 * Get date according to format
	 * @throws \Exception
	 * @return NULL
	 */
	public function getDate($matches, $options, $renamedPatternNames) {
		$return = null;

		$referenceDate = $this->referenceDate;
		if ($options['referenceDate']) {
			$referenceDate = \Engine\DateTime::createFromIsoDate($options['referenceDate']);
		}

		if ($options['allowSpaces'] && trim($matches['rawData'])=='') {
			return null;
		}
		else {
			$formats = [];
			if ($options['fmt']) {
				$formats[] = $options['fmt'];
			}
			if ($options['fmts']) {
				$formats = array_merge($formats, $options['fmts']);
			}

			/*
			 * because we do not know which of the possible input patterns was matched,
			 * 	I'm trying here to reconstruct the matched pattern by looking at the matched data
			 *
			 * $renamedPatternNames might be sent as null, and the reverse pattern building will be skipped.
			 * 	This should be used from lower level classes that call these functions directly
			 *
			 */
			if ($renamedPatternNames===null) {
				$fmt = reset($formats);
			}
			else {
				$fmt = "";
				$rawMatches = $matches;
				$matches = [];
				$matches['rawData'] = $rawMatches['rawData'];
				foreach ($rawMatches as $k=>$v) {
					if ($v && $renamedPatternNames[$k]) {
						$fmt.= $renamedPatternNames[$k];
						$matches[$renamedPatternNames[$k]] = $v;
					}
				}
			}

			// check that we actually correctly reconstructed the input format
			if (!in_array($fmt, $formats)) {
				throw new \Exception("Invalid reconstructed date format: '{$fmt}'");
			}

			switch ($fmt) {
				case 'YYYYMMMDD':
				case 'YYYYDDMMM':
				case 'DDMMMYYYY':
					$return = sprintf("%04d-%02d-%02d", $matches['YYYY'], $this->monthsAssoc[$matches['MMM']], $matches['DD']);
				break;

				case 'DDMMYYYY':
					$return = sprintf("%04d-%02d-%02d", $matches['YYYY'], $matches['MM'	], $matches['DD']);
				break;

				case 'MMMDD':
				case 'DDMMM':
					$return = sprintf("%04d-%02d-%02d", $referenceDate->format("Y"), $this->monthsAssoc[$matches['MMM']], $matches['DD']);
					$return = $this->fixYearAccordingToReference($return, $referenceDate->formatDateIso(), $options);
				break;


				case 'DDMMMYY':
					$return = sprintf("20%02d-%02d-%02d", $matches['YY'], $this->monthsAssoc[$matches['MMM']], $matches['DD']);
				break;


				case 'YYMMDD':
					$return = sprintf("20%02d-%02d-%02d", $matches['YY'], $matches['MM'], $matches['DD']);
				break;
				case 'YYYYMMDD':
					$return = sprintf("%04d-%02d-%02d", $matches['YYYY'], $matches['MM'], $matches['DD']);
				break;

				case 'MMDD':
				case 'DDMM':
					$return = sprintf("%04d-%02d-%02d", $referenceDate->format("Y"), $matches['MM'], $matches['DD']);
					$return = $this->fixYearAccordingToReference($return, $referenceDate->formatDateIso(), $options);
				break;

				case 'MMYYYY':
					$return = sprintf("%04d-%02d-01", $matches['YYYY'], $matches['MM']);
				break;
				case 'MMYY':
					$return = sprintf("20%02d-%02d-01", $matches['YY'], $matches['MM']);
				break;
				case 'MYY':
					$return = sprintf("20%02d-%02d-01", $matches['YY'], $matches['M']);
				break;

				default:
					throw new \Exception("Unknown date format '{$fmt}'");
			}
		}

		if ($return===null) {
			return null;
		}

		// check if date is not valid, throw exception
		if ($options['req'] && !\Engine\DateTime::createFromIsoDate($return)->isValid()) {
			// if we're allowed to return invalid data, return null
			if ($options['allowInvalidData']=='any') {
				// this was needed to parse in Sabre a file with 29'th feb date
				return null;
			}

			// colect a list of class parents to be displayed in the thrown exception
			$parents = [];
			$loops = 100;
			$p = $this;
			do {
				$p = $p->parent;
				$loops--;
				$parents[] = get_class($p);
				// $parents[] = $p->options['key'];
			} while($loops && $p && $p->parent);

			$exceptionStr = sprintf("Invalid date field. Field: %s, value: %s, fmt: %s", $options['key'], $return, $fmt);
			if ($options['ref']) {
				$exceptionStr = sprintf("%s. Reference: %s (%s)", $exceptionStr, $options['ref'], $referenceDate->formatDateIso());
			}

			if ($this->parent && $options['key']) {
				$exceptionStr = sprintf("%s. Parents: %s", $exceptionStr, implode(", ", array_filter($parents)));
 				throw new \App\Utils\BaseParser\TextParser\Exception\MissingRequiredElementException($exceptionStr, null, null, $this);
			}
			else {
 				throw new \App\Utils\BaseParser\TextParser\Exception\MissingRequiredElementException($exceptionStr, null, null, $this);
			}
		}

		// the date should be in the past, relative to the internal reference date
		if ($options['onlyPast'] && \Engine\DateTime::createFromIsoDate($return)->isValid()) {
			$referenceDateStr = $referenceDate->formatDateIso();
			while ($return > $referenceDateStr) {
				$return = \Engine\DateTime::createFromIsoDate($return)->modify("-1year")->formatDateIso();
			}
		}

		// the date should be in the future, relative to the internal reference date
		if ($options['onlyFuture'] && \Engine\DateTime::createFromIsoDate($return)->isValid()) {
			$referenceDateStr = $referenceDate->formatDateIso();
			while ($return < $referenceDateStr) {
				$return = \Engine\DateTime::createFromIsoDate($return)->modify("+1year")->formatDateIso();
			}
		}

		// store the element in a global container, to be able to access it later (via 'ref'=>'XYZ')
		if ($this->getBaseParent() && $options['store']) {
			$this->getBaseParent()->setStoredData($options['store'], $return);
		}

		return $return;
	}

	/**
	 * Get the internal configured months assoc
	 */
	public function getMonthsAssoc($type)
	{
		return $this->monthsAssoc;
	}

	protected function formatExtractedData($str, $matchesObj)
	{
		return $this->getDate($matchesObj->matches, $this->options, $this->renamedPatternNames);
	}

	/**
	 * Use the hints in $options to make the $dateStr in the future or in the past conmpared to $referenceDate, ignoring the date overall validity
	 * NOTE: this HAS to do text-based-processing, in order to avoid the DateTime class declasing certain dates as invalid (Ex: 2019-02-29)
	 * NOTE: this is a workaround to DateTime's built-in validity tests
	 * Ex:   $referenceDateStr=2019-06-01, input=2019-00-00, ouput=2020-00-00
	 * @param string $dateStr
	 * @param string $referenceDateStr
	 * @param array $options
	 * @return string
	 */
	protected function fixYearAccordingToReference($dateStr, $referenceDateStr, $options)
	{
		$dy = (int)substr($dateStr, 0, 4);
		$dyRemaining = substr($dateStr, 4);

		if ($options['onlyPast']) {
			$loops = 200;
			while ($dateStr > $referenceDateStr && $loops--) {
				$dy--;
				$dateStr = sprintf("%04d", $dy).$dyRemaining;
			}
			if ($loops<=0) {
				throw new \Exception("Looped too many times searching for a valid past year. Aborting. Date: {$dateStr}, reference: {$referenceDateStr}");
			}
		}

		if ($options['onlyFuture']) {
			$loops = 200;
			while ($dateStr < $referenceDateStr && $loops--) {
				$dy++;
				$dateStr = sprintf("%04d", $dy).$dyRemaining;
			}
			if ($loops<=0) {
				throw new \Exception("Looped too many times searching for a valid future year. Aborting. Date: {$dateStr}, reference: {$referenceDateStr}");
			}
		}

		return $dateStr;
	}
}
