<?php

namespace ReviewX\Nahid\QArray;

use ReviewX\Nahid\QArray\Exceptions\ConditionNotAllowedException;
use ReviewX\Nahid\QArray\Exceptions\InvalidNodeException;
use ReviewX\Nahid\QArray\Exceptions\KeyNotPresentException;
use function ReviewX\DeepCopy\deep_copy;
class Clause
{
    /**
     * store node path
     * @var string|array
     */
    protected $_node = '';
    /**
     * contain prepared data for process
     * @var mixed
     */
    protected $_data;
    /**
     * contains column names
     * @var array
     */
    protected $_select = [];
    /**
     * @var int
     */
    protected $_offset = 0;
    /**
     * @var null
     */
    protected $_take = null;
    /**
     * contains column names for except
     * @var array
     */
    protected $_except = [];
    /**
     * Stores base contents.
     *
     * @var array
     */
    protected $_original = [];
    /**
     * Stores all conditions.
     *
     * @var array
     */
    protected $_conditions = [];
    /**
     * @var bool
     */
    protected $_isProcessed = \false;
    /**
     * @var string
     */
    protected $_traveler = '.';
    /**
     * map all conditions with methods
     * @var array
     */
    protected static $_conditionsMap = ['=' => 'equal', 'eq' => 'equal', '==' => 'strictEqual', 'seq' => 'strictEqual', '!=' => 'notEqual', 'neq' => 'notEqual', '<>' => 'notEqual', '!==' => 'strictNotEqual', 'sneq' => 'strictNotEqual', '>' => 'greaterThan', 'gt' => 'greaterThan', '<' => 'lessThan', 'lt' => 'lessThan', '>=' => 'greaterThanOrEqual', 'gte' => 'greaterThanOrEqual', '<=' => 'lessThanOrEqual', 'lte' => 'lessThanOrEqual', 'in' => 'in', 'notin' => 'notIn', 'inarray' => 'inArray', 'notinarray' => 'notInArray', 'null' => 'isNull', 'notnull' => 'isNotNull', 'exists' => 'exists', 'notexists' => 'notExists', 'startswith' => 'startWith', 'endswith' => 'endWith', 'match' => 'match', 'contains' => 'contains', 'dates' => 'dateEqual', 'instance' => 'instance', 'type' => 'type', 'any' => 'any'];
    /**
     * @param array $props
     * @return $this
     */
    public function fresh($props = [])
    {
        $properties = ['_data' => [], '_original' => [], '_select' => [], '_isProcessed' => \false, '_node' => '', '_except' => [], '_conditions' => [], '_take' => null, '_offset' => 0, '_traveler' => '.'];
        foreach ($properties as $property => $value) {
            if (isset($props[$property])) {
                $value = $props[$property];
            }
            $this->{$property} = $value;
        }
        return $this;
    }
    /**
     * import parsed data from raw json
     *
     * @param array|object $data
     * @return self
     */
    public function collect($data)
    {
        $this->reProcess();
        $this->fresh();
        $this->_data = deep_copy($data);
        $this->_original = deep_copy($data);
        return $this;
    }
    /**
     * Prepare data from desire conditions
     *
     * @return $this
     */
    protected function prepare()
    {
        if ($this->_isProcessed) {
            return $this;
        }
        if (\count($this->_conditions) > 0) {
            $calculatedData = $this->processQuery();
            if (!\is_null($this->_take)) {
                $calculatedData = \array_slice($calculatedData, $this->_offset, $this->_take);
            }
            $this->_data = $calculatedData;
            $this->_isProcessed = \true;
            return $this;
        }
        $this->_isProcessed = \true;
        if (!\is_null($this->_take)) {
            $this->_data = \array_slice($this->_data, $this->_offset, $this->_take);
        }
        $this->_data = $this->getData();
        return $this;
    }
    /**
     * Our system will cache processed data and prevend multiple time processing. If
     * you want to reprocess this method can help you
     *
     * @return $this
     */
    public function reProcess()
    {
        $this->_isProcessed = \false;
        return $this;
    }
    /**
     * Parse object to array
     *
     * @param object $obj
     * @return array|mixed
     */
    protected function objectToArray($obj)
    {
        if (!\is_array($obj) && !\is_object($obj)) {
            return $obj;
        }
        if (\is_array($obj)) {
            return $obj;
        }
        if (\is_object($obj)) {
            $obj = \get_object_vars($obj);
        }
        return \array_map([$this, 'objectToArray'], $obj);
    }
    /**
     * Check given value is multidimensional array
     *
     * @param array $arr
     * @return bool
     */
    protected function isMultiArray($arr)
    {
        if (!\is_array($arr)) {
            return \false;
        }
        \rsort($arr);
        return isset($arr[0]) && \is_array($arr[0]);
    }
    /**
     * Check the given array is a collection
     *
     * @param $array
     * @return bool
     */
    protected function isCollection($array)
    {
        if (!\is_array($array)) {
            return \false;
        }
        return \array_keys($array) === \range(0, \count($array) - 1);
    }
    /**
     * Set node path, where QArray start to prepare
     *
     * @param null $node
     * @return $this
     * @throws InvalidNodeException
     */
    public function from($node = null)
    {
        $this->_isProcessed = \false;
        if (\is_null($node) || $node == '') {
            throw new InvalidNodeException();
        }
        $this->_node = $node;
        return $this;
    }
    /**
     * Taking desire columns from result
     *
     * @param $array
     * @return array
     */
    public function takeColumn($array)
    {
        return $this->selectColumn($this->exceptColumn($array));
    }
    /**
     * selecting specific column
     *
     * @param $array
     * @return array
     */
    protected function selectColumn($array)
    {
        $keys = $this->_select;
        if (\count($keys) == 0) {
            return $array;
        }
        $select = \array_keys($keys);
        $columns = \array_intersect_key($array, \array_flip((array) $select));
        $row = [];
        foreach ($columns as $column => $val) {
            $fn = null;
            if (\array_key_exists($column, $keys)) {
                $fn = $keys[$column];
            }
            if (\is_callable($fn)) {
                $val = \call_user_func_array($fn, [$val, $array]);
            }
            $row[$column] = $val;
        }
        return $row;
    }
    /**
     * selecting specific column
     *
     * @param $array
     * @return array
     */
    protected function exceptColumn($array)
    {
        $keys = $this->_except;
        if (\count($keys) == 0) {
            return $array;
        }
        return \array_diff_key($array, \array_flip((array) $keys));
    }
    /**
     * select desired column
     *
     * @param array $columns
     * @return $this
     */
    public function select($columns = [])
    {
        if (!\is_array($columns)) {
            $columns = \func_get_args();
        }
        $this->setSelect($columns);
        return $this;
    }
    /**
     * setter for select columns
     *
     * @param array $columns
     */
    protected function setSelect($columns = [])
    {
        if (\count($columns) <= 0) {
            return;
        }
        foreach ($columns as $key => $column) {
            if (\is_string($column)) {
                $this->_select[$column] = $key;
            } elseif (\is_callable($column)) {
                $this->_select[$key] = $column;
            } else {
                $this->_select[$column] = $key;
            }
        }
    }
    /**
     * Set offset value for slice of array
     *
     * @param $offset
     * @return $this
     */
    public function offset($offset)
    {
        $this->_offset = $offset;
        return $this;
    }
    /**
     * Set taken value for slice of array
     *
     * @param $take
     * @return $this
     */
    public function take($take)
    {
        $this->_take = $take;
        return $this;
    }
    /**
     * select desired column for except
     *
     * @param array $columns
     * @return $this
     */
    public function except($columns = [])
    {
        if (!\is_array($columns)) {
            $columns = \func_get_args();
        }
        if (\count($columns) > 0) {
            $this->_except = $columns;
        }
        return $this;
    }
    /**
     * Prepare data for result
     *
     * @param mixed $data
     * @param bool $newInstance
     * @return array|mixed
     */
    protected function makeResult($data, $newInstance = \false)
    {
        if (!$newInstance || \is_null($data) || \is_scalar($data) || !\is_array($data)) {
            $this->_data = $data;
            return $this;
        }
        /*
                foreach ($data as $key => $val) {
                    $output[$key] = $this->generateResultData($val);
                }*/
        return $this->instanceWithValue($data, ['_select' => $this->_select, '_except' => $this->_except]);
    }
    /**
     * Create/Copy new instance with given value
     *
     * @param       $value
     * @param array $meta
     * @return mixed
     */
    protected function instanceWithValue($value, $meta = [])
    {
        $instance = new static();
        $instance = $instance->collect($value);
        $instance->fresh($meta);
        return $instance;
    }
    /**
     * Set traveler delimiter
     *
     * @param $delimiter
     * @return $this
     */
    public function setTraveler($delimiter)
    {
        $this->_traveler = $delimiter;
        return $this;
    }
    /**
     * Get data from nested array
     *
     * @param array $data
     * @param string $node
     * @param mixed $default
     * @return mixed
     */
    protected function arrayGet($data, $node, $default = null)
    {
        if (empty($node) || $node == $this->_traveler) {
            return $data;
        }
        if (!$node) {
            return new KeyNotExists();
        }
        if (isset($data[$node])) {
            return $data[$node];
        }
        if (\strpos($node, $this->_traveler) === \false) {
            return $default;
        }
        $items = $data;
        foreach (\explode($this->_traveler, $node) as $segment) {
            if (!\is_array($items) || !isset($items[$segment])) {
                return $default;
            }
            $items =& $items[$segment];
        }
        return $items;
    }
    /**
     * get data from node path
     *
     * @return mixed
     */
    protected function getData()
    {
        return $this->arrayGet($this->_data, $this->_node);
    }
    /**
     * Process the given queries
     *
     * @return array
     * @throws ConditionNotAllowedException
     */
    protected function processQuery()
    {
        $_data = $this->getData();
        $conditions = $this->_conditions;
        /*return array_filter($data, function ($data) use ($conditions) {
              return $this->applyConditions($conditions, $data);
          });*/
        $result = [];
        if (!\is_array($_data)) {
            return null;
        }
        foreach ($_data as $key => $data) {
            $keep = $this->applyConditions($conditions, $data);
            if ($keep) {
                $result[$key] = $this->takeColumn($data);
            }
        }
        return $result;
    }
    /**
     * All the given conditions applied here
     *
     * @param $conditions
     * @param $data
     * @return bool
     * @throws ConditionNotAllowedException
     */
    protected function applyConditions($conditions, $data)
    {
        $decision = \false;
        foreach ($conditions as $cond) {
            $orDecision = \true;
            $this->processEachCondition($cond, $data, $orDecision);
            $decision |= $orDecision;
        }
        return $decision;
    }
    /**
     * Apply every conditions for each row
     *
     * @param $rules
     * @param $data
     * @param $orDecision
     * @return bool|mixed
     * @throws ConditionNotAllowedException
     */
    protected function processEachCondition($rules, $data, &$orDecision)
    {
        if (!\is_array($rules)) {
            return \false;
        }
        $andDecision = \true;
        foreach ($rules as $rule) {
            $params = [];
            $function = null;
            $value = $this->arrayGet($data, $rule['key']);
            if (!\is_callable($rule['condition'])) {
                $function = $this->makeConditionalFunctionFromOperator($rule['condition']);
                $params = [$value, $rule['value']];
            }
            if (\is_callable($rule['condition'])) {
                $function = $rule['condition'];
                $params = [$data];
            }
            if ($value instanceof KeyNotExists) {
                $andDecision = \false;
            }
            $andDecision = \call_user_func_array($function, $params);
            /*
                        if (! $value instanceof KeyNotExists) {
                            $andDecision = call_user_func_array($function, $params);
                        }*/
            //$andDecision = $value instanceof KeyNotExists ? false :  call_user_func_array($function, [$value, $rule['value']]);
            $orDecision &= $andDecision;
        }
        return $orDecision;
    }
    /**
     * Build or generate a function for applies condition from operator
     * @param $condition
     * @return array
     * @throws ConditionNotAllowedException
     */
    protected function makeConditionalFunctionFromOperator($condition)
    {
        if (!isset(self::$_conditionsMap[$condition])) {
            throw new ConditionNotAllowedException("Exception: {$condition} condition not allowed");
        }
        $function = self::$_conditionsMap[$condition];
        if (!\is_callable($function)) {
            if (!\method_exists(ConditionFactory::class, $function)) {
                throw new ConditionNotAllowedException("Exception: {$condition} condition not allowed");
            }
            $function = [ConditionFactory::class, $function];
        }
        return $function;
    }
    /**
     * make WHERE clause
     *
     * @param string $key
     * @param string $condition
     * @param mixed $value
     * @return $this
     */
    public function where($key, $condition = null, $value = null)
    {
        if (!\is_null($condition) && \is_null($value)) {
            $value = $condition;
            $condition = '=';
        }
        if (\count($this->_conditions) < 1) {
            \array_push($this->_conditions, []);
        }
        if (\is_callable($key)) {
            $key($this);
            return $this;
        }
        return $this->makeWhere($key, $condition, $value);
    }
    /**
     * make WHERE clause with OR
     *
     * @param string $key
     * @param string $condition
     * @param mixed $value
     * @return $this
     */
    public function orWhere($key = null, $condition = null, $value = null)
    {
        if (!\is_null($condition) && \is_null($value)) {
            $value = $condition;
            $condition = '=';
        }
        \array_push($this->_conditions, []);
        if (\is_callable($key)) {
            $key($this);
            return $this;
        }
        return $this->makeWhere($key, $condition, $value);
    }
    /**
     * make a callable where condition for custom logic implementation
     *
     * @param callable $fn
     * @return $this
     */
    public function callableWhere(callable $fn)
    {
        if (\count($this->_conditions) < 1) {
            \array_push($this->_conditions, []);
        }
        return $this->makeWhere(null, $fn, null);
    }
    /**
     * make a callable orwhere condition for custom logic implementation
     *
     * @param callable $fn
     * @return $this\
     */
    public function orCallableWhere(callable $fn)
    {
        \array_push($this->_conditions, []);
        $fn($this);
        return $this;
        //return $this->makeWhere(null, $fn, null);
    }
    /**
     * generator for AND and OR where
     *
     * @param string $key
     * @param string $condition
     * @param mixed $value
     * @return $this
     */
    protected function makeWhere($key, $condition = null, $value = null)
    {
        $current = \end($this->_conditions);
        $index = \key($this->_conditions);
        \array_push($current, ['key' => $key, 'condition' => $condition, 'value' => $value]);
        $this->_conditions[$index] = $current;
        $this->_isProcessed = \false;
        return $this;
    }
    /**
     * make WHERE IN clause
     *
     * @param string $key
     * @param array $value
     * @return $this
     */
    public function whereIn($key = null, $value = [])
    {
        $this->where($key, 'in', $value);
        return $this;
    }
    /**
     * make WHERE DATA TYPE clause
     *
     * @param null $key
     * @param $value
     * @return $this
     */
    public function whereDataType($key, $value)
    {
        $this->where($key, 'type', $value);
        return $this;
    }
    /**
     * make WHERE NOT IN clause
     *
     * @param string $key
     * @param mixed $value
     * @return $this
     */
    public function whereNotIn($key = null, $value = [])
    {
        $this->where($key, 'notin', $value);
        return $this;
    }
    /**
     * check the given value are contains in the given array key
     *
     * @param $key
     * @param $value
     * @return $this
     */
    public function whereInArray($key, $value)
    {
        $this->where($key, 'inarray', $value);
        return $this;
    }
    /**
     * make a callable wherenot condition for custom logic implementation
     *
     * @param $key
     * @param $value
     * @return $this
     */
    public function whereNotInArray($key, $value)
    {
        $this->where($key, 'notinarray', $value);
        return $this;
    }
    /**
     * make WHERE NULL clause
     *
     * @param string $key
     * @return $this
     */
    public function whereNull($key = null)
    {
        $this->where($key, 'null', 'null');
        return $this;
    }
    /**
     * make WHERE Boolean clause
     *
     * @param string $key
     * @param mixed $value
     * @return $this
     */
    public function whereBool($key, $value)
    {
        if (\is_bool($value)) {
            $this->where($key, '==', $value);
        }
        return $this;
    }
    /**
     * make WHERE NOT NULL clause
     *
     * @param string $key
     * @return $this
     */
    public function whereNotNull($key)
    {
        $this->where($key, 'notnull', 'null');
        return $this;
    }
    /**
     * Check the given key is exists in row
     *
     * @param $key
     * @return $this
     */
    public function whereExists($key)
    {
        $this->where($key, 'exists', 'null');
        return $this;
    }
    /**
     * Check the given key is not exists in row
     *
     * @param $key
     * @return $this
     */
    public function whereNotExists($key)
    {
        $this->where($key, 'notexists', 'null');
        return $this;
    }
    /**
     * make WHERE START WITH clause
     *
     * @param string $key
     * @param string $value
     * @return $this
     */
    public function whereStartsWith($key, $value)
    {
        $this->where($key, 'startswith', $value);
        return $this;
    }
    /**
     * make WHERE ENDS WITH clause
     *
     * @param string $key
     * @param string $value
     * @return $this
     */
    public function whereEndsWith($key, $value)
    {
        $this->where($key, 'endswith', $value);
        return $this;
    }
    /**
     * make WHERE MATCH clause
     *
     * @param string $key
     * @param string $value
     * @return $this
     */
    public function whereMatch($key, $value)
    {
        $this->where($key, 'match', $value);
        return $this;
    }
    /**
     * make WHERE CONTAINS clause
     *
     * @param string $key
     * @param string $value
     * @return $this
     */
    public function whereContains($key, $value)
    {
        $this->where($key, 'contains', $value);
        return $this;
    }
    /**
     * make WHERE LIKE clause
     *
     * @param string $key
     * @param string $value
     * @return $this
     */
    public function whereLike($key, $value)
    {
        $this->where($key, 'contains', \strtolower($value));
        return $this;
    }
    /**
     * make WHERE DATE clause
     *
     * @param string $key
     * @param string $condition
     * @param string $value
     * @return $this
     */
    public function whereDate($key, $condition, $value = null)
    {
        return $this->callableWhere(function ($row) use($key, $condition, $value) {
            $haystack = isset($row[$key]) ? $row[$key] : null;
            $haystack = \date('Y-m-d', \strtotime($haystack));
            $function = $this->makeConditionalFunctionFromOperator($condition);
            return \call_user_func_array($function, [$haystack, $value]);
        });
    }
    /**
     * make WHERE Instance clause
     *
     * @param string $key
     * @param object|string $object
     * @return $this
     */
    public function whereInstance($key, $object)
    {
        $this->where($key, 'instance', $object);
        return $this;
    }
    /**
     * make WHERE any clause
     *
     * @param string $key
     * @param mixed
     * @return $this
     */
    public function whereAny($key, $value)
    {
        $this->where($key, 'any', $value);
        return $this;
    }
    /**
     * make WHERE any clause
     *
     * @param string $key
     * @param mixed
     * @param mixed
     * @return $this
     */
    public function whereCount($key, $condition, $value = null)
    {
        return $this->where($key, function ($columnValue, $row) use($value, $condition) {
            $count = 0;
            if (\is_array($columnValue)) {
                $count = \count($columnValue);
            }
            $function = $this->makeConditionalFunctionFromOperator($condition);
            return \call_user_func_array($function, [$count, $value]);
        });
    }
    /**
     * make macro for custom where clause
     *
     * @param string $name
     * @param callable $fn
     * @return bool
     */
    public static function macro($name, callable $fn)
    {
        if (!\array_key_exists($name, self::$_conditionsMap)) {
            self::$_conditionsMap[$name] = $fn;
            return \true;
        }
        return \false;
    }
}
