<?php

declare(strict_types=1);

/**
 * This file is part of an ADDON for use with LEPTON Core.
 * This ADDON is released under the GNU GPL.
 * Additional license terms can be seen in the info.php of this module.
 *
 * @module          lib_lepton
 * @author          LEPTON Project
 * @copyright       2010-2022 LEPTON Project
 * @link            https://lepton-cms.org
 * @license         http://www.gnu.org/licenses/gpl.html
 * @license_terms   please see info.php of this module
 *
 */

class lib_lepton_datetools
{

    /**
     *  Private var for the current version of this class.
     *
     *  @var    string
     *  @access public
     *
     */
    private string $version = "1.2.0.0";

    /**
     *  The reference to *Singleton* instance of this class
     *
     *  @var    object
     *  @access private
     *
     */
    private static $instance;

    /**
     *  The (default) format-string.
     *  Default is dd.mm.yyyy
     *
     *  @var    string
     *  @access public
     *
     */
    public string $format = "%d.%m.%Y";

    public string $sINTLFormat = "d.M.yyyy";

    /**
     *  The language-flags as an array.
     *  Default are some settings for German.
     *
     *  @var    array
     *  @access public
     *
     */
    public array $lang = Array('de_DE@euro', 'de_DE.UTF-8', 'de_DE', 'de', 'ge');

    /**
     *  The mode we are using. Default is LC_ALL for all.
     *
     *  @var    string
     *  @access public
     *
     */
    public string $mode = "LC_ALL";

    /**
     *  Used for "_force_year" to determiante if the year belongs to 1900 or 2000.
     *  Default setting is 2, so if the current year is 2008 a value of 10 will be force
     *  to 2010 instead of 11 will be force to 1911.
     *
     *  @var    integer
     *  @see    _force_year
     *  @access public
     *
     */
    public int $force_year = 2;

    /**
     *  Translation-array for the LEPTON-CMS internal date-formats.
     *
     *  @var    array
     *  @access public
     *
     */
    public array $CORE_date_formats = array(
        'l, jS F, Y'=> '%A, %e %B, %Y',
        'jS F, Y'   => '%e %B, %Y',
        'd M Y'     => '%d %a %Y',
        'M d Y'     => '%a %d %Y',
        'D M d, Y'  => '%a %b %d, %Y',  ##
        'd-m-Y'     => '%d-%m-%Y',      #1
        'm-d-Y'     => '%m-%d-%Y',
        'd.m.Y'     => '%d.%m.%Y',      #2
        'm.d.Y'     => '%m.%d.%Y',
        'd/m/Y'     => '%d/%m/%Y',      #3
        'm/d/Y'     => '%m/%d/%Y',
        'j.n.Y'     => '%e.%n.%Y'       #4! Day in month without leading zero
    );

    /**
     *  Translation-array for the LEPTON-CMS internal date-formats.
     *
     *  @var    array
     *  @access public
     *  @see    https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_date-format
     */
    public array $CORE_date_formats_for_MYSQL = array(
        'l, jS F, Y'=> '%W, %D %M, %Y', // 1
        'jS F, Y'   => '%D %M, %Y',     // 2
        'd M Y'     => '%e. %M %Y',     // 3 e.g. 24. Juli 2022
        'M d Y'     => '%b %e %Y',      // 4
        'D M d, Y'  => '%a %b %e, %Y',  // 5
        'd-m-Y'     => '%d-%m-%Y',      // 6
        'm-d-Y'     => '%m-%d-%Y',      // 7
        'd.m.Y'     => '%d.%m.%Y',      // 8
        'm.d.Y'     => '%m.%d.%Y',      // 9
        'd/m/Y'     => '%d/%m/%Y',      // 10
        'm/d/Y'     => '%m/%d/%Y',      // 11
        'j.n.Y'     => '%e.%c.%Y'       // 12 Day in month without leading zero
    );

    /**
     *  Translation-array for the LEPTON-CMS internal time-formats.
     *
     *  @var    array
     *  @access public
     *
     */
    public array $CORE_time_formats = array(
        'g:i A' => '%I:%M %p',
        'g:i a' => '%I:%M %P',
        'H:i:s' => '%H:%M:%S',
        'H:i'   => '%H:%M'
    );

    /**
     *  Translation-array for the LEPTON-CMS internal date-formats for datepicker(-s).
     *
     *  @var    array
     *  @access public
     *
     */
    public array $CORE_date_formats_toDatePicker = array(
        'l, jS F, Y'=> 'A, e B, yy',
        'jS F, Y'   => 'e B, yy',       // 1
        'd M Y'     => 'd M yy',
        'M d Y'     => 'M d yy',
        'D M d, Y'  => 'DD d MM, yy',   // 'a m d, yy',	// 2 [Aldus: 21.01.2020 - Buggy in LepSem!]
        'd-m-Y'     => 'd-m-yy',        // 3
        'm-d-Y'     => 'm-d-yy',
        'd.m.Y'     => 'd.m.yy',        // 4
        'm.d.Y'     => 'm.d.yy',
        'd/m/Y'     => 'd/m/yy',        // 5
        'm/d/Y'     => 'm/d/yy',
        'j.n.Y'     => 'j.n.yy'         // 6! Day in month without leading zero
    );

    /**
     *  Boolean to switch to the IntlDateFormatter
     *
     *  @see: https://www.php.net/manual/de/intldateformatter.format.php
     *  @see: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#formatting-dates
     *
     *  @access public
     *  @var    bool
     *
     */
    public bool $useINTL = false;
    public bool $intl_installed = false;

    /**
     *  Holds information for some dateFormatter patterns (M.f.i!)
     *  @access public
     *  @var    array
     */
    public array $CORE_date_formats_INTL = array(
        'l, jS F, Y'=> 'A, e B, yyyy',
        'jS F, Y'   => 'e B, yyyy',     // 1
        'd M Y'     => 'd M yyyy',
        'M d Y'     => 'M d yyyy',
        'D M d, Y'  => 'DD d MM, yyyy', // 'a m d, yy',
        'd-m-Y'     => 'd-M-yy',        // 3
        'm-d-Y'     => 'M-d-yy',
        'd.m.Y'     => 'd.M.yy',        // 4
        'm.d.Y'     => 'M.d.yy',
        'd/m/Y'     => 'd/M/yy',        // 5
        'm/d/Y'     => 'M/d/yy',
        'j.n.Y'     => 'j.n.yyyy'       // 6! Day in month without leading zero
    );

    /**
     *  Return the "internal" instance of this class
     *
     *  @param  boolean $bUseINTL    Optional flag for the use of INTL DateTimeObject. Default is false.
     *  @return object
     *
     */
    public static function getInstance(bool $bUseINTL = false): object
    {
        if (null === static::$instance) {
            static::$instance = new static();
        }
        static::$instance->useINTL = $bUseINTL;

        if(true === $bUseINTL)
        {
            static::$instance->intl_installed = static::$instance->checkINTLExtensionExists();
        }

        return static::$instance;
    }

    /**
     *  The constructor
     */
    protected function __construct ()
    {
        // nothing
    }

    /**
     *  Public function to add a Language-Setting-Flag.
     *
     *  Only added if not found inside the lang-array.
     *
     */
    public function addLanguage ($aString=""): bool
    {
        if ($aString == "") {
            return false;
        }
        if (false === in_array($aString, $this->lang)) {
            $this->lang[] = $aString;
        }
        return true;
    }

    /**
     *  Public function to set up the format-string
     *
     *  @param  string  $aString The formatString, even empty.
     *
     */
    public function setFormat( string $aString="" ): bool
    {
        if($this->useINTL)
        {
            $this->sINTLFormat = $aString;
        } else {
            $this->format=$aString;
        }
        return true;
    }

    /**
     *  Public function to get the format-string.
     *
     *  @param int      $aTimestamp A valid Timestamp
     *                  If no timestamp is given the local time will be used.
     *
     *  @return string  The formatted date string
     *
     *  @todo   Testing the timestamp before generating the html-return.
     *
     */
    public function toHTML(int $aTimestamp = 0 ): string
    {
        if (NULL == $aTimestamp)
        {
            $aTimestamp = TIME();
        }

        $aTempLocale = setlocale(LC_ALL, $this->lang);
        if( false === $aTempLocale )
        {
            $aTempLocale = setlocale( LC_ALL, 0 );
        }

        if( ( true === $this->useINTL ) && ( true === $this->intl_installed ) )
        {
            $fmt = datefmt_create(
                $aTempLocale, // 'de-DE',
                IntlDateFormatter::FULL,
                IntlDateFormatter::FULL,
                DEFAULT_TIMEZONE_STRING, // 'Europe/Berlin',
                IntlDateFormatter::GREGORIAN,
                $this->sINTLFormat
                //$this->format // "yyyy.MM.dd G 'um' HH:mm:ss zzz" // "MMMM | d | y | H"
            );
            if($fmt === NULL)
            {
                // Huston: we've got a problem!
                $this->useINTL = false;
                return strftime($this->format, $aTimestamp);
            }
            else
            {
                return datefmt_format($fmt,  $aTimestamp );
            }
        } else {
            /**
             * Aldus:   2022-01-25
             * @notice  In PHP 8.1.1 the strftime function produce a deprecated warning and will be removed in PHP 9.0
             * @see     https://php.watch/versions/8.1/strftime-gmstrftime-deprecated
             */
            if(class_exists("IntlDateFormatter"))
            {
                $oTempFormatter = new IntlDateFormatter(
                    $aTempLocale, // 'en_US',
                    IntlDateFormatter::LONG,
                    IntlDateFormatter::NONE,
                    DEFAULT_TIMEZONE_STRING,        // 1 timezone
                    IntlDateFormatter::GREGORIAN,   // 2 calendar
                    $this->sINTLFormat              // 3 format
                );
                return $oTempFormatter->format( $aTimestamp );
            } else {
                // Problem!
                return date($this->format, $aTimestamp);
            }
        }
    }

    /**
     *  Public function to set up the language at once
     *
     *  @param  array   $aArray A simple Array with the strings
     *
     *  @return bool    Always true.
     *
     *  @todo   Testing the array-values and returning false
     *          if something is not valid. How? No idea yet.
     */
    public function setLanguage (array $aArray = array() ): bool
    {
        $this->lang = array();
        foreach($aArray as $a)
        {
            $this->lang[] =$a;
        }
        return true;
    }

    /**
     *  Public function to transform dd.mm.yyyy into the current format
     *
     *  @param  string  $aDateString Date in dd.mm.yyyy
     *  @return string  $aFormat the formatted string
     *
     *  Following date-delimiter are supported
     *  "."     01.01.1971
     *  "-"     01-01-1971
     *  "/"     01/01/1971
     *
     *  Following format-settings are supported, including (space) and/or "." and/or "%".
     *  'dmy'   day - month - year
     *  'mdy'   month - day - year
     *  'ymd'   year - month - day
     *
     */
    public function transform (string $aDateString = "01.01.1971", string $aFormat="dmy"): string
    {
        $this->_force_date ($aDateString);
        $this->_force_format ($aFormat);

        $temp = explode(".", $aDateString);
        $temp = array_map(function($a){ return intval($a);}, $temp);
        switch ($aFormat)
        {
            case 'dmy':
                $this->_force_year($temp[2]);
                $temp_time = mktime( 1, 0, 0, $temp[1], $temp[0], $temp[2]);
                break;

            case 'mdy':
                $this->_force_year($temp[2]);
                $temp_time = mktime( 1, 0, 0, $temp[0], $temp[1], $temp[2]);
                break;

            case 'ymd':
                $this->_force_year($temp[0]);
                $temp_time = mktime( 1, 0, 0, $temp[1], $temp[2], $temp[0]);
                break;

            /**
             *  M.f.i!
             *
             *  At this time (0.1.0.0) the default time is
             *  used instead of Error-Handling, thrown by an invalid format-string!
             *
             */
            default:
                $temp_time = time();
                break;
        }
        return $this->toHTML($temp_time);
    }

    /**
     * Private function to force a given string into an internal
     * dot-based format: "MM.DD.YY" (month, day, year).
     *
     *  @param    string    $aDateString the DateString
     *
     *  @return    void    Param is called by reference!
     *
     * @code
     *  $date = "11-03-1988";
     *  $this->_force_date($date);
     *  echo $date;
     *
     *  results in: "11.03.1966"
     */
    private function _force_date(string &$aDateString ): void
    {
        $pattern = array("*[\\/|.|-]{1,}*");
        $replace = array(".");

        $aDateString = preg_replace($pattern, $replace, $aDateString);
    }

    /**
     *  Private function to force the format-string used in/for "transform"
     *
     *  @param  string  $aFormat The transform-format-string - called by reference!
     *
     *  @see    transform
     *
     *  @todo   Error-Handling; the format-string has to have at
     *          least three chars: "d", "m", and "y"
     *
     */
    private function _force_format(string &$aFormat): void
    {

        $aFormat = strtolower ($aFormat);

        $pattern = array("*[\\/|.|-]{1,}*", "*[ |%]{1,}*");
        $replace = array("", "");

        $aFormat = preg_replace($pattern, $replace, $aFormat);
    }

    /**
     *  private function that force a "short" Year to a "long" year
     *
     *  @param  string    $aYearStr The year - called by reference!
     *    @see  force_year
     *
     *    If the year is future oriented more than two years by default at runtime,
     *    19xx is assumed.
     */
    private function _force_year(string &$aYearStr = "1971"): void
    {
        if (strlen($aYearStr) == 2) {
            $aYearStr = (((int)$aYearStr > $this->force_year + (int)DATE("y", TIME())) ? "19" : "20") . $aYearStr;
        }
        if (strlen($aYearStr) > 4) {
            $aYearStr = substr($aYearStr, 0, 4);
        }
    }

    /**
     *  Public function to transform the date inside a given string
     *
     *  @param  string    $aStr     The string within the dates. Pass by reference.
     *  @param  string    $aPattern Own patter/regexp for other formats.
     *                              default is "dd.mm.yyyy" e.g. 11.03.1966
     */
    public function parse_string (string &$aStr = "", string $aPattern = "/([0-3][0-9].[01]{0,1}[0-9].[0-9]{2,4})/s"): void
    {
        $found=array();
        preg_match_all($aPattern, $aStr, $found );
        foreach ($found[1] as $a)
        {
            $aStr = str_replace($a, $this->transform($a), $aStr);
        }
    }

    /**
     *  Setting up the language via a single key,
     *  e.g. inside LEPTON-CMS
     *
     *  @param  string  $aKeyStr The language-key-str, e.g. "EN"...
     *  @return bool    True if the key is known, false if failed.
     *
     */
    public function set_core_language(string $aKeyStr = ""): bool
    {

        $return_value = true;

        switch ($aKeyStr)
        {

            case "DE":
                $this->lang = Array('de_DE.UTF-8', 'de_DE@euro', 'de_DE', 'de', 'ge');
                break;

            case "EN":
                $this->lang = Array('en_EN@euro', 'en_EN', 'en', 'EN', 'en_UK', 'UK', 'en_US', 'en_GB', 'en_CA');
                break;

            case "FR":
                $this->lang = Array('FR', 'fr_FR.UTF-8', 'fr_FR', 'fr_FR@euro', 'fr');
                break;

            case "IT":
                $this->lang = Array('it_IT@euro', 'it_IT', 'it');
                break;

            case "NL":
                $this->lang = Array('nl_NL@euro', 'nl_NL', 'nl', 'Dutch', 'nld_nld');
                break;

            case "RU":
                $this->lang = Array('RU', 'ru_RU.UTF-8', 'ru_RU', 'ru_RU@euro', 'ru');
                break;

            case "ZH":
                $this->lang = Array('zh_CN','zh_CN.eucCN','zh_CN.GB18030','zh_CN.GB2312','zh_CN.GBK','zh_CN.UTF-8','zh_HK','zh_HK.Big5HKSCS','zh_HK.UTF-8','zh_TW','zh_TW.Big5','zh_TW.UTF-8');
                break;

            default:
                $this->test_locale($aKeyStr);
                break;
        }
        return $return_value;
    }

    /**
     *  Public function to test a given LanguageKey
     *  against the server-implanted ones using "locale -a".
     *  If one or more are found the internal "lang" will be set.
     *
     *  @param  string    $aKey the LanguageKey, e.g. "EN", "fr_FR"
     *                    If only two chars are given, the rest will be
     *                    automatically formatted as "uu_LL".
     *
     *  @param  bool    $use_it If the key is found - use it inside the class.
     *
     *  @return array    all matches; could be empty.
     *
     */
    public function test_locale (string $aKey = "de_DE", bool $use_it = true): array
    {
        if (strlen($aKey) == 2)
        {
            $aKey = strtolower($aKey)."_".strtoupper($aKey);
        }

        $temp_array = array();
        ob_start();
            exec('locale -a', $temp_array);
        ob_end_flush();
        $all = array();

        foreach($temp_array as $lang_key)
        {
            if (substr($lang_key, 0,5) == $aKey)
            {
                $all[]=$lang_key;
            }
        }

        if (!empty($all) && (true === $use_it))
        {
            $this->lang = $all;
        }
        return $all;
    }

    /**
     *  Public function to translate a given internal format string for the datepickers (js).
     *
     *  @param  string  $sFormatString  A valid format string.
     *  @return string  The matching value inside the internal translation array as string or the current DATE_FORMAT or "" (empty) if non match.
     *
     */
    public function formatToDatepicker(string $sFormatString = ""): string
    {
        if (isset($this->CORE_date_formats_toDatePicker[$sFormatString])) {
            return $this->CORE_date_formats_toDatePicker[$sFormatString];
        } elseif (isset($this->CORE_date_formats_toDatePicker[DATE_FORMAT])) {
            return $this->CORE_date_formats_toDatePicker[DATE_FORMAT];
        } else {
            return "";
        }
    }

    /**
     *  Old jscalendar_to_timestamp function for backward compatibility
     *
     *  @param  string  $str    The given timestring.
     *  @param  integer $offset Optional offset. (timestamp!)
     *
     *  @return int A timestamp
     */
    public function calendarToTimestamp(string $str = "", int $offset = 0): int
    {
        $str = trim($str);
        if($str == '0' || $str == '')
        {
            return 0;
        }

        // convert to yyyy-mm-dd
        // "dd.mm.yyyy"?
        if(preg_match('/^\d{1,2}\.\d{1,2}\.\d{2}(\d{2})?/', $str)) {
            $str = preg_replace('/^(\d{1,2})\.(\d{1,2})\.(\d{2}(\d{2})?)/', '$3-$2-$1', $str);
        }

        // "mm/dd/yyyy"?
        if(preg_match('#^\d{1,2}/\d{1,2}/(\d{2}(\d{2})?)#', $str)) {
            $str = preg_replace('#^(\d{1,2})/(\d{1,2})/(\d{2}(\d{2})?)#', '$3-$1-$2', $str);
        }

        // use strtotime()
        if($offset != 0)
        {
            return (strtotime($str, $offset));

        } else {
            return (strtotime($str));
        }
    }

    /**
     * Returns an array of valid "keys" for set_locale.
     *
     * @return array
     */
    public function detectPageLanguage() : array
    {
        // [1] $_GET
        $sTempCurrentPageLanguage = ($_GET["lang"] ?? NULL);

        // [1.1] none found - page-language?
        if ($sTempCurrentPageLanguage === NULL)
        {
            $sTempCurrentPageLanguage = LEPTON_frontend::getInstance()->page['language'];
        }

        // [2]
        $sBasic = strtolower($sTempCurrentPageLanguage)."_".strtoupper($sTempCurrentPageLanguage);

        // [3]
        return [
            $sBasic,
            $sBasic.".UTF-8",
            $sBasic."@euro"
        ];
    }

    /**
     * Check if INTL is installed.
     *
     * @return bool
     */
    public function checkINTLExtensionExists() : bool
    {
        $aAllExtensions = get_loaded_extensions();
        if(!in_array("intl", $aAllExtensions))
        {
            return false;
        }
        return true;
    }

    /**
     * Format via MySQL
     * @param   string      $format
     * @param   int         $iTimestamp
     * @param   string      $optionalLang
     * @return  string|NULL
     *
     * @see https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_date-format
     *
     */
    public function formatWithMySQL(string $format = DEFAULT_DATE_FORMAT, int $iTimestamp = 0, string $optionalLang = "" ): string|null
    {
        if($iTimestamp === NULL)
        {
            $iTimestamp = time();
        }

        $sRealFormat = (array_key_exists($format, $this->CORE_date_formats_for_MYSQL))
            ? $this->CORE_date_formats_for_MYSQL[ $format ]
            : $format
            ;

        $tempLang = self::buildLanguageKey(
                (empty($optionalLang)
                    ? LANGUAGE
                    : $optionalLang
                )
        );

        $database = LEPTON_database::getInstance();

        if($tempLang !== "en_EN")
        {
            $database->simple_query("SET lc_time_names = '".$tempLang."';");
        } else {
            $database->simple_query("SET lc_time_names = 'en_gb';");
        }
        
        return $database->get_one("SELECT DATE_FORMAT( FROM_UNIXTIME(".$iTimestamp."),'".$sRealFormat."');");
    }

    /**
     * Forms a given string to the form "bb_BB", even if only "bb" is given.
     *
     * @param   string $key
     * @return  string
     *
     * @see https://database.guide/full-list-of-locales-in-mysql/
     *
     */
    static function buildLanguageKey(string $key = LANGUAGE): string
    {
        if (preg_match("~^[a-z]{2}_[A-Z]{2}$~", $key)) {
            // given string is "in correct form"
            return $key;
        } elseif (strlen($key) >= 2) {
            // more than two chars - use the first two ones
            $key = substr(trim($key), 0, 2);
            return strtolower($key) . "_" . strtoupper($key);
        } else {
            return "Error: need at least two chars for key! One given!";
        }
    }
}
