<?php

namespace IMATHUZH\Qfq\Core\Helper;

use IMATHUZH\Qfq\Core\Form\Form;
use IMATHUZH\Qfq\Core\Form\FormElement\DatetimeFormElement;
use IMATHUZH\Qfq\Core\Renderer\BaseRenderer;
use IMATHUZH\Qfq\Core\Store\Store;

/**
 *
 */
class DateTime {

    /**
     * Builds HTML 'input' element.
     * Format: <input name="$htmlFormElementName" <type="date" [autocomplete="autocomplete"] [autofocus="autofocus"]
     *           [maxlength="$maxLength"] [placeholder="$placeholder"] [size="$size"] [min="$min"] [max="$max"]
     *           [pattern="$pattern"] [required="required"] [disabled="disabled"] value="$value">
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param array $formSpec
     * @param Store $store
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function buildDateTime(array $formElement, $htmlFormElementName, $value, array &$json, array $formSpec, Store $store, $mode = FORM_LOAD, $wrappedClass = ''): string {
        $attribute = '';
        $placeholder = '';
        $datetimeKeys = array(
            FE_DATE_LOCALE,
            FE_DATE_DAYS_OF_WEEK_ENABLED,
            FE_DATE_FORMAT,
            FE_DATE_VIEWMODE_DEFAULT,
            F_FE_INPUT_CLEAR_ME,
            FE_DATE_SHOW_CALENDAR_WEEKS,
            FE_DATE_CURRENT_DATETIME,
            FE_DATE_DATETIME_SIDE_SIDE_BY_SIDE,
            FE_MIN,
            FE_MAX
        );

        $datetimeAttributes = array(
            "locale",
            "days-of-week-disabled",
            "format",
            "view-mode-default",
            "show-clear-button",
            "show-calendar-weeks",
            "use-current-datetime",
            "datetime-side-by-side",
            "minDate",
            "maxDate"
        );

        $datePickerClassName = self::getDatePickerClassName($formElement, $formSpec);

        $attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]);
        $attribute .= Support::doAttribute('name', $htmlFormElementName);
        $attribute .= Support::doAttribute('class', 'form-control ' . $datePickerClassName);
        $attribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE]);

        $showTime = ($formElement[FE_TYPE] == FE_TYPE_TIME || $formElement[FE_TYPE] == FE_TYPE_DATETIME) ? 1 : 0;
        if ($value == 'CURRENT_TIMESTAMP') {
            $value = date('Y-m-d H:i:s');
        }

        $defaultFormat = self::getDefaultDateFormat($formElement);

        $value = Support::convertDateTime($value, $defaultFormat['date'], $formElement[FE_SHOW_ZERO], $showTime, $formElement[FE_SHOW_SECONDS]);

        // Browser needs own formatted value to show date
        if ($formElement[FE_DATE_TIME_PICKER_TYPE] === DATE_TIME_PICKER_BROWSER && $value !== '') {
            $value = self::formatValueForBrowser($formElement[FE_TYPE], $value);
        }

        $tmpPattern = $formElement[FE_CHECK_PATTERN];
        //  If bootstrap datetimepicker (date and datetime FE Type) is used, check pattern is not needed. Keep pattern for FE Type time.
        if ($formElement[FE_TYPE] != FE_TYPE_DATETIME && $formElement[FE_TYPE] != FE_TYPE_DATE && $formElement[FE_TYPE] != FE_TYPE_TIME) {
            $formElement[FE_CHECK_PATTERN] = Support::dateTimeRegexp($formElement[FE_TYPE], $formElement[FE_DATE_FORMAT], $formElement[FE_TIME_IS_OPTIONAL]);
        }

        switch ($formElement[FE_CHECK_TYPE]) {
            case  SANITIZE_ALLOW_PATTERN:
                $formElement[FE_CHECK_PATTERN] = $tmpPattern;
                break;
            case SANITIZE_ALLOW_ALL:
            case SANITIZE_ALLOW_ALNUMX:
            case SANITIZE_ALLOW_ALLBUT:
                $formElement[FE_CHECK_TYPE] = SANITIZE_ALLOW_PATTERN;
                break;
            default:
                throw new \UserFormException("Checktype not applicable for date/time: '" . $formElement[FE_CHECK_TYPE] . "'", ERROR_NOT_APPLICABLE);
        }

        $tableColumnTypes = $store->getStore(STORE_TABLE_COLUMN_TYPES);
        // truncate if necessary
        if ($value != '' && $formElement[FE_MAX_LENGTH] > 0) {
            if ($formElement[FE_TYPE] === FE_TYPE_TIME && isset($tableColumnTypes[$formElement[FE_NAME]]) && $tableColumnTypes[$formElement[FE_NAME]] === DB_COLUMN_TYPE_DATETIME) {
                $value = explode(' ', $value);
                if (count($value) > 1) {
                    $value = $value[1];
                } else {
                    $value = $value[0];
                }
            } else {
                $value = substr($value, 0, $formElement[FE_MAX_LENGTH]);
            }
        }

        if ($formElement[FE_DATE_TIME_PICKER_TYPE] === DATE_TIME_PICKER_BROWSER) {
            $type = self::getDateTimeBrowserType($formElement[FE_TYPE]);
        } else {
            // date|time|datetime|datetime-local are not appropriate - only I18n representation possible.
            $type = 'text';
        }

        $attribute .= Support::doAttribute('type', $type);

        //Examples of accepted dateFormats: YYYY-MM-DD , DD.MM.YYYY, HH:mm(24h), hh:mm(12h AM PM)
        // Get dateformat default from T3 config and adjust it for datetimepicker because config default (dd.mm.yyyy) is not the default of bootstrap datetimepicker.
        if ($formElement[FE_DATE_TIME_PICKER_TYPE] === DATE_TIME_PICKER_QFQ) {
            $defaultFormat = self::getDateTimeFormatQfq($defaultFormat, $formElement[FE_TYPE]);
        } else if ($formElement[FE_TYPE] == FE_TYPE_DATETIME) {
            if ($defaultFormat['time'] === '') {
                $defaultFormat['time'] = 'hh:mm';
            }
            $defaultFormat['date'] .= ' ' . $defaultFormat['time'];
        }

        // if FE type datetime and showSeconds is set, corrected format is needed
        if ($formElement[FE_TYPE] === FE_TYPE_DATETIME && $formElement[FE_SHOW_SECONDS] == 1 && !isset($defaultFormat['timeParts'][2])) {
            $defaultFormat['date'] .= ':ss';
        }

        // if FE type 'time' is used, overwrite $defaultFormat['date']
        if ($formElement[FE_TYPE] === FE_TYPE_TIME) {
            $defaultFormat = self::getTimeFormat($defaultFormat, $formElement[FE_DATE_TIME_PICKER_TYPE], $formElement[FE_SHOW_SECONDS]);
        }

        $formElement[FE_DATE_DAYS_OF_WEEK_ENABLED] = $formElement[FE_DATE_DAYS_OF_WEEK_ENABLED] ?? '';

        // Set correct parameter value for daysOfWeekDisabled attribute in FE
        if ($formElement[FE_DATE_DAYS_OF_WEEK_ENABLED] != '' && isset($formElement[FE_DATE_DAYS_OF_WEEK_ENABLED])) {
            $disabledDays = self::getDateTimePickerDisabledDays($formElement[FE_DATE_DAYS_OF_WEEK_ENABLED]);
            $formElement[FE_DATE_DAYS_OF_WEEK_ENABLED] = '[' . $disabledDays . ']';
        }

        if ($formElement[FE_DATE_TIME_PICKER_TYPE] === DATE_TIME_PICKER_QFQ) {
            $formElement[F_FE_INPUT_CLEAR_ME] = empty($formElement[F_FE_INPUT_CLEAR_ME]) ? 'false' : 'true';
        }

        // Add datepicker attributes
        self::setDateTimePickerAttributes($formElement, $defaultFormat['date'], $datetimeKeys, $datetimeAttributes, $attribute);

        // 'maxLength' needs an upper 'L': naming convention for DB tables!
        $attribute .= HelperFormElement::getAttributeList($formElement, ['size', 'maxLength']);
        $attribute .= Support::doAttribute('value', htmlentities($value), false);

        if ($formElement[FE_PLACEHOLDER] == '' && ($formElement[FE_TYPE] === FE_TYPE_TIME || $formElement[FE_TYPE] === FE_TYPE_DATE || $formElement[FE_TYPE] === FE_TYPE_DATETIME)) {
            $placeholder = $defaultFormat['date'];
            $formElement[FE_PLACEHOLDER] = $placeholder;
        }

        if ($formElement[F_FE_DATA_PATTERN_ERROR] == '') {
            $formElement[F_FE_DATA_PATTERN_ERROR] = "Please match the format: " . $placeholder;
        }
        $attribute .= HelperFormElement::getAttributeList($formElement, [F_FE_DATA_PATTERN_ERROR, F_FE_DATA_REQUIRED_ERROR, F_FE_DATA_MATCH_ERROR, F_FE_DATA_ERROR]);

        $attribute .= HelperFormElement::getAttributeList($formElement, [FE_INPUT_AUTOCOMPLETE, 'autofocus', 'placeholder']);
        $attribute .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
        $attribute .= Support::doAttribute('title', $formElement[FE_TOOLTIP]);

        $pattern = Sanitize::getInputCheckPattern($formElement[FE_CHECK_TYPE], $formElement[FE_CHECK_PATTERN], '', $sanitizeMessage);
        $attribute .= ($pattern === '') ? '' : 'pattern="' . $pattern . '" ';

        // Use system message only,if no custom one is set.
        if ($formElement[F_FE_DATA_PATTERN_ERROR] == $formElement[F_FE_DATA_PATTERN_ERROR_SYSTEM]) {
            $formElement[F_FE_DATA_PATTERN_ERROR] = $sanitizeMessage;
        }

        // For other dateTimePicker than qfq, min/max values need to be converted
        if ($formElement[FE_DATE_TIME_PICKER_TYPE] !== DATE_TIME_PICKER_QFQ) {
            self::setNoQfqMinMax($formElement);
        }

        $attribute .= HelperFormElement::getAttributeList($formElement, [FE_MIN, FE_MAX]);


        $json = BaseRenderer::getInstance()->renderFormElementJson(new DatetimeFormElement($formElement, new Form($formSpec)));

        $formElement = HelperFormElement::prepareExtraButton($formElement, true);

        $attribute .= HelperFormElement::getAttributeFeMode($formElement[FE_MODE]);

        $input = "<input $attribute>" . HelperFormElement::getHelpBlock() . $formElement[FE_TMP_EXTRA_BUTTON_HTML];

        if ($formElement[FE_TMP_EXTRA_BUTTON_HTML] !== '') {
            $input = Support::wrapTag('<div class="input-group">', $input);
        }

        return $input . $formElement[FE_INPUT_EXTRA_BUTTON_INFO];

    }

    /**
     * @param $formElement
     * @param $formSpec
     * @return string
     */
    private static function getDatePickerClassName($formElement, $formSpec): string {
        $datePickerClassName = '';
        if ($formElement[FE_DATE_TIME_PICKER_TYPE] === DATE_TIME_PICKER_QFQ) {
            $datePickerClassName = 'qfq-datepicker';
        } elseif (isset($formElement[F_FE_INPUT_CLEAR_ME]) && $formElement[F_FE_INPUT_CLEAR_ME] != '0' && $formElement[FE_DATE_TIME_PICKER_TYPE] === DATE_TIME_PICKER_NO) {
            if (($formSpec[F_MULTI_MODE] ?? '') == F_MULTI_MODE_VERTICAL) {
                $datePickerClassName = ' qfq-clear-me-multiform';
            } else {
                $datePickerClassName = ' qfq-clear-me';
            }
        }
        return $datePickerClassName;
    }

    /**
     * @param $formElement
     * @return array
     */
    private static function getDefaultDateFormat(&$formElement): array {
        $defaultDateFormat = explode(' ', $formElement[FE_DATE_FORMAT], 2);
        $defaultFormat = array();
        if (isset($defaultDateFormat[1])) {
            $defaultFormat['time'] = $defaultDateFormat[1];
            $defaultFormat['date'] = $defaultDateFormat[0];
            $defaultFormat['timeParts'] = explode(':', $defaultFormat['time'], 3);
        } else {
            $defaultFormat['date'] = $formElement[FE_DATE_FORMAT];
            $defaultFormat['timeParts'] = explode(':', $formElement[FE_DATE_FORMAT], 3);
        }

        if ($defaultFormat['timeParts'][2] ?? '' === 'ss') {
            $formElement[FE_SHOW_SECONDS] = 1;
        }
        return $defaultFormat;
    }

    /**
     * @param $feType
     * @param $value
     * @return false|mixed|string
     */
    private static function formatValueForBrowser($feType, $value) {
        $dateOldFormat = date_create($value);
        if ($feType === FE_TYPE_DATE) {
            $value = date_format($dateOldFormat, "Y-m-d");
        } elseif ($feType === FE_TYPE_DATETIME) {
            $value = date_format($dateOldFormat, "Y-m-d H:i");
        }
        return $value;
    }

    /**
     * @param $feType
     * @return string
     */
    private static function getDateTimeBrowserType($feType): string {
        if ($feType == FE_TYPE_DATE) {
            $type = 'date';
        } elseif ($feType == FE_TYPE_TIME) {
            $type = 'time';
        } else {
            $type = 'datetime-local';
        }
        return $type;
    }

    /**
     * @param $defaultFormat
     * @param $feType
     * @return array|string
     */
    private static function getDateTimeFormatQfq($defaultFormat, $feType) {
        switch ($defaultFormat['date']) {
            case FORMAT_DATE_INTERNATIONAL:
            case FORMAT_DATE_INTERNATIONAL_QFQ:
            case FORMAT_DATE_GERMAN:
            case FORMAT_DATE_GERMAN_QFQ:
                if ($feType == FE_TYPE_DATETIME) {
                    $defaultFormat['date'] = strtoupper($defaultFormat['date']);
                    if (empty($defaultFormat['time'])) {
                        $defaultFormat['time'] = 'HH:mm';
                    }
                    $defaultFormat['date'] .= ' ' . $defaultFormat['time'];
                } else {
                    $defaultFormat['date'] = strtoupper($defaultFormat['date']);
                }
                break;
        }
        return $defaultFormat;
    }

    /**
     * @param $defaultFormat
     * @param $feDateTimePickerType
     * @param $feShowSeconds
     * @return mixed
     */
    private static function getTimeFormat($defaultFormat, $feDateTimePickerType, $feShowSeconds) {
        if ($defaultFormat['timeParts'][0] === 'HH' || $defaultFormat['timeParts'][0] === 'hh') {
            $defaultFormat['date'] = $defaultFormat['timeParts'][0] . ':' . $defaultFormat['timeParts'][1];
        } else if ($feDateTimePickerType === DATE_TIME_PICKER_QFQ) {
            $defaultFormat['date'] = 'HH:mm';
        } else {
            $defaultFormat['date'] = 'hh:mm';
        }

        if ($feShowSeconds == 1) {
            $defaultFormat['date'] .= ':ss';
        }
        return $defaultFormat;
    }

    /**
     * @param $enabledDays
     * @return false|string
     */
    private static function getDateTimePickerDisabledDays($enabledDays) {
        // convert enabled days from datetimepicker user input daysOfWeekEnabled to disabled days
        $enabledDays = explode(',', $enabledDays);
        $disabledDays = '';

        for ($i = 0; $i <= 6; $i++) {
            $flagDayPoint = false;
            foreach ($enabledDays as $day) {
                if ($day == $i) {
                    $flagDayPoint = true;
                }
            }
            if (!$flagDayPoint) {
                $disabledDays .= $i . ',';
            }
        }

        // if last char ',' exists, remove it
        $lastChar = substr($disabledDays, -1);
        if ($lastChar == ',') {
            $disabledDays = substr($disabledDays, 0, -1);
        }
        return $disabledDays;
    }

    /**
     * @param $formElement
     * @param $defaultDateFormat
     * @param $dateTimeKeys
     * @param $dateTimeAttributes
     * @param $attribute
     * @return void
     * @throws \CodeException
     */
    private static function setDateTimePickerAttributes($formElement, $defaultDateFormat, $dateTimeKeys, $dateTimeAttributes, &$attribute) {
        $keyCount = 0;
        foreach ($dateTimeKeys as $key) {
            if (isset($formElement[$key]) && $formElement[$key] != "" && $key != FE_DATE_FORMAT) {
                $attribute .= Support::doAttribute('data-' . $dateTimeAttributes[$keyCount], $formElement[$key]);
            } elseif ($key == FE_DATE_FORMAT) {
                $attribute .= Support::doAttribute('data-' . $dateTimeAttributes[$keyCount], $defaultDateFormat);
            }
            $keyCount++;
        }
    }

    /**
     * @param $formElement
     * @return void
     */
    private static function setNoQfqMinMax(&$formElement) {
        if ($formElement[FE_TYPE] == FE_TYPE_DATE) {
            $dateMinMaxFormat = "Y-m-d";
        } else {
            $dateMinMaxFormat = "Y-m-d H:i";
        }

        if (isset($formElement[FE_MIN]) && $formElement[FE_MIN] !== '') {
            $dateMin = date_create($formElement[FE_MIN]);
            $formElement[FE_MIN] = date_format($dateMin, $dateMinMaxFormat);
        }

        if (isset($formElement[FE_MAX]) && $formElement[FE_MAX] !== '') {
            $dateMax = date_create($formElement[FE_MAX]);
            $formElement[FE_MAX] = date_format($dateMax, $dateMinMaxFormat);
        }
    }

    /**
     * Depending on $dateFormatSave, take/replace day/month/year in array $date.
     * Keywords dynamically substituted:
     *
     *   dd, mm, yyyy: corresponding values in $date untouched
     *   dd-today, mm-today, year-todoy:  corresponding values in $date replaced by todays values.
     *   dd-last: value day $date replaced by last day of given month/year.
     *
     * @param string $dateFormatSave
     * @param array $date - we expect an array with three elements.
     * @return string
     */
    private static function dateReplacePlaceholder(string $dateFormatSave, array $date): string {
        $today = new \DateTime();

        // Checks if date is set in dateFormatSave (z.B. "dd.mm.yyyy HH:mm")
        $parts = explode(' ', $dateFormatSave, 2);
        $dateFormat = $parts[0];
        $timeFormat = $parts[1] ?? '';

        // If time format is appended: strip it
        $arr = explode(' ', $dateFormatSave);

        $arr = explode('.', $arr[0]);
        if (count($arr) != 3) {
            throw new \UserFormException("Option " . FE_DATE_FORMAT_SAVE . " unhandled format: " . $arr[0], ERROR_UNEXPECTED_TYPE);
        }

        # Year
        switch ($arr[IDX_YEAR] ?? '') {
            case FMT_DATE_YYYY;
                // Value is already given/set by datetimepicker
                break;
            case FMT_DATE_YYYY_TODAY:
                $date[IDX_YEAR] = (int)$today->format('Y');
                break;
            case '':
                throw new \UserFormException("Option " . FE_DATE_FORMAT_SAVE . " missing year: " . $dateFormatSave, ERROR_UNEXPECTED_TYPE);
            default:
                // A fixed number might be given.
                $date[IDX_YEAR] = $arr[IDX_YEAR];
                break;
        }

        # Month
        switch ($arr[IDX_MONTH] ?? '') {
            case FMT_DATE_MM;
                // Value is already set by datetimepicker
                break;
            case FMT_DATE_MM_TODAY:
                $date[IDX_MONTH] = (int)$today->format('m');
                break;
            case '':
                throw new \UserFormException("Option " . FE_DATE_FORMAT_SAVE . " missing month: " . $dateFormatSave, ERROR_UNEXPECTED_TYPE);
            default:
                // A fixed number might be given.
                $date[IDX_MONTH] = $arr[IDX_MONTH];
                break;
        }

        # Day
        switch ($arr[IDX_DAY] ?? '') {
            case FMT_DATE_DD;
                // Value is already set by datetimepicker
                break;
            case FMT_DATE_DD_TODAY:
                $date[IDX_DAY] = (int)$today->format('d');
                break;
            case FMT_DATE_DD_LAST:
                // Last day of given month/year
                $date[IDX_DAY] = cal_days_in_month(CAL_GREGORIAN, $date[IDX_MONTH], $date[IDX_YEAR]);
                break;
            case '':
                throw new \UserFormException("Option " . FE_DATE_FORMAT_SAVE . " missing day: " . $dateFormatSave, ERROR_UNEXPECTED_TYPE);

            default:
                // A fixed number might be given.
                $date[IDX_DAY] = $arr[IDX_DAY];
                break;
        }
        $date[IDX_DAY] = sprintf('%02d', $date[IDX_DAY]);
        $date[IDX_MONTH] = sprintf('%02d', $date[IDX_MONTH]);
        $date[IDX_YEAR] = sprintf('%04d', $date[IDX_YEAR]);
        $result = implode('.', $date);

        //  add time if set in dateFormatSave = dd.mm.2001 08:00
        if ($timeFormat !== '') {
            $result .= ' ' . $timeFormat;
        }

        return $result;
    }

    /**
     * Depnding on $dateFormat - $value might be:
     * dd (day only), dd.mm (day.month), dd.mm.yyyy (full date), mm (month only), mm.year (month.year), year (year only).
     *
     * @param $dateFormat
     * @param $dateFormatSave
     * @param $value
     * @return string
     * @throws \UserFormException
     */
    private static function completePartialDate($dateFormat, $dateFormatSave, $value): string {

        $newDate = ['', '', ''];
        $restoreTime = '';

        # Value: Currently there is no 'partial time' - if time is given, save it and restore later.
        $arr = explode(' ', $value);
        if (($arr[1] ?? '') != '') {
            $restoreTime = ' ' . $arr[1];
        }

        # Split date into pieces. Depending of $dateFormat, this might be 1, 2 or 3 pieces.
        $date = explode('.', $arr[0]);

        # If given, strip TIME format definition
        $arr = explode(' ', $dateFormat, 2);
        $fmtDate = $arr[0];
        switch ($fmtDate) {
            case FMT_DATE_DD:
                $newDate[IDX_DAY] = $date[0];
                break;
            case FMT_DATE_DD_MM:
                $newDate[IDX_DAY] = $date[0];
                $newDate[IDX_MONTH] = $date[1];
                break;

            case FMT_DATE_DD_MM_YYYY:
                # Full date: nothing to do here
                return $value;

            case FMT_DATE_MM:
                $newDate[IDX_MONTH] = $date[0];
                break;
            case FMT_DATE_MM_YYYY:
                $newDate[IDX_MONTH] = $date[0];
                $newDate[IDX_YEAR] = $date[1];
                break;
            case FMT_DATE_YYYY:
                $newDate[IDX_YEAR] = $date[0];
                break;

            # Only Time: nothing to do here
            case 'hh:mm':
            case 'hh:mm:ss':
                return $value;

            default:
                throw new \UserFormException("Option " . FE_DATE_FORMAT . " unhandled format: " . $dateFormat, ERROR_UNEXPECTED_TYPE);
        }

        $finalDate = self::dateReplacePlaceholder($dateFormatSave, $newDate);

        return $finalDate . $restoreTime;
    }


    /**
     * Validates and normalizes a date/time string based on QFQ form element configuration.
     *
     * Takes a user input, based on $formElement[FE_DATE_FORMAT] & $formElement[FE_DATE_FORMAT_SAVE].
     * (e.g., "2025-02-28" or "2025-02-28 14:30:00").
     * It supports partial input, appends default values for missing components, and resolves
     * placeholder tokens such as `dd-today` or `dd-last` as defined.
     *
     * Examples:
     * ---------
     * 1. dateFormat="dd.mm.yyyy", dateFormatSave=<empty> or "dd.mm.yyyy"
     * Input:     "15.02.2025"
     * Output:    "2025-02-15"
     *
     * 2. dateFormat=mm.yyyy", dateFormatSave="dd-last.mm.yyyy"
     * Input:     "03.2025"
     * Output:    "2025-03-31"   // last day of March
     *
     * 3. dateFormat="yyyy", dateFormatSave="dd-today.mm-today.yyyy"
     * Input:     "2025"
     * Output:    "2025-06-24"   // current day/month inserted
     *
     * 4. dateFormat="dd.mm.yyyy hh:mm", dateFormatSave=<empty> or "dd.mm.yyyy" or "dd.mm.yyyy hh:mm"
     * Input:     "15.02.2025 14:45"
     * Output:    "2025-02-15 14:45:00"
     *
     * 5. Browser Format Input (DATE_TIME_PICKER_BROWSER enabled)
     * Input:     "2025-02-15T08:30"
     * Output:    "2025-02-15 08:30:00"
     *
     * Special Cases:
     * --------------
     * - Placeholders such as `dd-today`, `mm-today`, `yyyy-today`, and `dd-last` are supported
     * - Supports mixed input with fixed values (e.g., "dd.01.yyyy") and user-supplied parts
     * - Supports optional or required seconds field (HH:mm:ss)
     * - Supports ISO-formatted browser input ("YYYY-MM-DDTHH:MM")
     *
     * @param array  &$formElement Reference to the form element configuration. Must contain at minimum:
     * - FE_DATE_FORMAT
     * - FE_DATE_FORMAT_SAVE
     *
     * @param string $value User-provided input string, e.g. "15.02.2025", "2025", or "2025-02-28 14:30"
     * @return string
     */
    public static function doDateTime(array &$formElement, $value): string {

        if ($value == '') {
            switch ($formElement[FE_TYPE]) {
                case 'time':
                    return '00:00:00';
                    break;
                case 'datetime':
                    return '0000-00-00 00:00:00';
                    break;
                default:
                    return '0000-00-00';
            }
        }

        // check for browser dateTimePickerType and adjust value
        $typeBrowser = false;

        if ($formElement[FE_DATE_TIME_PICKER_TYPE] === DATE_TIME_PICKER_BROWSER) {
            $typeBrowser = true;
            $datetimeArray = explode("T", $value, 2);
            if (preg_match("/^(?:2[0-3]|[01][0-9]):[0-5][0-9]$/", $datetimeArray[1])) {
                $value = $datetimeArray[0] . " " . $datetimeArray[1];
            }

            if (($formElement[FE_DATE_FORMAT_SAVE] ?? '') != '') {
                throw new \UserFormException("Option " . FE_DATE_FORMAT_SAVE . " is only supported for dateTimePicker=qfq (not browser).", ERROR_UNEXPECTED_TYPE);
            }
        }

        if (($formElement[FE_DATE_FORMAT_SAVE] ?? '') != '') {
            $value = self::completePartialDate($formElement[FE_DATE_FORMAT], $formElement[FE_DATE_FORMAT_SAVE], $value);
        }

        $dateFormat = explode(':', $formElement[FE_DATE_FORMAT], 3);
        if (($dateFormat[2] ?? '') === 'ss') {
            $formElement[FE_SHOW_SECONDS] = 1;
        }

        $regexp = Support::dateTimeRegexp($formElement[FE_TYPE], $formElement[FE_DATE_FORMAT], $formElement[FE_TIME_IS_OPTIONAL] ?? "1");

        if (1 !== preg_match('/' . $regexp . '/', $value, $matches) && !$typeBrowser) {
            throw new \UserFormException("DateTime format not recognized: '$value' ", ERROR_DATE_TIME_FORMAT_NOT_RECOGNISED);
        }

        $showTime = $formElement[FE_TYPE] == FE_TYPE_DATE ? '0' : '1';
        $value = Support::convertDateTime($value, FORMAT_DATE_INTERNATIONAL, '1', $showTime, $formElement[FE_SHOW_SECONDS]);
        $formElement[FE_MIN] = Support::convertDateTime($formElement[FE_MIN], FORMAT_DATE_INTERNATIONAL, '1', $showTime, $formElement[FE_SHOW_SECONDS]);
        if (isset($formElement[FE_MAX]) && $formElement[FE_MAX] !== '') {
            $formElement[FE_MAX] = Support::convertDateTime($formElement[FE_MAX], FORMAT_DATE_INTERNATIONAL, '1', $showTime, $formElement[FE_SHOW_SECONDS]);
        }

        if ($formElement[FE_TYPE] !== FE_TYPE_TIME) {
            // Validate date (e.g. 2010-02-31)
            $dateValue = explode(' ', $value)[0];
            $dateParts = explode('-', $dateValue);
            if ($dateValue != '0000-00-00' && !checkdate($dateParts[1], $dateParts[2], $dateParts[0]))
                throw new \UserFormException("$dateValue is not a valid date.", ERROR_INVALID_DATE);
        }
        return $value;
    }

}