<?php
/**
 * Copyright (C) Baluart.COM - All Rights Reserved
 *
 * @since 1.3.8
 * @author Baluart E.I.R.L.
 * @copyright Copyright (c) 2015 - 2024 Baluart E.I.R.L.
 * @license http://codecanyon.net/licenses/faq Envato marketplace licenses
 * @link https://easyforms.dev/ Easy Forms
 */

namespace app\components\rules;

/**
 * Class RuleEngine
 * @package app\components\rules
 */
class RuleEngine
{

    /**
     * @var array Operators
     */
    public $operators;

    /**
     * @var array Conditions
     */
    public $conditions;

    /**
     * @var array Options
     */
    public $actions;

    /**
     * RuleEngine constructor.
     * @param $rule
     */
    public function __construct($rule)
    {
        $this->conditions = is_string($rule['conditions']) ? json_decode($rule['conditions']) : $rule['conditions'];
        $this->actions = is_string($rule['actions']) ? json_decode($rule['actions']) : $rule['actions'];
        $this->loadOperators();
    }

    /**
     * Load Operators
     */
    public function loadOperators()
    {
        $this->operators = [
            'isPresent' => function ($actual, $target) {
                return !!$actual;
            },
            'isBlank' => function ($actual, $target) {
                return !$actual;
            },
            'equalTo' => function ($actual, $target) {
                $r = $this->parseTag($actual, $target);
                return $r['actual'] === $r['target'];
            },
            'notEqualTo' => function ($actual, $target) {
                return "" . $actual !== "" . $target;
            },
            'greaterThan' => function ($actual, $target) {
                return floatval($actual) > floatval($target);
            },
            'greaterThanEqual' => function ($actual, $target) {
                return floatval($actual) >= floatval($target);
            },
            'lessThan' => function ($actual, $target) {
                return floatval($actual) < floatval($target);
            },
            'lessThanEqual' => function ($actual, $target) {
                return floatval($actual) <= floatval($target);
            },
            'isIn' => function ($actual, $target) {
                $t = explode('|', $target);
                if (count($t) > 0) {
                    for ($i = 0; $i < count($t); $i++) {
                        if ($actual && false !== strpos($actual, $t[$i])) {
                            return true;
                        }
                    }
                }
                return $actual && false !== strpos($actual, $target);
            },
            'isNotIn' => function ($actual, $target) {
                $t = explode('|', $target);
                if (count($t) > 0) {
                    for ($i = 0; $i < count($t); $i++) {
                        if ($actual && false === strpos($actual, $t[$i])) {
                            return true;
                        }
                    }
                }
                return $actual && false === strpos($actual, $target);
            },
            'startsWith' => function ($actual, $target) {
                return $actual && strpos($actual, $target) === 0;
            },
            'endsWith' => function ($actual, $target) {
                $actualLen = strlen($actual);
                $targetLen = strlen($target);
                if ($targetLen > $actualLen) {
                    return false;
                }    
                return $actual && strpos($actual, $target, $actualLen - $targetLen) !== false;
            },
            'isBefore' => function ($actual, $target) {
                $r = $this->parseTag($actual, $target);
                return strtotime($r['actual']) < strtotime($r['target']);
            },
            'isAfter' => function ($actual, $target) {
                $r = $this->parseTag($actual, $target);
                return strtotime($r['actual']) > strtotime($r['target']);
            },
            'isChecked' => function ($actual, $target) {
                return !!$actual;
            },
            'isNotChecked' => function ($actual, $target) {
                return !$actual;
            },
            'hasFileSelected' => function ($actual, $target) {
                return !!$actual;
            },
            'hasNoFileSelected' => function ($actual, $target) {
                return !$actual;
            },
            'hasBeenClicked' => function ($actual, $target) {
                return !!$actual;
            },
            'hasBeenSubmitted' => function ($actual, $target) {
                return true;
            }
        ];
    }

    /**
     * Run Rule Builder
     *
     * @param $conditionsAdapter
     * @param $actionsAdapter
     * @param $oppositeAdapter
     */
    public function run($conditionsAdapter, $actionsAdapter, $oppositeAdapter)
    {
        if ($this->matches($conditionsAdapter)) {
            $this->runActions($actionsAdapter);
        } else {
            $this->runOppositeActions($oppositeAdapter, $actionsAdapter);
        }
    }

    /**
     * Run Actions
     *
     * @param $actionsAdapter
     */
    public function runActions ($actionsAdapter) {
        for($i = 0; $i < count($this->actions); $i++) {
            $actionData = $this->actions[$i];
            $actionName = $actionData->value;
            $actionFunction = isset($actionsAdapter[$actionName]) ? $actionsAdapter[$actionName] : null;
            if($actionFunction && is_callable($actionFunction)) {
                $actionFunction(new Finder($actionData));
            }
        }
    }

    /**
     * Run OppositeActions
     *
     * @param $oppositeAdapter
     * @param $actionsAdapter
     */
    public function runOppositeActions ($oppositeAdapter, $actionsAdapter) {
        for($i = 0; $i < count($this->actions); $i++) {
            $actionData = $this->actions[$i];
            if (isset($oppositeAdapter[$actionData->value])) {
                $actionName = $oppositeAdapter[$actionData->value];
                $actionFunction = $actionsAdapter[$actionName];
                if($actionFunction && is_callable($actionFunction)) {
                    $actionFunction(new Finder($actionData));
                }
            }
        }
    }

    /**
     * Matches the Conditions
     *
     * @param $conditionsAdapter
     * @return bool
     */
    public function matches($conditionsAdapter)
    {
        return $this->handleNode($this->conditions, $conditionsAdapter);
    }

    /**
     * Handle a Node
     *
     * @param $node
     * @param $adapter
     * @return bool
     */
    public function handleNode($node, $adapter)
    {
        if (isset($node->all) || isset($node->any) || isset($node->none)) {
            return $this->handleConditionalNode($node, $adapter);
        } else {
            return $this->handleRuleNode($node, $adapter);
        }
    }

    /**
     * Handle a Conditional Node
     *
     * @param $node
     * @param $adapter
     * @return bool
     */
    public function handleConditionalNode($node, $adapter)
    {
        // Type of Condition
        $isAll = isset($node->all) && !!$node->all ? true : false;
        $isAny = isset($node->any) && !!$node->any ? true : false;
        $isNone = isset($node->none) && !!$node->none ? true : false;

        $nodes = [];

        if ($isAll) {
            $nodes = $node->all;
        } else if ($isAny) {
            $nodes = $node->any;
        } else if ($isNone) {
            $nodes = $node->none;
        }

        // If no conditions
        if (count($nodes) === 0) {
            return true;
        }

        $result = false;

        foreach ($nodes as $node) {
            if ($node) {
                $result = $this->handleNode($node, $adapter);
                if ($isAll) {
                    if ($result) {
                        continue;
                    } else {
                        break;
                    }
                } elseif ($isAny) {
                    if ($result) {
                        break;
                    } else {
                        continue;
                    }
                } elseif ($isNone) {
                    if ($result) {
                        return false;
                    } else {
                        $result = true; // Invert result
                        continue;
                    }
                }
            } else {
                return $isNone ? true : $isAll;
            }
        }

        return $result;
    }

    /**
     * Handle a Rule Node
     *
     * @param $node
     * @param $adapter
     * @return bool
     */
    public function handleRuleNode($node, $adapter)
    {
        $value = isset($node->name, $adapter[$node->name]) ? $adapter[$node->name] : null;
        if (isset($value) && is_callable($value)) {
            $value = $value();
        }
        return $this->compareValues($value, $node->operator ?? null, $node->value ?? null);
    }

    /**
     * Compare values
     *
     * @param $actual
     * @param $operator
     * @param $value
     * @return bool
     */
    public function compareValues($actual, $operator, $value): bool
    {
        $operatorFunction = $this->operators[$operator] ?? null;
        if ($operatorFunction) {
            $actual = is_array($actual) ? implode('|', $actual) : $actual;
            return $operatorFunction($actual, $value);
        }
        return false;
    }

    /**
     * Parse Tag in Target Value
     *
     * @param $actual
     * @param $target
     * @return array
     */
    public function parseTag($actual, $target) {
        preg_match( '/{(.*?)(?::(.+?))?}/', $target, $matches);
        if (isset($matches[1]) && trim($actual) !== "") {
            $tag = strtolower($matches[1]);
            $modifier = isset($matches[2]) ? (int) $matches[2] : null;
            switch ($tag) {
                case 'today':
                    $time = $modifier ? strtotime($modifier . " days") : time();
                    $target = date('Y-m-d', $time);
                    $actual = substr($actual, 0, 10);
                    break;
                case 'monday':
                case 'tuesday':
                case 'wednesday':
                case 'thursday':
                case 'friday':
                case 'saturday':
                case 'sunday':
                    $days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
                    $target = array_search($tag, $days);
                    $actual = (int) date('w', strtotime($actual));
                break;
            }
        }
        return [
            'actual' => $actual,
            'target' => $target,
        ];
    }
}