<?php

namespace engine\skin;

use app\ThemesUtil;
use engine\http\HtmlResponse;
use engine\http\Response;
use engine\skin\TwigAddons\JsTagRuntime;
use engine\skin\TwigAddons\JsTwigExtension;

abstract class FeaturedSkin
    extends BaseSkin
{
    const array RESOURCE_INTEGRITY_HASHES = ['sha256', 'sha384', 'sha512'];

    public array $exportedStrings = [];

    protected array $js = [];
    public array $static = [];
    protected array $styleNames = [];
    protected array $svgDefs = [];
    public readonly Meta $meta;

    public function __construct() {
        parent::__construct();
        $this->meta = new Meta();

        $this->twig->addExtension(new JsTwigExtension($this));
        $this->twig->addRuntimeLoader(new \Twig\RuntimeLoader\FactoryRuntimeLoader([
            JsTagRuntime::class => function () {
                return new JsTagRuntime($this);
            },
        ]));
    }

    public function exportStrings(array|string $keys): void {
        $this->exportedStrings = array_merge($this->exportedStrings, is_string($keys) ? $this->strings->search($keys) : $keys);
    }

    public function addStatic(string ...$files): void {
        foreach ($files as $file)
            $this->static[] = $file;
    }

    public function addJS(string $js): void {
        if ($js != '')
            $this->js[] = $js;
    }

    protected function getJS(): string {
        if (empty($this->js))
            return '';
        return implode("\n", $this->js);
    }

    public function preloadSVG(string $name): void {
        if (isset($this->svgDefs[$name]))
            return;

        if (!preg_match_all('/\d+/', $name, $matches))
            throw new \InvalidArgumentException('icon name '.$name.' is invalid, it should follow following pattern: $name_$size[_$size]');

        $size = array_slice($matches[0], -2);
        $this->svgDefs[$name] = [
            'width' => $size[0],
            'height' => $size[1] ?? $size[0]
        ];
    }

    public function getSVG(string $name, bool $in_place = false): ?string {
        $this->preloadSVG($name);
        $w = $this->svgDefs[$name]['width'];
        $h = $this->svgDefs[$name]['height'];
        if ($in_place) {
            $svg = '<svg id="svgicon_'.$name.'" width="'.$w.'" height="'.$h.'" fill="currentColor" viewBox="0 0 '.$w.' '.$h.'">';
            $svg .= file_get_contents(APP_ROOT.'/src/skins/svg/'.$name.'.svg');
            $svg .= '</svg>';
            return $svg;
        } else {
            return '<svg width="'.$w.'" height="'.$h.'"><use xlink:href="#svgicon_'.$name.'"></use></svg>';
        }
    }

    public function renderPage(string $template, array $vars = []): Response {
        $this->applyGlobals();

        // render body first (in order for all svgPreload() calls to be executed)
        $b = $this->renderBody($template, $vars);

        // then everything else
        $h = $this->renderHeader();
        $f = $this->renderFooter();

        return new HtmlResponse($h.$b.$f);
    }

    public function renderBreadCrumbs(array $items, ?string $style = null, bool $mt = false): string {
        static $chevron = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M7.47 4.217a.75.75 0 0 0 0 1.06L12.185 10 7.469 14.72a.75.75 0 1 0 1.062 1.06l5.245-5.25a.75.75 0 0 0 0-1.061L8.531 4.218a.75.75 0 0 0-1.061-.001z" fill="currentColor"/></svg>';
        $buf = implode(array_map(function (array $i) use ($chevron): string {
            $buf = '';
            $has_url = array_key_exists('url', $i);

            if ($has_url)
                $buf .= '<a class="bc-item" href="'.htmlescape($i['url']).'">';
            else
                $buf .= '<span class="bc-item">';
            $buf .= htmlescape($i['text']);

            if ($has_url)
                $buf .= ' <span class="bc-arrow">'.$chevron.'</span></a>';
            else
                $buf .= '</span>';

            return $buf;
        }, $items));
        $class = 'bc';
        if ($mt)
            $class .= ' mt';
        return '<div class="'.$class.'"'.($style ? ' style="'.$style.'"' : '').'>'.$buf.'</div>';
    }

    public function renderPageNav(int $page, int $pages, string $link_template, ?array $opts = null): string {
        if ($opts === null) {
            $count = 0;
        } else {
            $opts = array_merge(['count' => 0], $opts);
            $count = $opts['count'];
        }

        $min_page = max(1, $page - 2);
        $max_page = min($pages, $page + 2);

        $pages_html = '';
        $base_class = 'pn-button no-hover no-select no-drag is-page';
        for ($p = $min_page; $p <= $max_page; $p++) {
            $class = $base_class;
            if ($p == $page)
                $class .= ' is-page-cur';
            $pages_html .= '<a class="'.$class.'" href="'.htmlescape(self::pageNavGetLink($p, $link_template)).'" data-page="'.$p.'" draggable="false">'.$p.'</a>';
        }

        if ($min_page > 2) {
            $pages_html = '<div class="pn-button-sep no-select no-drag">&nbsp;</div>'.$pages_html;
        }
        if ($min_page > 1) {
            $pages_html = '<a class="'.$base_class.'" href="'.htmlescape(self::pageNavGetLink(1, $link_template)).'" data-page="1" draggable="false">1</a>'.$pages_html;
        }

        if ($max_page < $pages - 1) {
            $pages_html .= '<div class="pn-button-sep no-select no-drag">&nbsp;</div>';
        }
        if ($max_page < $pages) {
            $pages_html .= '<a class="'.$base_class.'" href="'.htmlescape(self::pageNavGetLink($pages, $link_template)).'" data-page="'.$pages.'" draggable="false">'.$pages.'</a>';
        }

        $pn_class = 'pn';
        if ($pages < 2) {
            $pn_class .= ' no-nav';
            if (!$count) {
                $pn_class .= ' no-results';
            }
        }

        $html = <<<HTML
<div class="{$pn_class}">
    <div class="pn-buttons clearfix">
        {$pages_html}
    </div>
</div>
HTML;

        return $html;
    }

    protected static function pageNavGetLink($page, $link_template) {
        return is_callable($link_template) ? $link_template($page) : str_replace('{page}', $page, $link_template);
    }

    protected function getSVGTags(): string {
        $buf = '<svg style="display: none">';
        foreach ($this->svgDefs as $name => $icon) {
            $content = file_get_contents(APP_ROOT.'/src/skins/svg/'.$name.'.svg');
            $buf .= "<symbol id=\"svgicon_{$name}\" viewBox=\"0 0 {$icon['width']} {$icon['height']}\" fill=\"currentColor\">$content</symbol>";
        }
        $buf .= '</svg>';
        return $buf;
    }

    protected function getHeaderStaticTags(): string {
        $html = [];
        $theme = ThemesUtil::getUserTheme();
        $dark = $theme == 'dark' || ($theme == 'auto' && ThemesUtil::isUserSystemThemeDark());
        $this->styleNames = [];
        foreach ($this->static as $name) {
            // javascript
            if (str_starts_with($name, 'js/'))
                $html[] = $this->jsLink($name);

            // css
            else if (str_starts_with($name, 'css/')) {
                $html[] = $this->cssLink($name, 'light', $style_name_ptr);
                $this->styleNames[] = $style_name_ptr;

                if ($dark)
                    $html[] = $this->cssLink($name, 'dark', $style_name_ptr);
                else if (!isDev())
                    $html[] = $this->cssPrefetchLink($style_name_ptr.'_dark');
            } else
                logError(__FUNCTION__.': unexpected static entry: '.$name);
        }
        return implode("\n", $html);
    }

    protected function getFooterScriptTags(): string {
        global $config, $globalContext;

        $html = '<script type="text/javascript">';

        if (isDev())
            $versions = '{}';
        else {
            $versions = [];
            foreach ($config['assets'][$globalContext->project] as $name => $v) {
                list($type, $bname) = $this->getStaticNameParts($name);
                $versions[$type][$bname] = $v;
            }
            $versions = jsonEncode($versions);
        }
        $html .= 'StaticManager.init(\''.(isDev() ? 'dev' : $config['commit_hash']).'\', '.jsonEncode($this->styleNames).', '.$versions.');';
        $html .= 'ThemeSwitcher.init();';

        if (!empty($this->exportedStrings)) {
            $lang = [];
            foreach ($this->exportedStrings as $key)
                $lang[$key] = lang($key);
            $html .= 'extend(__lang, '.jsonEncode($lang).');';
        }

        $js = $this->getJS();
        if ($js)
            $html .= '(function(){try{'.$js.'}catch(e){window.console&&console.error("caught exception:",e)}})();';

        $html .= '</script>';
        return $html;
    }

    protected function jsLink(string $name): string {
        global $config;
        list (, $bname) = $this->getStaticNameParts($name);
        if (isDev()) {
            $href = '/js.php?name='.urlencode($bname).'&amp;v='.time();
        } else {
            $href = '/dist-js/'.$bname.'.js?v='.$config['commit_hash'];
        }
        return '<script src="'.$href.'" type="text/javascript"'.$this->getStaticIntegrityAttribute($name).'></script>';
    }

    protected function cssLink(string $name, string $theme, &$bname = null): string {
        global $config;
        list(, $bname) = $this->getStaticNameParts($name);

        $config_name = 'css/'.$bname.($theme == 'dark' ? '_dark' : '').'.css';

        if (isDev()) {
            $href = '/sass.php?name='.urlencode($bname).'&amp;theme='.$theme.'&amp;v='.time();
        } else {
            $href = '/dist-css/'.$bname.($theme == 'dark' ? '_dark' : '').'.css?v='.$config['commit_hash'];
        }

        $id = 'style_'.$bname;
        if ($theme == 'dark')
            $id .= '_dark';

        return '<link rel="stylesheet" id="'.$id.'" type="text/css" href="'.$href.'"'.$this->getStaticIntegrityAttribute($config_name).'>';
    }

    protected function cssPrefetchLink(string $name): string {
        global $config;
        $url = '/dist-css/'.$name.'.css?v='.$config['commit_hash'];
        $integrity = $this->getStaticIntegrityAttribute('css/'.$name.'.css');
        return '<link rel="prefetch" href="'.$url.'"'.$integrity.' />';
    }

    protected function getStaticNameParts(string $name): array {
        $dname = dirname($name);
        $bname = basename($name);
        if (($pos = strrpos($bname, '.'))) {
            $ext = substr($bname, $pos + 1);
            $bname = substr($bname, 0, $pos);
        } else {
            $ext = '';
        }
        return [$dname, $bname, $ext];
    }

    protected function getStaticIntegrityAttribute(string $name): string {
        if (isDev())
            return '';
        global $config, $globalContext;
        return ' integrity="'.implode(' ', array_map(fn($hash_type) => $hash_type.'-'.$config['assets'][$globalContext->project][$name]['integrity'][$hash_type], self::RESOURCE_INTEGRITY_HASHES)).'"';
    }
}