<?php

namespace App\Util;

use Exception;

class Helper
{

    public static $encoding = 'UTF-8';

    // Define the extensions for each file type
    private static $videoExtensions = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'];
    private static $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff'];
    private static $audioExtensions = ['mp3', 'wav', 'aac', 'flac', 'ogg', 'wma'];
    private static $documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf', 'odt'];
    private static $softwareExtensions = ['exe', 'msi', 'apk', 'dmg', 'pkg', 'deb', 'rpm'];

    public static function getDevice(): string
    {
        return $_SERVER['HTTP_USER_AGENT'];
    }

    public static function getDeviceId(): string
    {
        return hash('sha256', $_SERVER['HTTP_USER_AGENT']);
    }

    /**
     * Determine the type of the given filename.
     *
     * @param string $fileName The exact file name (e.g., 'example.mp4').
     * @return string Returns the type of the file ('video', 'audio', 'image', 'document', 'software', 'others').
     */
    public static function getFormat(string $fileName): string {
        // Extract the file extension
        $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));

        // Check the extension and return the corresponding type
        if (in_array($extension, self::$videoExtensions, true)) {
            return 'video';
        } elseif (in_array($extension, self::$imageExtensions, true)) {
            return 'image';
        } elseif (in_array($extension, self::$audioExtensions, true)) {
            return 'audio';
        } elseif (in_array($extension, self::$documentExtensions, true)) {
            return 'document';
        } elseif (in_array($extension, self::$softwareExtensions, true)) {
            return 'software';
        } else {
            return 'others';
        }
    }

    /**
     * Determine if the given filename matches the specified format.
     *
     * @param string $fileName The exact file name (e.g., 'example.mp4').
     * @param string $format The format to check ('video', 'image', or 'audio').
     * @return bool Returns true if the file matches the specified format, false otherwise.
     */
    public static function isFormat(string $fileName, string $format): bool {
        // Extract the file extension
        $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));

        // Check the format and validate the extension
        switch ($format) {
            case 'video':
                return in_array($extension, self::$videoExtensions, true);
            case 'image':
                return in_array($extension, self::$imageExtensions, true);
            case 'audio':
                return in_array($extension, self::$audioExtensions, true);
            case 'software':
                return in_array($extension, self::$softwareExtensions, true);
            case 'document':
                return in_array($extension, self::$documentExtensions, true);
            default:
                // Unsupported format
                return false;
        }
    }

    public static function is($param, $type) {
        switch($type) {
            case 'string':
                return is_string($param);
            case 'integer':
                return is_int($param);
            case 'float':
                return is_float($param);
            case 'boolean':
                return is_bool($param);
            case 'array':
                return is_array($param);
            case 'object':
                return is_object($param);
            default:
                return false; // Invalid type provided
        }
    }

    public static function removeDoubleQuotes($inputString) {
        return str_replace('"', '', $inputString);
    }

    public static function isFileExists($file_path) {
        return is_file(realpath($file_path));
    }
    

    public static function updateEnvVariable($key, $value)
    {
        $envFilePath = APPROOT . '/Config/.env';

        // Read the current content of the .env file
        $currentEnvContent = file_get_contents($envFilePath);

        // Replace the existing value or add a new line if the variable doesn't exist
        $updatedEnvContent = preg_replace("/$key=[^\r\n]*/", "$key=$value", $currentEnvContent, 1);

        // If the variable doesn't exist, add it to the end of the file
        if ($updatedEnvContent === $currentEnvContent) {
            $updatedEnvContent .= "$key=$value\n";
        }

        // Write the updated content back to the .env file
        file_put_contents($envFilePath, $updatedEnvContent);
    }

    public static function escape(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, static::$encoding);
    }

    public static function currentURL()
    {
        return (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
    }

    /**
     * Checks if the current request is made over a secure connection.
     *
     * @return boolean Returns true if the request is secure, false otherwise.
     */
    public function isSecure(): bool
    {
        return (isset($_SERVER['https']) && $_SERVER['https'] === 'on') || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
    }

    public static function getPrefixBeforeHyphen($inputString, $separator = '-')
    {
        $parts = explode($separator, $inputString);
        return $parts[0]; // Return the first part before the hyphen
    }

    public static function getPrefixAfterHyphen($inputString, $separator = '-')
    {
        $parts = explode($separator, $inputString);
        return $parts[1]; // Return the second part after the hyphen
    }

    public static function removeString($mainString, $stringToRemove) {
        return str_replace($stringToRemove, '', $mainString);
    }

    /**
     * Replaces all occurrences of strings in an array with a specified value in a given string.
     *
     * @param string $stringValue The string in which replacements will be made.
     * @param array $replaceArray An array of strings to be replaced.
     * @param string $change The value that will replace the strings in the given string.
     * @return string The modified string with the replacements made.
     */
    public static function replaceStrings(string $stringValue, array $replaceArray, string $change) {
        foreach ($replaceArray as $replaceString) {
            if (strpos($stringValue, $replaceString) !== false) {
                $stringValue = str_replace($replaceString, $change, $stringValue);
            }
        }
        return $stringValue;
    }

    public static function containsStringFromArray(string $variable, array $arrayCheck) {
        foreach ($arrayCheck as $checkString) {
            if (strpos($variable, $checkString) !== false) {
                return true;
            }
        }
        return false;
    }

    public static function limitString($mainValue, $limit, $endChar = '...')
    {
        if (strlen($mainValue) > $limit) {
            return substr($mainValue, 0, $limit) . $endChar;
        }
        return $mainValue;
    }

    public static function hasAllowedExtension($value, array $allowedExtensions = ['jpg', 'png', 'webp', 'svg', 'mp4', 'webm', 'mp3', 'ogg', 'wav', 'pdf', 'doc', 'docx', 'ppt', 'pptx', 'txt', 'zip'])
    {
        // Get the file extension using pathinfo
        $extension = pathinfo($value, PATHINFO_EXTENSION);
        
        // Check if the extension exists in the valid extensions array
        return in_array(strtolower($extension), $allowedExtensions);
    }

    public static function removeSpaceWith(string $text, $replacement = '-')
    {
        return str_replace(' ', $replacement, $text);
    }

    public function formatBytes($bytes)
    {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];

        for ($i = 0; $bytes > 1024; $i++) {
            $bytes /= 1024;
        }

        return round($bytes, 2) . ' ' . $units[$i];
    }

    public static function jsonResult(array $data): void
    {
        header('Content-Type: application/json');
        echo json_encode($data);
        exit;
    }

    public static function decodePostData()
    {
        $data = json_decode(file_get_contents("php://input"), true);
        return $data;
    }

    public static function isMenuActive($url, string $className = 'active')
    {
        $currentPage = rtrim($_SERVER['REQUEST_URI'], '/') ?: '/';
        // $urlSegments = explode('/', trim($currentPage, '/'));
        $urlSegments = \str_replace('/', '/', $currentPage);

        if ($url === '/' && $urlSegments === '/') {
            return $className;
        }

        if($url !== '/' && strpos($urlSegments, $url) !== false){
            return $className;
        }

        return '';
    }

    public static function getPositionString(int $number): string
    {
        // Check if the last digit of the number is a 1, 2, or 3, and pick the appropriate suffix from the array.
        // If the condition fails, it default to 'th' suffix.
        $suffix = ['st', 'nd', 'rd'][$number % 10 - 1] ?? 'th';
    
        // Special case for numbers ending with 11, 12, or 13 where 'th' suffix is always used.
        if ($number % 100 >= 11 && $number % 100 <= 13) {
            return "{$number}th";
        } else {
            // Append the suffix at the end of the number and return the result as a string.
            return "{$number}{$suffix}";
        }
    }

    /**
     * Return true if the request method is POST
     *
     * @return boolean
     */
    public function is_post_request(): bool
    {
        return strtoupper($_SERVER['REQUEST_METHOD']) === 'POST';
    }

    /**
     * Return true if the request method is GET
     *
     * @return boolean
     */
    public function is_get_request(): bool
    {
        return strtoupper($_SERVER['REQUEST_METHOD']) === 'GET';
    }

    /**
     * Redirects the user to a new URL with an optional HTTP status code.
     *
     * @param string $url The URL to redirect to.
     * @param int $statusCode The HTTP status code to use. Defaults to 303.
     * @throws \Exception If there is an error with the redirection.
     * @return void
     */
    public static function redirect(string $url, int $statusCode = 303): void
    {
        http_response_code($statusCode);
        header("Location: {$url}");
        exit;
    }

    public static function removeQueryParams(string $url, ...$paramsToRemove)
    {
        // Parse the URL into its components
        $parsedUrl = parse_url($url);

        // If the URL doesn't have a query string, just return the original URL
        if (!isset($parsedUrl['query'])) {
            return $url;
        }

        // Parse the query string into an associative array
        parse_str($parsedUrl['query'], $queryParams);

        // Remove the specified parameters from the array
        foreach ($paramsToRemove as $param) {
            unset($queryParams[$param]);
        }

        // Rebuild the query string
        $newQueryString = http_build_query($queryParams);

        // Reassemble the URL with the modified query string
        $newUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host'] . $parsedUrl['path'];
        if (!empty($newQueryString)) {
            $newUrl .= '?' . $newQueryString;
        }

        // Add back the fragment, if it exists
        if (isset($parsedUrl['fragment'])) {
            $newUrl .= '#' . $parsedUrl['fragment'];
        }

        return $newUrl;
    }

    public static function getAvatar(string $targetFolder, string $name, int $size = 150, int $length = 8): string
    {
        $bgColor = dechex(rand(0, 10000000));
        $avatarUrl = 'https://ui-avatars.com/api/?name=' . $name . '&background=' . $bgColor . '&color=fff&size=' . $size . '&length=' . $length . '&rounded=false&bold=false&uppercase=true';

        // Get the avatar content from the URL and generate a unique filename for it. 
        $avatarContent = file_get_contents($avatarUrl);
        $avatarFilename = uniqid((string) mt_rand(100000, 99999999)) . ".png";

        // Save the avatar content to the target folder with its unique filename. 
        file_put_contents($targetFolder . '/' . $avatarFilename, $avatarContent);

        // Return the unique filename of the avatar. 
        return $avatarFilename;
    }

    public static function paginate_range($total_page, $current_page)
    {
        $array_output = [];

        if ($total_page <= 1) {
            return $array_output; // No need to paginate if there's only one page.
        }

        // Always include the first page.
        $array_output[] = 1;

        // Calculate the start and end points for the pagination.
        $start = max(2, $current_page - 1);
        $end = min($total_page - 1, $start + 2);

        // Add an initial separator if necessary.
        if ($start > 2) {
            $array_output[] = 0;
        }

        // Page subgroup: pages from start to end.
        for ($i = $start; $i <= $end; $i++) {
            $array_output[] = $i;
        }

        // Add a final separator if necessary.
        if ($end < $total_page - 1) {
            $array_output[] = 0;
        }

        // Always include the last page.
        $array_output[] = $total_page;

        return $array_output;
    }

    public static function calculateProgressBar(float|int $currentVotes, float|int $targetVotes): float
    {
        $percent = ($currentVotes / $targetVotes) * 100;
        $percent = round($percent, 2); // Round to 2 decimal places

        $bar = '[';
        $completedBarLength = (int) ($percent / 10);

        for ($i = 0; $i < $completedBarLength; $i++) {
            $bar .= '=';
        }

        for ($i = $completedBarLength; $i < 10; $i++) {
            $bar .= ' ';
        }

        $bar .= ']';

        return $percent;
    }

    public static function generateUsername($firstName, $limit = 30)
    {
        // Sanitize the input name
        $slug = strtolower(trim(preg_replace('/[^a-zA-Z0-9-]/', '-', $firstName)));

        // Ensure slug starts with a letter or number
        $slug = preg_replace('/^[^a-z0-9]+/', '', $slug);

        // Generate a random string if the slug is empty
        if (empty($slug)) {
            $slug = substr(md5(microtime()), 0, 8);
        }

        // Enforce character limit and append a random string if needed
        if (strlen($slug) > $limit) {
            $slug = substr($slug, 0, $limit);
        } else {
            $randomSuffix = substr(md5(microtime()), 0, $limit - strlen($slug));
            $slug .= $randomSuffix;
        }

        // Ensure slug uniqueness (without database check) by appending a timestamp
        $slug .= '-' . time();

        return $slug;
    }


    public function sanitize_post($data = null)
    {
        $postData = $data ?? $_POST;
        foreach ($postData as $key => $value) {
            if (is_array($value)) {
                $postData[$key] = $this->sanitize_post($value);
            } else {
                $postData[$key] = filter_var($value, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
            }
        }
        return $postData;
    }


    public static function emptyDisplay(string $string): string
    {
        return empty($string) ? "N/A" : $string;
    }

    /**
     * Clears the cache by setting appropriate headers.
     *
     */
    public static function clearCache()
    {
        header("Expires: Tue, 01 Jan 2000 00:00:00 GMT");
        header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
        header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
        header("Cache-Control: post-check=0, pre-check=0", false);
        header("Pragma: no-cache");
    }

    /**
     * Generate a slug from a given string
     *
     * @param string $string
     * @return string
     */
    public static function slugify(string $string)
    {
        $slug = preg_replace('/[^a-z0-9-]+/', '-', strtolower($string));
        $slug = trim($slug, '-');
        return $slug;
    }

    public static function generateInvoiceNumber($input, $padLength = 7, $prefix = null)
    {
        if ($padLength <= strlen($input)) {
            trigger_error('<strong>$padLength</strong> cannot be less than or equal to the length of <strong>$input</strong> to generate invoice number', E_USER_ERROR);
        }
        if (is_string($prefix)) {
            return sprintf("%s%s", $prefix, str_pad($input, $padLength, "0", STR_PAD_LEFT));
        }
        return str_pad($input, $padLength, "0", STR_PAD_LEFT);
    }

    /**
     * Get the current date and time
     *
     * @param string $format
     * @return string
     */
    public static function getCurrentDateTime($format = 'Y-m-d H:i:s')
    {
        return date($format);
    }

    public static function getTimeLeft(string $closingDatetime, $interval = '1 Hour'): string
    {
        $now = new \DateTime();
        $closingTime = new \DateTime($closingDatetime);

        // Calculate the time difference
        $interval = $now->diff($closingTime);

        $hoursLeft = $interval->h + $interval->days * 24;
        $minutesLeft = $interval->i;

        // Generate the result string
        $result = '';

        if ($hoursLeft > 0) {
            $result .= $hoursLeft . ' hour' . ($hoursLeft > 1 ? 's' : '');

            if ($minutesLeft > 0) {
                $result .= ' and ';
            }
        }

        if ($minutesLeft > 0) {
            $result .= $minutesLeft . ' minute' . ($minutesLeft > 1 ? 's' : '');
        }

        // Handle the case when the closing time is in the past
        if ($now > $closingTime) {
            $result = 'Closed';
        }

        return $result;
    }

    /**
     * Checks if a specified time falls within a given interval.
     *
     * @param string $intervalTime The interval time in the format "X days", "X hours", "X minutes", etc.
     * @param string $specifiedTime The specified time in the format "HH:MM".
     * @throws Exception If the interval format is invalid.
     * @return bool Returns true if the specified time falls within the interval, false otherwise.
     */
    public static function isWithinInterval($intervalTime, $specifiedTime)
    {
        $interval = date_interval_create_from_date_string($intervalTime);

        if (!$interval) {
            throw new Exception("Invalid interval format");
        }

        $now = new \DateTime();
        $endTime = clone $now;
        $endTime->add($interval);

        $specifiedDateTime = \DateTime::createFromFormat('H:i', $specifiedTime);

        return $specifiedDateTime >= $now && $specifiedDateTime <= $endTime;
    }


    /**
     * Finds the nearest closed day based on the current day of the week.
     *
     * @param array $closedDays An array of closed days.
     * @return string The nearest closed day.
     */
    public static function findNearestDay(array $closedDays)
    {
        $today = date('N'); // Get the current day of the week (1 for Monday, 7 for Sunday)

        $minDistance = INF; // Initialize the minimum distance to infinity
        $nearestClosedDay = null; // Initialize the nearest closed day

        foreach ($closedDays as $closedDay) {
            $closedDay = date('N', strtotime($closedDay));
            if ($closedDay >= $today) {
                $distance = $closedDay - $today; // Calculate the distance between the current day and the closed day

                if ($distance < $minDistance) {
                    $minDistance = $distance;
                    $nearestClosedDay = $closedDay;
                }
            }
        }

        return date('l', strtotime($nearestClosedDay));
    }

    public static function timeAgo($datetime): string
    {
        $now = new \DateTime();
        $ago = new \DateTime($datetime);
        $interval = $now->diff($ago);

        $timeAgoString = "";

        if ($interval->y > 0) {
            $timeAgoString = $interval->y . " year" . ($interval->y === 1 ? "" : "s") . " ago";
        } elseif ($interval->m > 0) {
            $timeAgoString = $interval->m . " month" . ($interval->m === 1 ? "" : "s") . " ago";
        } elseif ($interval->d > 0) {
            $timeAgoString = $interval->d . " day" . ($interval->d === 1 ? "" : "s") . " ago";
        } elseif ($interval->h > 0) {
            $timeAgoString = $interval->h . " hour" . ($interval->h === 1 ? "" : "s") . " ago";
        } elseif ($interval->i > 0) {
            $timeAgoString = $interval->i . " minute" . ($interval->i === 1 ? "" : "s") . " ago";
        } else {
            $timeAgoString = "just now";
        }

        return $timeAgoString;
    }


    public static function copyright($year = false)
    {
        if ($year == false) {
            $year = date('Y');
        }
        if (intval($year) == date('Y')) {
            return intval($year);
        }
        if (intval($year) < date('Y')) {
            return intval($year) . ' - ' . date('Y');
        }
        if (intval($year) > date('Y')) {
            return date('Y');
        }
    }

    /**
     * Formats the given amount as currency.
     *
     * @param mixed $amount The amount to be formatted.
     * @param string $currencySymbol The symbol of the currency. Defaults to 'INR'.
     * @param string $position The position of the currency symbol. Defaults to 'left'.
     *                         Possible values: 'left', 'right'.
     * @return string The formatted currency string.
     */
    public static function formatCurrency($amount, $currencySymbol = 'INR', $position = 'left')
    {
        $formattedAmount = $amount; // Format the amount with two decimal places

        if ($position === 'left') {
            $formattedCurrency = $currencySymbol . $formattedAmount; // Add the symbol to the left
        } else {
            $formattedCurrency = $formattedAmount . $currencySymbol; // Add the symbol to the right
        }

        return $formattedCurrency;
    }

    public static function downloadFile(string $file, ?string $name = null): void
    {
        $mime = 'application/force-download';
        header('Pragma: public');
        header('Expires: 0');
        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
        header('Cache-Control: private', false);
        header('Content-Type: ' . $mime);

        // Changed the order of the headers to make it more readable. 
        header('Content-Disposition: attachment; filename="' . ($name ?: basename($file)) . '"');
        header('Content-Transfer-Encoding: binary');
        header('Connection: close');

        readfile($file);

        // Added exit statement to ensure that the script stops executing after the file is downloaded. 
        exit;
    }

    public static function downloadImageFromURL($imageUrl, $savePath) {
        $content = @file_get_contents($imageUrl);
        if ($content === false) {
            return false;
        }
        if (@file_put_contents($savePath, $content) === false) {
            return false;
        }
        return true;
    }
    

    public static function number_format_short($n, $precision = 1)
    {
        if ($n < 900) {
            // 0 - 900
            $n_format = number_format($n, $precision);
            $suffix = '';
        } else if ($n < 900000) {
            // 0.9k-850k
            $n_format = number_format($n / 1000, $precision);
            $suffix = 'K';
        } else if ($n < 900000000) {
            // 0.9m-850m
            $n_format = number_format($n / 1000000, $precision);
            $suffix = 'M';
        } else if ($n < 900000000000) {
            // 0.9b-850b
            $n_format = number_format($n / 1000000000, $precision);
            $suffix = 'B';
        } else {
            // 0.9t+
            $n_format = number_format($n / 1000000000000, $precision);
            $suffix = 'T';
        }
        if ($precision > 0) {
            $dotzero = '.' . str_repeat('0', $precision);
            $n_format = str_replace($dotzero, '', $n_format);
        }
        return $n_format . $suffix;
    }

    public static function generateAcronym(string $str): string
    {
        $words = explode(' ', $str);
        $acronym = '';

        foreach ($words as $word) {
            if (!empty($word)) {
                // Check if the word is not empty
                $acronym .= $word[0];
            }
        }

        return $acronym;
    }

    /**
     * Sanitize user input to prevent SQL injection
     *
     * @param string $input
     * @return string
     */
    public static function sanitizeInput($input)
    {
        $sanitizedInput = trim($input);
        $sanitizedInput = stripslashes($sanitizedInput);
        $sanitizedInput = htmlspecialchars($sanitizedInput);
        return $sanitizedInput;
    }

    /**
     * Calculate the age based on the given date of birth
     *
     * @param string $dateOfBirth
     * @return int
     */
    public static function calculateAge($dateOfBirth)
    {
        $birthDate = new \DateTime($dateOfBirth);
        $now = new \DateTime();
        $age = $now->diff($birthDate)->y;
        return $age;
    }

    public static function trim(string $input): string
    {
        return trim($input);
    }

    public static function lower(string $input): string
    {
        return mb_strtolower($input, 'UTF-8');
    }

    public static function upper(string $input): string
    {
        return mb_strtoupper($input, 'UTF-8');
    }

    public static function capitalize(string $input): string
    {
        return mb_convert_case($input, MB_CASE_TITLE, 'UTF-8');
    }

    public static function webalize(string $input): string
    {
        $input = self::lower($input);
        $input = iconv('UTF-8', 'ASCII//TRANSLIT', $input);
        $input = preg_replace('~[^\\pL0-9_]+~u', '-', $input);
        $input = trim($input, "-");
        $input = preg_replace('~[^-a-z0-9_]+~', '', $input);
        return $input;
    }

    public static function substring(string $input, int $start, int $length = null): string
    {
        if (is_null($length)) {
            return mb_substr($input, $start, mb_strlen($input), 'UTF-8');
        } else {
            return mb_substr($input, $start, $length, 'UTF-8');
        }
    }

    public static function length(string $input): int
    {
        return mb_strlen($input, 'UTF-8');
    }

    public static function startsWith(string $haystack, string $needle): bool
    {
        return mb_substr($haystack, 0, mb_strlen($needle, 'UTF-8')) === $needle;
    }

    public static function endsWith(string $haystack, string $needle): bool
    {
        return mb_substr($haystack, -mb_strlen($needle, 'UTF-8')) === $needle;
    }

    public static function contains(string $haystack, string $needle): bool
    {
        return mb_strpos($haystack, $needle, 0, 'UTF-8') !== false;
    }

    public static function fixEncoding(string $input): string
    {
        return mb_convert_encoding($input, 'UTF-8', 'UTF-8');
    }

    /**
     * Generate a random string of given length from characters specified in the charlist.
     *
     * @param int $length
     * @param string $charlist
     * @return string
     */
    public static function generateRandom(int $length = 10, string $charlist = '0-9a-z'): string
    {
        $characters = self::expandCharList($charlist);
        $randomString = '';

        for ($i = 0; $i < $length; $i++) {
            $randomString .= $characters[random_int(0, strlen($characters) - 1)];
        }

        return $randomString;
    }

    /**
     * Debugs the given code by printing a formatted representation of the code.
     *
     * @param mixed $code The code to be debugged.
     * @return void
     */
    public static function debug_code($code)
    {
        echo "<pre>";
        var_dump($code);
        echo "</pre>";
    }

    /**
     * Expand a charlist that may contain intervals like '0-9' or 'A-Z'.
     *
     * @param string $charlist
     * @return string
     */
    private static function expandCharList(string $charlist): string
    {
        $expanded = '';
        $length = strlen($charlist);

        for ($i = 0; $i < $length; $i++) {
            if ($charlist[$i] === '-') {
                $start = ord($charlist[$i - 1]) + 1;
                $end = ord($charlist[$i + 1]);

                for ($j = $start; $j <= $end; $j++) {
                    $expanded .= chr($j);
                }
            } else {
                $expanded .= $charlist[$i];
            }
        }

        return $expanded;
    }
}
