#!/usr/bin/env python3
"""Generate vector/geospatial overlays from a FOPEN summary TSV."""

import argparse
import csv
import html
import json
import math
from pathlib import Path


EARTH_RADIUS_M = 6378137.0


def parse_float(value):
    if value is None:
        return None
    value = str(value).strip()
    if not value:
        return None
    try:
        return float(value)
    except ValueError:
        return None


def read_summary(path):
    with open(path, newline="", encoding="utf-8") as f:
        reader = csv.DictReader(f, delimiter="\t")
        return list(reader)


def geometry_from_row(row):
    coords = []
    for idx in range(1, 5):
        lat = parse_float(row.get(f"corner{idx}_lat"))
        lon = parse_float(row.get(f"corner{idx}_long"))
        if lat is None or lon is None:
            return None
        coords.append((lat, lon))
    coords.append(coords[0])
    return coords


def center_from_row(row):
    lat = parse_float(row.get("center_lat"))
    lon = parse_float(row.get("center_long"))
    if lat is None or lon is None:
        return None
    return lat, lon


def collect_features(rows, include_missing):
    features = []
    for row in rows:
        coords = geometry_from_row(row)
        center = center_from_row(row)
        if coords is None or center is None:
            continue
        if not include_missing and row.get("status") != "ok":
            continue
        features.append((row, coords, center))
    return features


def bounds(features):
    min_lat = min(lat for _, coords, _ in features for lat, _ in coords[:-1])
    max_lat = max(lat for _, coords, _ in features for lat, _ in coords[:-1])
    min_lon = min(lon for _, coords, _ in features for _, lon in coords[:-1])
    max_lon = max(lon for _, coords, _ in features for _, lon in coords[:-1])
    return min_lat, max_lat, min_lon, max_lon


def project_factory(min_lat, max_lat, min_lon, max_lon):
    mid_lat = 0.5 * (min_lat + max_lat)
    cos_mid = math.cos(math.radians(mid_lat))

    def project(lat, lon):
        x = math.radians(lon - min_lon) * EARTH_RADIUS_M * cos_mid
        y = math.radians(max_lat - lat) * EARTH_RADIUS_M
        return x, y

    width_m = math.radians(max_lon - min_lon) * EARTH_RADIUS_M * cos_mid
    height_m = math.radians(max_lat - min_lat) * EARTH_RADIUS_M
    return project, width_m, height_m, mid_lat


def value_color(value, value_min=0.0, value_max=100.0):
    if value is None:
        return "#808080"
    if value_max <= value_min:
        t = 0.5
    else:
        t = max(0.0, min(1.0, (value - value_min) / (value_max - value_min)))
    # Brown/yellow through green, intentionally subdued for satellite-map overlays.
    r0, g0, b0 = 185, 130, 55
    r1, g1, b1 = 35, 150, 70
    r = round(r0 + t * (r1 - r0))
    g = round(g0 + t * (g1 - g0))
    b = round(b0 + t * (b1 - b0))
    return f"#{r:02x}{g:02x}{b:02x}"


def row_properties(row):
    keys = [
        "scene_id",
        "scene_label",
        "status",
        "group_index",
        "index_id",
        "agl_m",
        "forestation_percent",
        "center_lat",
        "center_long",
        "flight_heading_deg",
        "rect_x_axis_heading_deg",
        "heading_source",
        "has_elevations_histogram",
        "has_egomotion",
        "has_ground_plane_row",
        "warnings",
        "linked_path",
    ]
    props = {}
    for key in keys:
        value = row.get(key, "")
        number = parse_float(value)
        props[key] = number if number is not None and key not in {"scene_id", "scene_label", "status", "index_id", "heading_source", "warnings", "linked_path"} else value
    return props


def feature_key(row):
    return row.get("index_id") or row.get("group_index") or row.get("scene_id", "")


def label_keys(features, label_mode):
    if label_mode not in {"first-per-group", "last-per-group"}:
        return set()

    selected = {}
    for idx, (row, _, _) in enumerate(features):
        key = feature_key(row)
        if label_mode == "first-per-group" and key in selected:
            continue
        selected[key] = idx
    return set(selected.values())


def color_range(features, color_by):
    if color_by == "status":
        return None, None
    if color_by == "forestation_percent":
        return 0.0, 100.0

    values = [parse_float(row.get(color_by)) for row, _, _ in features]
    values = [value for value in values if value is not None]
    if not values:
        return 0.0, 100.0
    return min(values), max(values)


def row_color(row, color_by, value_min, value_max):
    if color_by == "status":
        return "#1f77b4" if row.get("status") == "ok" else "#b00020"
    return value_color(parse_float(row.get(color_by)), value_min, value_max)


def point_between(p0, p1, fraction):
    return (
        p0[0] + fraction * (p1[0] - p0[0]),
        p0[1] + fraction * (p1[1] - p0[1]),
    )


def svg_path_from_points(points, direction_cue):
    if direction_cue == "none":
        point_text = " ".join(f"{x:.3f},{y:.3f}" for x, y in points)
        return "polygon", point_text

    c1, c2, c3, c4 = points
    if direction_cue == "open-right":
        path = f"M {c2[0]:.3f},{c2[1]:.3f} L {c3[0]:.3f},{c3[1]:.3f} L {c4[0]:.3f},{c4[1]:.3f} L {c1[0]:.3f},{c1[1]:.3f}"
        return "path", path

    c1_stub = point_between(c1, c2, 1.0 / 3.0)
    c2_stub = point_between(c2, c1, 1.0 / 3.0)
    path = (
        f"M {c2[0]:.3f},{c2[1]:.3f} "
        f"L {c3[0]:.3f},{c3[1]:.3f} "
        f"L {c4[0]:.3f},{c4[1]:.3f} "
        f"L {c1[0]:.3f},{c1[1]:.3f} "
        f"L {c1_stub[0]:.3f},{c1_stub[1]:.3f} "
        f"M {c2[0]:.3f},{c2[1]:.3f} "
        f"L {c2_stub[0]:.3f},{c2_stub[1]:.3f}"
    )
    return "path", path


def write_bounds(path, features, min_lat, max_lat, min_lon, max_lon, width_m, height_m, mid_lat):
    centers = [center for _, _, center in features]
    min_center_lat = min(lat for lat, _ in centers)
    max_center_lat = max(lat for lat, _ in centers)
    min_center_lon = min(lon for _, lon in centers)
    max_center_lon = max(lon for _, lon in centers)
    with open(path, "w", encoding="utf-8", newline="") as f:
        writer = csv.writer(f, delimiter="\t", lineterminator="\n")
        writer.writerow(["name", "value"])
        writer.writerow(["features", len(features)])
        writer.writerow(["corner_min_lat", f"{min_lat:.10f}"])
        writer.writerow(["corner_max_lat", f"{max_lat:.10f}"])
        writer.writerow(["corner_min_long", f"{min_lon:.10f}"])
        writer.writerow(["corner_max_long", f"{max_lon:.10f}"])
        writer.writerow(["center_min_lat", f"{min_center_lat:.10f}"])
        writer.writerow(["center_max_lat", f"{max_center_lat:.10f}"])
        writer.writerow(["center_min_long", f"{min_center_lon:.10f}"])
        writer.writerow(["center_max_long", f"{max_center_lon:.10f}"])
        writer.writerow(["mid_lat", f"{mid_lat:.10f}"])
        writer.writerow(["width_m", f"{width_m:.3f}"])
        writer.writerow(["height_m", f"{height_m:.3f}"])
        writer.writerow(["aspect_w_over_h", f"{width_m / height_m:.6f}"])


def write_geojson(path, features):
    collection = {
        "type": "FeatureCollection",
        "features": [],
    }
    for row, coords, center in features:
        props = row_properties(row)
        props["label_lat"] = center[0]
        props["label_long"] = center[1]
        collection["features"].append(
            {
                "type": "Feature",
                "properties": props,
                "geometry": {
                    "type": "Polygon",
                    "coordinates": [[[lon, lat] for lat, lon in coords]],
                },
            }
        )
    path.write_text(json.dumps(collection, indent=2) + "\n", encoding="utf-8")


def write_kml(path, features):
    lines = [
        '<?xml version="1.0" encoding="UTF-8"?>',
        '<kml xmlns="http://www.opengis.net/kml/2.2">',
        "  <Document>",
        "    <name>FOPEN NC rectangles</name>",
        "    <Style id=\"rect-ok\"><LineStyle><color>cc00ffff</color><width>1</width></LineStyle><PolyStyle><color>2200ff80</color></PolyStyle></Style>",
        "    <Style id=\"rect-missing\"><LineStyle><color>cc0000ff</color><width>1</width></LineStyle><PolyStyle><color>220000ff</color></PolyStyle></Style>",
        "    <Style id=\"label\"><IconStyle><scale>0</scale></IconStyle><LabelStyle><scale>0.55</scale><color>ffffffff</color></LabelStyle></Style>",
    ]
    for row, coords, center in features:
        label = html.escape(row.get("scene_label", row.get("scene_id", "")))
        desc = html.escape(
            f"scene_id={row.get('scene_id','')}\n"
            f"AGL={row.get('agl_m','')} m\n"
            f"forestation={row.get('forestation_percent','')} %\n"
            f"warnings={row.get('warnings','')}"
        )
        style = "rect-ok" if row.get("status") == "ok" else "rect-missing"
        coord_text = " ".join(f"{lon:.10f},{lat:.10f},0" for lat, lon in coords)
        lines.extend(
            [
                "    <Placemark>",
                f"      <name>{label}</name>",
                f"      <description>{desc}</description>",
                f"      <styleUrl>#{style}</styleUrl>",
                "      <Polygon><outerBoundaryIs><LinearRing>",
                f"        <coordinates>{coord_text}</coordinates>",
                "      </LinearRing></outerBoundaryIs></Polygon>",
                "    </Placemark>",
                "    <Placemark>",
                f"      <name>{label}</name>",
                "      <styleUrl>#label</styleUrl>",
                f"      <Point><coordinates>{center[1]:.10f},{center[0]:.10f},0</coordinates></Point>",
                "    </Placemark>",
            ]
        )
    lines.extend(["  </Document>", "</kml>"])
    path.write_text("\n".join(lines) + "\n", encoding="utf-8")


def write_svg(path, features, page_width_mm, page_height_mm, label_every, label_mode, svg_fill, color_by, direction_cue):
    min_lat, max_lat, min_lon, max_lon = bounds(features)
    project, width_m, height_m, mid_lat = project_factory(min_lat, max_lat, min_lon, max_lon)
    margin_m = max(width_m, height_m) * 0.025
    view_x = -margin_m
    view_y = -margin_m
    view_w = width_m + 2.0 * margin_m
    view_h = height_m + 2.0 * margin_m

    if page_height_mm is None:
        page_height_mm = page_width_mm * view_h / view_w

    selected_label_keys = label_keys(features, label_mode)
    value_min, value_max = color_range(features, color_by)

    lines = [
        '<?xml version="1.0" encoding="UTF-8"?>',
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{page_width_mm:.3f}mm" height="{page_height_mm:.3f}mm" viewBox="{view_x:.3f} {view_y:.3f} {view_w:.3f} {view_h:.3f}">',
        "  <title>FOPEN NC reference-scene rectangles</title>",
        "  <desc>Projected using local equirectangular coordinates. Units in viewBox are meters.</desc>",
        "  <style>",
        "    .frame { fill: #f7f7f2; stroke: #222; stroke-width: 4; }",
        "    .rect { fill-opacity: 0.18; stroke-opacity: 0.8; stroke-width: 1.25; }",
        "    .missing { stroke-dasharray: 8 5; }",
        "    .label { font-family: DejaVu Sans, Arial, sans-serif; font-size: 14px; fill: #111; paint-order: stroke; stroke: #fff; stroke-width: 3px; stroke-linejoin: round; }",
        "    .meta { font-family: DejaVu Sans, Arial, sans-serif; font-size: 28px; fill: #111; }",
        "  </style>",
        f'  <rect class="frame" x="0" y="0" width="{width_m:.3f}" height="{height_m:.3f}"/>',
        "  <g id=\"rectangles\">",
    ]
    for idx, (row, coords, center) in enumerate(features, start=1):
        points = []
        for lat, lon in coords[:-1]:
            x, y = project(lat, lon)
            points.append((x, y))
        color = row_color(row, color_by, value_min, value_max)
        fill = "none" if svg_fill == "none" or direction_cue != "none" else color
        css_class = "rect" if row.get("status") == "ok" else "rect missing"
        element, geometry = svg_path_from_points(points, direction_cue)
        if element == "polygon":
            lines.append(f'    <polygon class="{css_class}" points="{geometry}" fill="{fill}" stroke="{color}"/>')
        else:
            lines.append(f'    <path class="{css_class}" d="{geometry}" fill="none" stroke="{color}"/>')

        if (
            label_mode == "every" and label_every > 0 and idx % label_every == 0
        ) or (
            label_mode in {"first-per-group", "last-per-group"} and (idx - 1) in selected_label_keys
        ):
            cx, cy = project(center[0], center[1])
            label = html.escape(row.get("scene_label") or row.get("scene_id", ""))
            lines.append(f'    <text class="label" x="{cx:.3f}" y="{cy:.3f}" text-anchor="middle" dominant-baseline="central">{label}</text>')

    lines.extend(
        [
            "  </g>",
            f'  <text class="meta" x="0" y="{-0.35 * margin_m:.3f}">FOPEN NC rectangles: {len(features)} scenes, bounds {min_lat:.6f},{min_lon:.6f} to {max_lat:.6f},{max_lon:.6f}</text>',
            f'  <text class="meta" x="0" y="{height_m + 0.55 * margin_m:.3f}">Approx. coverage {width_m:.0f} m x {height_m:.0f} m, mid-lat {mid_lat:.6f}</text>',
            "</svg>",
        ]
    )
    path.write_text("\n".join(lines) + "\n", encoding="utf-8")
    return width_m, height_m, mid_lat


def main():
    parser = argparse.ArgumentParser(description="Generate FOPEN map overlay files from a summary TSV.")
    parser.add_argument("summary_tsv", type=Path, help="Summary TSV from fopen_nc_summary.py")
    parser.add_argument("--out-prefix", type=Path, required=True, help="Output prefix without extension")
    parser.add_argument("--ok-only", action="store_true", help="Only include rows whose summary status is ok")
    parser.add_argument("--page-width-mm", type=float, default=1189.0, help="SVG page width in mm (default A0 landscape width)")
    parser.add_argument("--page-height-mm", type=float, default=841.0, help="SVG page height in mm (default A0 landscape height)")
    parser.add_argument("--label-every", type=int, default=1, help="Label every Nth rectangle in SVG; 0 disables labels")
    parser.add_argument(
        "--svg-fill",
        choices=["color", "none"],
        default="color",
        help="SVG rectangle fill mode (default: color)",
    )
    parser.add_argument(
        "--svg-color-by",
        choices=["forestation_percent", "agl_m", "status"],
        default="forestation_percent",
        help="SVG frame/fill color source (default: forestation_percent)",
    )
    parser.add_argument(
        "--svg-label-mode",
        choices=["every", "none", "first-per-group", "last-per-group"],
        default="every",
        help="SVG label selection mode; --label-every applies only to 'every' (default: every)",
    )
    parser.add_argument(
        "--svg-direction-cue",
        choices=["none", "broken-right", "open-right"],
        default="none",
        help="SVG direction cue on the image-right side from corner1 to corner2 (default: none)",
    )
    args = parser.parse_args()

    rows = read_summary(args.summary_tsv)
    features = collect_features(rows, include_missing=not args.ok_only)
    if not features:
        raise SystemExit("ERROR: no rows with complete rectangle geometry")

    out_prefix = args.out_prefix.resolve()
    out_prefix.parent.mkdir(parents=True, exist_ok=True)

    min_lat, max_lat, min_lon, max_lon = bounds(features)
    _, width_m, height_m, mid_lat = project_factory(min_lat, max_lat, min_lon, max_lon)
    write_bounds(out_prefix.with_suffix(".bounds.tsv"), features, min_lat, max_lat, min_lon, max_lon, width_m, height_m, mid_lat)
    write_geojson(out_prefix.with_suffix(".geojson"), features)
    write_kml(out_prefix.with_suffix(".kml"), features)
    write_svg(
        out_prefix.with_suffix(".svg"),
        features,
        args.page_width_mm,
        args.page_height_mm,
        args.label_every,
        args.svg_label_mode,
        args.svg_fill,
        args.svg_color_by,
        args.svg_direction_cue,
    )

    print(f"features: {len(features)}")
    print(f"bounds: lat {min_lat:.10f}..{max_lat:.10f}, long {min_lon:.10f}..{max_lon:.10f}")
    print(f"coverage_m: {width_m:.1f} x {height_m:.1f}")
    print(f"wrote: {out_prefix.with_suffix('.bounds.tsv')}")
    print(f"wrote: {out_prefix.with_suffix('.geojson')}")
    print(f"wrote: {out_prefix.with_suffix('.kml')}")
    print(f"wrote: {out_prefix.with_suffix('.svg')}")


if __name__ == "__main__":
    main()
