<?PHP
/**
 * Gathers wrappers for handling inputs.
 */
declare(strict_types = 1);

/**
 * Encapsulates functions for handling inputs.
 */
final class MD_STD_IN {
    /**
     * Validates and sanitizes input integers to be in line with MySQL
     * autoincrement IDs.
     *
     * @param mixed $input Input string.
     *
     * @return integer
     */
    public static function sanitize_id(mixed $input):int {

        $input = \filter_var($input, \FILTER_VALIDATE_INT, [
                'options' => [
                    'min_range' => 1,           // Minimum number of an ID generated.
                    'max_range' => 4294967295   // Max value for MySQL's int data type
                ],
            ]
        );

        if (!$input) {
            throw new MDpageParameterNotNumericException("Value is not numeric.");
        }

        return $input;

    }

    /**
     * Sanitizes and validates input integers to be either valid IDs or 0.
     *
     * @param mixed $input Input string.
     *
     * @return integer
     */
    public static function sanitize_id_or_zero(mixed $input):int {

        if ($input === "" || $input === 0) {
            return 0;
        }

        $input = \filter_var($input, \FILTER_VALIDATE_INT, [
                'options' => [
                    'min_range' => 0,           // Minimum number of an ID generated.
                    'max_range' => 4294967295   // Max value for MySQL's int data type
                ],
            ]
        );

        if ($input === false) {
            throw new MDpageParameterNotNumericException("Value is not numeric.");
        }

        return $input;

    }

    /**
     * General string sanitization for all purposes. For use of inputs with MySQL's
     * MATCH AGAINST, use the dedicated sanitization function.
     *
     * @param mixed $input Input string.
     *
     * @return string
     */
    public static function sanitize_text(mixed $input):string {

        $output = \filter_var($input, FILTER_UNSAFE_RAW);

        if ($output === false) {
            return "";
        }
        $output = strip_tags($output);
        while (strpos($output, "  ") !== false) {
            $output = str_replace("  ", " ", $output);
        }

        return trim($output);

    }

    /**
     * String sanitization for 3 o4 6 characters RGB color codes (sans the leading #).
     *
     * @param mixed $input Input string.
     *
     * @return string
     */
    public static function sanitize_rgb_color(mixed $input):string {

        if (empty($output = \filter_var($input, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH))) {
            throw new MDInvalidColorCode("Invalid color code provided: " . $output);
        }

        $output = \strtoupper($output);

        if (!in_array(strlen($output), [3, 6], true)
            || (MD_STD::preg_replace_str('/[A-F0-9]/', '', $output) !== '')
        ) {
            throw new MDInvalidColorCode("Invalid color code provided: " . $output);
        }

        return $output;

    }

    /**
     * Valiates a list of entries, ensuring all returned values are valid IDs.
     *
     * @param array<mixed> $inputs Input array.
     *
     * @return list<integer>
     */
    public static function sanitize_id_array(array $inputs):array {

        $output = [];

        foreach ($inputs as $input) {
            $output[] = self::sanitize_id($input);
        }

        return $output;

    }

    /**
     * Retrieves HTTP input texts from GET or POST variables, whatever is provided.
     * If neither is given, returns a provided default.
     *
     * @param non-empty-string $var_name Variable name.
     * @param string           $default  Default value for the output.
     * @param array<string>    $allowed  List of allowed values. Defaults to empty (all values allowed).
     *
     * @return string
     */
    public static function get_http_input_text(string $var_name, string $default = "", array $allowed = []):string {

        if (isset($_GET[$var_name])) {
            $output = self::sanitize_text($_GET[$var_name]);
        }
        else if (isset($_POST[$var_name])) {
            $output = self::sanitize_text($_POST[$var_name]);
        }
        else {
            $output = self::sanitize_text($default);
        }

        if (!empty($allowed) and !\in_array($output, $allowed, true)) {
            throw new MDpageParameterNotFromListException("Parameter `{$var_name}` must be any of the allowed values: '" . implode('\', \'', $allowed) . "'");
        }

        return $output;

    }

    /**
     * Retrieves HTTP input texts from POST variables.
     * If none is given, returns a provided default.
     *
     * @param non-empty-string $var_name Variable name.
     * @param string           $default  Default value for the output.
     * @param array<string>    $allowed  List of allowed values. Defaults to empty (all values allowed).
     *
     * @return string
     */
    public static function get_http_post_text(string $var_name, string $default = "", array $allowed = []):string {

        if (isset($_POST[$var_name])) {
            $output = self::sanitize_text($_POST[$var_name]);
        }
        else {
            $output = self::sanitize_text($default);
        }

        if (!empty($allowed) and !\in_array($output, $allowed, true)) {
            throw new MDpageParameterNotFromListException("Parameter `{$var_name}` must be any of the allowed values: '" . implode('\', \'', $allowed) . "'");
        }

        return $output;

    }

    /**
     * Sanitizes and validates a URL. An empty string passes.
     *
     * @param mixed $input Input string.
     *
     * @return string
     */
    public static function sanitize_url(mixed $input):string {

        if ($input === "") {
            return "";
        }

        try {
            if (($output = \filter_var($input, FILTER_VALIDATE_URL)) === false) {
                throw new MDInvalidUrl("Invalid input URL");
            }
        }
        catch (MDInvalidUrl $e) {
            if (($parsed = parse_url($input)) === false || empty($parsed['scheme']) || empty($parsed['host']) || empty($parsed['path'])) {
                throw new MDInvalidUrl("Invalid input URL");
            }
            $rewritten = $parsed['scheme'] . '://';
            if (!empty($parsed['user']) && !empty($parsed['pass'])) {
                $rewritten .= $parsed['user'] . ':' . $parsed['pass'] . '@';
            }
            $rewritten .= $parsed['host'];
            if (!empty($parsed['port'])) $rewritten .= ':' . $parsed['port'];
            $rewritten .= str_replace('%2F' , '/', urlencode($parsed['path']));
            if (!empty($parsed['query'])) {
                $rewritten .= '?' . str_replace('%3D', '=', urlencode($parsed['query']));
            }
            if (($output = \filter_var($rewritten, FILTER_VALIDATE_URL)) === false) {
                throw new MDInvalidUrl("Invalid input URL" . \urlencode($input));
            }
        }

        if (empty($output)) return '';
        // As per the RFC, URLs should not exceed 2048. Enough real-world ones
        // do. But they certainly should not exceed 10000 characters.
        if (\strlen($output) > 10000) {
            throw new MDInvalidUrl("The entered URL seems to be valid otherwise, but is overly long.");
        }

        // Check for valid schemes
        try {
            if (MD_STD::startsWithAny($output, ['https://', 'http://', 'ftp://']) === false) {
                throw new MDInvalidUrl("Invalid input URL" . PHP_EOL . $output . PHP_EOL . strtolower($output));
            }
        }
        catch (MDInvalidUrl $e) {
            if (MD_STD::startsWithAny(strtolower($output), ['https://', 'http://', 'ftp://']) === true) {
                $output = strtolower(substr($output, 0, 8)) . substr($output, 8);
            }
            else throw $e;
        }

        if (\str_contains($output, '.') === false) {
            throw new MDInvalidUrl("Invalid input URL");
        }

        return $output;

    }

    /**
     * Sanitizes and validates an e-mail address. An empty string passes.
     *
     * @param mixed $input Input string.
     *
     * @return string
     */
    public static function sanitize_email(mixed $input):string {

        if ($input === "") {
            return "";
        }

        if (($output = \filter_var($input, FILTER_VALIDATE_EMAIL)) === false) {
            throw new MDInvalidEmail("Invalid input email address" . ' '. $input);
        }

        return $output;

    }

    /**
     * Validates a password (minimum requirements: 8 characters, including
     * one number and one special char) and returns a list of errors,
     * if there are any.
     *
     * @param string $input Input string.
     *
     * @return array<string>
     */
    public static function validate_password(string $input):array {

        $errors = [];
        if (mb_strlen($input) < 8) {
            $errors[] = 'password_too_short';
        }

        if (!(\preg_match('@[0-9]@', $input)) && !(\preg_match('@[^\w]@', $input))) {
            $errors[] = 'password_has_no_number_no_special_char';
        }

        return $errors;

    }

    /**
     * Sanitizes and validates a phone number. An empty string passes.
     *
     * @param mixed $input Input string.
     *
     * @return string
     */
    public static function validate_phone_number(mixed $input):string {

        if ($input === "") {
            return "";
        }

        if (!preg_match("#^[()0-9/ +-]+$#", $input)) {
            throw new MDgenericInvalidInputsException("Invalid phone number entered.");
        }

        return $input;

    }

    /**
     * Sanitizes a string to a float.
     *
     * @param string $input Input string.
     *
     * @return float
     */
    public static function sanitize_float(string $input):float {

        $output = \str_replace(",", ".", $input);
        if (($output = \filter_var($output, FILTER_VALIDATE_FLOAT)) === false) {
            throw new MDgenericInvalidInputsException("Input is readable as a floating point value");
        }
        return $output;

    }

    /**
     * Validates a coordinate.
     *
     * @param string|integer $input Input string.
     *
     * @return float
     */
    public static function validate_longitude(string|int|float $input):float {

        if (is_string($input)) $output = self::sanitize_float($input);
        else $output = $input;

        if ($output < -180 || $output > 180) {
            throw new MDCoordinateOutOfRange("Longitude out of range");
        }

        return $output;

    }

    /**
     * Validates a coordinate.
     *
     * @param string|integer $input Input string.
     *
     * @return float
     */
    public static function validate_latitude(string|int|float $input):float {

        if (is_string($input)) $output = self::sanitize_float($input);
        else $output = $input;

        if ($output < -90 || $output > 90) {
            throw new MDCoordinateOutOfRange("Latitude out of range");
        }

        return $output;

    }

    /**
     * Validates ISBNs. Empty strings are accepted as well.
     *
     * @param string $input Input string.
     *
     * @return string
     */
    public static function validate_isbn(string $input):string {

        if ($input === "") {
            return "";
        }

        // Remove hyphens
        $input = trim(strtr($input, ["-" => "", "–" => ""]));

        // ISBN 10
        if (\mb_strlen($input) === 10) {

            if (\preg_match('/\d{9}[0-9xX]/i', $input)) {
                return $input;
            }

        }

        // ISBN 13
        if (\mb_strlen($input) === 13) {

            if (\preg_match('/\d{13}/i', $input)) {
                return $input;
            }

        }

        throw new MDgenericInvalidInputsException("ISBNs must be either 10 or 13 characters long.");

    }

    /**
     * Validates a ZIP code.
     *
     * @param string $input Input string.
     *
     * @return string
     */
    public static function validate_zip_code(string $input):string {

        if (($input = trim($input)) === "") {
            return "";
        }

        if (\mb_strlen($input) > 7) {
            throw new MDgenericInvalidInputsException("ZIP code is too long");
        }

        return $input;

    }

    /**
     * Returns an UTF8 version of a string.
     *
     * @param string $input Input string.
     *
     * @return string
     */
    public static function ensureStringIsUtf8(string $input):string {

        // If the input is valid UTF8 from the start, it is simply returned in its
        // original form.
        if (\mb_check_encoding($input, 'UTF-8')) {
            return $input;
        }

        // To detect and convert the encoding for non-UTF8 strings, the list of
        // encodings known to PHP's mbstring functions is checked against the input string.
        // If any encoding matches the string, it will be converted to UTF8 accordingly.
        $suitableEncodings = [];
        $encodings = \mb_list_encodings();
        foreach ($encodings as $encoding) {
            if (\mb_detect_encoding($input, $encoding, true) !== false) {
                $suitableEncodings[] = $encoding;
            }
        }

        // If ISO-8859-1 is in the list of suitable encodings, try to convert with that.
        if (\in_array('ISO-8859-1', $suitableEncodings, true)) {
            if (($converted = \iconv('ISO-8859-1', "UTF-8//TRANSLIT", $input)) !== false) {
                return $converted;
            }
        }

        // If a conversion from ISO-8859-1 doesn't work, just take any of the other ones.
        $suitableEncodings = \array_reverse($suitableEncodings);
        foreach ($suitableEncodings as $encoding) {
            if (($converted = \iconv($encoding, "UTF-8//TRANSLIT", $input)) !== false) {
                return $converted;
            }
        }

        /*
        if (count($suitableEncodings) === 1) {
            return mb_convert_encoding($input, 'UTF-8', );
        }
         */

        return $input;

    }

    /**
     * Wrapper around move_uploaded_file that throws errors in case the upload failed
     * for an identifiable reason.
     *
     * @param non-empty-string $filename    Name of the file to upload.
     * @param non-empty-string $destination Destination to move the file to.
     * @param array<string>    $mime_types  Optional array of acceptable mime types. If this is
     *                                      not empty, the file will be checked for having one
     *                                      of the given mime types. If it does not, an error
     *                                      will be thrown.
     *
     * @return boolean
     */
    public static function move_uploaded_file(string $filename, string $destination, array $mime_types = []):bool {

        MD_STD::ensure_file($filename, $mime_types);
        if (empty($destDir = dirname($destination))) {
            return false;
        }
        MD_STD::check_is_writable($destDir);

        if (!(\move_uploaded_file($filename, $destination))) {
            return false;
        }

        return true;

    }
}