#!/usr/bin/env python3
"""Compare PBC-projected A matrix: MATLAB vs C++, and verify PBC projection pipeline."""
import re
import math
from pathlib import Path
import numpy as np
from scipy import sparse

ROOT = Path(__file__).resolve().parents[1]
MATLAB_DIR = ROOT.parent / "三维matlab代码" / "matlab 3D一阶基+散射边界条件+单周期边界"
CPP_OUT = ROOT / "build" / "Release" / "OutFile"
MAT_OUT = MATLAB_DIR / "OutFile"
MAT_ASM = MATLAB_DIR / "OutFile_asm"
MESH_FILE = MATLAB_DIR / "PBCmesh.dat"
PBC_ANGLE = math.pi / 3
PBC_PHI = complex(0.5, 0.8660254037844386)


def load_coo(prefix: Path) -> sparse.csr_matrix:
    def read_col(p):
        with open(p, encoding="utf-8", errors="ignore") as f:
            return np.array([int(x.strip()) for x in f if x.strip()], dtype=np.int64)

    def read_val(p):
        vals = []
        with open(p, encoding="utf-8", errors="ignore") as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                m = re.match(r"\(([-+0-9.eE]+),([-+0-9.eE]+)\)", line)
                if m:
                    vals.append(complex(float(m.group(1)), float(m.group(2))))
                else:
                    vals.append(complex(float(line), 0.0))
        return np.array(vals, dtype=np.complex128)

    ai = read_col(prefix / "Ai.txt")
    aj = read_col(prefix / "Aj.txt")
    av = read_val(prefix / "Av.txt")
    n = int(max(ai.max(), aj.max()) + 1)
    return sparse.csr_matrix((av, (ai, aj)), shape=(n, n))


def read_mesh_dat(path: Path):
    """Minimal PBCmesh.dat reader (0-based indices)."""
    lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
    i = 0

    def read_block(name):
        nonlocal i
        while i < len(lines) and lines[i].strip() != name:
            i += 1
        if i >= len(lines):
            raise KeyError(name)
        i += 1
        return i

    def read_int():
        nonlocal i
        v = int(lines[i].strip())
        i += 1
        return v

    def read_floats(n):
        nonlocal i
        vals = list(map(float, lines[i].split()))
        i += 1
        return np.array(vals[:n])

    read_block("NbrVertex")
    n_vertex = read_int()
    read_block("Vertex")
    vertex = np.zeros((n_vertex, 3))
    for r in range(n_vertex):
        vertex[r] = read_floats(3)

    read_block("NbrEdge")
    n_edge = read_int()
    read_block("Edge")
    edge = np.zeros((n_edge, 2), dtype=np.int64)
    for r in range(n_edge):
        a, b = map(int, lines[i].split())
        edge[r] = (a - 1, b - 1)
        i += 1

    read_block("NbrTet")
    n_tet = read_int()
    read_block("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()))) - 1
        i += 1

    read_block("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()))) - 1
        i += 1

    read_block("NbrTri")
    n_tri = read_int()
    read_block("ConnOfTri")
    conn_of_tri = np.zeros((n_tri, 2), dtype=np.int64)
    for r in range(n_tri):
        a, b = map(int, lines[i].split())
        conn_of_tri[r] = (a - 1, b - 1)
        i += 1

    read_block("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())  # keep 1-based like MATLAB mesh
        i += 1

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


def find_tri(domains, mesh):
    domains = np.atleast_1d(domains)
    out = []
    for j in range(mesh["conn_of_tri"].shape[0]):
        num_tet, _ = mesh["conn_of_tri"][j]
        dom = mesh["domain_of_tri"][j]
        # DomainOfTri in dat is 1-based after +1 in C++ loader; MATLAB c6 may differ.
        # PBCmesh.dat stores 0-based domain ids before C++ +1.
        if dom in domains or (dom + 1) in domains:
            out.append(j)
    return np.array(out, dtype=np.int64)


def face_edges(mesh, tri_idx):
    num_tet, num_face = mesh["conn_of_tri"][tri_idx]
    e = mesh["edge_of_tet"][num_tet]
    face = num_face + 1
    if face == 1:
        return [e[0], e[1], e[3]]
    if face == 2:
        return [e[0], e[2], e[4]]
    if face == 3:
        return [e[1], e[2], e[5]]
    if face == 4:
        return [e[3], e[4], e[5]]
    raise ValueError(face)


def find_pbc_index(src, dst, theta, mesh):
    src_tris = find_tri(src, mesh)
    dst_tris = find_tri(dst, mesh)
    src_edges, dst_edges = [], []
    for tri in src_tris:
        src_edges.extend(face_edges(mesh, tri))
    for tri in dst_tris:
        dst_edges.extend(face_edges(mesh, tri))
    src_edges = np.unique(src_edges)
    dst_edges = np.unique(dst_edges)

    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]]
    )
    tol = 0.01 * 0.00005
    pairs = []
    for si in src_edges:
        v1 = mesh["vertex"][mesh["edge"][si, 0]]
        v2 = mesh["vertex"][mesh["edge"][si, 1]]
        matched = False
        for dj, di in enumerate(dst_edges):
            v3 = tra @ mesh["vertex"][mesh["edge"][di, 0]]
            v4 = tra @ mesh["vertex"][mesh["edge"][di, 1]]
            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:
                pairs.append((si, di, 1))
                matched = True
                break
            if l3 + l4 < tol:
                pairs.append((si, di, -1))
                matched = True
                break
        if not matched:
            pairs.append((si, -1, 0))
    pairs = [(s, d, sg) for s, d, sg in pairs if s != d and d >= 0]
    return pairs


def build_P(dof, pairs):
    P = sparse.lil_matrix((dof, dof), dtype=np.complex128)
    for i in range(dof):
        P[i, i] = 1.0
    dst_cols = []
    for src, dst, sign in pairs:
        P[dst, src] = sign * PBC_PHI
        dst_cols.append(dst)
    keep = [c for c in range(dof) if c not in dst_cols]
    return P[:, keep].tocsr()


def diff_stats(A, B, label):
    D = A - B
    print(f"\n=== {label} ===")
    print(f"  max |diff|      = {np.max(np.abs(D.data)) if D.nnz else 0:.6g}")
    print(f"  max |real diff| = {np.max(np.abs(D.real.data)) if D.nnz else 0:.6g}")
    print(f"  max |imag diff| = {np.max(np.abs(D.imag.data)) if D.nnz else 0:.6g}")
    if D.nnz and A.shape[0] == B.shape[0]:
        di = np.abs(A.diagonal().imag - B.diagonal().imag)
        print(f"  diag imag max diff = {di.max():.6g}, >10 count = {(di > 10).sum()}")


def main():
    print("Loading matrices...")
    A_cpp = load_coo(CPP_OUT)
    A_mat = load_coo(MAT_OUT)
    A_asm = load_coo(MAT_ASM)
    diff_stats(A_cpp, A_mat, "C++ OutFile vs MATLAB OutFile (after PBC)")

    print("\nLoading mesh and building PBC pairs...")
    mesh = read_mesh_dat(MESH_FILE)
    # domains in case_config: src=[1,4], dst=[2,5] (1-based in MATLAB)
    pairs = find_pbc_index(np.array([1, 4]), np.array([2, 5]), PBC_ANGLE, mesh)
    print(f"  PBC pairs: {len(pairs)}")
    signs = [p[2] for p in pairs]
    print(f"  sign +1: {signs.count(1)}, sign -1: {signs.count(-1)}")

    P = build_P(mesh["n_edge"], pairs)
    A_proj = P.conj().T @ A_asm @ P
    diff_stats(A_proj, A_mat, "MATLAB asm projected with Python P vs MATLAB OutFile")
    diff_stats(A_proj, A_cpp, "MATLAB asm projected with Python P vs C++ OutFile")

    # Test sign-flip hypothesis on all -1 pairs
    pairs_flip = [(s, d, -sg if sg < 0 else sg) for s, d, sg in pairs]
    Pf = build_P(mesh["n_edge"], pairs_flip)
    Af = Pf.conj().T @ A_asm @ Pf
    diff_stats(Af, A_cpp, "MATLAB asm with flipped -1 signs vs C++ OutFile")


if __name__ == "__main__":
    main()
