<?php
declare(strict_types=1);

/**
 * ============================================================
 *  軽量テンプレエンジン（Mustache風）
 * ------------------------------------------------------------
 *  {{key}}        … 値を出力（HTMLエスケープ）
 *  {{{key}}}      … 値を生(raw)で出力
 *  {{&key}}       … 同上（raw）
 *  {{#key}}…{{/key}}  … セクション
 *        ・key が「行配列」: 各行をコンテキストにして繰り返し
 *        ・key が真値      : 1回だけ出力
 *        ・key が偽値/空    : 出力しない
 *  {{^key}}…{{/key}}  … 反転セクション（偽値/空のとき出力）
 * ============================================================
 */

function render_template(string $tpl, array $data): string {
    $tokens = render_tokenize($tpl);
    $i = 0;
    return render_nodes($tokens, $i, [$data], null);
}

/** テンプレ文字列をトークン列へ */
function render_tokenize(string $tpl): array {
    $pattern = '/\{\{\{\s*([\w.]+)\s*\}\}\}|\{\{\s*([#^\/&]?)\s*([\w.]+)\s*\}\}/';
    $tokens = [];
    $offset = 0;
    if (preg_match_all($pattern, $tpl, $m, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
        foreach ($m as $match) {
            $whole = $match[0][0];
            $pos   = $match[0][1];
            if ($pos > $offset) {
                $tokens[] = ['t' => 'text', 'v' => substr($tpl, $offset, $pos - $offset)];
            }
            if (isset($match[1]) && $match[1][0] !== '' && $match[1][1] !== -1) {
                // {{{ raw }}}
                $tokens[] = ['t' => 'var', 'name' => $match[1][0], 'raw' => true];
            } else {
                $sig  = $match[2][0];
                $name = $match[3][0];
                if ($sig === '#')       $tokens[] = ['t' => 'sec',  'name' => $name, 'inv' => false];
                elseif ($sig === '^')   $tokens[] = ['t' => 'sec',  'name' => $name, 'inv' => true];
                elseif ($sig === '/')   $tokens[] = ['t' => 'end',  'name' => $name];
                elseif ($sig === '&')   $tokens[] = ['t' => 'var',  'name' => $name, 'raw' => true];
                else                    $tokens[] = ['t' => 'var',  'name' => $name, 'raw' => false];
            }
            $offset = $pos + strlen($whole);
        }
    }
    if ($offset < strlen($tpl)) {
        $tokens[] = ['t' => 'text', 'v' => substr($tpl, $offset)];
    }
    return $tokens;
}

/** トークンを描画（再帰）。$stop が指定されたら、その名前の end で止める */
function render_nodes(array $tokens, int &$i, array $stack, ?string $stop): string {
    $out = '';
    $n = count($tokens);
    while ($i < $n) {
        $tok = $tokens[$i];
        if ($tok['t'] === 'end') {
            if ($stop !== null && $tok['name'] === $stop) { $i++; return $out; }
            $i++; // 対応しない end は無視
            continue;
        }
        if ($tok['t'] === 'text') {
            $out .= $tok['v'];
            $i++;
            continue;
        }
        if ($tok['t'] === 'var') {
            $val = render_lookup($stack, $tok['name']);
            $str = render_scalar($val);
            $out .= $tok['raw'] ? $str : h($str);
            $i++;
            continue;
        }
        if ($tok['t'] === 'sec') {
            $name = $tok['name'];
            $inv  = $tok['inv'];
            $i++; // セクション開始を消費
            $startIndex = $i;
            $val = render_lookup($stack, $name);
            $truthy = render_truthy($val);

            if ($inv) {
                // 反転：偽のときだけ中身を出力
                if (!$truthy) {
                    $out .= render_nodes($tokens, $i, $stack, $name);
                } else {
                    render_skip($tokens, $i, $name);
                }
            } else {
                if (is_array($val) && render_is_list($val)) {
                    // 行配列 → 繰り返し
                    $buf = '';
                    foreach ($val as $row) {
                        $ctx = is_array($row) ? $row : ['.' => $row];
                        $j = $startIndex;
                        $buf .= render_nodes($tokens, $j, array_merge($stack, [$ctx]), $name);
                        $i = $j; // 最後の位置で確定
                    }
                    if (count($val) === 0) { render_skip($tokens, $i, $name); }
                    $out .= $buf;
                } elseif ($truthy) {
                    $ctx = is_array($val) ? $val : [];
                    $out .= render_nodes($tokens, $i, array_merge($stack, [$ctx]), $name);
                } else {
                    render_skip($tokens, $i, $name);
                }
            }
            continue;
        }
        $i++;
    }
    return $out;
}

/** セクションを描画せず読み飛ばす（ネスト対応） */
function render_skip(array $tokens, int &$i, string $name): void {
    $depth = 0;
    $n = count($tokens);
    while ($i < $n) {
        $tok = $tokens[$i];
        if ($tok['t'] === 'sec' && $tok['name'] === $name) { $depth++; }
        elseif ($tok['t'] === 'end' && $tok['name'] === $name) {
            if ($depth === 0) { $i++; return; }
            $depth--;
        }
        $i++;
    }
}

/** コンテキストスタックから名前を解決（上から探す） */
function render_lookup(array $stack, string $name) {
    if ($name === '.') {
        $top = end($stack);
        return $top['.'] ?? $top;
    }
    for ($k = count($stack) - 1; $k >= 0; $k--) {
        if (is_array($stack[$k]) && array_key_exists($name, $stack[$k])) {
            return $stack[$k][$name];
        }
    }
    return null;
}

/** 真偽判定（mustache準拠：null/false/""/空配列 は偽） */
function render_truthy($v): bool {
    if ($v === null || $v === false) return false;
    if ($v === '' ) return false;
    if (is_array($v)) return count($v) > 0;
    return true;
}

/** 連番配列（行リスト）かどうか */
function render_is_list($v): bool {
    if (!is_array($v) || $v === []) return is_array($v);
    return array_keys($v) === range(0, count($v) - 1);
}

/** スカラーへ変換 */
function render_scalar($v): string {
    if ($v === null || $v === false) return '';
    if ($v === true) return '1';
    if (is_array($v)) return '';
    return (string)$v;
}
