<?php

namespace TablePress\PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;

use TablePress\PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use TablePress\PhpOffice\PhpSpreadsheet\Exception;
use TablePress\PhpOffice\PhpSpreadsheet\Style\Conditional;
use TablePress\PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellMatcher;
use TablePress\PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;

/**
 * @method CellValue equals($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 * @method CellValue notEquals($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 * @method CellValue greaterThan($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 * @method CellValue greaterThanOrEqual($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 * @method CellValue lessThan($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 * @method CellValue lessThanOrEqual($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 * @method CellValue between($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 * @method CellValue notBetween($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 * @method CellValue and($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 */
class CellValue extends WizardAbstract implements WizardInterface
{
	protected const MAGIC_OPERATIONS = [
		'equals' => Conditional::OPERATOR_EQUAL,
		'notEquals' => Conditional::OPERATOR_NOTEQUAL,
		'greaterThan' => Conditional::OPERATOR_GREATERTHAN,
		'greaterThanOrEqual' => Conditional::OPERATOR_GREATERTHANOREQUAL,
		'lessThan' => Conditional::OPERATOR_LESSTHAN,
		'lessThanOrEqual' => Conditional::OPERATOR_LESSTHANOREQUAL,
		'between' => Conditional::OPERATOR_BETWEEN,
		'notBetween' => Conditional::OPERATOR_NOTBETWEEN,
	];

	protected const SINGLE_OPERATORS = CellMatcher::COMPARISON_OPERATORS;

	protected const RANGE_OPERATORS = CellMatcher::COMPARISON_RANGE_OPERATORS;

	protected string $operator = Conditional::OPERATOR_EQUAL;

	/** @var array<int|string> */
	protected array $operand = [0];

	/**
	 * @var string[]
	 */
	protected array $operandValueType = [];

	public function __construct(string $cellRange)
	{
		parent::__construct($cellRange);
	}

	protected function operator(string $operator): void
	{
		if ((!isset(self::SINGLE_OPERATORS[$operator])) && (!isset(self::RANGE_OPERATORS[$operator]))) {
			// should not happen - compareKeys confirms
			throw new Exception('Invalid Operator for Cell Value CF Rule Wizard 1'); // @codeCoverageIgnore
		}

		$this->operator = $operator;
	}

	/**
				 * @param mixed $operand
				 */
				protected function operand(int $index, $operand, string $operandValueType = Wizard::VALUE_TYPE_LITERAL): void
	{
		if (is_string($operand)) {
			$operand = $this->validateOperand($operand, $operandValueType);
		}

		$this->operand[$index] = $operand; //* @phpstan-ignore-line
		$this->operandValueType[$index] = $operandValueType;
	}

	/**
				 * @param mixed $value value to be wrapped
				 * @return float|int|string */
				protected function wrapValue($value, string $operandValueType)
	{
		if (!is_numeric($value) && !is_bool($value) && null !== $value) {
			if ($operandValueType === Wizard::VALUE_TYPE_LITERAL) {
				return '"' . str_replace('"', '""', $value) . '"';
			}

			return $this->cellConditionCheck($value);
		}

		if (null === $value) {
			$value = 'NULL';
		} elseif (is_bool($value)) {
			$value = $value ? 'TRUE' : 'FALSE';
		}

		return $value;
	}

	public function getConditional(): Conditional
	{
		if (!isset(self::RANGE_OPERATORS[$this->operator])) {
			unset($this->operand[1], $this->operandValueType[1]);
		}
		$values = array_map([$this, 'wrapValue'], $this->operand, $this->operandValueType);

		$conditional = new Conditional();
		$conditional->setConditionType(Conditional::CONDITION_CELLIS);
		$conditional->setOperatorType($this->operator);
		$conditional->setConditions($values);
		$conditional->setStyle($this->getStyle());
		$conditional->setStopIfTrue($this->getStopIfTrue());

		return $conditional;
	}

	protected static function unwrapString(string $condition): string
	{
		if ((str_starts_with($condition, '"')) && (str_starts_with(strrev($condition), '"'))) {
			$condition = (string) substr($condition, 1, -1);
		}

		return str_replace('""', '"', $condition);
	}

	public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface
	{
		if ($conditional->getConditionType() !== Conditional::CONDITION_CELLIS) {
			throw new Exception('Conditional is not a Cell Value CF Rule conditional');
		}

		$wizard = new self($cellRange);
		$wizard->style = $conditional->getStyle();
		$wizard->stopIfTrue = $conditional->getStopIfTrue();

		$wizard->operator = $conditional->getOperatorType();
		$conditions = $conditional->getConditions();
		foreach ($conditions as $index => $condition) {
			// Best-guess to try and identify if the text is a string literal, a cell reference or a formula?
			$operandValueType = Wizard::VALUE_TYPE_LITERAL;
			if (is_string($condition)) {
				if (Calculation::keyInExcelConstants($condition)) {
					$condition = Calculation::getExcelConstants($condition);
				} elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '$/i', $condition)) {
					$operandValueType = Wizard::VALUE_TYPE_CELL;
					$condition = self::reverseAdjustCellRef($condition, $cellRange);
				} elseif (
					preg_match('/\(\)/', $condition)
					|| preg_match('/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', $condition)
				) {
					$operandValueType = Wizard::VALUE_TYPE_FORMULA;
					$condition = self::reverseAdjustCellRef($condition, $cellRange);
				} else {
					$condition = self::unwrapString($condition);
				}
			}
			$wizard->operand($index, $condition, $operandValueType);
		}

		return $wizard;
	}

	/**
	 * @param mixed[] $arguments
	 */
	public function __call(string $methodName, array $arguments): self
	{
		if ($methodName === 'and') {
			if (!isset(self::RANGE_OPERATORS[$this->operator])) {
				throw new Exception('AND Value is only appropriate for range operators');
			}

			$this->operand(1, ...$arguments);

			return $this;
		}

		if (!isset(self::MAGIC_OPERATIONS[$methodName])) {
			throw new Exception('Invalid Operator for Cell Value CF Rule Wizard');
		}

		$this->operator(self::MAGIC_OPERATIONS[$methodName]);
		//$this->operand(0, ...$arguments);
		if (count($arguments) < 2) {
			$this->operand(0, $arguments[0]);
		} else {
			/** @var string */
			$arg1 = $arguments[1];
			$this->operand(0, $arguments[0], $arg1);
		}

		return $this;
	}

	/** @internal */
	public static function compareKeys(): bool
	{
		$retVal = true;
		$array = array_merge(array_keys(self::SINGLE_OPERATORS), array_keys(self::RANGE_OPERATORS));
		foreach ($array as $value) {
			// PhpStan is correct about next statement, but we want to test anyhow
			$retVal = $retVal && in_array($value, self::MAGIC_OPERATIONS, true); // @phpstan-ignore-line
		}

		return $retVal;
	}
}
