<?php

namespace Combodo\iTop\Setup\ModuleDependency;

use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;

/**
 * Class that handles a module dependency
 * Dependency expression example : (moduleA/123 || moduleB>456)
 */
class DependencyExpression
{
	private static PhpExpressionEvaluator $oPhpExpressionEvaluator;

	private string $sDependencyExpression;
	private bool $bValid = true;
	private bool $bResolved = false;

	/**
	 * @var array<string, bool> $aRemainingModuleNamesToResolve
	 */
	private array $aRemainingModuleNamesToResolve;

	/**
	 * @var array<string, array> $aParamsPerModuleId
	 */
	private array $aParamsPerModuleId;

	public function __construct(string $sDependencyExpression)
	{
		$this->sDependencyExpression = $sDependencyExpression;
		$this->aParamsPerModuleId = [];
		$this->aRemainingModuleNamesToResolve = [];

		if (preg_match_all('/([^\(\)&| ]+)/', $sDependencyExpression, $aMatches)) {
			foreach ($aMatches as $aMatch) {
				foreach ($aMatch as $sModuleId) {
					if (!array_key_exists($sModuleId, $this->aParamsPerModuleId)) {
						// $sModuleId in the dependency string is made of a <name>/<optional_operator><version>
						// where the operator is < <= = > >= (by default >=)
						$aModuleMatches = [];
						if (preg_match('|^([^/]+)/(<?>?=?)([^><=]+)$|', $sModuleId, $aModuleMatches)) {
							$sModuleName = $aModuleMatches[1];
							$this->aRemainingModuleNamesToResolve[$sModuleName] = true;
							$sOperator = $aModuleMatches[2];
							if ($sOperator == '') {
								$sOperator = '>=';
							}
							$sExpectedVersion = $aModuleMatches[3];
							$this->aParamsPerModuleId[$sModuleId] = [$sModuleName, $sOperator, $sExpectedVersion];
						}
					}
				}
			}
		} else {
			$this->bValid = false;
		}
	}

	public static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
	{
		if (!isset(self::$oPhpExpressionEvaluator)) {
			self::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], ModuleFileReader::STATIC_CALL_AUTOSELECT_WHITELIST);
		}

		return self::$oPhpExpressionEvaluator;
	}

	/**
	 * Return module names potentially required by current dependency
	 *
	 * @return array<string>
	 */
	public function GetRemainingModuleNamesToResolve(): array
	{
		return array_keys($this->aRemainingModuleNamesToResolve);
	}

	public function IsResolved(): bool
	{
		return $this->bResolved;
	}

	/**
	 * Check if dependency is resolved with current list of module versions
	 *
	 * @param array $aResolvedModuleVersions : versions by module names dict
	 * @param array $aAllModuleNames : modules names dict
	 *
	 * @return void
	 */
	public function UpdateModuleResolutionState(array $aResolvedModuleVersions, array $aAllModuleNames): void
	{
		if (!$this->bValid) {
			return;
		}

		$aReplacements = [];

		$bDelayEvaluation = false;
		foreach ($this->aParamsPerModuleId as $sModuleId => list($sModuleName, $sOperator, $sExpectedVersion)) {
			if (array_key_exists($sModuleName, $aResolvedModuleVersions)) {
				// module is resolved, check the version
				$sCurrentVersion = $aResolvedModuleVersions[$sModuleName];
				if (version_compare($sCurrentVersion, $sExpectedVersion, $sOperator)) {
					if (array_key_exists($sModuleName, $this->aRemainingModuleNamesToResolve)) {
						unset($this->aRemainingModuleNamesToResolve[$sModuleName]);
					}
					$aReplacements[$sModuleId] = '(true)'; // Add parentheses to protect against invalid condition causing
					// a function call that results in a runtime fatal error
				} else {
					$aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
					// a function call that results in a runtime fatal error
				}
			} else {
				// module is not resolved yet

				if (array_key_exists($sModuleName, $aAllModuleNames)) {
					//Weird piece of code that covers below usecase:
					//module B dependency: 'moduleA || true'
					// if moduleA not present on disk, whole expression can be evaluated and may be resolved
					// if moduleA present on disk, we need to sort moduleB after moduleA. expression cannot be resolved	yet
					$bDelayEvaluation = true;
				} else {
					$aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
				}

			}
		}

		if ($bDelayEvaluation) {
			return;
		}

		$bResult = false;
		$sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $this->sDependencyExpression);
		try {
			$bResult = self::GetPhpExpressionEvaluator()->ParseAndEvaluateBooleanExpression($sBooleanExpr);
		} catch (ModuleFileReaderException $e) {
			//logged already
			echo "Failed to parse the boolean Expression = '$sBooleanExpr'<br/>";
		}

		$this->bResolved = $bResult;
	}

	public function IsValid(): bool
	{
		return $this->bValid;
	}
}
