<?php

declare(strict_types=1);

namespace PhpMyAdmin\SqlParser\Tools;

use Exception;
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 is_dir;
use function ksort;
use function preg_match;
use function round;
use function rtrim;
use function scandir;
use function sort;
use function sprintf;
use function str_repeat;
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 FILE_IGNORE_NEW_LINES;
use const FILE_SKIP_EMPTY_LINES;
use const SORT_STRING;

require_once __DIR__ . '/../vendor/autoload.php';

/**
 * Used for context generation.
 */
class ContextGenerator
{
    /**
     * Labels and flags that may be used when defining keywords.
     *
     * @var array
     */
    public static $LABELS_FLAGS = [
        '(R)' => 2, // reserved
        '(D)' => 8, // data type
        '(K)' => 16, // keyword
        '(F)' => 32, // function name
    ];

    /**
     * Documentation links for each context.
     *
     * @var array
     */
    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',
        'MariaDb100000' => 'https://mariadb.com/kb/en/the-mariadb-library/reserved-words/',
        'MariaDb100100' => 'https://mariadb.com/kb/en/the-mariadb-library/reserved-words/',
        'MariaDb100200' => 'https://mariadb.com/kb/en/the-mariadb-library/reserved-words/',
        'MariaDb100300' => 'https://mariadb.com/kb/en/the-mariadb-library/reserved-words/',
    ];

    /**
     * The template of a context.
     *
     * Parameters:
     *     1 - name
     *     2 - class
     *     3 - link
     *     4 - keywords array
     */
    public const TEMPLATE =
        '<?php' . "\n" .
        '/**' . "\n" .
        ' * Context for %1$s.' . "\n" .
        ' *' . "\n" .
        ' * This file was auto-generated.' . "\n" .
        ' *' . "\n" .
        ' * @see %3$s' . "\n" .
        ' */' . "\n" .
        'declare(strict_types=1);' . "\n" .
        '' . "\n" .
        'namespace PhpMyAdmin\\SqlParser\\Contexts;' . "\n" .
        '' . "\n" .
        'use PhpMyAdmin\\SqlParser\\Context;' . "\n" .
        'use PhpMyAdmin\\SqlParser\\Token;' . "\n" .
        '' . "\n" .
        '/**' . "\n" .
        ' * Context for %1$s.' . "\n" .
        ' *' . "\n" .
        ' * @category   Contexts' . "\n" .
        ' *' . "\n" .
        ' * @license    https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+' . "\n" .
        ' */' . "\n" .
        'class %2$s extends Context' . "\n" .
        '{' . "\n" .
        '    /**' . "\n" .
        '     * List of keywords.' . "\n" .
        '     *' . "\n" .
        '     * The value associated to each keyword represents its flags.' . "\n" .
        '     *' . "\n" .
        '     * @see Token::FLAG_KEYWORD_RESERVED Token::FLAG_KEYWORD_COMPOSED' . "\n" .
        '     *      Token::FLAG_KEYWORD_DATA_TYPE Token::FLAG_KEYWORD_KEY' . "\n" .
        '     *      Token::FLAG_KEYWORD_FUNCTION' . "\n" .
        '     *' . "\n" .
        '     * @var array' . "\n" .
        '     */' . "\n" .
        '    public static $KEYWORDS = [' . "\n" .
        '%4$s' .
        '    ];' . "\n" .
        '}' . "\n";

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

        return $arr;
    }

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

        $types = [];

        for ($i = 0, $count = count($words); $i !== $count; ++$i) {
            $type = 1;
            $value = trim($words[$i]);

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

            // Composed keyword.
            if (strstr($value, ' ') !== false) {
                $type |= 2; // Reserved keyword.
                $type |= 4; // Composed keyword.
            }

            $len = strlen($words[$i]);
            if ($len === 0) {
                continue;
            }

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

        $ret = [];
        foreach ($types as $word => $type) {
            $len = strlen($word);
            if (! isset($ret[$type])) {
                $ret[$type] = [];
            }

            if (! isset($ret[$type][$len])) {
                $ret[$type][$len] = [];
            }

            $ret[$type][$len][] = $word;
        }

        return static::sortWords($ret);
    }

    /**
     * Prints an array of a words in PHP format.
     *
     * @param array $words  the list of words to be formatted
     * @param int   $spaces the number of spaces that starts every line
     * @param int   $line   the length of a line
     *
     * @return string
     */
    public static function printWords($words, $spaces = 8, $line = 140)
    {
        $typesCount = count($words);
        $ret = '';
        $j = 0;

        foreach ($words as $type => $wordsByType) {
            foreach ($wordsByType as $len => $wordsByLen) {
                $count = round(($line - $spaces) / ($len + 9)); // strlen("'' => 1, ") = 9
                $i = 0;

                foreach ($wordsByLen as $word) {
                    if ($i === 0) {
                        $ret .= str_repeat(' ', $spaces);
                    }

                    $ret .= sprintf('\'%s\' => %s, ', $word, $type);
                    if (++$i === $count || ++$i > $count) {
                        $ret .= "\n";
                        $i = 0;
                    }
                }

                if ($i !== 0) {
                    $ret .= "\n";
                }
            }

            if (++$j < $typesCount) {
                $ret .= "\n";
            }
        }

        // Trim trailing spaces and return.
        return str_replace(" \n", "\n", $ret);
    }

    /**
     * Generates a context's class.
     *
     * @param array $options the options that are used in generating this context
     *
     * @return string
     */
    public static function generate($options)
    {
        if (isset($options['keywords'])) {
            $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('/([^[0-9]*)([0-9]*)/', $name, $parts) === false) {
            return $name;
        }

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

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

        $version = array_map('intval', str_split($ver_str, 2));
        /* Remove trailing zero */
        if ($version[count($version) - 1] === 0) {
            $version = array_slice($version, 0, count($version) - 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
     */
    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.
         *
         * @var string
         */
        $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.
         *
         * @var string
         */
        $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
     */
    public static function buildAll($input, $output)
    {
        $files = scandir($input);

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

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

// Test generator.
//
// Example of usage:
//
//      php ContextGenerator.php data data
//
// Input data must be in the `data` folder.
// The output will be generated in the same `data` folder.
if (count($argv) >= 3) {
    // Extracting directories' name from command line and trimming unnecessary
    // slashes at the end.
    $input = rtrim($argv[1], '/');
    $output = rtrim($argv[2], '/');

    // Checking if all directories are valid.
    if (! is_dir($input)) {
        throw new Exception('The input directory does not exist.');
    } elseif (! is_dir($output)) {
        throw new Exception('The output directory does not exist.');
    }

    // Finally, building the tests.
    ContextGenerator::buildAll($input, $output);
}
