<?php

declare(strict_types=1);

namespace EaLab\WP_Deployer\vendor\PhpMyAdmin\SqlParser\Tools;

use EaLab\WP_Deployer\vendor\PhpMyAdmin\SqlParser\Token;

use function array_filter;
use function array_map;
use function array_merge;
use function array_slice;
use function basename;
use function count;
use function dirname;
use function file;
use function file_put_contents;
use function implode;
use function ksort;
use function preg_match;
use function scandir;
use function sort;
use function sprintf;
use function str_replace;
use function str_split;
use function strlen;
use function strstr;
use function strtoupper;
use function substr;
use function trim;

use const ARRAY_FILTER_USE_KEY;
use const SORT_STRING;

/**
 * Used for context generation.
 */
class ContextGenerator
{
    /**
     * Labels and flags that may be used when defining keywords.
     *
     * @var array<string, int>
     */
    public static $LABELS_FLAGS = [
        '(R)' => Token::FLAG_KEYWORD_RESERVED,
        '(D)' => Token::FLAG_KEYWORD_DATA_TYPE,
        '(K)' => Token::FLAG_KEYWORD_KEY,
        '(F)' => Token::FLAG_KEYWORD_FUNCTION,
    ];

    /**
     * Documentation links for each context.
     *
     * @var array<string, string>
     */
    public static $LINKS = [
        'MySql50000' => 'https://dev.mysql.com/doc/refman/5.0/en/keywords.html',
        'MySql50100' => 'https://dev.mysql.com/doc/refman/5.1/en/keywords.html',
        'MySql50500' => 'https://dev.mysql.com/doc/refman/5.5/en/keywords.html',
        'MySql50600' => 'https://dev.mysql.com/doc/refman/5.6/en/keywords.html',
        'MySql50700' => 'https://dev.mysql.com/doc/refman/5.7/en/keywords.html',
        'MySql80000' => 'https://dev.mysql.com/doc/refman/8.0/en/keywords.html',
        'MySql80100' => 'https://dev.mysql.com/doc/refman/8.1/en/keywords.html',
        'MySql80200' => 'https://dev.mysql.com/doc/refman/8.2/en/keywords.html',
        'MySql80300' => 'https://dev.mysql.com/doc/refman/8.3/en/keywords.html',
        'MySql80400' => 'https://dev.mysql.com/doc/refman/8.4/en/keywords.html',
        'MySql90000' => 'https://dev.mysql.com/doc/refman/9.0/en/keywords.html',
        'MySql90100' => 'https://dev.mysql.com/doc/refman/9.1/en/keywords.html',
        'MariaDb100000' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb100100' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb100200' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb100300' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb100400' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb100500' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb100600' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb100700' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb100800' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb100900' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb101000' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb101100' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb110000' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb110100' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb110200' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb110300' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb110400' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb110500' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb110600' => 'https://mariadb.com/kb/en/reserved-words/',
        'MariaDb110700' => 'https://mariadb.com/kb/en/reserved-words/',
    ];

    /**
     * Reversed const <=> int from {@see Token} class to write the constant name instead of its value.
     *
     * @var array<int, string>
     */
    private static $typesNumToConst = [
        1 => 'Token::FLAG_KEYWORD',
        2 => 'Token::FLAG_KEYWORD_RESERVED',
        4 => 'Token::FLAG_KEYWORD_COMPOSED',
        8 => 'Token::FLAG_KEYWORD_DATA_TYPE',
        16 => 'Token::FLAG_KEYWORD_KEY',
        32 => 'Token::FLAG_KEYWORD_FUNCTION',
    ];

    /**
     * The template of a context.
     *
     * Parameters:
     *     1 - name
     *     2 - class
     *     3 - link
     *     4 - keywords array
     */
    public const TEMPLATE = <<<'PHP'
<?php

declare(strict_types=1);

namespace EaLab\WP_Deployer\vendor\PhpMyAdmin\SqlParser\Contexts;

use EaLab\WP_Deployer\vendor\PhpMyAdmin\SqlParser\Context;
use EaLab\WP_Deployer\vendor\PhpMyAdmin\SqlParser\Token;

/**
 * Context for %1$s.
 *
 * This class was auto-generated from tools/contexts/*.txt.
 * Use tools/run_generators.sh for update.
 *
 * @see %3$s
 */
class %2$s extends Context
{
    /**
     * List of keywords.
     *
     * The value associated to each keyword represents its flags.
     *
     * @see Token
     *
     * @var array<string,int>
     * @phpstan-var non-empty-array<non-empty-string,Token::FLAG_KEYWORD_*|int>
     */
    public static $KEYWORDS = [
%4$s    ];
}

PHP;

    /**
     * Sorts an array of words.
     *
     * @param array<int, list<string>> $arr
     *
     * @return array<int, list<string>>
     */
    public static function sortWords(array &$arr)
    {
        ksort($arr);
        foreach ($arr as &$words) {
            sort($words, SORT_STRING);
        }

        return $arr;
    }

    /**
     * Reads a list of words and sorts it by type, length and keyword.
     *
     * @param list<string> $files
     *
     * @return array<int, list<string>>
     */
    public static function readWords(array $files)
    {
        /** @psalm-var list<string> $words */
        $words = [];
        foreach ($files as $file) {
            $words = array_merge($words, file($file));
        }

        /** @var array<string, int> $types */
        $types = [];

        for ($i = 0, $count = count($words); $i !== $count; ++$i) {
            $value = trim($words[$i]);
            if ($value === '') {
                continue;
            }

            $type = Token::FLAG_KEYWORD;

            // Reserved, data types, keys, functions, etc. keywords.
            foreach (static::$LABELS_FLAGS as $label => $flags) {
                if (strstr($value, $label) === false) {
                    continue;
                }

                $type |= $flags;
                $value = trim(str_replace($label, '', $value));
            }

            // Composed keyword.
            if (strstr($value, ' ') !== false) {
                $type |= Token::FLAG_KEYWORD_RESERVED;
                $type |= Token::FLAG_KEYWORD_COMPOSED;
            }

            $value = strtoupper($value);
            if (! isset($types[$value])) {
                $types[$value] = $type;
            } else {
                $types[$value] |= $type;
            }
        }

        // Prepare an array in a way to sort by type, then by word.
        $ret = [];
        foreach ($types as $word => $type) {
            $ret[$type][] = $word;
        }

        return static::sortWords($ret);
    }

    /**
     * Prints an array of a words in PHP format.
     *
     * @param array<int, list<string>> $words the list of words to be formatted
     */
    public static function printWords(array $words): string
    {
        $ret = '';
        foreach ($words as $type => $wordsByType) {
            foreach ($wordsByType as $word) {
                $ret .= sprintf("        '%s' => %s,\n", $word, self::translateIntTypeToTextConstant($type));
            }
        }

        return $ret;
    }

    private static function translateIntTypeToTextConstant(int $type): string
    {
        $matchingFlags = array_filter(
            self::$typesNumToConst,
            static function (int $num) use ($type): bool {
                return ($type & $num) !== 0;
            },
            ARRAY_FILTER_USE_KEY
        );

        return implode(' | ', $matchingFlags);
    }

    /**
     * Generates a context's class.
     *
     * @param array<string, string|array<int, list<string>>> $options the options for this context
     * @psalm-param array{
     *   name: string,
     *   class: string,
     *   link: string,
     *   keywords: array<int, list<string>>
     * } $options
     *
     * @return string
     */
    public static function generate($options)
    {
        $options['keywords'] = static::printWords($options['keywords']);

        return sprintf(self::TEMPLATE, $options['name'], $options['class'], $options['link'], $options['keywords']);
    }

    /**
     * Formats context name.
     *
     * @param string $name name to format
     *
     * @return string
     */
    public static function formatName($name)
    {
        /* Split name and version */
        $parts = [];
        if (preg_match('/^(\D+)(\d+)$/', $name, $parts) === 0) {
            return $name;
        }

        /* Format name */
        $base = $parts[1];
        switch ($base) {
            case 'MySql':
                $base = 'MySQL';
                break;
            case 'MariaDb':
                $base = 'MariaDB';
                break;
        }

        /* Parse version to array */
        $versionString = $parts[2];
        if (strlen($versionString) % 2 === 1) {
            $versionString = '0' . $versionString;
        }

        $version = array_map('intval', str_split($versionString, 2));
        /* Remove trailing zero */
        if ($version[count($version) - 1] === 0) {
            $version = array_slice($version, 0, -1);
        }

        /* Create name */
        return $base . ' ' . implode('.', $version);
    }

    /**
     * Builds a test.
     *
     * Reads the input file, generates the data and writes it back.
     *
     * @param string $input  the input file
     * @param string $output the output directory
     *
     * @return void
     */
    public static function build($input, $output)
    {
        /**
         * The directory that contains the input file.
         *
         * Used to include common files.
         *
         * @var string
         */
        $directory = dirname($input) . '/';

        /**
         * The name of the file that contains the context.
         */
        $file = basename($input);

        /**
         * The name of the context.
         *
         * @var string
         */
        $name = substr($file, 0, -4);

        /**
         * The name of the class that defines this context.
         *
         * @var string
         */
        $class = 'Context' . $name;

        /**
         * The formatted name of this context.
         */
        $formattedName = static::formatName($name);

        file_put_contents(
            $output . '/' . $class . '.php',
            static::generate(
                [
                    'name' => $formattedName,
                    'class' => $class,
                    'link' => static::$LINKS[$name],
                    'keywords' => static::readWords(
                        [
                            $directory . '_common.txt',
                            $directory . '_functions' . $file,
                            $directory . $file,
                        ]
                    ),
                ]
            )
        );
    }

    /**
     * Generates recursively all tests preserving the directory structure.
     *
     * @param string $input  the input directory
     * @param string $output the output directory
     *
     * @return void
     */
    public static function buildAll($input, $output)
    {
        $files = scandir($input);

        foreach ($files as $file) {
            // Skipping current and parent directories.
            // Skipping _functions* and _common.txt files
            if (($file[0] === '.') || ($file[0] === '_')) {
                continue;
            }

            // Skipping README.md
            if ($file === 'README.md') {
                continue;
            }

            // Building the context.
            echo sprintf("Building context for %s...\n", $file);
            static::build($input . '/' . $file, $output);
        }
    }
}
