<?php

if (!defined('AOWOW_REVISION'))
    die('illegal access');

if (!CLI)
    die('not in cli mode');


class CLISetup
{
    public  static $locales       = [];

    public  static $srcDir        = 'setup/mpqdata/';

    private static $mpqFiles      = [];
    public  static $expectedPaths = array(                  // update paths [yes, you can have en empty string as key]
        ''     => LOCALE_EN,    'enGB' => LOCALE_EN,    'enUS' => LOCALE_EN,
        'frFR' => LOCALE_FR,
        'deDE' => LOCALE_DE,
        'zhCN' => LOCALE_CN,    'enCN' => LOCALE_CN,
        'esES' => LOCALE_ES,
        'ruRU' => LOCALE_RU
    );

    public  const SQL_BATCH       = 1000;                   // max. n items per sql insert

    public  const LOCK_OFF        = 0;
    public  const LOCK_ON         = 1;
    public  const LOCK_RESTORE    = 2;

    private static $lock          = self::LOCK_ON;

    public const ARGV_NONE        = 0x00;
    public const ARGV_REQUIRED    = 0x01;
    public const ARGV_OPTIONAL    = 0x02;
    public const ARGV_PARAM       = 0x04;                   // parameter to another argument
    public const ARGV_ARRAY       = 0x10;                   // arg accepts list of values

    public const OPT_GRP_SETUP    = 0;
    public const OPT_GRP_UTIL     = 1;
    public const OPT_GRP_MISC     = 2;

    private const GLOBALSTRINGS_LUA = '%s%sinterface/framexml/globalstrings.lua';

    private static $opts          = [];
    private static $optGroups     = ['AoWoW Setup', 'Utility Functions', 'Additional Options'];
    private static $optDefs       = array(                  // cmd => [groupId, aliases[], argvFlags, description, appendix]
        'delete'  => [self::OPT_GRP_MISC, ['d'], self::ARGV_NONE,                        'Delete dbc_* tables generated by this prompt when done. (not recommended)',                                 ''               ],
        'log'     => [self::OPT_GRP_MISC, [],    self::ARGV_REQUIRED,                    'Write CLI ouput to file.',                                                                                  '=logfile'       ],
        'help'    => [self::OPT_GRP_MISC, ['h'], self::ARGV_NONE,                        'Display contextual help, if available.',                                                                    ''               ],
        'force'   => [self::OPT_GRP_MISC, ['f'], self::ARGV_NONE,                        'Force existing files to be overwritten.',                                                                   ''               ],
        'locales' => [self::OPT_GRP_MISC, [],    self::ARGV_ARRAY | self::ARGV_OPTIONAL, 'Limit setup to enUS, frFR, deDE, zhCN, esES and/or ruRU. (does not override config settings)',              '=<regionCodes,>'],
        'datasrc' => [self::OPT_GRP_MISC, [],    self::ARGV_OPTIONAL,                    'Manually point to directory with extracted mpq files. This is limited to setup/ (default: setup/mpqdata/)', '=path/'         ],
    );

    private static $utilScriptRefs  = [];
    private static $setupScriptRefs = [];
    private static $tmpStore        = [];
    private static $gsFiles         = [];

    public static function registerUtility(UtilityScript $us) : void
    {
        if (isset(self::$optDefs[$us::COMMAND]) || isset(self::$utilScriptRefs[$us::COMMAND]))
        {
            CLI::write(' Utility function '.CLI::bold($us::COMMAND).' already defined.', CLI::LOG_ERROR);
            return;
        }
        self::$optDefs[$us::COMMAND] = [$us->optGroup, $us->argvOpts, $us->argvFlags, $us::DESCRIPTION, $us::APPENDIX];
        self::$utilScriptRefs[$us::COMMAND] = $us;
    }

    public static function registerSetup(string $invoker, SetupScript $ss) : void
    {
        if (isset(self::$optDefs[$invoker]) || isset(self::$utilScriptRefs[$invoker]))
        {
            CLI::write(' Utility function '.CLI::bold($invoker).' not defined. Can\'t attach Subscript '.CLI::bold($ss->getName()).', invoker is missing. Skipping...', CLI::LOG_ERROR);
            return;
        }

        if (isset(self::$setupScriptRefs[$invoker][$ss->getName()]))
        {
            CLI::write(' Subscript function '.CLI::bold($ss->getName()).' already defined for invoker '.CLI::bold($invoker).'. Skipping...', CLI::LOG_ERROR);
            return;
        }

        if ($childArgs = $ss->getSubCommands())
        {
            if ($duplicates = array_intersect(array_keys($childArgs), array_keys(self::$optDefs)))
            {
                CLI::write(' Subscript function '.CLI::bold($ss->getName()).'\'s child arguments --'.implode(', --', $duplicates).' are already defined. Skipping...', CLI::LOG_ERROR);
                return;
            }

            $newIdx = count(self::$optGroups);
            self::$optGroups[] = '--' . $invoker . '=' . $ss->getName();

            foreach ($childArgs as $cmd => [$aliases, $argFlags, $description])
                self::$optDefs[$cmd] = [$newIdx, $aliases, $argFlags, $description, ''];
        }

        // checks done ... store SetupScript
        if (self::checkDependencies($ss))
        {
            self::$setupScriptRefs[] = [$invoker, $ss->getName(), $ss];

            // recheck temp stored dependencies
            foreach (self::$tmpStore as $idx => [$invoker, $ts])
            {
                if (!self::checkDependencies($ts))
                    continue;

                self::$setupScriptRefs[] = [$invoker, $ts->getName(), $ts];
                unset(self::$tmpStore[$idx]);
            }
        }
        else                                                // if dependencies haven't been stored yet, put aside for later use
            self::$tmpStore[] = [$invoker, $ss];
    }

    private static function checkDependencies(SetupScript &$ss) : bool
    {
        if ($ss->isOptional)                                // optional scripts should no depend on anything
            return true;

        [$sDep, $bDep] = $ss->getSelfDependencies();

        return ((!$sDep || $sDep == array_intersect($sDep, array_column(array_filter(self::$setupScriptRefs, function($x) { return $x[0] == 'sql';   }), 1))) &&
                (!$bDep || $bDep == array_intersect($bDep, array_column(array_filter(self::$setupScriptRefs, function($x) { return $x[0] == 'build'; }), 1))));
    }

    public static function loadScripts() : void
    {
        foreach (glob('setup/tools/clisetup/*.us.php') as $file)
            include_once $file;

        if (self::$tmpStore)
        {
            CLI::write('Some SubScripts have unresolved dependencies and have not been loaded', CLI::LOG_ERROR);
            CLI::write();
            $tbl = [['Name', '--sql dep.', '--build dep.']];
            foreach (self::$tmpStore as [$_, $ssRef])
            {
                [$sDep, $bDep] = $ssRef->getSelfDependencies();

                $missS = array_intersect($sDep, array_column(array_filter(self::$setupScriptRefs, function($x) { return $x[0] == 'sql';   }), 1));
                $missB = array_intersect($sDep, array_column(array_filter(self::$setupScriptRefs, function($x) { return $x[0] == 'build'; }), 1));

                array_walk($sDep, function (&$x) use($missS) { $x = in_array($x, $missS) ? $x : CLI::red($x); });
                array_walk($bDep, function (&$x) use($missB) { $x = in_array($x, $missB) ? $x : CLI::red($x); });

                $tbl[] = [$ssRef->getName(), implode(', ', $sDep), implode(', ', $bDep)];
            }

            CLI::writeTable($tbl);
        }

        // link SubScipts back to UtilityScript after all UtilityScripts have been loaded
        foreach (self::$utilScriptRefs as $name => $us)
            if (in_array('TrSubScripts', class_uses($us)))
                $us->assignGenerators($name);

        self::evalOpts();
    }

    public static function getSubScripts(string $invoker = '') : generator
    {
        foreach (self::$setupScriptRefs as [$src, $name, $ref])
            if (!$invoker || $src == $invoker)
                yield $name => [$src, $ref];
    }

    public static function setLocales() : bool
    {
        // optional limit handled locales
        if (isset(self::$opts['locales']))
        {
            // engb and enus are identical for all intents and purposes
            $from = ['engb', 'encn'];
            $to   = ['enus', 'zhcn'];

            self::$opts['locales'] = str_ireplace($from, $to, self::$opts['locales']);

            self::$locales = array_intersect(Util::$localeStrings, array_map('strtolower', self::$opts['locales']));
        }
        if (!self::$locales)
            self::$locales = array_filter(Util::$localeStrings);

        // restrict actual locales
        foreach (self::$locales as $idx => $_)
            if (($l = Cfg::get('LOCALES')) && !($l & (1 << $idx)))
                unset(self::$locales[$idx]);

        return !!self::$locales;
    }

    public static function init() : void
    {
        self::evalOpts();

        // optional logging
        if (isset(self::$opts['log']))
            CLI::initLogFile(trim(self::$opts['log']));

        // alternative data source (no quotes, use forward slash)
        if (isset(self::$opts['datasrc']))
            self::$srcDir = CLI::nicePath('', self::$opts['datasrc']);

        if (!self::setLocales())
            CLI::write('No valid locale specified. Check your config or --locales parameter, if used', CLI::LOG_ERROR);

        // get site status
        if (DB::isConnected(DB_AOWOW))
            self::$lock = Cfg::get('MAINTENANCE');
        else
            self::$lock = self::LOCK_ON;
    }

    public static function writeCLIHelp(bool $full = false) : void
    {
        $cmd = self::getOpt(1 << self::OPT_GRP_SETUP | 1 << self::OPT_GRP_UTIL);
        if (!$cmd || !self::$utilScriptRefs[$cmd[0]]->writeCLIHelp())
        {
            $lines = [];

            foreach (self::$optGroups as $idx => $og)
            {
                if (!$full && $idx > self::OPT_GRP_SETUP)
                    continue;

                $lines[] = [$og, ''];

                foreach (self::$optDefs as $opt => [$group, $alias, , $desc, $app])
                {
                    if ($group != $idx)
                        continue;

                    $cmd = '  --'.$opt;
                    foreach ($alias as $a)
                        $cmd .= ' | '.(strlen($a) == 1 ? '-'.$a : '--'.$a);

                    $lines[] = [$cmd.$app, $desc];
                }
            }

            CLI::writeTable($lines);
            CLI::write();
        }
    }

    // called from Setup
    public static function runInitial() : void
    {
        global $argc, $argv;                                // todo .. find better way? argv, argc are effectivley already global

        // get arguments present in argGroup 1 or 2, if set. Pick first.
        $cmd   = self::getOpt(1 << self::OPT_GRP_SETUP | 1 << self::OPT_GRP_UTIL)[0];
        $us    = &self::$utilScriptRefs[$cmd];
        $inOut = [null, null, null, null];
        $allOk = true;

        $i = 0;
        if ($us::USE_CLI_ARGS)
            foreach ($argv as $n => $arg)
            {
                if (!$n || ($arg && $arg[0] == '-'))        // not parent; not handled by getOpt()
                    continue;

                $inOut[$i++] = $arg;

                if ($i > 3)
                    break;
            }

        if ($dbError = array_filter($us::REQUIRED_DB, function ($x) { return !DB::isConnected($x); }))
        {
            CLI::write('Database on index '.implode(', ', $dbError).' not yet set up!', CLI::LOG_ERROR);
            CLI::write('Please use '.CLI::bold('"php aowow --db"').' for setup', CLI::LOG_BLANK);
            CLI::write();
            return;
        }

        if ($us::LOCK_SITE != self::LOCK_OFF)
            self::siteLock(self::LOCK_ON);

        if ($us::NOTE_START)
            CLI::write($us::NOTE_START);

        if (!$us->run($inOut))
            $allOk = false;

        $error = [];
        if ($allOk && !$us->test($error))
        {
            if ($us::NOTE_ERROR)
                CLI::write($us::NOTE_ERROR, CLI::LOG_ERROR);

            foreach ($error as $e)
                CLI::write($e, CLI::LOG_BLANK);

            CLI::write();
            $allOk = false;
        }

        if ($allOk)
            if ($ff = $us->followupFn)
                if (array_filter($inOut))
                    self::run($ff, $inOut);

        self::siteLock($us::LOCK_SITE == self::LOCK_RESTORE ? self::LOCK_RESTORE : self::LOCK_OFF);

        // end
        if ($us::NOTE_END_OK && $allOk)
            CLI::write($us::NOTE_END_OK, CLI::LOG_OK);
        else if($us::NOTE_END_FAIL && !$allOk)
            CLI::write($us::NOTE_END_FAIL, CLI::LOG_ERROR);
    }

    // consecutive calls
    public static function run(string $cmd, &$args) : bool
    {
        if (!isset(self::$utilScriptRefs[$cmd]))
            return false;

        $us = &self::$utilScriptRefs[$cmd];

        if ($dbError = array_filter($us::REQUIRED_DB, function ($x) { return !DB::isConnected($x); }))
        {
            CLI::write('Database on index '.implode(', ', $dbError).' not yet set up!', CLI::LOG_ERROR);
            CLI::write('Please use '.CLI::bold('"php aowow --db"').' for setup', CLI::LOG_BLANK);
            CLI::write();
            return false;
        }

        if ($us::PROMPT)
        {
            CLI::write($us::PROMPT, -1, false);
            CLI::write();

            if (!CLI::read(['x' => ['Press any key to continue', true, true]], $_))                // we don't actually care about the input
                return false;
        }

        $args = array_pad($args, 4, null);

        $success = $us->run($args);

        $error = [];
        if ($us::NOTE_ERROR && $success && !$us->test($error))
        {
            CLI::write($us::NOTE_ERROR, CLI::LOG_ERROR);
            foreach ($error as $e)
                CLI::write($e, CLI::LOG_BLANK);

            CLI::write();
            return false;
        }

        if ($success)
            if ($ff = $us->followupFn)
                if (array_filter($args))
                    if (!self::run($ff, $args))
                        $success = false;

        return $success;
    }


    /**************************/
    /* command line arguments */
    /**************************/

    public static function evalOpts() : void
    {
        $short = '';
        $long  = [];
        $alias = [];

        foreach (self::$optDefs as $opt => [, $aliases, $flags, , ])
        {
            foreach ($aliases as $i => $a)
            {
                if (isset($alias[$a]))
                    $alias[$a][] = $opt;
                else
                    $alias[$a] = [$opt];

                if ($flags & self::ARGV_REQUIRED)
                    $a .= ':';
                else if ($flags & self::ARGV_OPTIONAL)
                    $a .= '::';

                if (strlen($aliases[$i]) == 1)
                    $short .= $a;
                else
                    $long[] = $a;
            }

            if ($flags & self::ARGV_REQUIRED)
                $opt .= ':';
            else if ($flags & self::ARGV_OPTIONAL)
                $opt .= '::';

            $long[] = $opt;
        }

        if ($opts = getopt($short, $long))
        {
            foreach ($opts as $o => $v)
            {
                if (!isset($alias[$o]))
                    self::$opts[$o] = (self::$optDefs[$o][2] & self::ARGV_ARRAY) ? ($v ? explode(',', $v) : []) : ($v ?: true);
                else
                    foreach ($alias[$o] as $a)
                        self::$opts[$a] = (self::$optDefs[$a][2] & self::ARGV_ARRAY) ? ($v ? explode(',', $v) : []) : ($v ?: true);
            }
        }
    }

    public static function getOpt(/* string|int */ ...$args) // : bool|array|string
    {
        if (!$args)
            return false;

        $result = [];

        // groupMask case
        if (is_int($args[0]))
        {
            foreach (self::$optDefs as $o => [$group, , , , ])
                if (((1 << $group) & $args[0]) && isset(self::$opts[$o]))
                    $result[] = $o;

            return $result;
        }

        // single key case
        if (count($args) == 1)
            return self::$opts[$args[0]] ?? false;

        // multiple keys case
        foreach ($args as $a)
            if (isset(self::$optDefs[$a]))
                $result[$a] = self::$opts[$a] ?? false;

        return $result;
    }


    /*******************/
    /* web page access */
    /*******************/

    private static function siteLock(int $mode = self::LOCK_RESTORE) : void
    {
        if (DB::isConnected(DB_AOWOW))
            Cfg::set('MAINTENANCE', $mode == self::LOCK_RESTORE ? self::$lock : $mode);
    }


    /*******************/
    /* MPQ-file access */
    /*******************/

    /*  the problem
        1) paths provided in dbc files are case-insensitive and random
        2) paths to the actual textures contained in the mpq archives are case-insensitive and random
        unix systems will throw a fit if you try to get from one to the other, so lets save the paths from 2) and cast it to lowercase
        lookups will be done in lowercase. A successfull match will return the real path.
    */
    private static function buildFileList() : bool
    {
        CLI::write('indexing game data from '.self::$srcDir.' for first time use...', CLI::LOG_INFO, true, true);

        $setupDirs = glob('setup/*');
        foreach ($setupDirs as $sd)
        {
            if (mb_substr($sd, -1) == DIRECTORY_SEPARATOR)
                $sd = mb_substr($sd, 0, -1);

            if (Util::lower($sd) == Util::lower(self::$srcDir))
            {
                self::$srcDir = $sd.DIRECTORY_SEPARATOR;
                break;
            }
        }

        try
        {
            $iterator = new RecursiveDirectoryIterator(self::$srcDir);
            $iterator->setFlags(RecursiveDirectoryIterator::SKIP_DOTS);

            foreach (new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::SELF_FIRST) as $path)
            {
                $_ = CLI::nicePath($path->getPathname());
                self::$mpqFiles[strtolower($_)] = $_;
            }

            CLI::write('indexing game data from '.self::$srcDir.' for first time use... done!', CLI::LOG_INFO);
        }
        catch (UnexpectedValueException $e)
        {
            CLI::write('- mpqData dir '.self::$srcDir.' does not exist', CLI::LOG_ERROR);
            return false;
        }

        return true;
    }

    public static function fileExists(string &$file) : bool
    {
        // read mpq source file structure to tree
        if (!self::$mpqFiles)
            if (!self::buildFileList())
                return false;

        // backslash to forward slash
        $_ = strtolower(CLI::nicePath($file));

        // remove trailing slash
        if (mb_substr($_, -1, 1) == DIRECTORY_SEPARATOR)
            $_ = mb_substr($_, 0, -1);

        if (isset(self::$mpqFiles[$_]))
        {
            $file = self::$mpqFiles[$_];
            return true;
        }

        return false;
    }

    public static function filesInPath(string $path, bool $useRegEx = false) : array
    {
        $result = [];

        // read mpq source file structure to tree
        if (!self::$mpqFiles)
            if (!self::buildFileList())
                return [];

        // backslash to forward slash
        $_ = strtolower(CLI::nicePath($path));

        foreach (self::$mpqFiles as $lowerFile => $realFile)
        {
            if (!$useRegEx && strstr($lowerFile, $_))
                $result[] = $realFile;
            else if ($useRegEx && preg_match($path, $lowerFile))
                $result[] = $realFile;
        }

        return $result;
    }

    public static function filesInPathLocalized(string $pathPattern, ?bool &$status = true, bool $matchAll = true) : array
    {
        $result = [];

        foreach (self::$expectedPaths as $xp => $locId)
        {
            if (!in_array($locId, array_keys(self::$locales)))
                continue;

            if (isset($result[$locId]))
                continue;

            if ($xp)                                        // if in subDir add trailing slash
                $xp .= DIRECTORY_SEPARATOR;

            $path = sprintf($pathPattern, $xp);
            if (self::fileExists($path))
            {
                $result[$locId] = $path;
                continue;
            }
        }

        if (!$matchAll && !$result)
            $status = false;

        if ($matchAll && array_diff_key(self::$locales, $result))
            $status = false;

        return $result;
    }

    public static function loadGlobalStrings() : bool
    {
        CLI::write('loading required GlobalStrings', CLI::LOG_INFO);

        // try to load globalstrings for all selected locales
        foreach (self::$expectedPaths as $xp => $lId)
        {
            if (isset(self::$gsFiles[$lId]))
                continue;

            if ($xp)
                $xp .= DIRECTORY_SEPARATOR;

            $gsFile = sprintf(self::GLOBALSTRINGS_LUA, self::$srcDir, $xp);
            if (self::fileExists($gsFile))
                self::$gsFiles[$lId] = file($gsFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        }

        if ($missing = array_diff_key(self::$locales, self::$gsFiles))
        {
            ClI::write('GlobalStrings.lua not found for locale '. Lang::concat($missing), CLI::LOG_WARN);
            return false;
        }

        return true;
    }

    public static function searchGlobalStrings(string $pattern) : generator
    {
        if (!self::$gsFiles)
            return;

        foreach (self::$gsFiles as $lId => $globalStrings)
            foreach ($globalStrings as $gs)
                if (preg_match($pattern, $gs, $result))
                    yield $lId => $result;
    }


    /*****************/
    /* file handling */
    /*****************/

    public static function writeFile(string $file, string $content) : bool
    {
        if (Util::writeFile($file, $content))
        {
            CLI::write('created file '. CLI::bold($file), CLI::LOG_OK, true, true);
            return true;
        }

        return false;
    }

    public static function writeDir(string $dir, bool &$exist = true) : bool
    {
        if (Util::writeDir($dir, $exist))
        {
            if (!$exist)
                CLI::write('created dir '. CLI::bold($dir), CLI::LOG_OK, true, true);
            return true;
        }

        return false;
    }

    public static function loadDBC( string $name) : bool
    {
        if (!DB::isConnected(DB_AOWOW))
        {
            CLI::write('CLISetup::loadDBC() - not connected to DB. Cannot write results!', CLI::LOG_ERROR);
            return false;
        }

        if (DB::Aowow()->selectCell('SHOW TABLES LIKE ?', 'dbc_'.$name) && DB::Aowow()->selectCell('SELECT count(1) FROM ?#', 'dbc_'.$name))
            return true;

        $dbc = new DBC($name, ['temporary' => self::getOpt('delete')]);
        if ($dbc->error)
        {
            CLI::write('CLISetup::loadDBC() - required DBC '.$name.'.dbc not found!', CLI::LOG_ERROR);
            return false;
        }

        if (!$dbc->readFile())
        {
            CLI::write('CLISetup::loadDBC() - DBC '.$name.'.dbc could not be written to DB!', CLI::LOG_ERROR);
            return false;
        }

        return true;
    }
}

?>
