#!/usr/bin/env python3
"""Photomosaic overlay: place each frame at (east, north, heading) on 2D canvas.

KISS: cv2 only, running-mean compositing.
"""
import argparse
import csv
import glob
import math
import os
import sys
import time

import cv2
import numpy as np


def load_traj(path):
    """Return list of dicts with frame_idx, east, north, heading."""
    rows = []
    with open(path) as f:
        rdr = csv.DictReader(f)
        for r in rdr:
            try:
                fi = int(r["frame_idx"])
            except (KeyError, ValueError):
                continue
            # Prefer corrected east/north, fallback to raw east_m/north_m
            if "east_m_corr" in r and r["east_m_corr"] != "":
                e = float(r["east_m_corr"])
                n = float(r["north_m_corr"])
            elif "east_m" in r:
                e = float(r["east_m"])
                n = float(r["north_m"])
            else:
                continue
            h = float(r["heading_deg"]) if "heading_deg" in r and r["heading_deg"] != "" else None
            rows.append({"frame_idx": fi, "east": e, "north": n, "heading": h})
    return rows


def attach_headings(traj, heading_csv):
    """Join heading_deg from secondary CSV by frame_idx (loopclosed CSVs miss heading)."""
    if all(r["heading"] is not None for r in traj):
        return traj
    by_idx = {}
    with open(heading_csv) as f:
        rdr = csv.DictReader(f)
        for r in rdr:
            try:
                by_idx[int(r["frame_idx"])] = float(r["heading_deg"])
            except (KeyError, ValueError):
                pass
    for r in traj:
        if r["heading"] is None:
            r["heading"] = by_idx.get(r["frame_idx"], 0.0)
    return traj


def find_frame(frames_dir, frame_idx):
    """Try both naming conventions: frame_0001.jpg or frame_00001.jpg."""
    for digits in (4, 5, 6):
        p = os.path.join(frames_dir, f"frame_{frame_idx+1:0{digits}d}.jpg")
        if os.path.exists(p):
            return p
        p = os.path.join(frames_dir, f"frame_{frame_idx:0{digits}d}.jpg")
        if os.path.exists(p):
            return p
    return None


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--frames-dir", required=True)
    ap.add_argument("--traj-csv", required=True)
    ap.add_argument("--heading-csv", default=None, help="Fallback CSV for heading_deg if missing")
    ap.add_argument("--altitude", type=float, default=1.5)
    ap.add_argument("--fov-h", type=float, default=122.0)
    ap.add_argument("--fov-v", type=float, default=80.0)
    ap.add_argument("--alpha", type=float, default=0.3)
    ap.add_argument("--sample-every", type=int, default=5)
    ap.add_argument("--out", required=True)
    ap.add_argument("--max-canvas-px", type=int, default=4000)
    ap.add_argument("--heading-sign", type=int, default=-1, help="-1 for clockwise-from-north (default)")
    args = ap.parse_args()

    t0 = time.time()

    # 1. Load trajectory
    traj = load_traj(args.traj_csv)
    if args.heading_csv:
        traj = attach_headings(traj, args.heading_csv)
    else:
        # Auto-fallback: try dvl_full_*.csv next to traj_csv
        if any(r["heading"] is None for r in traj):
            base = os.path.basename(args.traj_csv)
            # Extract video tag like GX039839 / GX019818
            for token in base.replace(".", "_").split("_"):
                if token.startswith("GX") and len(token) >= 6:
                    cand = f"/tmp/dvl_full_{token}.csv"
                    if os.path.exists(cand):
                        print(f"[heading] joining from {cand}")
                        traj = attach_headings(traj, cand)
                        break

    if not traj:
        print("ERROR: empty trajectory", file=sys.stderr)
        sys.exit(1)

    # Sample
    traj = traj[:: args.sample_every]
    print(f"[traj] {len(traj)} frames after sampling (every {args.sample_every})")

    # 2. Footprint at altitude
    fp_w = 2.0 * args.altitude * math.tan(math.radians(args.fov_h / 2.0))
    fp_h = 2.0 * args.altitude * math.tan(math.radians(args.fov_v / 2.0))
    print(f"[footprint] {fp_w:.2f}m wide x {fp_h:.2f}m tall (alt={args.altitude}m)")

    # 3. World bbox + margin
    es = [r["east"] for r in traj]
    ns = [r["north"] for r in traj]
    margin = 1.5 * max(fp_w, fp_h)
    e_min, e_max = min(es) - margin, max(es) + margin
    n_min, n_max = min(ns) - margin, max(ns) + margin
    world_w = e_max - e_min
    world_h = n_max - n_min
    print(f"[bbox] east [{e_min:.1f},{e_max:.1f}] north [{n_min:.1f},{n_max:.1f}] = {world_w:.1f}m x {world_h:.1f}m")

    # 4. Canvas pixel size
    ppm = args.max_canvas_px / max(world_w, world_h)
    canvas_w = int(world_w * ppm)
    canvas_h = int(world_h * ppm)
    print(f"[canvas] {canvas_w}x{canvas_h} px ({ppm:.1f} px/m)")

    # Compositing buffers: sum (float32 BGR) + count (int)
    acc = np.zeros((canvas_h, canvas_w, 3), dtype=np.float32)
    cnt = np.zeros((canvas_h, canvas_w), dtype=np.int32)

    fp_px_w = max(2, int(fp_w * ppm))
    fp_px_h = max(2, int(fp_h * ppm))
    print(f"[footprint-px] {fp_px_w}x{fp_px_h}")

    placed = 0
    skipped = 0
    for i, r in enumerate(traj):
        path = find_frame(args.frames_dir, r["frame_idx"])
        if not path:
            skipped += 1
            continue
        img = cv2.imread(path)
        if img is None:
            skipped += 1
            continue

        # Resize image to footprint pixel size first (keep aspect)
        img_resized = cv2.resize(img, (fp_px_w, fp_px_h), interpolation=cv2.INTER_AREA)

        # Rotate by heading. Convention: heading 0 = north, positive clockwise.
        # We rotate image so image "up" aligns with north direction.
        # cv2 rotation positive = counterclockwise → use -heading * sign
        heading = r["heading"] if r["heading"] is not None else 0.0
        angle = args.heading_sign * heading  # default -1 = clockwise

        # Build canvas the size of the rotated bounding box
        diag = int(math.ceil(math.sqrt(fp_px_w ** 2 + fp_px_h ** 2)))
        # Pad to diag x diag for safe rotation
        pad_h = (diag - fp_px_h) // 2
        pad_w = (diag - fp_px_w) // 2
        padded = cv2.copyMakeBorder(
            img_resized, pad_h, diag - fp_px_h - pad_h,
            pad_w, diag - fp_px_w - pad_w,
            cv2.BORDER_CONSTANT, value=0,
        )
        # Mask = 1 where valid pixels
        mask = np.zeros((padded.shape[0], padded.shape[1]), dtype=np.uint8)
        mask[pad_h:pad_h + fp_px_h, pad_w:pad_w + fp_px_w] = 255

        M = cv2.getRotationMatrix2D((diag / 2, diag / 2), angle, 1.0)
        rotated = cv2.warpAffine(padded, M, (diag, diag), flags=cv2.INTER_LINEAR, borderValue=0)
        rotated_mask = cv2.warpAffine(mask, M, (diag, diag), flags=cv2.INTER_NEAREST, borderValue=0)

        # Place at world (east, north).
        # Canvas: x = east (left→right), y = -north (top→bottom, north up)
        cx_world = r["east"]
        cy_world = r["north"]
        px = int((cx_world - e_min) * ppm)
        py = int((n_max - cy_world) * ppm)  # flip Y for image coords

        # Top-left of paste
        x0 = px - diag // 2
        y0 = py - diag // 2
        x1 = x0 + diag
        y1 = y0 + diag

        # Clip to canvas
        cx0 = max(0, x0)
        cy0 = max(0, y0)
        cx1 = min(canvas_w, x1)
        cy1 = min(canvas_h, y1)
        if cx1 <= cx0 or cy1 <= cy0:
            skipped += 1
            continue

        sx0 = cx0 - x0
        sy0 = cy0 - y0
        sx1 = sx0 + (cx1 - cx0)
        sy1 = sy0 + (cy1 - cy0)

        sub_img = rotated[sy0:sy1, sx0:sx1].astype(np.float32)
        sub_mask = rotated_mask[sy0:sy1, sx0:sx1] > 0

        # Running mean: add to acc + increment cnt only where mask
        acc[cy0:cy1, cx0:cx1][sub_mask] += sub_img[sub_mask]
        cnt[cy0:cy1, cx0:cx1][sub_mask] += 1
        placed += 1

        if (i + 1) % 100 == 0:
            print(f"  ... {i+1}/{len(traj)} placed={placed} skipped={skipped}")

    # Finalize: divide by count
    out = np.zeros_like(acc, dtype=np.uint8)
    valid = cnt > 0
    out[valid] = (acc[valid] / cnt[valid, None]).astype(np.uint8)

    # Draw trajectory polyline (thin blue)
    pts = []
    for r in traj:
        px = int((r["east"] - e_min) * ppm)
        py = int((n_max - r["north"]) * ppm)
        pts.append((px, py))
    for i in range(1, len(pts)):
        cv2.line(out, pts[i - 1], pts[i], (255, 200, 0), 1, cv2.LINE_AA)
    # Mark start (green) and end (red)
    if pts:
        cv2.circle(out, pts[0], 8, (0, 255, 0), 2)
        cv2.circle(out, pts[-1], 8, (0, 0, 255), 2)

    cv2.imwrite(args.out, out)
    dt = time.time() - t0
    print(f"[done] placed={placed} skipped={skipped} canvas={canvas_w}x{canvas_h} time={dt:.1f}s out={args.out}")


if __name__ == "__main__":
    main()
