#!/usr/bin/env python3
"""Summarize unclassified layer0 tile IDs by map for passability review."""
from __future__ import annotations

import argparse
import json
from collections import Counter, defaultdict
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 fallback_kind(pass_count: int, block_count: int) -> str:
    if pass_count and block_count:
        return "mixed"
    if pass_count:
        return "pass"
    return "block"


def connected_components(points: list[dict]) -> list[list[dict]]:
    by_key = {(point["x"], point["y"]): point for point in points}
    remaining = set(by_key)
    components = []
    while remaining:
        start = min(remaining, key=lambda point: (point[1], point[0]))
        remaining.remove(start)
        stack = [start]
        component = [by_key[start]]
        while stack:
            x, y = stack.pop()
            for neighbor in ((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)):
                if neighbor not in remaining:
                    continue
                remaining.remove(neighbor)
                stack.append(neighbor)
                component.append(by_key[neighbor])
        components.append(sorted(component, key=lambda point: (point["y"], point["x"])))
    return sorted(components, key=lambda component: (-len(component), component[0]["y"], component[0]["x"]))


def component_bounds(points: list[dict]) -> dict[str, int]:
    xs = [point["x"] for point in points]
    ys = [point["y"] for point in points]
    return {"minX": min(xs), "minY": min(ys), "maxX": max(xs), "maxY": max(ys)}


def bounds_text(bounds: dict[str, int]) -> str:
    if bounds["minX"] == bounds["maxX"] and bounds["minY"] == bounds["maxY"]:
        return f"{bounds['minX']},{bounds['minY']}"
    return f"{bounds['minX']},{bounds['minY']}-{bounds['maxX']},{bounds['maxY']}"


def component_summaries(points_by_pair: dict[tuple[int, int], list[dict]], tile: int, limit: int = 6) -> list[dict]:
    components = []
    for (_tile, layer1), points in points_by_pair.items():
        if _tile != tile:
            continue
        for component in connected_components(points):
            components.append({
                "layer1": layer1,
                "count": len(component),
                "bounds": component_bounds(component),
                "sample": component[0],
                "fallbackKind": fallback_kind(
                    len(component) if tile > 0 else 0,
                    len(component) if tile <= 0 else 0,
                ),
            })
    return sorted(
        components,
        key=lambda item: (-item["count"], item["bounds"]["minY"], item["bounds"]["minX"], item["layer1"]),
    )[:limit]


def summarize(
    maps: dict[str, dict],
    classes: dict[str, dict[str, list[int]]],
    limit_per_map: int = 12,
    sample_limit: int = 5,
) -> list[dict]:
    rows = []
    for map_name, info in sorted(maps.items()):
        tileset = (info.get("layerTilesets") or [info.get("tileset")])[0]
        entry = classes.get(tileset, {})
        known = set(entry.get("pass", [])) | set(entry.get("block", []))
        known_pairs = {tuple(pair) for pair in entry.get("passPairs", [])} | {
            tuple(pair) for pair in entry.get("blockPairs", [])
        }
        counts: Counter[int] = Counter()
        fallback_pass: Counter[int] = Counter()
        fallback_block: Counter[int] = Counter()
        pair_counts: dict[int, Counter[int]] = defaultdict(Counter)
        pair_samples: dict[tuple[int, int], dict] = {}
        pair_points: dict[tuple[int, int], list[dict]] = defaultdict(list)
        samples: dict[int, list[dict]] = defaultdict(list)

        for index, (tile, mask) in enumerate(zip(info["layers"][0], info["layers"][1])):
            if tile in known or (tile, mask) in known_pairs:
                continue
            counts[tile] += 1
            pair_counts[tile][mask] += 1
            pair_samples.setdefault((tile, mask), {
                "x": index % info["width"],
                "y": index // info["width"],
                "layer1": mask,
            })
            pair_points[(tile, mask)].append({
                "x": index % info["width"],
                "y": index // info["width"],
                "layer1": mask,
            })
            if tile > 0:
                fallback_pass[tile] += 1
            else:
                fallback_block[tile] += 1
            if len(samples[tile]) < sample_limit:
                samples[tile].append({
                    "x": index % info["width"],
                    "y": index // info["width"],
                    "layer1": mask,
                })

        for tile, count in counts.most_common(limit_per_map):
            pass_count = fallback_pass[tile]
            block_count = fallback_block[tile]
            rows.append({
                "map": map_name,
                "tileset": tileset,
                "tile": tile,
                "count": count,
                "fallbackPass": pass_count,
                "fallbackBlock": block_count,
                "fallbackKind": fallback_kind(pass_count, block_count),
                "pairs": [
                    {
                        "layer1": layer1,
                        "count": pair_count,
                        "fallbackKind": fallback_kind(
                            pair_count if tile > 0 else 0,
                            pair_count if tile <= 0 else 0,
                        ),
                        "sample": pair_samples[(tile, layer1)],
                    }
                    for layer1, pair_count in pair_counts[tile].most_common()
                ],
                "components": component_summaries(pair_points, tile),
                "samples": samples[tile],
            })
    return rows


def markdown(rows: list[dict]) -> str:
    return markdown_table(
        "# Map Tile Class Gaps",
        rows,
        "../web",
    )


def markdown_table(title: str, rows: list[dict], web_prefix: str) -> str:
    lines = [
        title,
        "",
        "Generated from `out/maps.js` and `data/tile_classes.json`.",
        "",
        "These rows show the highest-use unclassified `layer0` tile IDs per map. "
        "`review` opens the map around the sample tile in `map_review.html`; component and sample links open the overview game view with collision and focus enabled.",
        "",
        "| map | tileset | tile | count | fallback | components | pairs | fallback pass | fallback block | review | samples |",
        "| --- | --- | ---: | ---: | --- | --- | --- | ---: | ---: | --- | --- |",
    ]
    for row in rows:
        first = row["samples"][0] if row["samples"] else {"x": 0, "y": 0}
        review = (
            f"{web_prefix}/map_review.html?map={row['map']}"
            "&tileLabels=1"
        )
        samples = ", ".join(
            f"[{point['x']},{point['y']} L1={point.get('layer1', '?')}]({web_prefix}/game.html?map={row['map']}"
            f"&startTile={point['x']},{point['y']}&focusTile={point['x']},{point['y']}&collision=1&overview=1)"
            for point in row["samples"]
        )
        pairs = " ".join(
            f"[L1={pair['layer1']}:{pair['count']}/{pair['fallbackKind']}]({web_prefix}/game.html?map={row['map']}"
            f"&startTile={pair['sample']['x']},{pair['sample']['y']}"
            f"&focusTile={pair['sample']['x']},{pair['sample']['y']}&collision=1&overview=1)"
            for pair in row.get("pairs", [])
        )
        components = " ".join(
            f"[{bounds_text(component['bounds'])} L1={component['layer1']} x{component['count']}/{component['fallbackKind']}]"
            f"({web_prefix}/game.html?map={row['map']}"
            f"&startTile={component['sample']['x']},{component['sample']['y']}"
            f"&focusTile={component['sample']['x']},{component['sample']['y']}&collision=1&overview=1)"
            for component in row.get("components", [])
        )
        lines.append(
            f"| {row['map']} | {row['tileset']} | {row['tile']} | {row['count']} | "
            f"{row['fallbackKind']} | {components} | {pairs} | {row['fallbackPass']} | {row['fallbackBlock']} | "
            f"[review]({review}) | {samples} |"
        )
    lines.append("")
    return "\n".join(lines)


def rows_by_map(rows: list[dict]) -> dict[str, list[dict]]:
    grouped: dict[str, list[dict]] = {}
    for row in rows:
        grouped.setdefault(row["map"], []).append(row)
    return {name: grouped[name] for name in sorted(grouped)}


def write_map_pages(rows: list[dict], out_dir: Path) -> list[dict]:
    out_dir.mkdir(parents=True, exist_ok=True)
    index_rows = []
    for map_name, map_rows in rows_by_map(rows).items():
        path = out_dir / f"{map_name}.md"
        path.write_text(
            markdown_table(f"# {map_name} Tile Class Gaps", map_rows, "../../web"),
            encoding="utf-8",
        )
        index_rows.append({
            "map": map_name,
            "rows": len(map_rows),
            "uses": sum(row["count"] for row in map_rows),
            "path": path,
        })
    return index_rows


def index_markdown(index_rows: list[dict], out_dir: Path) -> str:
    lines = [
        "# Map Tile Class Gap Index",
        "",
        "Generated from `out/map_tile_class_gaps.json`.",
        "",
        "| map | rows | uses | page |",
        "| --- | ---: | ---: | --- |",
    ]
    for row in index_rows:
        rel = row["path"].relative_to(out_dir)
        lines.append(f"| {row['map']} | {row['rows']} | {row['uses']} | [{rel.name}]({rel.as_posix()}) |")
    lines.append("")
    return "\n".join(lines)


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 / "map_tile_class_gaps.json")
    parser.add_argument("--out", type=Path, default=OUT / "map_tile_class_gaps.md")
    parser.add_argument("--split-dir", type=Path, default=OUT / "map_tile_class_gaps")
    parser.add_argument("--limit-per-map", type=int, default=12)
    parser.add_argument("--sample-limit", type=int, default=5)
    args = parser.parse_args()

    rows = summarize(load_maps(args.maps), load_tile_classes(args.classes), args.limit_per_map, args.sample_limit)
    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")
    args.out.write_text(markdown(rows), encoding="utf-8")
    index_rows = write_map_pages(rows, args.split_dir)
    (args.split_dir / "index.md").write_text(index_markdown(index_rows, args.split_dir), encoding="utf-8")
    print(f"wrote {len(rows)} map tile class gap rows -> {args.out}")


if __name__ == "__main__":
    main()
