<?php

namespace Velis;

use ArrayIterator;
use ArrayObject;
use DateTime;
use Iterator;
use Laminas\Filter\BaseName;
use Laminas\Filter\Dir;
use Laminas\Filter\RealPath;
use Laminas\Filter\StripNewlines;
use Laminas\Filter\StripTags;
use Laminas\Validator;
use LogicException;
use Phalcon\Support\HelperFactory;
use Velis\App\PhalconVersion;
use Velis\Exception as VelisException;
use Velis\Filter\IpInNetwork;

/**
 * Input Filter
 * @author Olek Procki <olo@velis.pl>
 */
class Filter extends ParameterBag
{
    /**
     * Postgres db max integer value
     * @var int
     */
    public const POSTGRES_MAX_INT = 2147483647;

    /**
     * Postgres db min integer value
     * @var int
     */
    public const POSTGRES_MIN_INT = -2147483648;

    /**
     * check only date yyyy-mm-dd
     */
    public const DATE_STRICT   = 1;

    /**
     * check date or datetime yyyy-mm-dd lub yyyy-mm-dd hh24-mi-ss
     */
    public const DATE_DATETIME = 2;


    /**
     * check all date formats:
     *
     * yyyy-mm-dd
     * yyyy-mm-dd hh24-mi-ss
     * yyyy-mm-dd hh24-mi
     *
     */
    public const DATE_ALL      = 3;

    private static ?HelperFactory $helperFactory = null;

    /**
     * Autofiltering when getting values from filter
     * @var bool
     */
    protected $_autoFiltering = true;

    /**
     * Constructor
     *
     * @param array|ArrayObject $data
     * @param bool $autoFiltering
     */
    public function __construct($data = [], $autoFiltering = true)
    {
        parent::__construct($data);
        $this->setAutoFiltering($autoFiltering);
    }


    /**
     * Sets autofiltering chain
     *
     * @param bool $autoFiltering
     * @return void
     */
    public function setAutoFiltering($autoFiltering = true)
    {
        $this->_autoFiltering = $autoFiltering;
    }


    /**
     * Alias offsetExists
     *
     * @param mixed $key
     * @return bool
     */
    public function keyExists($key)
    {
        return $this->offsetExists($key);
    }


    /**
     * Check if keys exist
     *
     * @param string,string...
     * @return bool
     */
    public function keysExists()
    {
        foreach (func_get_args() as $key) {
            if (!$this->keyExists($key)) {
                return false;
            }
        }
        return true;
    }


    /**
     * Check if elements has values
     *
     * <b>Usage</b>:
     * <code>if ($filter->hasValuesOf('offerId','offerVersion')</code>
     * equals to:
     * <code>if (strlen($filter[offerId]) && strlen($filter[offerVersion]))</code>
     *
     * @param string,string...
     * @return bool
     */
    public function hasValuesOf()
    {
        foreach (func_get_args() as $key) {
            if (!$this->hasValue($key)) {
                return false;
            }
        }
        return true;
    }


    /**
     * Checks if any field has some value
     * @return bool
     */
    public function hasAnyValue()
    {
        foreach (func_get_args() as $key) {
            if ($this->hasValue($key)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks if field has some value
     * @return bool
     */
    public function hasValue(string $key): bool
    {
        if (!isset($this[$key])) {
            return false;
        }

        if (is_string($this[$key]) && mb_strlen($this[$key]) === 0) {
            return false;
        }

        return true;
    }

    /**
     * Return values of fields
     *
     * @param string, string...
     * @return array
     */
    public function getValuesOf()
    {
        $result = array();
        foreach (func_get_args() as $key) {
            $result[$key] = $this[$key];
        }
        return $result;
    }


    /**
     * Retrieves filtered value
     *
     * @param mixed $key
     * @return mixed
     */
    public function get($key, $unfiltered = null)
    {
        if ($this->_autoFiltering) {
            $value = $this->getRaw($key);

            if (is_array($value) || $value instanceof ArrayObject) {
                array_walk_recursive($value, [$this, 'autoFilter'], $unfiltered);
                return $value;
            } elseif (is_scalar($value)) {
                return $this->autoFilter($value, $key, $unfiltered);
            } else {
                return $value;
            }
        }
        return $this->getRaw($key);
    }

    /**
     * @inheritDoc
     */
    public function getIterator(): Iterator
    {
        $iterator = parent::getIterator();
        return new ArrayIterator($this->getArrayCopy(), $iterator->getFlags());
    }

    /**
     * Retrieves value filtered by Washtml filter
     *
     * @param mixed $key
     * @return mixed
     */
    public function getWashed($key, $unfiltered = null)
    {
        $value = $this->getRaw($key);
        if (is_string($value)) {
            $value = trim($value);
        }

        if (is_array($value) || $value instanceof ArrayObject) {
            array_walk_recursive($value, [$this, 'getWashed'], $unfiltered);
            return $value;
        } elseif (is_string($value) && !empty($value)) {
            return $this->washFilter($value, $key, $unfiltered);
        } else {
            return $value;
        }
    }


    /**
     * Returns autofiltered value
     *
     * @param string $key
     * @return mixed
     */
    public function offsetGet($key)
    {
        if (!$this->offsetExists($key)) {
            return null;
        }

        return $this->get($key);
    }


    /**
     * Returns array copy of filter contents
     *
     * @param mixed $unfiltered
     * @return array
     */
    public function getArrayCopy($unfiltered = null): array
    {
        if ($this->_autoFiltering) {
            $copy = parent::getArrayCopy();
            array_walk_recursive($copy, [$this, 'autoFilter'], $unfiltered);
            return $copy;
        } else {
            return parent::getArrayCopy();
        }
    }


    /**
     * Auto-filtering function
     *
     * @param mixed $item
     * @param string $key
     * @param string|array $unfiltered
     * @return mixed|string
     */
    public function autoFilter(&$item, $key, $unfiltered = null)
    {
        if (!is_array($unfiltered)) {
            $unfiltered = [$unfiltered];
        }

        if (!in_array($key, $unfiltered, true)) {
            $item = self::filterXss($item);
        }

        return $item;
    }


    /**
     * Washtml filtering function
     *
     * @param mixed $item
     * @param string $key
     *
     * @param mixed $unfiltered
     */
    public function washFilter(&$item, $key, $unfiltered = null)
    {
        if (!is_array($unfiltered)) {
            $unfiltered = [$unfiltered];
        }
        if (!in_array($key, $unfiltered)) {
            $filter = new Filter\Washtml(['ommit_dump_comments' => true]);
            $item = $filter($item);
        }
        return $item;
    }


    /**
     * Returns filter contents copy without filtering
     * @return array
     */
    public function getRawCopy()
    {
        return parent::getArrayCopy();
    }



    /**
     * Returns only the alphabetic characters in value.
     *
     * @param      mixed $key
     * @return     mixed
     */
    public function getAlpha($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::filterAlpha($this[$key]);
    }


    /**
     * Returns only the alphabetic characters and digits in value.
     *
     * @param      mixed $key
     * @return     mixed
     */
    public function getAlnum($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::filterAlnum($this[$key]);
    }


    /**
     * @param string $key
     * @return DateTime|false
     */
    public function getDateTime($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }

        return self::filterDateTime($this[$key]);
    }


    /**
     * Returns only the digits in value. This differs from getInt().
     *
     * @param      mixed $key
     * @return     mixed
     */
    public function getDigits($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::filterDigits($this[$key]);
    }


    /**
     * Returns dirname(value).
     *
     * @param      mixed $key
     * @return     mixed
     */
    public function getDir($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::filterDir($this[$key]);
    }


    /**
     * Returns (int) value.
     *
     * @param      mixed $key
     * @return     mixed
     */
    public function getInt($key): ?int
    {
        if (!$this->keyExists($key) || is_null($this[$key]) || (is_string($this[$key]) && trim($this[$key]) === '')) {
            return null;
        }
        return self::filterInt($this[$key]);
    }


    /**
     * Returns array of ine values
     *
     * @param mixed $key
     * @return int[]|false
     */
    public function getInts($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::filterInts($this[$key]);
    }


    /**
     * @return array<string>
     */
    public function getAlnums(string $key): array
    {
        if (!$this->keyExists($key)) {
            return [];
        }

        return self::filterAlnums($this[$key]);
    }

    /**
     * Returns (bool) value.
     *
     * @param mixed $key
     * @return ?bool
     */
    public function getBool($key): ?bool
    {
        if (!$this->keyExists($key)) {
            return null;
        }
        return self::filterBool($this[$key]);
    }


    /**
     * Returns filtered ZIP code
     *
     * @param mixed $key
     * @return string
     */
    public function getZipCode($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::filterZipCode($this[$key]);
    }


    /**
     * Returns realpath(value).
     *
     * @param      mixed $key
     * @return     mixed
     */
    public function getPath($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::filterPath($this[$key]);
    }


    /**
     * Returns value.
     *
     * @param mixed $key
     * @return mixed
     */
    public function getRaw($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }

        return parent::offsetGet($key);
    }


    /**
     * Strips sql chars from key
     *
     * @param mixed $key
     * @return Filter|false
     */
    public function stripSQL($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        $this[$key] = self::filterSQL($this[$key]);
        return $this;
    }


    /**
     * @param      mixed $key
     * @return     bool
     */
    public function isAlnum($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::validateAlnum($this[$key]);
    }


    /**
     * @param      mixed $key
     * @return     bool
     */
    public function isAlpha($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::validateAlpha($this[$key]);
    }


    /**
     * @param      mixed $key
     * @param      mixed $min
     * @param      mixed $max
     * @param      bool $inc
     * @return     bool
     */
    public function isBetween($key, $min, $max, $inc = true)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::validateBetween($this[$key], $min, $max, $inc);
    }


    /**
     * @param      mixed $key
     * @param      int $mode
     * @return     bool
     */
    public function isDate($key, $mode = self::DATE_DATETIME)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::validateDate($this[$key], $mode);
    }


    /**
     * @param      mixed $key
     * @return     bool
     */
    public function isDigits($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::validateDigits($this[$key]);
    }


    /**
     * @param      mixed $key
     * @return     bool
     */
    public function isEmail($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::validateEmail($this[$key]);
    }


    /**
     * @param      mixed $key
     * @return     mixed
     */
    public function isFloat($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::validateFloat($this[$key]);
    }


    /**
     * @param      mixed $key
     * @return     bool
     */
    public function isInt($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::validateInt($this[$key]);
    }


    /**
     * @param      mixed $key
     * @return     bool
     */
    public function isIp($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::validateIp($this[$key]);
    }


    /**
     * @param      mixed $key
     * @return     bool
     */
    public function isOneOf($key, array $allowed)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::validateOneOf($this[$key], $allowed);
    }


    /**
     * @param      mixed $key
     * @return     mixed
     */
    public function noTags($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::filterStripTags($this[$key]);
    }


    /**
     * @param      mixed $key
     * @return     mixed
     */
    public function noPath($key)
    {
        if (!$this->keyExists($key)) {
            return false;
        }
        return self::filterBasename($this[$key]);
    }


    /**
     * Returns only the alphabetic characters in value.
     *
     * @param      mixed $value
     * @return     string
     */
    public static function filterAlpha($value)
    {
        return preg_replace('/[^\p{L}]/u', '', $value);
    }


    /**
     * Returns only the alphabetic characters and digits in value.
     *
     * @param      mixed $value
     * @return     string
     */
    public static function filterAlnum($value)
    {
        return preg_replace('/[^\p{L}\p{N}]/u', '', $value);
    }

    /**
     * Returns alphabetic characters and digits in value with separator except first character.
     *
     * @param      mixed $value
     * @return     string
     */
    public static function filterAlnumSeparated($value, string $delimiter = '_')
    {
        return preg_replace('/(?<=^.)[^\p{L}\p{N}]/u', $delimiter, $value);
    }


    public static function filterDateTime($value)
    {
        if (!$value) {
            return null;
        }

        try {
            $dateTime = new DateTime($value);
            $dateTime->setTimezone(App::getTimezone());
        } catch (\Exception $e) {
            return null;
        }

        return $dateTime;
    }


    /**
     * Returns only the digits in value. This differs from getInt().
     *
     * @param      mixed $value
     * @return     string
     */
    public static function filterDigits($value)
    {
        return preg_replace('/[\p{^N}]/u', '', $value);
    }


    /**
     * Returns dirname(value).
     *
     * @param      mixed $value
     * @return     string
     */
    public static function filterDir($value)
    {
        $filter = new Dir();
        return $filter->filter($value);
    }


    /**
     * Returns (int) value.
     *
     * @param mixed $value
     * @return integer
     */
    public static function filterInt($value)
    {
        return (int) ((string) $value);
    }


    /**
     * Returns value or array of values cast to integer
     *
     * @param mixed $value
     * @return int|int[]
     */
    public static function filterInts($value)
    {
        if (is_scalar($value)) {
            return self::filterInt($value);
        }

        if (is_array($value) || ($value instanceof ArrayObject)) {
            return array_map(
                fn ($v) => self::filterInt($v),
                array_filter($value, fn ($v) => is_numeric($v))
            );
        }
        return [];
    }

    /**
     * @return array<string>
     */
    public static function filterAlnums(mixed $value): array
    {
        if (is_scalar($value)) {
            $value = self::filterAlnum($value);
            return $value ? [$value] : [];
        }

        if (is_array($value) || ($value instanceof ArrayObject)) {
            return array_map(
                fn ($v) => self::filterAlnum($v),
                array_filter($value, fn ($v) => is_scalar($v))
            );
        }
        return [];
    }

    /**
     * Returns bool value or null if not a bool
     *
     * @param mixed $value
     * @return ?bool
     */
    public static function filterBool($value): ?bool
    {
        if (is_bool($value)) {
            return $value;
        }

        if ($value === 'true' || $value === '1' || $value === 1) {
            return true;
        }

        if ($value === 'false' || $value === '0' || $value === 0) {
            return false;
        }

        return null;
    }

    /**
     * Returns unique array of int values in string
     *
     * @param string $str
     * @return int[]
     */
    public static function filterIntsFromString($str)
    {
        preg_match_all('!\d+!', $str, $matches);
        return array_unique(
            array_reduce($matches, function ($previous, $current) {
                return array_merge($previous, $current);
            }, [])
        );
    }

    /**
     * Returns realpath(value).
     *
     * @param      mixed $value
     * @return     string
     */
    public static function filterPath($value)
    {
        $filter = new RealPath();
        return $filter->filter($value);
    }


    /**
     * Returns value with all tags removed.
     *
     * @param      mixed $value
     * @return     string
     */
    public static function filterStripTags($value)
    {
        $filter = new StripTags();
        return $filter->filter($value);
    }


    /**
     * Default XSS filter method
     *
     * @param string $value
     * @return string
     */
    public static function filterXss($value)
    {
        if (is_bool($value) || is_null($value) || is_int($value)) {
            return $value;
        }

        $value = str_replace(
            ['>', '<'],
            ['﹥', '﹤'],
            $value
        );

        $filter = new StripTags();
        $filtered = $filter($value);

        if (is_object($filtered) && !method_exists($filtered, '__toString')) {
            error_log(print_r($filtered, true)); //log additional object info which caused error
            VelisException::raise('Object does not have _toString() method, cannot run str_replace()');
            $filtered = '';
        }

        return str_replace(
            ['"', "'", '\\'],
            // replace back-slash with unicode character.
            // @url https://www.compart.com/en/unicode/U+FF3C
            ['”', '’', '＼'],
            $filtered
        );
    }

    /**
     * Returns filtered CSS hex color
     *
     * @param string $value
     * @return string
     */
    public static function filterCssColor($value)
    {
        $colors = [];
        if (preg_match('/#[0-9a-fA-F]{3,6}/', $value, $colors)) {
            return $colors[0];
        } else {
            return '';
        }
    }


    /**
     * Strips SQL special chars: '%' and '_'
     *
     * @param mixed $value
     * @return string
     */
    public static function filterSQL($value)
    {
        $value = str_replace('%', '', $value);

        if (preg_match('/^_+$/', trim($value))) {
            return '';
        }
        return $value;
    }


    /**
     * Returns basename(value).
     *
     * @param      mixed $value
     * @return     string
     */
    public static function filterBasename($value)
    {
        $filter = new BaseName();
        return $filter->filter($value);
    }


    /**
     * @param $value
     * @return string
     */
    public static function filterFilename($value)
    {
        $lastDotPos = strpos($value, '.', -1);
        if ($lastDotPos !== false) {
            [$name, $ext] = str_split($value, $lastDotPos);
            $name = self::filterBasename($name);
            $name = self::filterNewLine($name);
            $name = stripslashes($name);
            $ext = self::filterAlnum($ext);
            return $name . '.' . $ext;
        }

        $value = self::filterBasename($value);
        $value = self::filterNewLine($value);
        $forbiddenCharacters = ['/', '\\', '?', ':', '"', '*', '|', '<', '>'];
        $value = str_replace($forbiddenCharacters, '', $value);

        return stripslashes($value);
    }


    /**
     * @param string $value
     * @return string
     */
    public static function filterNewLine($value)
    {
        $filter = new StripNewlines();
        return $filter->filter($value);
    }


    /**
     * Returns filtered ZIP code
     *
     * @param string $zipCode
     * @return string
     */
    public static function filterZipCode($zipCode)
    {
        $zipParts = array();
        $isZip = preg_match('/^\s*(\d{2})[-]{0,1}(\d{3})\s*$/', $zipCode, $zipParts);
        if ($isZip) {
            return $zipParts[1] . '-' . $zipParts[2];
        } else {
            return null;
        }
    }


    /**
     * Executes camel case to dash filter
     *
     * @param string $camelCaseValue
     * @return string
     */
    public static function filterToDash($camelCaseValue)
    {
        return strtolower(str_replace('_', '-', Output::toSnakeCase($camelCaseValue)));
    }


    /**
     * Convert dashed string to CamelCase string
     *
     * @param string $string
     * @return string
     */
    public static function dashToCamel($string)
    {
        return str_replace(' ', '', ucwords(str_replace('-', ' ', $string)));
    }


    /**
     * Returns acronym
     *
     * @param string $str
     * @return string
     */
    public static function filterAcronym($str, $length = 45)
    {
        $filter = new \Velis\Filter\Acronym();
        return mb_substr($filter->filter($str), 0, $length, 'UTF-8');
    }

    /**
     * Returns text from number
     *
     * @param float $number
     * @param string $currency
     * @return string
     */
    public static function filterNumberToText($number, $currency = null, $langId = null)
    {
        $filter = new \Velis\Filter\NumberToText($langId);
        return $filter->filter($number, $currency);
    }


    /**
     * Parse BBCode
     *
     * @param string $str
     * @return string
     */
    public static function filterBbcode($str)
    {
        $bbcode = \Zend\Markup\Markup::factory('Bbcode');
        return $bbcode->render($str);
    }


    /**
     * Returns string with washed HTML
     *
     * @param string $str
     * @param array $params
     * @return string
     */
    public static function wash($str, $params = [])
    {
        $wash = new Filter\Washtml($params);
        return $wash($str);
    }


    /**
     * Parse textile content
     * @param string $str
     * @param bool $entities
     * @param string $prefix
     * @return string
     */
    public static function filterTextile($str, $entities = false, $prefix = ''): string
    {
        $textile = new Textile();

        if ($prefix) {
            $textile
                ->setImagePrefix($prefix)
                ->setLinkPrefix($prefix)
            ;
        }

        $text = $textile->textileRestricted($str, false, false);
        $text = preg_replace("/javascript:document\.innerHTML='.*'/i", '', $text);

        if ($entities) {
            $match = preg_match_all('~(&#([0-9]+);)~', $text, $matches);

            if ($match) {
                foreach ($matches[1] as $key => $found) {
                    $text = str_replace($matches[1][$key], mb_convert_encoding($matches[1][$key], 'UTF-8', 'HTML-ENTITIES'), $text);
                }
            }
        }

        if (App::$config->layout->emoji) {
            $emojies = [
                ":slightly_smiling_face:" => "&#x1F642",
                ":grinning:" => "&#x1F600",
                ":smile:" => "&#x1F604",
                ":wink:" => "&#x1F609",
                ":relieved:" => "&#x1F60C",
                ":star-struck:" => "&#x1F929",
                ":sunglasses:" => "&#x1F60E",
                ":smirk:" => "&#x1F60F",
                ":innocent:" => "&#x1F607",
                ":face_with_cowboy_hat:" => "&#x1F920",
                ":kissing_heart:" => "&#x1F618",
                ":slightly_frowning_face:" => "&#x1F641",
                ":disappointed_relieved:" => "&#x1F625",
                ":sob:" => "&#x1F62D",
                ":sweat:" => "&#x1F613",
                ":fearful:" => "&#x1F628",
                ":angry:" => "&#x1F620",
                ":triumph:" => "&#x1F624",
                ":scream:" => "&#x1F631",
                ":smiling_imp:" => "&#x1F608",
                ":imp:" => "&#x1F47F",
                ":boom:" => "&#x1F4A5",
                ":green_heart:" => "&#x1F49A",
                ":white_check_mark:" => "&#x2705",
                ":tada:" => "&#x1F389",
                ":ok_hand:" => "&#x1F44C",
                ":muscle:" => "&#x1F4AA",
                ":+1:" => "&#x1F44D",
                ":-1:" => "&#x1F44E",
                ":pray:" => "&#x1F64F",
                ":shrug:" => "&#x1F937",
                ":man-shrugging:" => "&#x1F937&#x1F3FC&#x200D&#x2642&#xFE0F",
                ":woman-shrugging:" => "&#x1F937&#x1F3FC&#x200D&#x2640&#xFE0F",
                ":woman-running:" => "&#x1F3C3&#x1F3FC&#x200D&#x2640&#xFE0F",
                ":man-running:" => "&#x1F3C3&#x1F3FC&#x200D&#x2642&#xFE0F",
                ":female-construction-worker:" => "&#x1F477&#x1F3FC&#x200D&#x2640&#xFE0F",
                ":male-construction-worker:" => "&#x1F477&#x1F3FC&#x200D&#x2642&#xFE0F",
                ":male-mechanic:" => "&#x1F468&#x1F3FC&#x200D&#x1F527",
                ":female-mechanic:" => "&#x1F469&#x1F3FC&#x200D&#x1F527",
                ":heartbeat:" => "&#x1f493",
                ":heart:" => "&#x2764&#xfe0f",
                ":red_heart:" => "&#x2764&#xfe0f",
                ":smiling_cat_heart_eyes:" => "&#128571;",
                ":heart_eyes_cat:" => "&#128571;",
                ":smiling_cat:" => "&#128570;",
            ];

            $match = preg_match_all('~(:([a-z0-9\+\-_]+):)~', $text, $matches);

            if ($match) {
                $text = str_replace(array_keys($emojies), array_values($emojies), $text);
            }
        }

        return $text;
    }


    /**
     * Returns TRUE if every character is alphabetic or a digit,
     * FALSE otherwise.
     *
     * @param      mixed $value
     * @return     bool
     */
    public static function validateAlnum(string $value): bool
    {
        return preg_match_all('/^[a-z0-9]+$/i', $value);
    }


    /**
     * Returns TRUE if every character is alphabetic, FALSE
     * otherwise.
     *
     * @param      mixed $value
     * @return     bool
     */
    public static function validateAlpha(string $value): bool
    {
        return preg_match_all('/^[a-z]+$/i', $value);
    }


    /**
     * Returns TRUE if value is greater than or equal to $min and less
     * than or equal to $max, FALSE otherwise. If $inc is set to
     * FALSE, then the value must be strictly greater than $min and
     * strictly less than $max.
     *
     * @param $value
     * @param mixed $min
     * @param mixed $max
     * @param bool $inc
     * @return bool
     */
    public static function validateBetween($value, $min, $max, $inc = true)
    {
        $validator = new Validator\Between($min, $max, $inc);

        return $validator->isValid($value);
    }


    /**
     * Sprawdze date (dopuszczalne 3 formaty: YYYY-MM-DD lub z czasem: YYYY-MM-DD HH24:MI:SS oraz YYYY-MM-DD HH24:MI)
     *
     * nadanie parametrowi <b>$allowTime</b> wartosci <b>false</b> powoduje ze sprawdzana jest data
     * wylacznie w pierwszym z formatow i nie jest brany pod uwage czas
     *
     * @param mixed $value
     * @param int $mode
     * @return bool
     */
    public static function validateDate($value, $mode = self::DATE_DATETIME): bool
    {
        $validator = new Validator\Date();

        if (strlen(trim($value)) == 10 || $mode == self::DATE_STRICT) {
            return $validator->isValid($value);
        }

        $validTime = false;

        $validSeparator = in_array(substr($value, 10, 1), array(' ', 'T'));

        if (strlen(trim($value)) == 16) {
            if ($mode == self::DATE_ALL) {
                $validTime = preg_match('/^(([0-1][0-9])|(2[0-3])):[0-5][0-9]$/', substr(trim($value), 11, 5));
            } else {
                return false;
            }
        } elseif (strlen(trim($value)) == 19 || strlen(trim($value)) == 25) {
            $validTime = preg_match('/^(([0-1][0-9])|(2[0-3])):[0-5][0-9]:[0-5][0-9]$/', substr(trim($value), 11, 8));
        }

        return $validTime && $validSeparator && $validator->isValid(substr(trim($value), 0, 10));
    }


    /**
     * Returns true if input is valid time string (HH:MM)
     *
     * @param $time string
     * @return bool
     */
    public static function validateTime($time)
    {
        return preg_match("/(2[0-4]|[01][1-9]|10):([0-5][0-9])/", $time);
    }


    /**
     * Returns TRUE if every character is a digit, FALSE otherwise.
     * This is just like isInt(), except there is no upper limit.
     *
     * @param      mixed $value
     * @return     bool
     */
    public static function validateDigits($value)
    {
        $validator = new Validator\Digits();

        return $validator->isValid($value);
    }


    /**
     * Returns TRUE if value is a valid email format, FALSE otherwise.
     *
     * @param      mixed $value
     * @return     bool
     */
    public static function validateEmail($value)
    {
        $validator = new Validator\EmailAddress();

        return $validator->isValid($value);
    }


    /**
     * Returns TRUE if value is a valid float value, FALSE otherwise.
     *
     * @param      mixed $value
     * @return     bool
     */
    public static function validateFloat($value)
    {
        return filter_var($value, FILTER_VALIDATE_FLOAT) !== false;
    }


    /**
     * Returns TRUE if value is a valid integer value, FALSE otherwise.
     *
     * @param      mixed $value
     * @return     bool
     */
    public static function validateInt($value)
    {
        return filter_var($value, FILTER_VALIDATE_INT) !== false;
    }


    /**
     * Returns TRUE if value is a valid postgres integer value, FALSE otherwise.
     *
     * @param      mixed $value
     * @return     bool
     */
    public static function validateDbInt($value)
    {
        return self::validateInt($value) && self::POSTGRES_MIN_INT <= $value && $value <= self::POSTGRES_MAX_INT;
    }


    /**
     * Returns TRUE if value is a valid integer or float value, FALSE otherwise.
     *
     * @param      mixed $value
     * @return     bool
     */
    public static function validateNumber($value)
    {
        if (preg_match('/^[\+\-]?(\d+\\.?\d*|\d*\\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $value)) {
            $tValue = ltrim($value, '+-');
            if (is_string($value) && $tValue[0] === '0' && strlen($tValue) > 1 && $tValue[1] !== '.') {
                return false;
            } elseif ((strpos($value, '.') === false) && ($value > PHP_INT_MAX)) {
                return false;
            }

            return true;
        }

        return false;
    }


    /**
     * Returns TRUE if array has only int values
     *
     * @param array|ArrayObject $value
     * @return bool
     */
    public static function validateInts($value)
    {

        if (is_array($value) || ($value instanceof ArrayObject)) {
            foreach ($value as $val) {
                if (!self::validateInt($val)) {
                    return false;
                }
            }
        } else {
            return false;
        }

        return true;
    }


    /**
     * Returns TRUE if value is a valid IP format, FALSE otherwise.
     *
     * @param      mixed $value
     * @return     bool
     */
    public static function validateIp($value)
    {
        $validator = new Validator\Ip();

        return $validator->isValid($value);
    }


    /**
     * Returns TRUE if value is one of $allowed, FALSE otherwise.
     *
     * @param      mixed $value
     * @param      mixed $allowed
     * @return     bool
     */
    public static function validateOneOf($value, array $allowed)
    {
        $validator = new Validator\InArray($allowed);

        return $validator->isValid($value);
    }


    /**
     * Returns TRUE if value is one of $allowed, FALSE otherwise.
     *
     * @param string $value
     * @return bool
     */
    public static function validateUrl($value)
    {
        // @codingStandardsIgnoreStart
        $pattern = '/([\d\w\-.]+?\.(a[cdefgilmnoqrstuwz]|b[abdefghijmnorstvwyz]|c[acdfghiklmnoruvxyz]|d[ejkmnoz]|e[ceghrst]|f[ijkmnor]|g[abdefghilmnpqrstuwy]|h[kmnrtu]|i[delmnoqrst]|j[emop]|k[eghimnprwyz]|l[abcikrstuvy]|m[acdghklmnopqrstuvwxyz]|n[acefgilopruz]|om|p[aefghklmnrstwy]|qa|r[eouw]|s[abcdeghijklmnortuvyz]|t[cdfghjkmnoprtvwz]|u[augkmsyz]|v[aceginu]|w[fs]|y[etu]|z[amw]|aero|arpa|biz|com|coop|edu|info|int|gov|mil|museum|name|net|org|pro)(\b|\W(?<!&|=)(?!\.\s|\.{3}).*?))(\s|$)/';
        // @codingStandardsIgnoreEnd
        return (bool) preg_match($pattern, $value);
    }


    /**
     * Returns true if $value is valid acronym
     *
     * @param string $value
     * @return bool
     */
    public static function validateAcronym($value, $allowUnderscore = false)
    {
        $pattern = $allowUnderscore ? '/^[\w-]+$/' : '/^[a-z0-9\-]+$/i';
        return (bool) preg_match($pattern, $value);
    }


    /**
     * Returns email address or only the alphabetic characters and digits in value.
     *
     * @param      mixed $value
     * @return     string
     */
    public static function filterIdentityDocNo($value)
    {
        if (!self::validateEmail(trim($value))) {
            $value = self::filterAlnum($value);
        }

        return trim(mb_strtoupper($value));
    }

    /**
     * @param string $haystack
     * @param string $needle
     * @return bool
     */
    public static function startsWith(string $haystack, string $needle): bool
    {
        if (App::getPhalconMajorVersion() >= PhalconVersion::PHALCON5) {
            $helperFactory = self::getHelperFactory();

            return $helperFactory->startsWith($haystack, $needle);
        } else {
            return \Phalcon\Text::startsWith($haystack, $needle);
        }
    }

    /**
     * @return HelperFactory
     */
    private static function getHelperFactory(): HelperFactory
    {
        if (App::getPhalconMajorVersion() < PhalconVersion::PHALCON5) {
            throw new LogicException('This method works only in Phalcon 5.0 version or above');
        }

        if (null === self::$helperFactory) {
            self::$helperFactory = new HelperFactory();
        }

        return self::$helperFactory;
    }

    /**
     * @param string $haystack
     * @param string $needle
     * @return bool
     */
    public static function endsWith(string $haystack, string $needle): bool
    {
        if (App::getPhalconMajorVersion() >= PhalconVersion::PHALCON5) {
            $helperFactory = self::getHelperFactory();

            return $helperFactory->endsWith($haystack, $needle);
        } else {
            return \Phalcon\Text::endsWith($haystack, $needle);
        }
    }

    /**
     * Convert Filter to ParameterBag
     * @return ParameterBag
     */
    public function toParameterBag(): ParameterBag
    {
        $arrayCopy = $this->getArrayCopy();

        return new ParameterBag($arrayCopy);
    }

    /**
     * Parses strings that contains array and returns them as array
     * Example:
     *  ['test' => ["[1,2,3]", "[4,5,6]"]] -> ['test' => [1,2,3,4,5,6]]
     * @return string
     */
    public function parseArrayParameters(): array
    {
        $params = $this->getRawCopy();
        foreach ($params as $key => $value) {
            if (is_array($value)) {
                $tmpValue = [];
                foreach ($value as $v) {
                    $decodedData = json_decode($v, false, 2);
                    if (
                        json_last_error() === JSON_ERROR_NONE
                        && is_array($decodedData)
                        && count($decodedData) > 0
                    ) {
                        $tmpValue = array_merge($tmpValue, $decodedData);
                        continue;
                    }

                    $tmpValue[] = $v;
                }

                if (count($tmpValue) > 0) {
                    $params[$key] = $tmpValue;
                }
            }
        }

        return $params;
    }

    /**
     * Check if an IP address is within a specified network.
     *
     * @param string $ip The IP address to check.
     * @param string $network The network in CIDR notation (e.g., 192.168.1.0/24).
     *
     * @return bool True if the IP address is within the specified network, false otherwise.
     *
     * @example
     *     $ipAddress = '192.168.1.100';
     *     $network = '192.168.1.0/24';
     *     $isInNetwork = ipInNetwork($ipAddress, $network); // true
     */
    public static function ipInNetwork(string $ip, string $network): bool
    {
        if (!self::validateIp($ip)) {
            return false;
        }

        return IpInNetwork::check($ip, $network);
    }
}
