<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/2/16
 * Time: 10:57 PM
 */

namespace IMATHUZH\Qfq\Core\Helper;


/**
 * Class Sanitize
 * @package qfq
 */
class Sanitize {

    private static $sanitizePattern = [
        SANITIZE_ALLOW_ALNUMX => PATTERN_ALNUMX, // ':alnum:' does not work here in FF
        SANITIZE_ALLOW_DIGIT => PATTERN_DIGIT,
        SANITIZE_ALLOW_NUMERICAL => PATTERN_NUMERICAL,
        SANITIZE_ALLOW_EMAIL => PATTERN_EMAIL,
        SANITIZE_ALLOW_ALLBUT => PATTERN_ALLBUT,
        SANITIZE_ALLOW_ALL => PATTERN_ALL,
        SANITIZE_ALLOW_PATTERN => '',
    ];

    private static $sanitizeMessage = [
        SANITIZE_ALLOW_ALNUMX => SANITIZE_ALLOW_ALNUMX_MESSAGE,
        SANITIZE_ALLOW_DIGIT => SANITIZE_ALLOW_DIGIT_MESSAGE,
        SANITIZE_ALLOW_NUMERICAL => SANITIZE_ALLOW_NUMERICAL_MESSAGE,
        SANITIZE_ALLOW_EMAIL => SANITIZE_ALLOW_EMAIL_MESSAGE,
        SANITIZE_ALLOW_ALLBUT => SANITIZE_ALLOW_ALLBUT_MESSAGE,
        SANITIZE_ALLOW_ALL => '',
        SANITIZE_ALLOW_PATTERN => F_FE_DATA_PATTERN_ERROR_DEFAULT,
    ];

    private function __construct() {
        // Class should never be instantiated
    }

    /**
     * Check $value against given checkType/pattern. If check succeed, returns values.
     * If check fails, depending on $mode
     * a) throws an UserException
     * b) return message:
     *    $typeMessageViolate=SANITIZE_TYPE_MESSAGE_VIOLATE_EMPTY  - return empty string
     *    $typeMessageViolate=SANITIZE_TYPE_MESSAGE_VIOLATE_ZERO  - return '0'
     *    $typeMessageViolate=SANITIZE_TYPE_MESSAGE_VIOLATE_EMPTY  - return '!!<sanitize class>!!'
     *    $typeMessageViolate=<nothing from the above>  - return '$typeMessageViolate'
     *
     * @param string $value value to check
     * @param string $sanitizeClass
     * @param string $pattern Pattern as regexp
     * @param string $decimalFormat with 'size,precision'
     * @param string $mode SANITIZE_EXCEPTION | SANITIZE_EMPTY_STRING
     * @param string $dataPatternErrorText
     * @param string $typeMessageViolate SANITIZE_TYPE_MESSAGE_VIOLATE_EMPTY | SANITIZE_TYPE_MESSAGE_VIOLATE_ZERO | SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS | <custom>
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    public static function sanitize($value, $sanitizeClass = SANITIZE_DEFAULT, $pattern = '', $decimalFormat = '',
                                    $mode = SANITIZE_EMPTY_STRING, $dataPatternErrorText = '', $typeMessageViolate = SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS) {

        $pattern = self::getInputCheckPattern($sanitizeClass, $pattern, $decimalFormat, $sanitizeMessage);

        // Pattern check
        if ($pattern === '' || preg_match("/$pattern/", $value) === 1) {
            return $value;
        }

        // check failed
        if ($mode === SANITIZE_EXCEPTION) {
            $errorCode = ERROR_PATTERN_VIOLATION;

            // Depending on default regexp & custom data-pattern-error, define error text.
            if ($dataPatternErrorText == '') {
                if ($sanitizeMessage == '') {
                    $errorText = "Value '$value' violates check rule " . $sanitizeClass . " with pattern '$pattern'.";
                } else {
                    $errorText = $sanitizeMessage;
                }
            } else {
                $errorText = $dataPatternErrorText;
            }

            throw new \UserFormException($errorText, $errorCode);
        }

        switch ($typeMessageViolate) {
            case SANITIZE_TYPE_MESSAGE_VIOLATE_EMPTY:
                $message = '';
                break;
            case SANITIZE_TYPE_MESSAGE_VIOLATE_ZERO:
                $message = '0';
                break;
            case SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS:
                $message = SANITIZE_VIOLATE . $sanitizeClass . SANITIZE_VIOLATE;
                break;
            default:
                $message = $typeMessageViolate;
        }

        return $message;
    }

    /**
     * Returns the final validation pattern based on a given $checkType, $pattern, and $decimalFormat.
     *
     * @param string $checkType SANITIZE_DEFAULT, ...
     * @param string $pattern
     * @param string $decimalFormat e.g. "10,2"
     *
     * @param string $rcSanitizeMessage Message specific to a pattern
     * @return string
     * @throws \CodeException
     */
    public static function getInputCheckPattern($checkType, $pattern, $decimalFormat, &$rcSanitizeMessage) {

        $rcSanitizeMessage = self::$sanitizeMessage[$checkType] ?? '';

        switch ($checkType) {
            case SANITIZE_ALLOW_PATTERN:
                return $pattern;

            case SANITIZE_ALLOW_ALL:
                $pattern = '';
                break;

            case SANITIZE_ALLOW_DIGIT:
            case SANITIZE_ALLOW_NUMERICAL:
            case SANITIZE_ALLOW_EMAIL:
            case SANITIZE_ALLOW_ALNUMX:
            case SANITIZE_ALLOW_ALLBUT:
                $pattern = self::$sanitizePattern[$checkType];
                break;

            default:
                throw new \CodeException("Unknown checktype/sanitize class: " . $checkType, ERROR_UNKNOWN_CHECKTYPE);
        }

        // decimalFormat
        if ($decimalFormat != '' && $checkType !== SANITIZE_ALLOW_DIGIT) {
            // overwrite pattern with decimalFormat pattern
            $decimalFormatArray = explode(',', $decimalFormat);
            $pattern = "^-?[0-9]{0," . ($decimalFormatArray[0] - $decimalFormatArray[1]) . "}(\.[0-9]{0,$decimalFormatArray[1]})?$";
            $rcSanitizeMessage = "Requested decimal format (mantis,decimal): $decimalFormat";
        }

        return $pattern;
    }

    /**
     * Check $value against $formElement's min/max values. If check succeeds, returns value.
     *   If check fails, depending on $mode, throws an UserException or return an empty string.
     *
     * @param string $value value to check
     * @param $min
     * @param $max
     * @param string $mode SANITIZE_EXCEPTION | SANITIZE_EMPTY_STRING
     * @return string
     * @throws \UserFormException
     */
    public static function checkMinMax($value, $min, $max, $mode = SANITIZE_EMPTY_STRING) {
        $errorCode = 0;
        $errorText = '';

        if ($min !== '' && $value < $min) {
            $errorCode = ERROR_SMALLER_THAN_MIN;
            $errorText = "Value '$value' is smaller than the allowed minimum of '$min'.";
        }
        if ($max !== '' && $value > $max) {
            $errorCode = ERROR_LARGER_THAN_MAX;
            $errorText = "Value '$value' is larger than the allowed maximum of '$max'.";
        }

        if ($errorCode == 0)
            return $value;

        // check failed
        if ($mode === SANITIZE_EXCEPTION) {
            throw new \UserFormException($errorText, $errorCode);
        }

        return '';
    }

    /**
     * Sanitizes a filename. Copied from http://www.phpit.net/code/filename-safe/
     *
     * @param $filename
     *
     * @param bool $flagBaseName
     * @param bool $allowSlash
     * @return mixed
     */
    public static function safeFilename($filename, $flagBaseName = false, $allowSlash = false, $fileNameToLower = false) {

        // Disallow 'none alphanumeric'. Allow dot or underscore and conditionally '/'.
        $pattern = ($allowSlash) ? '([^[:alnum:]._\-/])' : '([^[:alnum:]._\-])';

        $search = array(
            // Definition of German Umlauts START
            '/ß/',
            '/ä/', '/Ä/',
            '/ö/', '/Ö/',
            '/ü/', '/Ü/',
            // Definition of German Umlauts ENDE
            $pattern,
        );

        $replace = array(
            'ss',
            'ae', 'Ae',
            'oe', 'Oe',
            'ue', 'Ue',
            '_',
        );

        if ($flagBaseName) {
            $filename = basename($filename);
        }

        if ($fileNameToLower) {
            $file = pathinfo($filename);
            $filename = strtolower($file['filename']) . '.' . $file['extension'];

        }

        return preg_replace($search, $replace, $filename);
    } // safeFilename()

    /**
     * htmlentities($data) - if $data is an array, convert it recursively.
     *
     * @param string|array $data
     * @param int $mode
     *
     * @return array|string
     */
    public static function htmlentitiesArr($data, $mode = ENT_QUOTES) {

        if (is_string($data)) {
            htmlentities($data, $mode);
        }

        if (is_array($data)) {
            foreach ($data as $key => $value) {
                $data[$key] = self::htmlentitiesArr($value, $mode);
            }
        }

        return $data;
    }

    /**
     * Take the given $item (or iterates over all elements of the given array) and normalize the content.
     * Only strings will be normalized. Sub arrays will be normalized recursive. Numeric content is skipped.
     * Throws an exception for unknown content.
     *
     * It's important to normalize the user input: e.g. someone is searching for a record and input the search string
     * with composed characters (happens e.g. on Apple Mac / Safari without special user invention).
     *
     * @param array|string $item
     *
     * @return array|string
     * @throws \CodeException
     */
    public static function normalize($item) {

        if (is_array($item)) {
            foreach ($item as $key => $value) {
                $value = self::normalize($value);
                $item[$key] = $value;
            }
        } else {
            if (is_string($item)) {
                $item = \normalizer::normalize($item, \Normalizer::FORM_C);
            } elseif (!is_numeric($item)) {
                throw new \CodeException ("Expect type 'string / numeric / array' - but there is something else.", ERROR_UNEXPECTED_TYPE);
            }
        }

        return $item;
    }


    /**
     * Check a given $_GET[$key] is digit.
     * If yes: do nothing
     * If no: set to the first character (if it is a digit) else to an empty string.
     *
     * @param $key
     */
    public static function digitCheckAndCleanGet($key) {

        if (!isset($_GET[$key])) {
            $_GET[$key] = '';
            return;
        }

        if (ctype_digit((string)$_GET[$key])) {
            return;
        }

        if (ctype_digit($_GET[$key][0] ?? '')) {
            $_GET[$key] = $_GET[$key][0];
        } else {
            $_GET[$key] = '';
        }
    }
}