#!/usr/bin/env python3
"""Compare C++ wave-only A with Python replication of MATLAB PhysicMatrixAssembly."""

from __future__ import annotations

import re
from pathlib import Path

import h5py
import numpy as np
from scipy import sparse

PI = np.pi


def bf_edge(num: int, u: float, v: float, w: float) -> np.ndarray:
    out = np.zeros(3)
    if num == 1:
        out[:] = [-v, u, 0.0]
    elif num == 2:
        out[:] = [-w, 0.0, u]
    elif num == 3:
        out[:] = [-1 + v + w, -u, -u]
    elif num == 4:
        out[:] = [0.0, -w, v]
    elif num == 5:
        out[:] = [-v, -1 + u + w, -v]
    elif num == 6:
        out[:] = [-w, -w, -1 + u + v]
    else:
        raise ValueError(num)
    return out


def bf_curl_edge(num: int, u: float, v: float, w: float) -> np.ndarray:
    out = np.zeros(3)
    if num == 1:
        out[2] = 2.0
    elif num == 2:
        out[1] = -2.0
    elif num == 3:
        out[1] = 2.0
        out[2] = -2.0
    elif num == 4:
        out[0] = 2.0
    elif num == 5:
        out[0] = -2.0
        out[2] = 2.0
    elif num == 6:
        out[0] = 2.0
        out[1] = -2.0
    else:
        raise ValueError(num)
    return out


def gauss_tet_order2() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    xt = np.array([0.25, 0.166666666667, 0.166666666667, 0.166666666667, 0.5])
    yt = np.array([0.25, 0.166666666667, 0.166666666667, 0.5, 0.166666666667])
    zt = np.array([0.25, 0.166666666667, 0.5, 0.166666666667, 0.166666666667])
    pt = np.array([-0.133333333333, 0.075, 0.075, 0.075, 0.075])
    return xt, yt, zt, pt


def build_edges(elements: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    el2no = elements.T
    n1 = el2no[[0, 0, 0, 1, 1, 2], :]
    n2 = el2no[[1, 2, 3, 2, 3, 3], :]
    el_ed = np.column_stack([n1.reshape(-1, order="F"), n2.reshape(-1, order="F")])
    edge, ic = np.unique(el_ed, axis=0, return_inverse=True)
    eoe = ic.reshape(6, -1, order="F").T
    return edge, eoe


def assemble_wave_matlab(
    nodes: np.ndarray, elements: np.ndarray, domains: np.ndarray
) -> sparse.csr_matrix:
    lam0 = 1.55e-6
    k0 = 2 * PI / lam0
    epsilonr = np.array([4.0, 11.9, 11.9, 11.9], dtype=complex)
    sigma = np.array([0.0, 0.0, 5000.0, 0.0])
    mur = np.ones(4)
    temp = 1 / k0 * 120 * PI
    epsilon = epsilonr - 1j * sigma * temp

    _, eoe = build_edges(elements)
    xt, yt, zt, pt = gauss_tet_order2()
    trips: list[tuple[int, int, complex]] = []

    for n in range(elements.shape[0]):
        idx = elements[n] - 1
        x = nodes[idx, 0]
        y = nodes[idx, 1]
        z = nodes[idx, 2]
        l = np.ones(6)
        pairs = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
        for i, (a, b) in enumerate(pairs):
            l[i] = np.linalg.norm(nodes[idx[a]] - nodes[idx[b]])
        jac = np.column_stack([x[:3] - x[3], y[:3] - y[3], z[:3] - z[3]])
        inv_jac = np.linalg.inv(jac)
        t_jac = jac.T / np.linalg.det(jac)
        det_jac = abs(np.linalg.det(jac))
        domain = int(domains[n]) - 1
        mu = 1.0 / mur[domain]
        eps = epsilon[domain]

        ne = np.zeros((3, 6, len(xt)), dtype=complex)
        curl_ne = np.zeros((3, 6, len(xt)), dtype=complex)
        for i in range(6):
            for k in range(len(xt)):
                temp_bf = bf_edge(i + 1, xt[k], yt[k], zt[k])
                ne[:, i, k] = inv_jac @ temp_bf * l[i]
                temp_c = bf_curl_edge(i + 1, xt[k], yt[k], zt[k])
                curl_ne[:, i, k] = t_jac @ temp_c * l[i]

        ae = np.zeros((6, 6), dtype=complex)
        for i in range(6):
            for j in range(6):
                for k in range(len(pt)):
                    ae[i, j] += pt[k] * det_jac * (
                        mu * np.vdot(curl_ne[:, i, k], curl_ne[:, j, k])
                        - k0 * k0 * eps * np.vdot(ne[:, i, k], ne[:, j, k])
                    )
        for i in range(6):
            for j in range(6):
                ii = int(eoe[n, i])
                jj = int(eoe[n, j])
                trips.append((ii, jj, ae[i, j]))

    ai = np.array([t[0] for t in trips], dtype=np.int64)
    aj = np.array([t[1] for t in trips], dtype=np.int64)
    av = np.array([t[2] for t in trips])
    n = int(max(ai.max(), aj.max()) + 1)
    return sparse.coo_matrix((av, (ai, aj)), shape=(n, n)).tocsr()


def load_cpp_wave(cpp_asm: Path) -> sparse.csr_matrix:
    ai = np.loadtxt(cpp_asm / "Ai.txt", dtype=np.int64)
    aj = np.loadtxt(cpp_asm / "Aj.txt", dtype=np.int64)
    av = []
    for line in (cpp_asm / "Av.txt").read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("//"):
            continue
        m = re.match(r"\(([^,]+),([^)]+)\)", line)
        av.append(complex(float(m.group(1)), float(m.group(2))))
    av = np.array(av)
    n = int(max(ai.max(), aj.max()) + 1)
    return sparse.coo_matrix((av, (ai, aj)), shape=(n, n)).tocsr()


def main() -> int:
    root = Path(__file__).resolve().parents[2]
    mat_path = next(root.rglob("MeshData2x.mat"))
    cpp_asm = root / "3D opticsfem-master/port/Release/OutFile_asm"

    with h5py.File(mat_path, "r") as f:
        nodes = np.asarray(f["Mesh/Nodes"]).T
        elements = np.asarray(f["Mesh/Elements"]).T.astype(int)
        domains = np.asarray(f["Mesh/Domains"]).reshape(-1).astype(int)

    print("Assembling MATLAB-style wave matrix ...")
    a_py = assemble_wave_matlab(nodes, elements, domains)
    a_cpp = load_cpp_wave(cpp_asm)
    print(f"Python nnz={a_py.nnz}  C++ nnz={a_cpp.nnz}")

    diff = a_py - a_cpp
    print(f"max |A_py - A_cpp| = {max(abs(diff.data).max() if diff.nnz else 0.0, 0.0):.6e}")
    rel = diff.data / (a_cpp.data + 1e-30)
    if diff.nnz:
        print(f"median rel diff on overlapping nnz = {np.median(np.abs(rel)):.6e}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
