<?php

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

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


trait TrDBCcopy
{
    public function __construct()
    {
        $this->info = array(
            $this->command => [[], CLISetup::ARGV_PARAM, 'COPY: ' . $this->dbcSourceFiles[0] . '.dbc -> aowow_'.$this->command],
        );
    }

    public function generate() : bool
    {
        if (!$this->dbcSourceFiles)
        {
            CLI::write('[sql] SetupScript '.$this->command.' is set up for DBCcopy but has no source set!', CLI::LOG_ERROR);
            return false;
        }
        else if (count($this->dbcSourceFiles) != 1)
            CLI::write('[sql] SetupScript '.$this->command.' is set up for DBCcopy but has multiple sources set!', CLI::LOG_WARN);

        CLI::write('[sql] copying '.$this->dbcSourceFiles[0].'.dbc into aowow_'.$this->command);

        $dbc = new DBC($this->dbcSourceFiles[0], ['temporary' => false, 'tableName' => 'aowow_'.$this->command]);
        if ($dbc->error)
            return false;

        return !!$dbc->readFile();
    }
}

trait TrCustomData
{
    // apply post generator custom data
    public function applyCustomData() : bool
    {
        $ok = true;
        foreach ((DB::Aowow()->selectCol('SELECT `entry` AS ARRAY_KEY, `field` AS ARRAY_KEY2, `value` FROM ?_setup_custom_data WHERE `command` = ?', $this->getName()) ?: []) as $id => $data)
        {
            try
            {
                DB::Aowow()->query('UPDATE ?_'.$this->getName().' SET ?a WHERE id = ?d', $data, $id);
            }
            catch (Exception $e)
            {
                trigger_error('custom data for entry #'.$id.': '.$e->getMessage(), E_USER_ERROR);
                $ok = false;
            }
        }

        return $ok;
    }
}

trait TrTemplateFile
{
    public function generate() : bool
    {
        $this->templateFill();
        return $this->success;
    }

    private function &templateCopy() : iterable
    {
        if (!$this->fileTemplateSrc || count($this->fileTemplateSrc) != count($this->fileTemplateDest))
        {
            CLI::write('[build] template file definitions missing or malformed', CLI::LOG_ERROR);
            $this->success = false;
            return;
        }

        foreach ($this->fileTemplateSrc as $idx => $srcFile)
        {
            $file = $this->fileTemplatePath.$srcFile;

            if (!file_exists($file))
            {
                CLI::write('[build] template file is missing - '.CLI::bold($file), CLI::LOG_ERROR);
                $this->success = false;
                return;
            }

            $content = file_get_contents($file);
            if (!$content)
            {
                CLI::write('[build] template file is not readable - '.CLI::bold($file), CLI::LOG_ERROR);
                $this->success = false;
                return;
            }

            // replace constants
            $content = Cfg::applyToString($content);

            yield $content;

            if (CLISetup::writeFile($this->fileTemplateDest[$idx], $content))
                continue;

            $this->success = false;
            return;
        }
    }

    private function templateFill() : void
    {
        foreach ($this->templateCopy() as &$content)
        {
            if (!$this->success)                            //templateCopy() fucked up somehow?
                return;

            // PH format: /*setup:<setupFunc>*/
            if (preg_match_all('/\/\*setup:([\w\-_]+)\*\//i', $content, $m))
            {
                foreach ($m[1] as $func)
                {
                    if (method_exists($this, $func))
                        $content = str_replace('/*setup:'.$func.'*/', $this->$func(), $content);
                    else
                    {
                        CLI::write('['.$this->getName().'] No function for was registered for placeholder '.$func.'().', CLI::LOG_ERROR);
                        $this->success = false;
                        return;
                    }
                }
            }
        }
    }
}

trait TrImageProcessor
{
    private $imgPath = '%sInterface/';
    private $status  = '';
    private $maxExecTime  = 30;

    private static $GEN_IDX_SRC_PATH  = 0;                  // php v8.0 make static > const
    private static $GEN_IDX_SRC_REAL  = 1;
    private static $GEN_IDX_LOCALE    = 2;
    private static $GEN_IDX_SRC_INFO  = 3;
    private static $GEN_IDX_DEST_INFO = 4;

    private static $JPEG_QUALITY = 85;                      // 0: worst - 100: best

    private function checkSourceDirs() : bool
    {
        $outTblLen  = 0;
        $foundCache = [];

        foreach ($this->genSteps as $i => [$subDir, $realPaths, $localized, , ])
        {
            if ($realPaths)
                continue;

            // multiple genSteps can require the same resource
            if (isset($foundCache[$subDir]))
            {
                $this->genSteps[$i][self::$GEN_IDX_SRC_REAL] = $foundCache[$subDir];
                continue;
            }

            $outTblLen = max($outTblLen, strlen($subDir));

            $path = $this->imgPath.$subDir;
            if ($p = CLISetup::filesInPathLocalized($path, $this->success, $localized))
            {
                $foundCache[$subDir] = $p;
                $this->genSteps[$i][self::$GEN_IDX_SRC_REAL] = $p;  // php v8.2+ - make GEN_IDX_SRC_REAL a const
            }
            else
                $this->success = false;
        }

        $locList = [];
        foreach (CLISetup::$expectedPaths as $xp => $locId)
            if (in_array($locId, array_keys(CLISetup::$locales)))
                $locList[] = $xp;

        CLI::write('[img-proc] required resources overview:', CLI::LOG_INFO);

        $foundCache = [];
        foreach ($this->genSteps as [$subDir, $realPaths, $localized, , ])
        {
            // one line per unique resource
            if (isset($foundCache[$subDir]))
                continue;

            $foundCache[$subDir] = true;
            if (!$realPaths)
            {
                CLI::write(CLI::red('MISSING').' - '.str_pad($subDir, $outTblLen).' @ '.sprintf($this->imgPath, '['.implode('/,', $locList).'/]').$subDir);
                $this->success = false;
            }
            else if ($localized)
            {
                $foundLoc = [];
                foreach (CLISetup::$expectedPaths as $xp => $lId)
                    if (in_array($lId, array_keys(CLISetup::$locales)))
                        if (isset($realPaths[$lId]) && ($n = stripos($realPaths[$lId], DIRECTORY_SEPARATOR.$xp.DIRECTORY_SEPARATOR)))
                            $foundLoc[$lId] = substr($realPaths[$lId], $n + 1, 4);

                if ($diff = array_diff_key(CLISetup::$locales, $foundLoc))
                {
                    $buff = [];
                    foreach ($diff as $d => $_)
                        $buff[] = CLI::red(Util::$localeStrings[$d]);
                    foreach ($foundLoc as $str)
                        $buff[] = CLI::green($str);

                    CLI::write(CLI::yellow('PARTIAL').' - '.str_pad($subDir, $outTblLen).' @ '.sprintf($this->imgPath, '['.implode('/,', $buff).'/]').$subDir);
                }
                else
                    CLI::write(CLI::green('FOUND  ').' - '.str_pad($subDir, $outTblLen).' @ '.sprintf($this->imgPath, '['.implode('/,', $foundLoc).'/]').$subDir);
            }
            else
                CLI::write(CLI::green('FOUND  ').' - '.str_pad($subDir, $outTblLen).' @ '.reset($realPaths));
        }

        CLI::write();

        // if not localized directly return result
        foreach ($this->genSteps as $i => [$subDir, $realPaths, $localized, , ])
            if (!$localized && $realPaths)
                $this->genSteps[$i][self::$GEN_IDX_SRC_REAL] = reset($realPaths);

        return $this->success;
    }

    // prefer manually converted PNG files (as the imagecreatefromblp-script has issues with some formats)
    // alpha channel issues observed with locale deDE Hilsbrad and Elwynn - maps
    // see: https://github.com/Kanma/BLPConverter
    private function loadImageFile(string $path, ?bool &$noSrc = false) // : ?GdImage
    {
        $result = null;
        $noSrc  = false;
        $path   = preg_replace('/\.(png|blp)$/i', '', $path);

        $file = $path.'.png';
        if (CLISetup::fileExists($file))
        {
            CLI::write('[img-proc] manually converted png file present for '.$file, CLI::LOG_INFO);
            $result = imagecreatefrompng($file);
        }

        if (!$result)
        {
            $file = $path.'.blp';
            if (CLISetup::fileExists($file))
                $result = imagecreatefromblp($file);
            else
                $noSrc = true;
        }

        return $result;
    }

    private function writeImageFile(/* GdImage */ $src, string $outFile, array $srcDims, array $destDims) : bool
    {
        $success = false;
        $outRes  = imagecreatetruecolor($destDims['w'], $destDims['h']);
        $ext     = substr($outFile, -3, 3);

        imagesavealpha($outRes, true);
        if ($ext == 'png')
        {
            imagealphablending($outRes, false);
            $transparentindex = imagecolorallocatealpha($outRes, 255, 255, 255, 127);
            imagefill($outRes, 0, 0, $transparentindex);
        }

        imagecopyresampled($outRes, $src, $destDims['x'], $destDims['x'], $srcDims['x'], $srcDims['y'], $destDims['w'], $destDims['h'], $srcDims['w'], $srcDims['h']);

        switch ($ext)
        {
            case 'jpg':
                $success = imagejpeg($outRes, $outFile, self::$JPEG_QUALITY);
                break;
            case 'gif':
                $success = imagegif($outRes, $outFile);
                break;
            case 'png':
                $success = imagepng($outRes, $outFile);
                break;
            default:
                CLI::write('[img-proc] '.$this->status.' - unsupported file fromat: '.$ext, CLI::LOG_WARN);
        }

        imagedestroy($outRes);

        if ($success)
        {
            chmod($outFile, Util::FILE_ACCESS);
            CLI::write('[img-proc] '.$this->status.' - image '.$outFile.' written', CLI::LOG_OK, true, true);
        }
        else
            CLI::write('[img-proc] '.$this->status.' - could not create image '.$outFile, CLI::LOG_ERROR);

        return $success;
    }
}

trait TrComplexImage
{
    use TrImageProcessor { TrImageProcessor::writeImageFile as _writeImageFile; }

    private function writeImageFile(/* GdImage */ $src, string $outFile, int $w, int $h) : bool
    {
        $srcDims = array(
            'x' => 0,
            'y' => 0,
            'w' => imagesx($src),
            'h' => imagesy($src)
        );
        $destDims = array(
            'x' => 0,
            'y' => 0,
            'w' => $w,
            'h' => $h
        );

        return $this->_writeImageFile($src, $outFile, $srcDims, $destDims);
    }

    private function createAlphaImage(int $w, int $h) //: ?GdImage
    {
        $img = imagecreatetruecolor($w, $h);
        if (!$img)
            return null;

        imagesavealpha($img, true);
        imagealphablending($img, false);

        $bgColor = imagecolorallocatealpha($img, 0, 0, 0, 127);
        imagefilledrectangle($img, 0, 0, imagesx($img) - 1, imagesy($img) - 1, $bgColor);

        imagecolortransparent($img, $bgColor);
        imagealphablending($img, true);

        imagecolordeallocate($img, $bgColor);

        return $img;
    }

    private function assembleImage(string $baseName, array $tileData, int $destW, int $destH) //: ?GdImage
    {
        $dest = imagecreatetruecolor($destW, $destH);
        if (!$dest)
            return null;

        imagesavealpha($dest, true);
        imagealphablending($dest, false);

        $tileH = $destH;
        foreach ($tileData as $y => $row)
        {
            $tileW = $destW;
            foreach ($row as $x => $suffix)
            {
                $src = $this->loadImageFile($baseName.$suffix, $noSrcFile);
                if (!$src)
                {
                    if ($noSrcFile)
                        CLI::write('[img-proc-c] tile '.$baseName.$suffix.'.blp missing.', CLI::LOG_ERROR);

                    unset($dest);
                    return null;
                }

                imagecopyresampled($dest, $src, 256 * $x, 256 * $y, 0, 0, min($tileW, 256), min($tileH, 256), min($tileW, 256), min($tileH, 256));
                $tileW -= 256;

                unset($src);
            }
            $tileH -= 256;
        }

        return $dest;
    }
}

abstract class SetupScript
{
    // FileGen
    protected $requiredDirs       = [];
    protected $fileTemplateDest   = [];
    protected $fileTemplatePath   = 'setup/tools/filegen/templates/';
    protected $fileTemplateSrc    = [];

    // SQLGen
    protected $result = '';

    // FileGen + SQLGen
    protected $dbcSourceFiles     = [];                     // relies on these dbc files. Read into db if related table is missing
    protected $worldDependency    = [];                     // query when this table changed (--sync command)

    protected $info               = [];                     // arr: 0 => self, n => genSteps        cmd => [[arr<str>:optionalArgs], int:argFlags, str:description]
    protected $setupAfter         = [[], []];               // [[sqlgen], [filegen]]                used to sort scripts that rely on each other being executed in the right order (script names are not nessecarily the same as their table names)

    protected $success            = true;
    protected $localized          = false;                  // push locale directories onto $requiredDirs?
    protected $useGlobalStrings   = false;                  // uses data from interface/framexml/globalstrings.lua

    public $isOptional            = false;                  // not a part of the setup chain


    abstract public function generate() : bool;

    public function getRequiredDBCs() : array
    {
        return $this->dbcSourceFiles;
    }

    public function getSelfDependencies() : array
    {
        return $this->setupAfter;
    }

    public function getRemoteDependencies() : array
    {
        return $this->worldDependency;
    }

    public function getName() : string
    {
        reset($this->info);
        return key($this->info);
    }

    public function getInfo() // : string|int
    {
        // info: name => [param, paramFlags, description]
        return (reset($this->info)[2] ?? '').($this->isOptional ? ' - '.Cli::yellow('[omitted by setup]') : '');
    }

    public function getSubCommands() : array
    {
        $sub = [];

        if (count($this->info) > 1)
            $sub = array_slice($this->info, 1, null, true);

        return $sub;
    }

    public function getRequiredDirs(): array
    {
        return $this->requiredDirs;
    }

    public function fulfillRequirements() : bool
    {
        // create directory structure
        $newDirs = 0;
        $existed = false;
        foreach ($this->getRequiredDirs() as $dir)
        {
            $dirs = [];
            if (!$this->localized)
                $dirs[] = $dir;
            else
                foreach (CLISetup::$locales as $str)
                    $dirs[] = $dir . $str . DIRECTORY_SEPARATOR;

            foreach ($dirs as $d)
            {
                if (!CLISetup::writeDir($d, $existed))
                {
                    CLI::write('[build] could not create directory: '.CLI::bold($d), CLI::LOG_ERROR);
                    return false;
                }
            }

            $newDirs += ($existed ? 0 : 1);
        }

        if ($newDirs)
            CLI::write('[build] created '.$newDirs.' extra paths');

        // load DBC files
        if (!in_array('TrDBCcopy', class_uses($this)))
        {
            foreach ($this->getRequiredDBCs() as $req)
            {
                if (CLISetup::loadDBC($req))
                    continue;

                CLI::write('[sql/build] '. $this->getName() . ' is missing dbc file ' . $req . '. Skipping...', CLI::LOG_ERROR);
                return false;
            }
        }

        if ($this->useGlobalStrings)
            if (!CLISetup::loadGlobalStrings())
                return false;

        return true;
    }

    public function writeCLIHelp() : bool
    {
        if (count($this->info) < 2)
            return false;                                   // help not provided, display parents help text

        $lines = [];
        foreach ($this->info as $cmd => [$shortOpts, $flags, $text])
        {
            $line = ($flags & CLISetup::ARGV_PARAM ? '' : '--').$cmd;
            foreach ($shortOpts as $so)
                $line .= ' | '.(strlen($so) == 1 ? '-'.$so : '--'.$so);

            $lines[] = [$line, $text];
        }

        CLI::writeTable($lines);

        if ($this->dbcSourceFiles)
        {
            sort($this->dbcSourceFiles);
            CLI::write('Will use client data tables:', CLI::LOG_NONE, false);
            foreach ($this->dbcSourceFiles as $dbc)
                CLI::write(' * '.$dbc.'.dbc', CLI::LOG_NONE, false);
            CLI::write();
        }

        if ($this->worldDependency)
        {
            sort($this->worldDependency);
            CLI::write('Depends on world db tables:', CLI::LOG_NONE, false);
            foreach ($this->worldDependency as $tbl)
                CLI::write(' * '.$tbl, CLI::LOG_NONE, false);
            CLI::write();
        }

        if ($this->setupAfter[0])
        {
            sort($this->setupAfter[0]);
            CLI::write('Requires data generators:', CLI::LOG_NONE, false);
            foreach ($this->setupAfter[0] as $sql)
                CLI::write(' * '.$sql, CLI::LOG_NONE, false);
            CLI::write();
        }

        if ($this->setupAfter[1])
        {
            sort($this->setupAfter[1]);
            CLI::write('Requires file generators:', CLI::LOG_NONE, false);
            foreach ($this->setupAfter[1] as $build)
                CLI::write(' * '.$build, CLI::LOG_NONE, false);
            CLI::write();
        }

        CLI::write();
        return true;                                        // help was provided, skip help from parent
    }

    protected function reapplyCCFlags(string $tbl, int $type) : void
    {
        // reapply flags for community content as these are lost when the table is rebuild

        if (preg_match('/\W/i', $tbl))
        {
            trigger_error('[sql] reapplyCCFlags() - invalid table name');
            return;
        }

        DB::Aowow()->query('UPDATE ?_'.$tbl.' x, ?_comments    y SET x.`cuFlags` = x.`cuFlags` | ?d WHERE x.`id` = y.`typeId` AND y.`type` = ?d AND y.`flags`  & ?d', CUSTOM_HAS_COMMENT,    $type, CC_FLAG_APPROVED);
        DB::Aowow()->query('UPDATE ?_'.$tbl.' x, ?_screenshots y SET x.`cuFlags` = x.`cuFlags` | ?d WHERE x.`id` = y.`typeId` AND y.`type` = ?d AND y.`status` & ?d', CUSTOM_HAS_SCREENSHOT, $type, CC_FLAG_APPROVED);
        DB::Aowow()->query('UPDATE ?_'.$tbl.' x, ?_videos      y SET x.`cuFlags` = x.`cuFlags` | ?d WHERE x.`id` = y.`typeId` AND y.`type` = ?d AND y.`status` & ?d', CUSTOM_HAS_VIDEO,      $type, CC_FLAG_APPROVED);
    }
}

?>
