<?php

namespace FCA\StockApi\Collection\Filter\Builder;

use FCA\StockApi\Collection\Sort\Sort;
use FCA\StockApi\Exception\ApiException;

class Validator
{
    public static $operators = ['$lte', '$lt', '$gte', '$gt', '$in', '!$in', '$ne'];
    /**
     * @param $field
     * @param $value
     * @return bool
     */
    public static function isValidValue($field, $value)
    {
        if ($value === 'geometry') {
            if (in_array('geometry', ValueTypes::getValueMap()[$field])) {
                return true;
            }
            return false;
        }

        if (in_array(gettype($value), ValueTypes::getValueMap()[$field])) {
            return true;
        }

        return false;
    }

    /**
     * @param $field
     * @return bool
     */
    public static function isValidField($field)
    {
        try {
            $reflect = new \ReflectionClass(Fields::class);
            $constants = $reflect->getConstants();
        } catch (\ReflectionException $e) {
            return false;
        }

        foreach ($constants as $constant) {
            if ($constant === $field) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param $filter
     * @return bool
     * @throws ApiException
     */
    public static function validateFilter($filter)
    {
        if (is_array($filter) and isset($filter['$and']) and count($filter) === 1) {
            foreach ($filter['$and'] as $f) {
                self::validateFilter($f);
            }
        } elseif (is_array($filter) and isset($filter['$and']) and count($filter) !== 1) {
            throw new ApiException('Invalid filter: ' . json_encode($filter));
        } elseif (is_array($filter) and isset($filter['$or']) and count($filter) === 1) {
            foreach ($filter['$or'] as $f) {
                self::validateFilter($f);
            }
        } elseif (is_array($filter) and isset($filter['$or']) and count($filter) !== 1) {
            throw new ApiException('Invalid filter: ' . json_encode($filter));
        } else {
            self::validateExpression($filter);
        }

        return true;
    }

    /**
     * @param $filter
     * @throws ApiException
     */
    public static function validateExpression($filter)
    {
        if (is_array($filter)) {
            foreach ($filter as $key => $value) {
                if (is_int($key)) {
                    self::validateFilter($value);
                    continue;
                }

                if (!self::isValidField($key)) {
                    throw new ApiException('Invalid field "' . $key . '"');
                }

                if (!is_array($value) && !self::isValidValue($key, $value)) {
                    throw new ApiException('Invalid value type ("' . $key . '")');
                }

                if (is_array($value)) {
                    if (count($value) === 1) {
                        if (isset($value['$not'])) {
                            self::validateExpression([$key => $value['$not']]);
                        } elseif (isset($value['$near'])) {
                            if (self::isValidValue($key, 'geometry')) {
                                self::validateNearFilter($value['$near']);
                            } else {
                                throw new ApiException('Invalid filter: ' . json_encode($filter));
                            }
                        } else {
                            self::validatePartialExpression($value, $key);
                        }
                    } else {
                        throw new ApiException('Invalid filter: ' . json_encode($value));
                    }
                }
            }
        } else {
            if ($filter != []) {
                throw new ApiException('Invalid filter: ' . json_encode($filter));
            }
        }
    }

    /**
     * @param $filter
     * @param $key
     * @throws ApiException
     */
    public static function validatePartialExpression($filter, $key)
    {
        $operator = array_keys($filter)[0];
        $val = $filter[$operator];

        if (!in_array($operator, self::$operators)) {
            throw new ApiException('Invalid operator: ' . $operator);
        }

        if ($operator === '$in' or $operator === '$nin') {
            if (!is_array($val)) {
                throw new ApiException('Value must be an array!');
            }

            foreach ($val as $val2) {
                if (!self::isValidValue($key, $val2)) {
                    throw new ApiException('Invalid value type ("' . $key . '")');
                }
            }
        } else {
            if (!self::isValidValue($key, $val)) {
                throw new ApiException('Invalid value type ("' . $key . '")');
            }
        }
    }

    /**
     * @param $filter
     * @throws ApiException
     */
    protected static function validateNearFilter($filter)
    {
        if (count($filter) === 2
            and isset($filter['$geometry'])
            and isset($filter['$maxDistance'])
        ) {
            if (!is_int($filter['$maxDistance'])) {
                throw new ApiException('Invalid filter: minDistance must be integer');
            }
            if (!is_array($filter['$geometry'])) {
                throw new ApiException('Invalid filter: $geometry must be array');
            }
            $geo = $filter['$geometry'];
            if (!isset($geo['type']) or $geo['type'] != "Point") {
                throw new ApiException('Unsupported geometryType: ' . $filter['$near']['$geometry']['type']);
            }
            if (!isset($geo['coordinates']) or !is_array($geo['coordinates'])) {
                throw new ApiException('Undefined Geo Coordinates');
            }
            if (!is_float($geo['coordinates'][0]) or !is_float($geo['coordinates'][1]) or count($geo['coordinates']) > 2) {
                throw new ApiException('Invalid Geo Coordinates');
            }
        } else {
            throw new ApiException('Invalid filter: ' . json_encode($filter));
        }
    }

    public static function isValidSortArray($sort)
    {
        if (!is_array($sort)) {
            return false;
        }

        foreach ($sort as $key => $value) {
            if (!self::isValidField($key) or ($value !== Sort::ASC and $value !== Sort::DESC)) {
                return false;
            }
        }

        return true;
    }
}
