#!/usr/bin/env python3
"""Shift CityJSON coordinates into tile-local space using tile_index.csv offsets."""
from __future__ import annotations

import argparse
import csv
import json
import sys
from pathlib import Path
from typing import Any, Iterable

DEFAULT_TILE_INDEX = Path("export_unity/tile_index.csv")


def parse_args(argv: Iterable[str] | None = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Rebase CityJSON coordinates so XY are relative to the tile bounds from tile_index.csv."
    )
    parser.add_argument(
        "--input-dir",
        type=Path,
        default=Path("work/cityjson_tri"),
        help="Directory containing CityJSON files (triangulated/split).",
    )
    parser.add_argument(
        "--output-dir",
        type=Path,
        default=Path("work/cityjson_tri_local"),
        help="Directory to write tile-local CityJSON files.",
    )
    parser.add_argument(
        "--tile-index",
        type=Path,
        default=DEFAULT_TILE_INDEX,
        help="Path to tile_index.csv produced by the heightmap export.",
    )
    parser.add_argument(
        "--pattern",
        default="**/*.city.json",
        help="Glob pattern for input files (defaults to any *.city.json under the input dir).",
    )
    return parser.parse_args(argv)


def resolve_input_file(path: Path) -> Path | None:
    """Handle both flat files and citygml-tools style output directories."""
    if path.is_file():
        return path
    if path.is_dir():
        candidate = path / f"{path.stem}.json"
        if candidate.is_file():
            return candidate
        matches = list(path.glob("*.json"))
        if len(matches) == 1:
            return matches[0]
    return None


def strip_suffixes(name: str) -> str:
    """Remove known suffixes (.tri, .roof, .wall, .ground, .closure, .city.json)."""
    trimmed = name
    if trimmed.endswith(".json"):
        trimmed = trimmed[: -len(".json")]
    if trimmed.endswith(".city"):
        trimmed = trimmed[: -len(".city")]
    for suffix in (".tri", ".roof", ".wall", ".ground", ".closure"):
        if trimmed.endswith(suffix):
            trimmed = trimmed[: -len(suffix)]
    return trimmed


def tile_suffix(tile_id: str) -> str:
    parts = tile_id.split("_")
    return "_".join(parts[-3:]) if len(parts) >= 3 else tile_id


def load_tile_offsets(tile_index: Path) -> dict[str, tuple[float, float]]:
    if not tile_index.exists():
        raise SystemExit(f"tile_index.csv missing: {tile_index}")

    offsets: dict[str, tuple[float, float]] = {}
    with tile_index.open("r", encoding="utf-8", newline="") as handle:
        reader = csv.DictReader(handle)
        for row in reader:
            tile_id = row.get("tile_id")
            if not tile_id:
                continue
            try:
                xmin = float(row["xmin"])
                ymin = float(row["ymin"])
            except (KeyError, TypeError, ValueError):
                continue

            offset = (xmin, ymin)
            offsets[tile_id] = offset
            offsets[tile_suffix(tile_id)] = offset
    return offsets


def read_json(path: Path) -> dict[str, Any]:
    with path.open("r", encoding="utf-8") as handle:
        return json.load(handle)


def write_json(path: Path, payload: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8") as handle:
        json.dump(payload, handle, ensure_ascii=True, indent=2)
        handle.write("\n")


def ensure_three(values: list[Any] | None, default: float) -> list[float]:
    resolved = [default, default, default]
    if not values:
        return resolved

    for idx, value in enumerate(values[:3]):
        try:
            resolved[idx] = float(value)
        except (TypeError, ValueError):
            resolved[idx] = default
    return resolved


def compute_extent(vertices: list[list[float]], scale: list[float], translate: list[float]) -> list[float] | None:
    usable = [vertex for vertex in vertices if len(vertex) >= 3]
    if not usable:
        return None

    xs = [vertex[0] * scale[0] + translate[0] for vertex in usable]
    ys = [vertex[1] * scale[1] + translate[1] for vertex in usable]
    zs = [vertex[2] * scale[2] + translate[2] for vertex in usable]
    return [min(xs), min(ys), min(zs), max(xs), max(ys), max(zs)]


def rebase_cityjson(cityjson: dict[str, Any], offset: tuple[float, float]) -> None:
    xmin, ymin = offset

    transform = cityjson.get("transform")
    if transform:
        translate = ensure_three(transform.get("translate"), 0.0)
        scale = ensure_three(transform.get("scale"), 1.0)

        translate[0] -= xmin
        translate[1] -= ymin

        transform["translate"] = translate
        transform["scale"] = scale
        cityjson["transform"] = transform
    else:
        for vertex in cityjson.get("vertices") or []:
            if len(vertex) >= 2:
                vertex[0] -= xmin
                vertex[1] -= ymin
        translate = [0.0, 0.0, 0.0]
        scale = [1.0, 1.0, 1.0]

    extent = compute_extent(cityjson.get("vertices") or [], scale, translate)
    if extent:
        metadata = cityjson.get("metadata") or {}
        metadata["geographicalExtent"] = extent
        cityjson["metadata"] = metadata


def process_file(path: Path, offsets: dict[str, tuple[float, float]], output_dir: Path) -> int:
    resolved = resolve_input_file(path)
    if not resolved:
        print(f"[skip] cannot resolve CityJSON file for {path}", file=sys.stderr)
        return 0

    tile_name = strip_suffixes(path.name)
    offset = offsets.get(tile_name) or offsets.get(tile_suffix(tile_name))
    if offset is None:
        print(f"[skip] no tile_index entry for {tile_name}", file=sys.stderr)
        return 0

    cityjson = read_json(resolved)
    rebase_cityjson(cityjson, offset)

    output_path = output_dir / path.name
    write_json(output_path, cityjson)
    return 1


def main(argv: Iterable[str] | None = None) -> int:
    args = parse_args(argv)
    offsets = load_tile_offsets(args.tile_index)

    files = sorted(args.input_dir.glob(args.pattern))
    if not files:
        print(f"No input files matched pattern '{args.pattern}' in {args.input_dir}", file=sys.stderr)
        return 1

    written = 0
    for path in files:
        written += process_file(path, offsets, args.output_dir)

    print(f"Wrote {written} tile-local file(s) to {args.output_dir}")
    return 0 if written else 1


if __name__ == "__main__":
    sys.exit(main())
