<?php

namespace FiloBlu\Esb\Core\Expression;

use Magento\Framework\DataObject;
use RuntimeException;

use function array_key_exists;
use function is_array;
use function is_object;

/**
 * Class BooleanEvaluator
 * @package FiloBlu\Esb\Core\Expression
 */
class BooleanEvaluator implements EvaluatorInterface
{
    /**
     * @var int
     */
    const LEFT = 0;
    /**
     * @var int
     */
    const RIGHT = 1;
    /**
     * @var array
     */
    protected $precedence = [
        'and' => 2,
        'or'  => 2
    ];
    /**
     * @var array
     */
    protected $assoc = [
        'and' => BooleanEvaluator::LEFT,
        'or'  => BooleanEvaluator::LEFT
    ];
    /**
     * @var array
     */
    protected $outputQueue = [];
    /**
     * @var array
     */
    protected $operatorStack = [];
    /**
     * @var ExpressionInterfaceFactory
     */
    private $expressionFactory;

    /**
     * BooleanEvaluator constructor.
     * @param ExpressionInterfaceFactory $expressionFactory
     */
    public function __construct(ExpressionInterfaceFactory $expressionFactory)
    {
        $this->expressionFactory = $expressionFactory;
    }

    /**
     * TODO: Validate
     * Compiles in Reverse Polish Notation
     *
     * @param array $tokens
     * @return EvaluatorInterface
     */
    public function compile(array $tokens): EvaluatorInterface
    {
        while ($tokens) {
            $token = array_shift($tokens);

            if (is_object($token) || $this->isExpression($token)) {
                $o = (object)$token;
                $this->outputQueue[] = $this->expressionFactory->create()
                    ->setField($o->field)
                    ->setOperator($o->operator)
                    ->setValue($o->value);
            } elseif (is_string($token)) {
                while ($this->operatorStack && $this->precedence[end(
                        $this->operatorStack
                    )] >= $this->precedence[$token] + $this->assoc[$token]) {
                    $this->outputQueue[] = array_pop($this->operatorStack);
                }

                $this->operatorStack[] = $token;
            } elseif (is_array($token)) {
                $op = null;
                if ($this->operatorStack) {
                    $op = array_pop($this->operatorStack);
                }
                $this->compile($token);

                if ($op !== null) {
                    $this->operatorStack[] = $op;
                }
            } else {
                throw new RuntimeException('Unexpected token');
            }
        }
        while ($this->operatorStack) {
            $this->outputQueue[] = array_pop($this->operatorStack);
        }

        return $this;
    }

    /**
     * @param $token
     * @return bool
     */
    protected function isExpression($token): bool
    {
        if (is_object($token)) {
            if (!property_exists($token, 'field')) {
                return false;
            }

            if (!property_exists($token, 'operator')) {
                return false;
            }

            if (!property_exists($token, 'value')) {
                return false;
            }
            return true;
        }

        if (is_array($token)) {
            return array_key_exists('field', $token) && array_key_exists('operator', $token) && array_key_exists(
                    'value',
                    $token
                );
        }

        return false;
    }

    /**
     * Evaluate compiled expression against $data
     *
     * @param DataObject $data
     * @return int
     */
    public function evaluate(DataObject $data): int
    {
        $result = null;

        $numeric = [];

        if (count($this->outputQueue) === 1) {
            if ($this->outputQueue[0] instanceof ExpressionInterface) {
                return $this->outputQueue[0]->evaluate($data);
            }
            // TODO :
            return 0;
        }

        foreach ($this->outputQueue as $operatorOrExpression) {
            if (is_object($operatorOrExpression)) {
                $numeric[] = (bool)$operatorOrExpression->evaluate($data);
            } else {
                /* Please be careful: do not move array_pop inside logical operations; they are shortcircuited! */
                $left = array_pop($numeric);
                $right = array_pop($numeric);
                switch ($operatorOrExpression) {
                    case 'and':
                        $result = $left && $right;
                        break;
                    case 'or':
                        $result = $left || $right;
                        break;
                }

                $numeric[] = $result;
            }
        }

        return (int)$result;
    }

    /**
     * @inheritDoc
     */
    public function reset(): EvaluatorInterface
    {
        $this->outputQueue = [];
        $this->operatorStack = [];
        return $this;
    }
}
