#!/usr/bin/env python3
"""Project MATLAB OutFile_asm with PBC and compare to OutFile / C++."""

from __future__ import annotations

import math
import re
from pathlib import Path

import numpy as np
from scipy import sparse

from compare_pbc_pairs import collect_edges, load_mesh, pair_matlab, remove_self


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

    def read_val(p: Path) -> np.ndarray:
        vals: list[complex] = []
        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)

    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 build_p_matrix(dof: int, pairs: list[tuple[int, int, int]], phi: complex) -> sparse.csr_matrix:
    # 1-based edge indices in mesh -> use as matrix indices directly (MATLAB convention)
    rows: list[int] = []
    cols: list[int] = []
    data: list[complex] = []
    for i in range(1, dof + 1):
        rows.append(i)
        cols.append(i)
        data.append(1.0)
    for src, dst, sign in pairs:
        rows.append(dst)
        cols.append(src)
        data.append(sign * phi)
    p_full = sparse.csr_matrix((data, (np.array(rows) - 1, np.array(cols) - 1)), shape=(dof, dof))
    dst_cols = sorted({dst - 1 for _, dst, _ in pairs})
    keep = np.ones(dof, dtype=bool)
    keep[dst_cols] = False
    return p_full[:, keep]


def main() -> None:
    root = Path(__file__).resolve().parents[2]
    mat_dir = root / "三维matlab代码" / "matlab 3D一阶基+散射边界条件+单周期边界"
    cpp_dir = root / "3D opticsfem-master" / "build" / "Release" / "OutFile"
    mesh = load_mesh(root / "3D opticsfem-master" / "build" / "Release" / "PBCmesh.dat")

    phi = complex(0.5, math.sqrt(3) / 2)
    theta = math.pi / 3
    src_edges = collect_edges(mesh, [1, 4])
    dst_edges = collect_edges(mesh, [2, 5])
    pairs = remove_self(pair_matlab(mesh, src_edges, dst_edges, theta))

    a_asm = load_coo(mat_dir / "OutFile_asm")
    a_out = load_coo(mat_dir / "OutFile")
    a_cpp = load_coo(cpp_dir)

    dof = a_asm.shape[0]
    p = build_p_matrix(dof, pairs, phi)
    a_proj = p.conj().T @ a_asm @ p

    d_self = a_proj - a_out
    d_cpp = a_cpp - a_out
    print("Projected asm vs mat OutFile:")
    print("  max |diff|", np.max(np.abs(d_self.data)))
    print("  max |real diff|", np.max(np.abs(d_self.real.data)))
    print("  max |imag diff|", np.max(np.abs(d_self.imag.data)))

    print("\nC++ vs mat OutFile:")
    print("  max |imag diff|", np.max(np.abs(d_cpp.imag.data)))

    i = 20552 - 1  # 0-based for scipy
    print(f"\nDOF 20552:")
    print("  mat out ", a_out[i, i])
    print("  proj asm", a_proj[i, i])
    print("  cpp out ", a_cpp[i, i])
    print("  asm pre ", a_asm[i, i])

    # If projected asm matches mat out, pre-PBC asm from C++ is wrong
    # Compare asm sizes / check if cpp pre-projection differs
    r, c, v = sparse.find(d_self)
    order = np.argsort(-np.abs(v.imag))
    print("\nTop projected-asm vs mat-out imag diffs:")
    for k in order[:5]:
        ri, ci = int(r[k]), int(c[k])
        print(f"  ({ri+1},{ci+1}) proj={a_proj[ri,ci]:.6g} mat={a_out[ri,ci]:.6g}")


if __name__ == "__main__":
    main()
