<?php

declare(strict_types=1);

namespace Acms\Services\Asset;

/**
 * AssetDefinition[] を HTML タグ文字列へ変換する純粋関数群。
 *
 * 並び替えは行わない（並び替えは {@see DependencyResolver} の責務）。
 * グローバル状態・I/O に依存しないため単体テストできるようにしている。
 */
final class AssetRenderer
{
    /**
     * @param AssetDefinition[] $definitions render() が呼ぶ時点で並び替え・position絞り込み済みであること
     * @param (callable(string $src, string $realPath): string)|null $cacheBuster
     *   $realPathResolver が実体パスを解決できた src をキャッシュバスティングする関数。
     *   省略時はグローバル関数 cacheBusting() を使う。テストでは差し替えて検証できる。
     * @param (callable(string $src): ?string)|null $realPathResolver
     *   src からファイルシステム上の実体パスを導出する関数（導出できなければ null を返し、
     *   その場合キャッシュバスティングは行われない）。省略時は DIR_OFFSET / SCRIPT_DIR 定数を用いた
     *   既定の解決（{@see AssetPathResolver}）を使う。テストでは差し替えて検証できる。
     */
    public static function buildTags(array $definitions, ?callable $cacheBuster = null, ?callable $realPathResolver = null): string
    {
        $cacheBuster ??= 'cacheBusting';
        $realPathResolver ??= self::defaultRealPathResolver();
        $html = '';
        foreach ($definitions as $definition) {
            $html .= self::buildTag($definition, $cacheBuster, $realPathResolver) . "\n";
        }
        return $html;
    }

    /**
     * @return callable(string $src): ?string
     */
    private static function defaultRealPathResolver(): callable
    {
        return static function (string $src): ?string {
            if (!defined('DIR_OFFSET') || !defined('SCRIPT_DIR')) {
                return null;
            }
            return AssetPathResolver::toRealPath($src, DIR_OFFSET, SCRIPT_DIR);
        };
    }

    /**
     * @param callable(string $src, string $realPath): string $cacheBuster
     * @param callable(string $src): ?string $realPathResolver
     */
    private static function buildTag(AssetDefinition $definition, callable $cacheBuster, callable $realPathResolver): string
    {
        switch ($definition->type) {
            case AssetDefinition::TYPE_SCRIPT:
                $src = self::resolveSrc($definition, $cacheBuster, $realPathResolver);
                return '<script src="' . self::escape($src) . '"' . self::buildAttributes($definition->attrs) . '></script>';
            case AssetDefinition::TYPE_STYLE:
                $src = self::resolveSrc($definition, $cacheBuster, $realPathResolver);
                return '<link rel="stylesheet" href="' . self::escape($src) . '"' . self::buildAttributes($definition->attrs) . '>';
            case AssetDefinition::TYPE_INLINE_SCRIPT:
                return '<script' . self::buildAttributes($definition->attrs) . '>' . $definition->content . '</script>';
            case AssetDefinition::TYPE_INLINE_STYLE:
                return '<style' . self::buildAttributes($definition->attrs) . '>' . $definition->content . '</style>';
            case AssetDefinition::TYPE_RAW:
            default:
                return (string) $definition->content;
        }
    }

    /**
     * @param callable(string $src, string $realPath): string $cacheBuster
     * @param callable(string $src): ?string $realPathResolver
     */
    private static function resolveSrc(AssetDefinition $definition, callable $cacheBuster, callable $realPathResolver): string
    {
        $src = (string) $definition->src;
        $realPath = $realPathResolver($src);
        if ($realPath !== null) {
            $src = $cacheBuster($src, $realPath);
        }
        return self::withVersion($src, $definition->version);
    }

    private static function withVersion(string $src, ?string $version): string
    {
        if ($version === null || $version === '') {
            return $src;
        }
        $separator = strpos($src, '?') !== false ? '&' : '?';
        return $src . $separator . 'v=' . $version;
    }

    /**
     * @param array<string, string|bool> $attrs
     */
    private static function buildAttributes(array $attrs): string
    {
        $parts = [];
        foreach ($attrs as $name => $value) {
            if ($value === false) {
                continue;
            }
            if ($value === true) {
                $parts[] = $name;
                continue;
            }
            $parts[] = $name . '="' . self::escape($value) . '"';
        }
        if ($parts === []) {
            return '';
        }
        return ' ' . implode(' ', $parts);
    }

    private static function escape(string $value): string
    {
        return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
    }
}
