#!/usr/bin/env python3
"""Compare MATLAB vs C++ PBC edge pairing on PBCmesh.dat."""

from __future__ import annotations

import math
from pathlib import Path

import numpy as np


def read_tag(lines: list[str], i: int, expected: str) -> int:
    while i < len(lines) and not lines[i].strip():
        i += 1
    if i >= len(lines) or lines[i].strip() != expected:
        got = lines[i].strip() if i < len(lines) else "EOF"
        raise ValueError(f"Expected '{expected}' at line {i + 1}, got '{got}'")
    return i + 1


def load_mesh(path: Path) -> dict:
    lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
    i = 0
    i = read_tag(lines, i, "NbrVertex")
    n_vertex = int(lines[i].strip())
    i += 1
    i = read_tag(lines, i, "Vertex")
    vertex = np.zeros((n_vertex, 3))
    for r in range(n_vertex):
        vertex[r] = np.array(list(map(float, lines[i].split())))
        i += 1

    i = read_tag(lines, i, "NbrTet")
    n_tet = int(lines[i].strip())
    i += 1
    i = read_tag(lines, i, "Tet")
    tet = np.zeros((n_tet, 4), dtype=np.int64)
    for r in range(n_tet):
        tet[r] = np.array(list(map(int, lines[i].split())), dtype=np.int64)
        i += 1

    i = read_tag(lines, i, "DomainOfTet")
    i += n_tet

    i = read_tag(lines, i, "NbrEdge")
    n_edge = int(lines[i].strip())
    i += 1
    i = read_tag(lines, i, "Edge")
    edge = np.zeros((n_edge, 2), dtype=np.int64)
    for r in range(n_edge):
        edge[r] = np.array(list(map(int, lines[i].split())), dtype=np.int64)
        i += 1

    i = read_tag(lines, i, "EdgeOfTet")
    edge_of_tet = np.zeros((n_tet, 6), dtype=np.int64)
    for r in range(n_tet):
        edge_of_tet[r] = np.array(list(map(int, lines[i].split())), dtype=np.int64)
        i += 1

    i = read_tag(lines, i, "NbrTri")
    n_tri = int(lines[i].strip())
    i += 1
    i = read_tag(lines, i, "Tri")
    i += n_tri

    i = read_tag(lines, i, "DomainOfTri")
    domain_of_tri = np.zeros(n_tri, dtype=np.int64)
    for r in range(n_tri):
        domain_of_tri[r] = int(lines[i].strip())
        i += 1

    i = read_tag(lines, i, "ConnOfTri")
    conn_of_tri = np.zeros((n_tri, 2), dtype=np.int64)
    for r in range(n_tri):
        conn_of_tri[r] = np.array(list(map(int, lines[i].split())), dtype=np.int64)
        i += 1

    return {
        "vertex": vertex,
        "edge": edge,
        "tet": tet,
        "edge_of_tet": edge_of_tet,
        "domain_of_tri": domain_of_tri,
        "conn_of_tri": conn_of_tri,
        "n_edge": n_edge,
    }


def tri_edges(mesh: dict, tri_idx: int) -> list[int]:
    num_tet, num_face = mesh["conn_of_tri"][tri_idx]
    num_tet -= 1
    num_face += 1
    eot = mesh["edge_of_tet"][num_tet]
    if num_face == 1:
        return [eot[0], eot[1], eot[3]]
    if num_face == 2:
        return [eot[0], eot[2], eot[4]]
    if num_face == 3:
        return [eot[1], eot[2], eot[5]]
    if num_face == 4:
        return [eot[3], eot[4], eot[5]]
    return []


def collect_edges(mesh: dict, domains: list[int]) -> list[int]:
    edges: list[int] = []
    for tri_idx, dom in enumerate(mesh["domain_of_tri"]):
        if dom in domains:
            edges.extend(tri_edges(mesh, tri_idx))
    return sorted(set(edges))


def edge_vertices(mesh: dict, edge_idx: int) -> tuple[np.ndarray, np.ndarray]:
    e = mesh["edge"][edge_idx - 1]
    v = mesh["vertex"]
    return v[e[0] - 1], v[e[1] - 1]


def match_pair(mesh: dict, src_e: int, dst_e: int, theta: float, tol: float) -> int | None:
    tra = np.array(
        [
            [math.cos(theta), -math.sin(theta), 0.0],
            [math.sin(theta), math.cos(theta), 0.0],
            [0.0, 0.0, 1.0],
        ]
    )
    v1, v2 = edge_vertices(mesh, src_e)
    v3, v4 = edge_vertices(mesh, dst_e)
    v3 = tra @ v3
    v4 = tra @ v4
    l1 = np.linalg.norm(v1 - v3)
    l2 = np.linalg.norm(v2 - v4)
    l3 = np.linalg.norm(v1 - v4)
    l4 = np.linalg.norm(v2 - v3)
    if l1 + l2 < tol:
        return 1
    if l3 + l4 < tol:
        return -1
    return None


def pair_matlab(mesh: dict, src_edges: list[int], dst_edges: list[int], theta: float) -> list[tuple[int, int, int]]:
    tol = 0.01 * 0.00005
    pairs: list[tuple[int, int, int]] = []
    for src_e in src_edges:
        matched = False
        for dst_e in dst_edges:
            sign = match_pair(mesh, src_e, dst_e, theta, tol)
            if sign is not None:
                pairs.append((src_e, dst_e, sign))
                matched = True
                break
        if not matched:
            pairs.append((src_e, src_e, 1))
    return pairs


def pair_cpp(mesh: dict, src_edges: list[int], dst_edges: list[int], theta: float) -> list[tuple[int, int, int]]:
    tol = 0.01 * 0.00005
    dst_used = [False] * len(dst_edges)
    pairs: list[tuple[int, int, int]] = []
    for src_e in src_edges:
        matched = False
        for j, dst_e in enumerate(dst_edges):
            if dst_used[j]:
                continue
            sign = match_pair(mesh, src_e, dst_e, theta, tol)
            if sign is not None:
                pairs.append((src_e, dst_e, sign))
                dst_used[j] = True
                matched = True
                break
        if not matched:
            pairs.append((src_e, src_e, 1))
    return pairs


def remove_self(pairs: list[tuple[int, int, int]]) -> list[tuple[int, int, int]]:
    return [(s, d, sg) for s, d, sg in pairs if s != d]


def main() -> None:
    mesh_path = Path(__file__).resolve().parents[1] / "build" / "Release" / "PBCmesh.dat"
    mesh = load_mesh(mesh_path)
    theta = math.pi / 3
    phi = complex(0.5, math.sqrt(3) / 2)

    src_domains = [1, 4]
    dst_domains = [2, 5]
    src_edges = collect_edges(mesh, src_domains)
    dst_edges = collect_edges(mesh, dst_domains)

    mat_pairs = remove_self(pair_matlab(mesh, src_edges, dst_edges, theta))
    cpp_pairs = remove_self(pair_cpp(mesh, src_edges, dst_edges, theta))

    mat_map = {(d, s): sg for s, d, sg in mat_pairs}
    cpp_map = {(d, s): sg for s, d, sg in cpp_pairs}

    print(f"src edges: {len(src_edges)}, dst edges: {len(dst_edges)}")
    print(f"MATLAB pairs (no self): {len(mat_pairs)}")
    print(f"C++ pairs (no self):    {len(cpp_pairs)}")

    sign_diff = []
    dst_only_mat = []
    dst_only_cpp = []
    for key, sg_mat in mat_map.items():
        if key not in cpp_map:
            dst_only_mat.append(key)
        elif cpp_map[key] != sg_mat:
            sign_diff.append((key, sg_mat, cpp_map[key]))
    for key in cpp_map:
        if key not in mat_map:
            dst_only_cpp.append(key)

    print(f"\nSign mismatches: {len(sign_diff)}")
    for key, a, b in sign_diff[:15]:
        print(f"  dst={key[0]} src={key[1]}: mat={a} cpp={b}")

    print(f"Only in MATLAB map: {len(dst_only_mat)}")
    print(f"Only in C++ map:    {len(dst_only_cpp)}")
    if dst_only_mat[:5]:
        print("  mat examples:", dst_only_mat[:5])
    if dst_only_cpp[:5]:
        print("  cpp examples:", dst_only_cpp[:5])

    # Check DOF 20552 (1-based edge index in mesh)
    dof = 20552
    for label, pairs in [("MATLAB", mat_pairs), ("C++", cpp_pairs)]:
        roles = []
        for s, d, sg in pairs:
            if s == dof:
                roles.append(f"src->{d} sign={sg} phi={sg*phi}")
            if d == dof:
                roles.append(f"dst<-{s} sign={sg} phi={sg*phi}")
        print(f"\nEdge {dof} in {label} PBC: {roles or 'not in pairs'}")


if __name__ == "__main__":
    main()
