# -*- coding: utf-8 -*-
"""
Export COLMAP-estimated camera intrinsics + extrinsics to readable JSON/TXT,
and validate against the provided ground-truth transforms.json / metadata.json.

Input:  COLMAP sparse model in TXT format (cameras.txt, images.txt)
        -> produced by `colmap model_converter --output_type TXT`
Output:
  output/colmap/cameras_export/cameras.json     (per-camera intrinsics)
  output/colmap/cameras_export/transforms.json  (nerfstudio/3DGS-compatible c2w)
  prints: estimated intrinsics vs GT, trajectory alignment error (Umeyama)
"""
import os, sys, json
import numpy as np


def quat_to_rot(q):
    """COLMAP quaternion (w,x,y,z) -> rotation matrix (world->camera)."""
    w, x, y, z = q
    return np.array([
        [1 - 2*(y*y + z*z), 2*(x*y - z*w), 2*(x*z + y*w)],
        [2*(x*y + z*w), 1 - 2*(x*x + z*z), 2*(y*z - x*w)],
        [2*(x*z - y*w), 2*(y*z + x*w), 1 - 2*(x*x + y*y)],
    ], dtype=np.float64)


def parse_cameras_txt(path):
    cams = {}
    with open(path) as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith('#'):
                continue
            t = line.split()
            cid = int(t[0]); model = t[1]; w = int(t[2]); h = int(t[3])
            params = [float(x) for x in t[4:]]
            cams[cid] = dict(model=model, width=w, height=h, params=params)
    for cid, c in cams.items():
        m = c['model']
        if m in ('PINHOLE', 'OPENCV', 'SIMPLE_RADIAL', 'RADIAL'):
            fx, fy = c['params'][0], c['params'][1]
            cx, cy = c['params'][2], c['params'][3]
            if m in ('SIMPLE_RADIAL', 'RADIAL'):
                fx = fy = c['params'][0]; cx = c['params'][1]; cy = c['params'][2]
        elif m == 'SIMPLE_PINHOLE':
            fx = fy = c['params'][0]; cx = c['params'][1]; cy = c['params'][2]
        else:
            fx = fy = cx = cy = None
        c['fx'], c['fy'], c['cx'], c['cy'] = fx, fy, cx, cy
    return cams


def parse_images_txt(path):
    imgs = []
    with open(path) as f:
        lines = f.readlines()
    i = 0
    while i < len(lines):
        line = lines[i].strip()
        if not line or line.startswith('#'):
            i += 1
            continue
        t = line.split()
        iid = int(t[0]); q = np.array([float(x) for x in t[1:5]])
        tvec = np.array([float(x) for x in t[5:8]])
        cam_id = int(t[8]); name = t[9]
        i += 1
        if i < len(lines):          # skip the points2D line
            i += 1
        R = quat_to_rot(q)          # world -> camera
        c2w = np.eye(4)
        c2w[:3, :3] = R.T
        c2w[:3, 3] = -R.T @ tvec
        imgs.append(dict(id=iid, name=name, cam_id=cam_id, q=q, t=tvec, R_w2c=R, c2w=c2w))
    return imgs


def umeyama(src, dst):
    """Similarity transform aligning src->dst (both Nx3). Returns s,R,t,rms."""
    src = np.asarray(src, np.float64); dst = np.asarray(dst, np.float64)
    mu_s = src.mean(0); mu_d = dst.mean(0)
    s = src - mu_s; d = dst - mu_d
    cov = d.T @ s / s.shape[0]
    U, S, Vt = np.linalg.svd(cov)
    R = U @ Vt
    if np.linalg.det(R) < 0:
        U[:, -1] *= -1; R = U @ Vt
    var_s = (s ** 2).sum() / s.shape[0]
    scale = np.trace(np.diag(S)) / var_s
    t = mu_d - scale * R @ mu_s
    aligned = (scale * R @ src.T).T + t
    rms = np.sqrt(((aligned - dst) ** 2).sum(1).mean())
    return scale, R, t, rms, aligned


def main():
    in_dir = sys.argv[1] if len(sys.argv) > 1 else r"output\colmap\sparse\0"
    out_dir = sys.argv[2] if len(sys.argv) > 2 else r"output\colmap\cameras_export"
    os.makedirs(out_dir, exist_ok=True)

    cams = parse_cameras_txt(os.path.join(in_dir, "cameras.txt"))
    imgs = parse_images_txt(os.path.join(in_dir, "images.txt"))
    print(f"parsed {len(cams)} camera(s), {len(imgs)} image(s)")

    # sort images by id for stable ordering
    imgs.sort(key=lambda x: x['id'])

    # --- export cameras.json (intrinsics) ---
    cams_json = {}
    for cid, c in cams.items():
        cams_json[str(cid)] = {
            "model": c['model'], "width": c['width'], "height": c['height'],
            "fx": c['fx'], "fy": c['fy'], "cx": c['cx'], "cy": c['cy'],
            "params": c['params'],
        }
    with open(os.path.join(out_dir, "cameras.json"), "w") as f:
        json.dump(cams_json, f, indent=2)

    # --- export transforms.json (nerfstudio/3DGS compatible, OpenCV convention) ---
    # use the dominant camera (single_camera=1 -> one camera)
    cam = cams[next(iter(cams))]
    fx, fy, cx, cy = cam['fx'], cam['fy'], cam['cx'], cam['cy']
    w, h = cam['width'], cam['height']
    angle_x = 2 * np.arctan(0.5 * w / fx)
    angle_y = 2 * np.arctan(0.5 * h / fy)
    transform = {
        "camera_model": "OPENCV",
        "fl_x": fx, "fl_y": fy, "cx": cx, "cy": cy,
        "w": w, "h": h,
        "camera_angle_x": float(angle_x), "camera_angle_y": float(angle_y),
        "k1": 0.0, "k2": 0.0, "p1": 0.0, "p2": 0.0,
        "frames": [
            {"file_path": "images/" + img['name'], "transform_matrix": img['c2w'].tolist()}
            for img in imgs
        ],
    }
    with open(os.path.join(out_dir, "transforms.json"), "w") as f:
        json.dump(transform, f, indent=2)

    # --- also write a compact c2w numpy file for our own NeRF ---
    np.savez(os.path.join(out_dir, "poses_c2w.npz"),
             names=np.array([img['name'] for img in imgs]),
             c2w=np.stack([img['c2w'] for img in imgs]),
             fx=fx, fy=fy, cx=cx, cy=cy, w=w, h=h)

    # --- validation against GT ---
    print("\n===== VALIDATION =====")
    gt_meta = json.load(open(r"mvs_project\metadata.json"))
    gt_intr = gt_meta['intrinsics']
    print(f"estimated intrinsics: fx={fx:.3f} fy={fy:.3f} cx={cx:.3f} cy={cy:.3f}")
    print(f"ground-truth intrinsics: fx={gt_intr['fx']:.3f} fy={gt_intr['fy']:.3f} "
          f"cx={gt_intr['cx']:.3f} cy={gt_intr['cy']:.3f}")
    print(f"intrinsics error: fx {abs(fx-gt_intr['fx']):.3f}px, fy {abs(fy-gt_intr['fy']):.3f}px, "
          f"cx {abs(cx-gt_intr['cx']):.3f}px, cy {abs(cy-gt_intr['cy']):.3f}px")

    # trajectory alignment: camera centers COLMAP vs GT
    gt_tr = json.load(open(r"mvs_project\poses\transforms.json"))
    by_name = {}
    for fr in gt_tr['frames']:
        by_name[fr['file_path'].split('/')[-1]] = np.array(fr['transform_matrix'])

    C_est, C_gt = [], []
    common = []
    for img in imgs:
        if img['name'] in by_name:
            common.append(img['name'])
            C_est.append(img['c2w'][:3, 3])
            C_gt.append(by_name[img['name']][:3, 3])
    C_est = np.array(C_est); C_gt = np.array(C_gt)
    s, R, t, rms, aligned = umeyama(C_est, C_gt)
    print(f"\ntrajectory: {len(common)} common views aligned (similarity s={s:.4f})")
    print(f"camera-center RMS after similarity alignment: {rms:.4f} m")
    print("(COLMAP recovers extrinsics up to a similarity gauge; "
          "this RMS measures trajectory-shape accuracy)")

    print("\nwrote:")
    print(" ", os.path.join(out_dir, "cameras.json"))
    print(" ", os.path.join(out_dir, "transforms.json"))
    print(" ", os.path.join(out_dir, "poses_c2w.npz"))


if __name__ == "__main__":
    main()
