#!/usr/bin/env python3
"""Diagnose double-PBC A matrix diff: MATLAB vs C++ projection."""
from __future__ import annotations

import math
import re
from collections import Counter, defaultdict
from pathlib import Path

import numpy as np
import scipy.io as sio
from scipy import sparse

ROOT = Path(__file__).resolve().parents[1]
MAT_ROOT = ROOT.parent / "三维matlab代码" / "matlab 3D一阶基+散射边界条件+周期边界"
CPP_OUT = ROOT / "build" / "Release" / "OutFile_double"
MAT_OUT = MAT_ROOT / "OutFile_double"
MAT_ASM = MAT_ROOT / "OutFile_double_asm"
MESH_MAT = MAT_ROOT / "doublePBC_mesh.mat"


def load_coo(prefix: Path) -> sparse.csr_matrix:
    ai = np.loadtxt(prefix / "Ai.txt", dtype=np.int64)
    aj = np.loadtxt(prefix / "Aj.txt", dtype=np.int64)
    av = []
    with open(prefix / "Av.txt", 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)
            av.append(complex(float(m.group(1)), float(m.group(2))))
    n = int(max(ai.max(), aj.max()) + 1)
    return sparse.csr_matrix((av, (ai, aj)), shape=(n, n))


def find_tri(domains, mesh) -> np.ndarray:
    domains = np.atleast_1d(domains)
    dom_tri = np.asarray(mesh.DomainOfTri).flatten()
    conn = np.asarray(mesh.ConnOfTri, dtype=int)
    out = []
    for j, d in enumerate(dom_tri):
        if d in domains:
            out.append(j)
    return np.array(out, dtype=int)


def face_edges(mesh, tri_idx: int) -> list[int]:
    num_tet, num_face = np.asarray(mesh.ConnOfTri[tri_idx], dtype=int) - 1
    e = np.asarray(mesh.EdgeOfTet[num_tet], dtype=int).flatten() - 1
    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, dis, mesh) -> np.ndarray:
    """MATLAB findPBCIndex (1-based edge ids in output)."""
    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)

    dis = np.asarray(dis, dtype=float).reshape(3)
    dl = np.linalg.norm(dis)
    err = dl * 0.00005
    vertex = np.asarray(mesh.Vertex, dtype=float)
    edge = np.asarray(mesh.Edge, dtype=int) - 1

    pairs = []
    for si in src_edges:
        v1 = vertex[edge[si, 0]]
        v2 = vertex[edge[si, 1]]
        matched = False
        for dj in dst_edges:
            v3 = vertex[edge[dj, 0]]
            v4 = vertex[edge[dj, 1]]
            l1 = abs(np.linalg.norm(v1 - v3) - dl)
            l2 = abs(np.linalg.norm(v2 - v4) - dl)
            l3 = abs(np.linalg.norm(v1 - v4) - dl)
            l4 = abs(np.linalg.norm(v2 - v3) - dl)
            if l1 + l2 < err:
                pairs.append((si + 1, dj + 1, 1))
                matched = True
                break
            if l3 + l4 < err:
                pairs.append((si + 1, dj + 1, -1))
                matched = True
                break
        if not matched:
            pairs.append((si + 1, si + 1, 1))
    return np.array(pairs, dtype=int)


def tabulate_duplicate_rows(values: np.ndarray) -> np.ndarray:
    """Return row indices where tabulate count > 1 (MATLAB assembly_pbc_double bug)."""
    vals = np.sort(values)
    uniq, counts = np.unique(vals, return_counts=True)
    rows = np.where(counts > 1)[0] + 1  # 1-based row index in tabulate table
    return rows


def duplicate_values(values: np.ndarray) -> np.ndarray:
    c = Counter(values.tolist())
    return np.array(sorted([v for v, n in c.items() if n > 1]), dtype=int)


def merge_pbc_double_matlab_fixed(pbc1: np.ndarray, pbc2: np.ndarray, phi1: float, phi2: float):
    """Intended MATLAB merge: ovDstIndex = tbl2(find(tbl2(:,2)>1), 1)."""
    return merge_pbc_double_correct(pbc1, pbc2, phi1, phi2)


def merge_pbc_double_correct(pbc1: np.ndarray, pbc2: np.ndarray, phi1: float, phi2: float):
    """C++ mergeDoublePbcPairs logic (duplicate dst values)."""
    ov = duplicate_values(np.concatenate([pbc1[:, 1], pbc2[:, 1]]))
    remove1 = np.zeros(len(pbc1), dtype=bool)
    remove2 = np.zeros(len(pbc2), dtype=bool)
    merged = []

    dst1 = pbc1[:, 1]
    src1 = pbc1[:, 0]
    dst2 = pbc2[:, 1]
    src2 = pbc2[:, 0]

    for ov_dst in ov:
        i1 = np.where(dst2 == ov_dst)[0]
        if len(i1) == 0:
            continue
        i1 = i1[0]
        s2 = src2[i1]
        i3 = np.where(dst1 == s2)[0]
        if len(i3) == 0:
            continue
        i3 = i3[0]
        s1 = src1[i3]
        sign = pbc2[i1, 2] * pbc1[i3, 2]
        merged.append((s1, ov_dst, sign, phi1 * phi2 * sign))
        remove1[i3] = True
        remove2[i1] = True
        remove1[dst1 == ov_dst] = True
        remove2[dst2 == ov_dst] = True

    for i in range(len(pbc1)):
        if not remove1[i]:
            merged.append((pbc1[i, 0], pbc1[i, 1], pbc1[i, 2], phi1 * pbc1[i, 2]))
    for i in range(len(pbc2)):
        if not remove2[i]:
            merged.append((pbc2[i, 0], pbc2[i, 1], pbc2[i, 2], phi2 * pbc2[i, 2]))

    arr = np.array([(m[0], m[1], m[2]) for m in merged], dtype=int)
    phi = np.array([m[3] for m in merged], dtype=float)
    return arr, phi


def build_p(dof: int, pbc: np.ndarray, phi: np.ndarray) -> sparse.csr_matrix:
    rows, cols, data = [], [], []
    for i in range(dof):
        rows.append(i)
        cols.append(i)
        data.append(1.0)
    for k in range(len(pbc)):
        src = int(pbc[k, 0]) - 1
        dst = int(pbc[k, 1]) - 1
        rows.append(dst)
        cols.append(src)
        data.append(phi[k])
    p_full = sparse.csr_matrix((data, (rows, cols)), shape=(dof, dof))
    dst_cols = sorted({int(pbc[k, 1]) - 1 for k in range(len(pbc))})
    keep = np.ones(dof, dtype=bool)
    keep[dst_cols] = False
    return p_full[:, keep]


def diff_stats(a: sparse.csr_matrix, b: sparse.csr_matrix, label: str) -> None:
    if a.shape != b.shape:
        print(f"{label}: shape mismatch {a.shape} vs {b.shape}")
        return
    d = a - b
    mx = float(np.max(np.abs(d.data))) if d.nnz else 0.0
    rel = mx / max(float(np.max(np.abs(b.data))), 1e-30)
    print(f"{label}: max|diff|={mx:.6g}, rel_max={rel:.6g}, diff_nnz={d.nnz}")


def main() -> None:
    mesh = sio.loadmat(MESH_MAT, squeeze_me=True, struct_as_record=False)["mesh"]
    dof = int(mesh.NbrEdge)

    pbc1 = find_pbc_index([1, 4, 7, 10, 13], [54, 55, 56, 57, 58], [2e-6, 0, 0], mesh)
    pbc2 = find_pbc_index([2, 5, 8, 11, 14], [17, 18, 19, 20, 21], [0, 2e-6, 0], mesh)
    print(f"PBC1 pairs: {len(pbc1)}, PBC2 pairs: {len(pbc2)}")

    ov_vals = duplicate_values(np.concatenate([pbc1[:, 1], pbc2[:, 1]]))
    print(f"Corner overlap dst count (correct): {len(ov_vals)}")

    pbc_ok, phi_ok = merge_pbc_double_correct(pbc1, pbc2, -1.0, 1.0)
    pbc_fix, phi_fix = merge_pbc_double_matlab_fixed(pbc1, pbc2, -1.0, 1.0)
    print(f"Merged constraints: correct={len(pbc_ok)}")

    a_asm = load_coo(MAT_ASM)
    a_mat = load_coo(MAT_OUT)
    a_cpp = load_coo(CPP_OUT)

    p_ok = build_p(dof, pbc_ok, phi_ok)
    a_proj_ok = p_ok.conj().T @ a_asm @ p_ok

    diff_stats(a_mat, a_cpp, "MAT out vs C++ out")
    diff_stats(a_mat, a_proj_ok, "MAT out vs asm+correct P")
    diff_stats(a_cpp, a_proj_ok, "C++ out vs asm+correct P")

    # normE quick
    ne_cpp = np.loadtxt(CPP_OUT / "normE", comments="//")
    ne_mat = np.loadtxt(MAT_OUT / "normE", comments="//")
    rel = np.linalg.norm(ne_cpp - ne_mat) / np.linalg.norm(ne_mat)
    print(f"normE L2 rel diff: {rel:.6g}, corr={np.corrcoef(ne_cpp, ne_mat)[0,1]:.6f}")


if __name__ == "__main__":
    main()
