<?php

namespace Velis;

use DateInterval;
use DateTime;
use DateTimeZone;
use DOMDocument;
use Exception;
use LogicException;
use NumberFormatter;
use Phalcon\Support\HelperFactory;
use RuntimeException;
use Velis\App\PhalconVersion;
use Velis\Filter\TruncateHtml;
use XmlWriter;

/**
 * Static class for string/output operations
 * @author Olek Procki <olo@velis.pl>
 */
class Output
{
    const ACRO_CAMELIZED   = 'Camelized';
    const ACRO_UNDERSCORE  = 'Underscore';

    /**
     * How objects should be encoded -- arrays or as stdClass. JSON_ARRAY is 1
     * so that it is a boolean true value, allowing it to be used with
     * ext/json's functions.
     */
    const JSON_ARRAY  = 1;
    const JSON_OBJECT = 0;

    /**
     * @var NumberFormatter
     */
    private static $_numberFormatter;

    private static ?HelperFactory $helperFactory = null;

    /**
     * Cuts string, but keep whole words
     *
     * @param string $str - input string
     * @param int $max - max length
     *
     * @return string
     */
    public static function cut($str, $max)
    {
        $len = mb_strlen($str);

        if ($len > $max) {
            if (strpos(mb_substr($str, 0, $max - 3), " ") === false) {
                return mb_substr($str, 0, $max - 3) . "...";
            } else {
                $shortName = '';
                $words     = explode(' ', $str);

                $i = 0;

                while (mb_strlen($shortName . ' ' . $words[$i] . '...') < $max) {
                    if ($i > 0) {
                        $shortName .= ' ';
                    }
                    $shortName .= $words[$i++];
                }
                $shortName .= '...';

                return $shortName;
            }
        } else {
            return $str;
        }
    }

    /**
     * Cuts html string, but keep whole words
     *
     * @param string $html
     * @param int $maxLength
     */
    public static function cutHtml($html, $maxLength)
    {
        $truncateHtml = new TruncateHtml($maxLength);

        return $truncateHtml->truncate($html);
    }

    /**
     * Returns correct format time for a location
     * @param string $dateString
     * @return string
     * @throws Exception
     */
    public static function time($dateString)
    {
        $date = new DateTime($dateString);

        if (Lang::getLanguage() == 'en') {
            return $date->format('g:i a');
        } else {
            return $date->format("H:i");
        }
    }


    /**
     * Returns correct format date for a location
     * @param string $dateString
     * @param bool   $year
     * @return string
     * @throws Exception
     */
    public static function date($dateString, $year = true)
    {
        $date = new DateTime($dateString);

        if (Lang::getLanguage() == 'en') {
            if (App::$config->db->datestyle->{Lang::getLanguage()}) {
                if ($year) {
                    return $date->format('m/d/Y');
                } else {
                    return $date->format('m/d');
                }
            } else {
                if ($year) {
                    return $date->format('d.m.Y');
                } else {
                    return $date->format('d.m');
                }
            }
        } else {
            return $date->format('d.m.Y');
        }
    }


    /**
     * Returns path to dojo
     * @return string
     */
    public static function dojoPath()
    {
        $path = App::$config->layout->dojoSource . '/dojo/dojo.js';

        if (App::devMode() && App::$config->layout->dojoSource != '/build' && App::$config->layout->dojoSource != '/res/js/dojo-toolkit/src') {
            $path .= '.uncompressed.js';
        }

        return $path;
    }


    /**
     * Changes links info <a> tags
     *
     * @param string      $string
     * @param string|null $projectId
     * @return string
     */
    public static function changeLinks($string, $projectId = null, $ticketModuleUrl = '/zgloszenia')
    {
        $string = preg_replace(
            [
                '#[^":]((ftp|http|https)://[^\n\r<()" ,]+)([^"<]?)#',
                "/([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,6})/i",
                "/(?<!style=\"color:)\s#([1-9][0-9]*)/",
            ],
            [
                ' <a data-instant="false" href="$1">$1</a>$3',
                '<a data-instant="false" href="mailto:$1">$1</a>',
                ' <a href="' . App::getBaseUrl() . $ticketModuleUrl . '/$1">#$1</a>',
            ],
            $string
        );

        $string = preg_replace('/<(p|li)\s+<a/', '<$1><a', $string);
        $string = preg_replace('~\."~', '"', $string);
        $string = preg_replace('~\.</a>~', '</a>.', $string);

        $matches = array();
        preg_match_all("/\[\[(.+)\]\]/", $string, $matches);

        foreach (array_unique($matches[1]) as $match) {
            if ($projectId) {
                $string = str_replace('[[' . $match . ']]', '<a href="/wiki/' . $projectId . '/' . urlencode(strip_tags($match)) . '">' . $match . '</a>', $string);
            } else {
                $string = str_replace('[[' . $match . ']]', '<a href="/wiki/' . urlencode(strip_tags($match)) . '">' . $match . '</a>', $string);
            }
        }

        return $string;
    }


    /**
     * Removes duplicates from string
     *
     * @param string $str
     * @return string
     */
    public static function removeDuplicates($str)
    {
        $arr = array();
        for ($i = 0; $i < strlen($str); $i++) {
            $arr[$str[$i]] = $str[$i];
        }

        return implode(null, $arr);
    }


    /**
     * Strips polish chars
     * w kodowaniu iso-8859-2 na litery bez ogonków
     *
     * @param string $str
     * @return string
     */
    public static function stripPolishChars($str)
    {
        return iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str);
    }


    /**
     * Returns formated data size (MB,KB,bytes)
     *
     * @param int $size liczba bajtów
     * @return string
     */
    public static function formatSize($size)
    {
        $numberFormatter = self::getNumberFormatter();

        if ($size < 1024) {
            if ($size != 1) {
                $suffix = 'ów';
            } elseif (($size % 10) >= 2 && ($size % 10) <= 4 && (($size / 10) % 10) != 1) {
                $suffix = 'y';
            } else {
                $suffix = '';
            }

            return $numberFormatter->format($size, NumberFormatter::TYPE_INT32) . ' bajt' . $suffix;
        }

        if ($size < 1048576) {
            return $numberFormatter->format(round($size / 1024, 2), NumberFormatter::TYPE_DOUBLE) . ' KB';
        }

        return $numberFormatter->format(round($size / 1048576, 2), NumberFormatter::TYPE_DOUBLE) . ' MB';
    }


    /**
     * Returns formatted distance(km, m)
     *
     * @param int  $distance
     * @param bool $round (to every 200m)
     * @return string
     */
    public static function formatDistance($distance, $round = false)
    {
        $distance = $distance * 1000;

        if ($round) {
            $distance = round($distance - $distance % 200);
            if ($distance == 0) {
                $distance = 200;
            }
        }

        if ($distance >= 1000) {
            $distance = round($distance / 1000, 1);
            $suffix = 'km';
        } else {
            $suffix = 'm';
        }

        return $distance . ' ' . $suffix;
    }


    /**
     * Returns time duration in specified format
     *
     * @param float $time seconds
     * @param string|null $format
     * @return string
     * @throws Exception
     */
    public static function formatTime(float $time, string $format = null): string
    {
        $time = abs($time);

        $hours = (int) ($time / 3600);
        $minutes = (int) (($time - $hours * 3600) / 60);
        $seconds = (int) ($time - $hours * 3600 - $minutes * 60);

        if ($format) {
            $interval = new DateInterval(sprintf('PT%dH%dM%dS', $hours, $minutes, $seconds));

            return $interval->format($format);
        }

        if ($time >= 3600) {
            return sprintf("%d %s %d %s", $hours, Lang::get('GENERAL_HOUR'), $minutes, Lang::get('GENERAL_MINUTES'));
        }

        return sprintf("%d %s", $minutes, Lang::get('GENERAL_MINUTES'));
    }


    /**
     * Returns formatted date value
     *
     * @param string $date
     * @param bool $includeTime
     * @param bool $includeSeconds
     * @return string
     */
    public static function formatDate($date, $includeTime = false, $includeSeconds = false)
    {
        if (!$date || $date == '') {
            return null;
        }

        if ($includeTime) {
            if ($includeSeconds) {
                return strftime("%x %X", strtotime($date));
            }

            return trim(preg_replace('/:\d{2}($|\s)/', ' ', strftime("%x %X", strtotime($date))));
        }

        return strftime("%x", strtotime($date));
    }


    /**
     * @param string $dateString
     * @return string
     * @throws Exception
     */
    public static function formatDateInWords($dateString)
    {
        if (!$dateString) {
            return '';
        }

        $date = new DateTime($dateString);

        $day = $date->format('j');
        $month = self::getMonth($dateString);
        $year = $date->format('Y');

        switch (Lang::getLanguage()) {
            case 'de':
                return sprintf('%s. %s %s', $day, $month, $year);
            case 'en':
                $ordinalNumberSuffix = self::getNumberSuffix($day);
                switch (setlocale(LC_ALL, '0')) {
                    case 'en_US':
                        return sprintf('%s %s%s, %s', $month, $day, $ordinalNumberSuffix, $year);
                    case 'en_GB':
                    default:
                        return sprintf('%s%s %s %s', $day, $ordinalNumberSuffix, $month, $year);
                }
                break;
            case 'es':
                return sprintf('%s de %s de %s', $day, $month, $year);
            case 'hu':
                return sprintf('%s. %s %s.', $year, $month, $day);
            case 'ja':
                return sprintf('%s日 %s %s', $day, $month, $year);
            default:
                return sprintf('%s %s %s', $day, $month, $year);
        }
    }


    /**
     * Returns formatted hour value
     *
     * @param string $hour
     * @param boolean $includeSeconds
     * @return string
     */
    public static function formatHour($hour, $includeSeconds = false)
    {
        if (!$hour) {
            return '';
        }
        if ($includeSeconds) {
            return strftime("%X", strtotime($hour));
        }

        return trim(preg_replace('/:\d{2}($|\s)/', ' ', strftime("%X", strtotime($hour))));
    }


    /**
     * Returns formatted money value
     *
     * @param float  $money
     * @param bool   $includeZero
     * @param string $currency
     *
     * @return string
     */
    public static function formatMoney($money, $includeZero = false, $currency = null)
    {

        if ($money || $includeZero) {
            if (App::$config->settings->disableNumberFormatter) {
                return self::_legacyFormatMoney($money, $currency);
            }

            $locale = setlocale(LC_NUMERIC, '0');

            if ($currency) {
                $numberFormatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
                $formattedValue = $numberFormatter->formatCurrency($money, $currency);

                if (false === $formattedValue) {
                    return self::formatFloat($money, 2) . ' ' . $currency;
                }

                return $formattedValue;
            } else {
                return self::formatFloat($money, 2);
            }
        }

        return false;
    }


    /**
     * Returns formatted money value (for ICU version < 49)
     *
     * @param float  $money
     * @param string $currency
     *
     * @return string
     */
    protected static function _legacyFormatMoney($money, $currency = null)
    {
        $locale = localeconv();

        if (!$locale['thousands_sep']) {
            if ($locale['decimal_point'] == ',') {
                $locale['thousands_sep'] = ' ';
            } else if ($locale['decimal_point'] == '.') {
                $locale['thousands_sep'] = ',';
            }
        }
        $string = number_format($money, 2, $locale['decimal_point'], $locale['thousands_sep']);

        if ($currency) {
            $string .= ' ' . $currency;
        }

        return $string;
    }


    /**
     * Returns numeric value
     *
     * @param mixed $value
     * @return string
     */
    public static function toNumeric($value)
    {
        return self::toDbNumeric((float) $value);
    }


    /**
     * Returns numeric value for database
     *
     * @param mixed $value
     * @return string
     */
    public static function toDbNumeric($value)
    {
        if (Filter::validateNumber($value)) {
            return $value;
        }

        $locale = localeconv();

        $parts = explode($locale['decimal_point'], $value);

        // Replace any non-numeric char to '' (empty string)
        $parts[0] = preg_replace('/[^0-9]/', '', $parts[0]);

        if (isset($parts[1])) {
            $parts[1] = preg_replace('/[^0-9]/', '', $parts[1]);
            $value = trim($parts[0]) . '.' . trim($parts[1]);
        } else {
            $value = trim($parts[0]);
        }

        if (Filter::validateNumber($value)) {
            return $value;
        }

        return false;
    }


    /**
     * Returns formatted vat number
     *
     * @param int $vatNo
     * @return string
     */
    public static function formatVatNumber($vatNo)
    {
        return substr($vatNo, 0, 3) . '-' . substr($vatNo, 3, 3) . '-' . substr($vatNo, 6, 2) . '-' . substr($vatNo, 8, 2);
    }


    /**
     * Returns valid acronym from entered $text
     *
     * @param string $text
     * @param string $acronymType
     * @return string
     */
    public static function acronym($text, $acronymType = self::ACRO_CAMELIZED)
    {
        $text = Output::stripPolishChars($text);

        if ($acronymType == self::ACRO_CAMELIZED) {
            return Output::toPascalCase($text);
        } else {
            return strtoupper(
                preg_replace(
                    ['/\s+/', '/[^A-Z0-9]/i'],
                    ['_', ''],
                    $text
                )
            );
        }
    }


    /**
     * Returns date color depending on delay
     *
     * @param string $date
     * @param string $dateCompareTo
     *
     * @return string
     */
    public static function getDateColor($date, $dateCompareTo = null)
    {
        $dateFormat = 'Y-m-d';

        if (strlen($date) > 10) {
            $dateFormat .= ' H:i';
        }

        if ($dateCompareTo == null) {
            $dateCompareTo = date($dateFormat);
        }

        if (strlen($dateCompareTo) > strlen($date)) {
            $dateCompareTo = substr($dateCompareTo, 0, strlen($date));
        } elseif (strlen($date) > strlen($dateCompareTo)) {
            $date = substr($date, 0, strlen($dateCompareTo));
        }


        if ($date < $dateCompareTo) {
            return 'red';
        } elseif (substr($date, 0, 10) == date('Y-m-d')) {
            return 'orange';
        } else {
            return 'green';
        }
    }


    /**
     * Returns nice date name
     *
     * @param string $date
     * @param bool   $time
     * @return string
     * @throws Exception
     */
    public static function getDateName($date, $time = false)
    {
        if (date('Y-m-d', strtotime('-1 day')) == date('Y-m-d', strtotime($date))) {
            $str = Lang::get('GENERAL_YESTERDAY');
        } elseif (date('Y-m-d') == date('Y-m-d', strtotime($date))) {
            $str = Lang::get('GENERAL_TODAY');
        } elseif (date('Y-m-d', strtotime('+1 day')) == date('Y-m-d', strtotime($date))) {
            $str = Lang::get('GENERAL_TOMORROW');
        } elseif (date('Y-W', strtotime($date)) == date('Y-W')) {
            $str = self::getDay(date('N', strtotime($date)));
        } elseif (date('Y', strtotime($date)) == date('Y')) {
            $str = self::date($date, false);
        } else {
            $str = self::date($date);
        }

        if ($time) {
            $str .= ' ' . self::time($date);
        }
        return trim($str);
    }


    /**
     * Returns default styles for XLS header
     * @return array
     */
    public static function getXlsHeadStyle()
    {
        return [
            'font' => [
                'bold' => true,
            ],
            'alignment' => [
                'horizontal' => Xls::HORIZONTAL_CENTER,
            ],
            'borders' => [
                'bottom' => [
                    'borderStyle' => Xls::BORDER_THIN,
                ],
                'right' => [
                    'borderStyle' => Xls::BORDER_THIN,
                ],
                'left' => [
                    'borderStyle' => Xls::BORDER_THIN,
                ],
            ],
            'fill' => [
                'fillType' => Xls::FILL_SOLID,
                'color' => [
                    'rgb' => 'DDDDDD',
                ],
            ],
        ];
    }


    /**
     * Decode escaped entities used by known XSS exploits.
     * See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples
     *
     * @param string CSS content to decode
     *
     * @return string Decoded string
     */
    public static function xssEntityDecode($content)
    {
        $out = html_entity_decode(html_entity_decode($content));
        $out = preg_replace_callback(
            '/\\\([0-9a-f]{4})/i',
            function ($matches) {
                return chr(hexdec($matches[1]));
            },
            $out
        );
        $out = preg_replace('#/\*.*\*/#Ums', '', $out);

        return $out;
    }


    /**
     * Get month name
     * @param string $dateString
     * @param bool $applyDeclension return month name in genitive instead of nominative (only in Polish)
     * @return string
     */
    public static function getMonth($dateString = null, $applyDeclension = true)
    {
        $currentLocale = setlocale(LC_TIME, '0');
        $langCode = Lang::getLanguage();

        if ($langCode != substr($currentLocale, 0, 2)) {
            $locale = Lang::getDefaultLocaleForLang($langCode);
            setlocale(LC_TIME, $locale);
        }

        if ($langCode == 'pl') {
            $nominative = [
                'styczeń',
                'luty',
                'marzec',
                'kwiecień',
                'maj',
                'czerwiec',
                'lipiec',
                'sierpień',
                'wrzesień',
                'październik',
                'listopad',
                'grudzień',
            ];

            $genitive = [
                'stycznia',
                'lutego',
                'marca',
                'kwietnia',
                'maja',
                'czerwca',
                'lipca',
                'sierpnia',
                'września',
                'października',
                'listopada',
                'grudnia',
            ];

            $date = new DateTime($dateString);
            $monthNumber = (int) $date->format('m');

            if ($applyDeclension) {
                return $genitive[$monthNumber - 1];
            } else {
                return $nominative[$monthNumber - 1];
            }
        } else {
            $month = strftime("%B", $dateString ? strtotime($dateString) : null);

            if (!mb_check_encoding($month)) {
                $month = mb_convert_encoding($month, 'utf-8', ['iso-8859-2', 'iso-8859-1']);
            }
        }

        setlocale(LC_TIME, $currentLocale);

        return $month;
    }


    /**
     * Returns month names
     * @return array
     */
    public static function getMonths(string $language = null): array
    {
        $currLang = Lang::getLanguage();
        $months = [];
        $languages = $language ? [$language] : array_keys(Lang::getLanguages());

        foreach ($languages as $lang) {
            Lang::switchLanguage($lang);
            $months[$lang] = [
                '01' => Lang::get('GENERAL_JANUARY'),
                '02' => Lang::get('GENERAL_FEBRUARY'),
                '03' => Lang::get('GENERAL_MARCH'),
                '04' => Lang::get('GENERAL_APRIL'),
                '05' => Lang::get('GENERAL_MAY'),
                '06' => Lang::get('GENERAL_JUNE'),
                '07' => Lang::get('GENERAL_JULY'),
                '08' => Lang::get('GENERAL_AUGUST'),
                '09' => Lang::get('GENERAL_SEPTEMBER'),
                '10' => Lang::get('GENERAL_OCTOBER'),
                '11' => Lang::get('GENERAL_NOVEMBER'),
                '12' => Lang::get('GENERAL_DECEMBER')
            ];
        }

        Lang::switchLanguage($currLang);

        return $language ? $months[$language] : $months;
    }


    /**
     * Returns day name in current language
     *
     * @param int $dayNo
     * @param bool $short
     *
     * @return string
     */
    public static function getDay($dayNo, $short = false)
    {
        $days = array(
            1 => 'MONDAY',
            'TUESDAY',
            'WEDNESDAY',
            'THURSDAY',
            'FRIDAY',
            'SATURDAY',
            'SUNDAY'
        );

        return Lang::get('GENERAL_' . $days[$dayNo] . ($short ? '_SHORT' : ''));
    }


    /**
     * Returns number suffixes
     * @param int $number
     * @return string
     */
    public static function getNumberSuffix($number): string
    {
        if (!$number || !is_numeric($number)) {
            return '';
        }

        if (strpos(App::$user->getLocale(), 'en') !== false) {
            $numberFormatter = new NumberFormatter('en_US', NumberFormatter::ORDINAL);
            return substr($numberFormatter->format($number), strlen($number));
        }

        return '';
    }


    /**
     * Convert encoding
     *
     * @param string $string
     * @return string|null
     */
    public static function convertEncoding($string)
    {
        $out = $string;

        if (!mb_check_encoding($string)) {
            $words     = explode(' ', $string);
            $wordsConv = array();

            foreach ($words as $word) {
                if (!mb_check_encoding($word)) {
                    $wordsConv[] = mb_convert_encoding($word, 'utf-8', array('iso-8859-2'));
                } else {
                    $wordsConv[] = $word;
                }
            }
            $out = implode(' ', $wordsConv);
        }

        return $out;
    }


    /**
     * Encodes JSON
     *
     * @param mixed $valueToEncode
     * @param int $options
     * @param int $depth
     * @return string JSON encoded object
     */
    public static function jsonEncode($valueToEncode, $options = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP, int $depth = 512)
    {
        if (is_object($valueToEncode)) {
            if (method_exists($valueToEncode, 'toJson')) {
                return $valueToEncode->toJson();
            } elseif (method_exists($valueToEncode, 'toArray')) {
                return static::jsonEncode($valueToEncode->toArray());
            }
        }

        // Encoding
        return json_encode($valueToEncode, $options, $depth);
    }


    /**
     * Decodes JSON value
     *
     * @param string $encodedValue Encoded in JSON format
     * @param int $objectDecodeType Optional; flag indicating how to decode objects.
     *
     * @return mixed
     *
     * @throws RuntimeException
     */
    public static function jsonDecode($encodedValue, $objectDecodeType = self::JSON_ARRAY)
    {
        if ($encodedValue === null) {
            return null;
        }

        $decode = json_decode($encodedValue, $objectDecodeType);

        switch (json_last_error()) {
            case JSON_ERROR_NONE:
                break;
            case JSON_ERROR_DEPTH:
                throw new RuntimeException('Decoding failed: Maximum stack depth exceeded');
            case JSON_ERROR_CTRL_CHAR:
                throw new RuntimeException('Decoding failed: Unexpected control character found');
            case JSON_ERROR_SYNTAX:
                throw new RuntimeException('Decoding failed: Syntax error');
            default:
                throw new RuntimeException('Decoding failed');
        }

        return $decode;
    }


    /**
     * Convert hex color to rgb color
     *
     * @param string $hex
     * @param bool   $asArray
     * @return array/string $rgb
     */
    public static function hex2rgb($hex, $asArray = false)
    {
        $hex = str_replace("#", "", $hex);

        list($r, $g, $b) = array_map('hexdec', str_split($hex, 2));

        if ($asArray) {
            $rgb = array(
                'red'   => $r,
                'green' => $g,
                'blue'  => $b,
            );
        } else {
            $rgb = $r . ', ' . $g . ', ' . $b;
        }

        return $rgb;
    }


    /**
     * Returns valid date from XLS cell value
     * @param $dateCell string|int
     * @param $timezone string|null
     * @return string|false
     * @throws Exception
     */
    public static function getDateFromXls($dateCell, $timezone = null)
    {
        if (!$dateCell) {
            return false;
        }

        if (is_int($dateCell) || is_numeric($dateCell)) {
            return date('Y-m-d', $dateCell);
        }

        if (Filter::validateDate($dateCell)) {
            return $dateCell;
        }

        $timestamp = strtotime($dateCell);

        if ($timestamp) {
            $date = date('Y-m-d', Xls::extractTimestamp($timestamp, $timezone));

            return $date ?: false;
        }

        try {
            $dateTime = new DateTime($dateCell);
            return $dateTime->format('Y-m-d');
        } catch (Exception $e) {
            return false;
        }
    }


    /**
     * Returns valid time from XLS cell value
     * @param $timeCell string
     * @return string
     */
    public static function getTimeFromXls($timeCell)
    {
        if (Filter::validateTime($timeCell)) {
            return $timeCell;
        } else {
            if (preg_match('/^[0-9]:/', $timeCell)) {
                $timeCell = '0' . $timeCell;
                if (Filter::validateTime($timeCell)) {
                    return $timeCell;
                }
            }
        }
        return false;
    }


    /**
     * Create XML by recursive array parsing
     *
     * @param XmlWriter $xml
     * @param array     $array
     */
    public static function xmlFromArray(XmlWriter $xml, array $array)
    {
        foreach ($array as $index => $element) {
            if (is_array($element)) {
                $xml->startElement(preg_replace('/[^a-zA-Z]/', '', $index));
                self::xmlFromArray($xml, $element);
                $xml->endElement();
            } else {
                if ($element instanceof Exception) {
                    $xml->startElement($index);
                    $xml->startCdata();
                    $xml->text($element);
                    $xml->endCdata();
                    $xml->endElement();
                } else {
                    $xml->writeElement($index, $element);
                }
            }
        }
    }


    /**
     * @return NumberFormatter
     */
    public static function getNumberFormatter()
    {
        $locale = setlocale(LC_NUMERIC, '0');

        if (!self::$_numberFormatter || self::$_numberFormatter->getLocale() != $locale) {
            self::$_numberFormatter = new NumberFormatter($locale, NumberFormatter::DECIMAL);
        }

        return self::$_numberFormatter;
    }


    /**
     * @param mixed $numeric
     *
     * @return float
     */
    public static function parseFloat($numeric)
    {
        return self::getNumberFormatter()->parse($numeric);
    }


    /**
     * @param mixed $number
     * @param int   $precision
     *
     * @return string
     */
    public static function formatFloat($number, $precision = null)
    {
        $formatter = self::getNumberFormatter();

        if (null !== $precision) {
            $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $precision);
        }

        return $formatter->format($number, NumberFormatter::TYPE_DOUBLE);
    }


    /**
     * Returns how long ago passed date occurred
     *
     * @param string $string
     * @param boolean $shortForm
     *
     * @return string
     */
    public static function getTimeAgo($string, $shortForm = false)
    {
        $cur_time = time();
        $time_elapsed = $cur_time - strtotime($string);
        $seconds = $time_elapsed;
        $minutes = round($time_elapsed / 60);
        $hours = round($time_elapsed / 3600);
        $days = round($time_elapsed / 86400);
        $weeks = round($time_elapsed / 604800);
        $months = round($time_elapsed / 2600640);
        $years = round($time_elapsed / 31207680);

        if ($seconds <= 60) {
            $rv = Lang::get('GENERAL_NOW');
        } elseif ($minutes <= 60) {
            $rv = self::getLangDateVariety($minutes, Lang::get('GENERAL_ONE_MINUTE_AGO'), Lang::get('GENERAL_MINUTE_AGO_234'), Lang::get('GENERAL_MINUTE_AGO_OTHER'), $shortForm);
        } elseif ($hours <= 24) {
            $rv = self::getLangDateVariety($hours, Lang::get('GENERAL_AN_HOUR_AGO'), Lang::get('GENERAL_HOURS_AGO_234'), Lang::get('GENERAL_HOURS_AGO_OTHER'), $shortForm);
        } elseif ($days <= 7) {
            $rv = self::getLangDateVariety($days, Lang::get('GENERAL_YESTERDAY'), Lang::get('GENERAL_DAYS_AGO_234'), Lang::get('GENERAL_DAYS_AGO_OTHER'), $shortForm);
        } elseif ($weeks <= 4.3) {
            $rv = self::getLangDateVariety($weeks, Lang::get('GENERAL_A_WEEK_AGO'), Lang::get('GENERAL_WEEKS_AGO_234'), Lang::get('GENERAL_WEEKS_AGO_OTHER'), $shortForm);
        } elseif ($months <= 12) {
            $rv = self::getLangDateVariety($months, Lang::get('GENERAL_A_MONTH_AGO'), Lang::get('GENERAL_MONTHS_AGO_234'), Lang::get('GENERAL_MONTHS_AGO_OTHER'), $shortForm);
        } else {
            $rv = self::getLangDateVariety($years, Lang::get('GENERAL_ONE_YEAR_AGO'), Lang::get('GENERAL_YEARS_AGO_234'), Lang::get('GENERAL_YEARS_AGO_OTHER'), $shortForm, true);
        }

        return $rv;
    }


    public static function getLangDateVariety($number, $variantSingle, $varietyFor234, $varietyForOthers, $shortForm = false, $ignoreFirstForm = false)
    {
        if ($number == 1) {
            if ($shortForm && !$ignoreFirstForm) {
                return '1 ' . substr($varietyFor234, 0, 1) . '. ' . Lang::get('GENERAL_AGO');
            } else {
                return $variantSingle;
            }
        }

        if (in_array($number % 10, array(2, 3, 4))) {
            if ($shortForm) {
                return $number . ' ' . substr($varietyFor234, 0, 1) . '. ' . Lang::get('GENERAL_AGO');
            } else {
                return $number . ' ' . $varietyFor234;
            }
        }

        if ($shortForm) {
            return $number . ' ' . substr($varietyForOthers, 0, 1) . '. ' . Lang::get('GENERAL_AGO');
        } else {
            return $number . ' ' . $varietyForOthers;
        }
    }


    /**
     * Spaces, tabs or line breaks (one or more) will be replaced by a single underscore.
     * @param string $_string
     * @return string
     */
    public static function replaceWhitespace($_string)
    {
        return preg_replace('/\s+/', '_', $_string);
    }

    /**
     * Removes from string characters between 0 - 31 and 127 decimal ASCII number
     * and other non printable ASCII characters to prevent JSON parse errors
     *
     * @param string $string
     * @return string
     */
    public static function removeNonPrintableChars($string)
    {
        return preg_replace('/[\x00-\x1F\x7F]/u', '', $string);
    }

    /**
     * Formats date and time according to ISO 8601 standard
     *
     * @param DateTime|string $input
     * @param DateTimeZone|null $timeZone
     * @return string
     * @throws Exception
     */
    public static function formatIsoDateTime($input, DateTimeZone $timeZone = null): string
    {
        if (!$input instanceof DateTime) {
            $input = new DateTime($input);
        }

        if ($timeZone) {
            $input->setTimezone($timeZone);
        }

        return $input->format('c');
    }

    /**
     * Formats currency code according to locale
     *
     * @param string $currency
     * @return string
     */
    public static function formatCurrencyCode($currency)
    {
        $out = $currency;

        if ($currency) {
            $out = self::formatMoney(0, true, $currency);
            $out = preg_replace('/[\d,.\s]+/u', '', $out);
        }

        return $out;
    }

    /**
     * @param string $input
     * @return string
     */
    public static function toPascalCase(string $input): string
    {
        if (App::getPhalconMajorVersion() >= PhalconVersion::PHALCON5) {
            $helperFactory = self::getHelperFactory();

            return $helperFactory->camelize($input);
        }

        return \Phalcon\Text::camelize($input);
    }

    public static function toCamelCase(string $input): string
    {
        $helperFactory = self::getHelperFactory();

        // if we remove `delimiters: null`, it doesn't work as expected.
        return $helperFactory->camelize(text: $input, delimiters: null, lowerFirst: true);
    }

    /**
     * @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 $input
     * @return string
     */
    public static function toSnakeCase(string $input): string
    {
        if (App::getPhalconMajorVersion() >= PhalconVersion::PHALCON5) {
            $helperFactory = self::getHelperFactory();

            return $helperFactory->uncamelize($input);
        }

        return \Phalcon\Text::uncamelize($input);
    }

    public static function toKebabCase(string $input): string
    {
        if (App::getPhalconMajorVersion() >= PhalconVersion::PHALCON5) {
            $helperFactory = self::getHelperFactory();

            return $helperFactory->kebabCase(self::toSnakeCase($input));
        }

        return str_replace('_', '-', \Phalcon\Text::uncamelize($input));
    }

    public static function getNextDayDate(int $days = 1, string $format = 'Y-m-d'): string
    {
        $now = date($format);

        return date($format, strtotime($now . ' +' . $days . ' day'));
    }

    public static function pointCoordinatesToArray(?string $coordinates): array
    {
        $default = [
            'lat' => 0,
            'lng' => 0,
        ];

        if (!$coordinates) {
            return $default;
        }

        $coordinates = str_replace(['(', ')'], '', $coordinates);
        $coordinates = explode(',', $coordinates);

        if (count($coordinates) != 2) {
            return $default;
        }

        return [
            'lat' => (float) $coordinates[0],
            'lng' => (float) $coordinates[1],
        ];
    }
}
