<?php

namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;

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

/**
 * @method TextValue contains(string $value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 * @method TextValue doesNotContain(string $value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 * @method TextValue doesntContain(string $value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 * @method TextValue beginsWith(string $value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 * @method TextValue startsWith(string $value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 * @method TextValue endsWith(string $value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
 */
class TextValue extends WizardAbstract implements WizardInterface
{
    protected const MAGIC_OPERATIONS = [
        'contains' => Conditional::OPERATOR_CONTAINSTEXT,
        'doesntContain' => Conditional::OPERATOR_NOTCONTAINS,
        'doesNotContain' => Conditional::OPERATOR_NOTCONTAINS,
        'beginsWith' => Conditional::OPERATOR_BEGINSWITH,
        'startsWith' => Conditional::OPERATOR_BEGINSWITH,
        'endsWith' => Conditional::OPERATOR_ENDSWITH,
    ];

    protected const OPERATORS = [
        Conditional::OPERATOR_CONTAINSTEXT => Conditional::CONDITION_CONTAINSTEXT,
        Conditional::OPERATOR_NOTCONTAINS => Conditional::CONDITION_NOTCONTAINSTEXT,
        Conditional::OPERATOR_BEGINSWITH => Conditional::CONDITION_BEGINSWITH,
        Conditional::OPERATOR_ENDSWITH => Conditional::CONDITION_ENDSWITH,
    ];

    protected const EXPRESSIONS = [
        Conditional::OPERATOR_CONTAINSTEXT => 'NOT(ISERROR(SEARCH(%s,%s)))',
        Conditional::OPERATOR_NOTCONTAINS => 'ISERROR(SEARCH(%s,%s))',
        Conditional::OPERATOR_BEGINSWITH => 'LEFT(%s,LEN(%s))=%s',
        Conditional::OPERATOR_ENDSWITH => 'RIGHT(%s,LEN(%s))=%s',
    ];

    protected string $operator;

    protected string $operand;

    protected string $operandValueType;

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

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

        $this->operator = $operator;
    }

    protected function operand(string $operand, string $operandValueType = Wizard::VALUE_TYPE_LITERAL): void
    {
        $operand = $this->validateOperand($operand, $operandValueType);

        $this->operand = $operand;
        $this->operandValueType = $operandValueType;
    }

    protected function wrapValue(string $value): string
    {
        return '"' . $value . '"';
    }

    protected function setExpression(): void
    {
        $operand = $this->operandValueType === Wizard::VALUE_TYPE_LITERAL
            ? $this->wrapValue(str_replace('"', '""', $this->operand))
            : $this->cellConditionCheck($this->operand);

        if (
            $this->operator === Conditional::OPERATOR_CONTAINSTEXT
            || $this->operator === Conditional::OPERATOR_NOTCONTAINS
        ) {
            $this->expression = sprintf(self::EXPRESSIONS[$this->operator], $operand, $this->referenceCell);
        } else {
            $this->expression = sprintf(self::EXPRESSIONS[$this->operator], $this->referenceCell, $operand, $operand);
        }
    }

    public function getConditional(): Conditional
    {
        $this->setExpression();

        $conditional = new Conditional();
        $conditional->setConditionType(self::OPERATORS[$this->operator]);
        $conditional->setOperatorType($this->operator);
        $conditional->setText(
            $this->operandValueType !== Wizard::VALUE_TYPE_LITERAL
                ? $this->cellConditionCheck($this->operand)
                : $this->operand
        );
        $conditional->setConditions([$this->expression]);
        $conditional->setStyle($this->getStyle());
        $conditional->setStopIfTrue($this->getStopIfTrue());

        return $conditional;
    }

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

        $wizard = new self($cellRange);
        $wizard->operator = (string) array_search($conditional->getConditionType(), self::OPERATORS, true);
        $wizard->style = $conditional->getStyle();
        $wizard->stopIfTrue = $conditional->getStopIfTrue();

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

        return $wizard;
    }

    /**
     * @param mixed[] $arguments
     */
    public function __call(string $methodName, array $arguments): self
    {
        if (!isset(self::MAGIC_OPERATIONS[$methodName])) {
            throw new Exception('Invalid Operation for Text Value CF Rule Wizard');
        }

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

        return $this;
    }

    /** @internal */
    public static function compareKeys(): bool
    {
        $retVal = true;
        $array = array_keys(self::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;
    }
}
