#!/usr/bin/env python3
"""Compare layer0 and layer1 tile-class fallback passability by map."""
from __future__ import annotations

import argparse
import html
import json
from pathlib import Path

from summarize_map_tiles import load_maps
from tile_classes import load as load_tile_classes


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
DATA = ROOT / "data"


def effective_layer1(info: dict, tile: int) -> int:
    return 0 if tile in set(info.get("emptyLayer1Tiles") or []) else tile


def summarize_map(name: str, info: dict, classes: dict[str, dict]) -> dict:
    tileset = (info.get("layerTilesets") or [info.get("tileset")])[0]
    entry = classes.get(tileset, {})
    pass_tiles = set(entry.get("pass", []))
    block_tiles = set(entry.get("block", []))
    pass_pairs = {tuple(pair) for pair in entry.get("passPairs", [])}
    block_pairs = {tuple(pair) for pair in entry.get("blockPairs", [])}

    explicit_pass = 0
    explicit_block = 0
    fallback_same_pass = 0
    fallback_same_block = 0
    newly_passable = 0
    examples = []

    for index, (layer0, raw_layer1) in enumerate(zip(info["layers"][0], info["layers"][1])):
        pair = (layer0, raw_layer1)
        if pair in pass_pairs or layer0 in pass_tiles:
            explicit_pass += 1
            continue
        if pair in block_pairs or layer0 in block_tiles:
            explicit_block += 1
            continue

        layer0_pass = layer0 > 0
        layer1_pass = layer0 > 0 and effective_layer1(info, raw_layer1) == 0
        if layer0_pass and not layer1_pass:
            newly_passable += 1
            if len(examples) < 5:
                examples.append({
                    "x": index % info["width"],
                    "y": index // info["width"],
                    "layer0": layer0,
                    "layer1": raw_layer1,
                })
        elif layer0_pass and layer1_pass:
            fallback_same_pass += 1
        else:
            fallback_same_block += 1

    tile_count = len(info["layers"][0])
    return {
        "map": name,
        "tileset": tileset,
        "tileCount": tile_count,
        "explicitPass": explicit_pass,
        "explicitBlock": explicit_block,
        "fallbackSamePass": fallback_same_pass,
        "fallbackSameBlock": fallback_same_block,
        "newlyPassable": newly_passable,
        "newlyPassablePct": round(newly_passable * 100 / tile_count, 2) if tile_count else 0,
        "examples": examples,
    }


def summarize(maps: dict[str, dict], classes: dict[str, dict]) -> list[dict]:
    return [
        summarize_map(name, info, classes)
        for name, info in sorted(maps.items())
    ]


def markdown(rows: list[dict], web_prefix: str = "../web") -> str:
    top_count = sorted(rows, key=lambda row: row["newlyPassable"], reverse=True)[:12]
    top_pct = sorted(rows, key=lambda row: row["newlyPassablePct"], reverse=True)[:12]
    lines = [
        "# Collision Fallback Delta",
        "",
        "Generated from `out/maps.js` and `data/tile_classes.json`.",
        "",
        "Compares the current runtime fallback (`layer0 > 0`) against the previous layer1-sensitive fallback "
        "(`layer0 > 0 && effective layer1 == 0`) after explicit pass/block tile and pair classes are applied. "
        "`newly passable` is the number of cells that the current fallback opens compared with the previous fallback.",
        "",
        "## Top By Count",
        "",
        "| map | tileset | newly passable | first examples |",
        "| --- | --- | ---: | --- |",
    ]
    for row in top_count:
        examples = ", ".join(
            f"[{item['x']},{item['y']}]({web_prefix}/game.html?map={row['map']}"
            f"&startTile={item['x']},{item['y']}&focusTile={item['x']},{item['y']}&collision=1&overview=1)"
            for item in row["examples"][:3]
        ) or "-"
        lines.append(
            f"| {row['map']} | {row['tileset']} | "
            f"{row['newlyPassable']}/{row['tileCount']} ({row['newlyPassablePct']}%) | {examples} |"
        )
    lines.extend([
        "",
        "## Top By Percent",
        "",
        "| map | tileset | newly passable | first examples |",
        "| --- | --- | ---: | --- |",
    ])
    for row in top_pct:
        examples = ", ".join(
            f"[{item['x']},{item['y']}]({web_prefix}/game.html?map={row['map']}"
            f"&startTile={item['x']},{item['y']}&focusTile={item['x']},{item['y']}&collision=1&overview=1)"
            for item in row["examples"][:3]
        ) or "-"
        lines.append(
            f"| {row['map']} | {row['tileset']} | "
            f"{row['newlyPassable']}/{row['tileCount']} ({row['newlyPassablePct']}%) | {examples} |"
        )
    lines.extend([
        "",
        "## All Maps",
        "",
        "| map | tileset | newly passable | explicit pass | explicit block | fallback same pass | fallback same block | examples |",
        "| --- | --- | ---: | ---: | ---: | ---: | ---: | --- |",
    ])
    for row in rows:
        examples = ", ".join(
            f"[{item['x']},{item['y']} L0={item['layer0']} L1={item['layer1']}]({web_prefix}/game.html?map={row['map']}"
            f"&startTile={item['x']},{item['y']}&focusTile={item['x']},{item['y']}&collision=1&overview=1)"
            for item in row["examples"]
        ) or "-"
        lines.append(
            f"| {row['map']} | {row['tileset']} | "
            f"{row['newlyPassable']}/{row['tileCount']} ({row['newlyPassablePct']}%) | "
            f"{row['explicitPass']} | {row['explicitBlock']} | "
            f"{row['fallbackSamePass']} | {row['fallbackSameBlock']} | {examples} |"
        )
    lines.append("")
    return "\n".join(lines)


def example_links(row: dict, web_prefix: str, limit: int = 3, with_tiles: bool = False) -> str:
    links = []
    for item in row["examples"][:limit]:
        label = f"{item['x']},{item['y']}"
        if with_tiles:
            label = f"{label} L0={item['layer0']} L1={item['layer1']}"
        href = (
            f"{web_prefix}/game.html?map={row['map']}"
            f"&startTile={item['x']},{item['y']}&focusTile={item['x']},{item['y']}&collision=1&overview=1"
        )
        links.append(f'<a href="{html.escape(href)}">{html.escape(label)}</a>')
    return ", ".join(links) or "-"


def table_html(rows: list[dict], web_prefix: str, compact: bool = False) -> str:
    body = []
    for row in rows:
        open_href = f"{web_prefix}/game.html?map={row['map']}&collision=1&overview=1"
        if compact:
            body.append(
                "\n".join(
                    [
                        "<tr>",
                        f'  <td><a href="{html.escape(open_href)}">{html.escape(row["map"])}</a></td>',
                        f"  <td>{html.escape(row['tileset'])}</td>",
                        f"  <td>{row['newlyPassable']}/{row['tileCount']} ({row['newlyPassablePct']}%)</td>",
                        f"  <td>{example_links(row, web_prefix)}</td>",
                        "</tr>",
                    ]
                )
            )
        else:
            body.append(
                "\n".join(
                    [
                        "<tr>",
                        f'  <td><a href="{html.escape(open_href)}">{html.escape(row["map"])}</a></td>',
                        f"  <td>{html.escape(row['tileset'])}</td>",
                        f"  <td>{row['newlyPassable']}/{row['tileCount']} ({row['newlyPassablePct']}%)</td>",
                        f"  <td>{row['explicitPass']}</td>",
                        f"  <td>{row['explicitBlock']}</td>",
                        f"  <td>{row['fallbackSamePass']}</td>",
                        f"  <td>{row['fallbackSameBlock']}</td>",
                        f"  <td>{example_links(row, web_prefix, 5, True)}</td>",
                        "</tr>",
                    ]
                )
            )
    headers = (
        "<tr><th>map</th><th>tileset</th><th>newly passable</th><th>examples</th></tr>"
        if compact
        else "<tr><th>map</th><th>tileset</th><th>newly passable</th><th>explicit pass</th><th>explicit block</th><th>fallback same pass</th><th>fallback same block</th><th>examples</th></tr>"
    )
    return "\n".join(["<table>", f"<thead>{headers}</thead>", "<tbody>", "\n".join(body), "</tbody>", "</table>"])


def html_page(rows: list[dict], web_prefix: str = "../web") -> str:
    top_count = sorted(rows, key=lambda row: row["newlyPassable"], reverse=True)[:12]
    top_pct = sorted(rows, key=lambda row: row["newlyPassablePct"], reverse=True)[:12]
    total_new = sum(row["newlyPassable"] for row in rows)
    total_tiles = sum(row["tileCount"] for row in rows)
    total_pct = round(total_new * 100 / total_tiles, 2) if total_tiles else 0
    return "\n".join(
        [
            "<!doctype html>",
            '<html lang="en">',
            "<head>",
            '  <meta charset="utf-8">',
            '  <meta name="viewport" content="width=device-width, initial-scale=1">',
            "  <title>Collision Fallback Delta</title>",
            "  <style>",
            "    :root { color-scheme: dark; font-family: system-ui, sans-serif; background: #111; color: #eee; }",
            "    body { margin: 0; padding: 24px; }",
            "    h1 { margin: 0 0 8px; font-size: 24px; }",
            "    h2 { margin: 26px 0 10px; font-size: 18px; }",
            "    p { margin: 0 0 14px; color: #bbb; max-width: 980px; line-height: 1.45; }",
            "    .summary { display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0 4px; }",
            "    .metric { border: 1px solid #333; background: #181818; border-radius: 6px; padding: 9px 11px; }",
            "    .metric strong { display: block; font-size: 18px; }",
            "    .metric span { color: #aaa; font-size: 12px; }",
            "    table { width: 100%; border-collapse: collapse; margin: 0 0 16px; font-size: 13px; }",
            "    th, td { border-bottom: 1px solid #303030; padding: 7px 8px; text-align: left; vertical-align: top; }",
            "    th { position: sticky; top: 0; background: #181818; z-index: 1; color: #ddd; }",
            "    td:nth-child(3), td:nth-child(4), td:nth-child(5), td:nth-child(6), td:nth-child(7) { white-space: nowrap; }",
            "    a { color: #7cc7ff; text-decoration: none; }",
            "    a:hover { text-decoration: underline; }",
            "  </style>",
            "</head>",
            "<body>",
            "  <h1>Collision Fallback Delta</h1>",
            "  <p>Compares the current runtime fallback (<code>layer0 &gt; 0</code>) against the previous layer1-sensitive fallback (<code>layer0 &gt; 0 &amp;&amp; effective layer1 == 0</code>) after explicit pass/block classes are applied.</p>",
            '  <section class="summary">',
            f'    <div class="metric"><strong>{total_new}</strong><span>newly passable cells</span></div>',
            f'    <div class="metric"><strong>{total_pct}%</strong><span>of {total_tiles} cells</span></div>',
            f'    <div class="metric"><strong>{sum(1 for row in rows if row["newlyPassable"])}</strong><span>maps affected</span></div>',
            "  </section>",
            "  <h2>Top By Count</h2>",
            table_html(top_count, web_prefix, True),
            "  <h2>Top By Percent</h2>",
            table_html(top_pct, web_prefix, True),
            "  <h2>All Maps</h2>",
            table_html(rows, web_prefix, False),
            "</body>",
            "</html>",
            "",
        ]
    )


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--maps", type=Path, default=OUT / "maps.js")
    parser.add_argument("--classes", type=Path, default=DATA / "tile_classes.json")
    parser.add_argument("--json-out", type=Path, default=OUT / "collision_fallback_delta.json")
    parser.add_argument("--html-out", type=Path, default=None, help="Optional HTML report output path.")
    args = parser.parse_args()

    rows = summarize(load_maps(args.maps), load_tile_classes(args.classes))
    args.json_out.parent.mkdir(parents=True, exist_ok=True)
    args.json_out.write_text(
        json.dumps(rows, ensure_ascii=False, separators=(",", ":")) + "\n",
        encoding="utf-8",
    )
    if args.html_out is not None:
        args.html_out.parent.mkdir(parents=True, exist_ok=True)
        args.html_out.write_text(html_page(rows), encoding="utf-8")
    print(f"wrote {len(rows)} collision fallback delta rows -> {args.json_out}")


if __name__ == "__main__":
    main()
