#!/usr/bin/env python3
"""Replicate MATLAB FemMatrixAssembly (wave+port+PEC) and compare with reference."""

from __future__ import annotations

import argparse
import json
from pathlib import Path

import h5py
import numpy as np
from scipy import sparse
from scipy.sparse.linalg import spsolve

from compare_wave_asm import assemble_wave_matlab, bf_edge, build_edges
from export_ab_coo import export_ab_coo


def gauss_tri_order2() -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    xft = np.array([1.0 / 3, 0.6, 0.2, 0.2])
    yft = np.array([1.0 / 3, 0.2, 0.6, 0.2])
    pft = np.array([-0.28125, 25.0 / 96, 25.0 / 96, 25.0 / 96])
    return xft, yft, pft


def bf_et(i: int, u: float, v: float) -> np.ndarray:
    if i == 1:
        return np.array([1.0 - v, u, 0.0])
    if i == 2:
        return np.array([v, 1.0 - u, 0.0])
    if i == 3:
        return np.array([-v, u, 0.0])
    raise ValueError(i)


def bf_ez(i: int, u: float, v: float) -> np.ndarray:
    return np.array([0.0, 0.0, [1 - u - v, u, v][i - 1]])


def bf_curl_et(i: int, u: float, v: float) -> np.ndarray:
    return np.array([0.0, 0.0, [2.0, -2.0, 2.0][i - 1]])


def bf_curl_ez(i: int, u: float, v: float) -> np.ndarray:
    if i == 1:
        return np.array([-1.0, 1.0, 0.0])
    if i == 2:
        return np.array([0.0, -1.0, 0.0])
    if i == 3:
        return np.array([1.0, 0.0, 0.0])
    raise ValueError(i)


def get_tet_face(num_face: int) -> tuple[list[int], list[int]]:
    mapping = {
        1: ([0, 1, 2], [0, 1, 3]),
        2: ([0, 1, 3], [0, 2, 4]),
        3: ([0, 2, 3], [1, 2, 5]),
        4: ([1, 2, 3], [3, 4, 5]),
    }
    return mapping[num_face]


def assemble_port_face(
    nodes: np.ndarray,
    elements: np.ndarray,
    eoe: np.ndarray,
    mode: dict,
    face_idx: int,
    is_input: bool,
) -> tuple[np.ndarray, np.ndarray, np.ndarray, complex, complex]:
    n_edges = int(eoe.max()) + 1
    fc = mode["facesConn"][face_idx]
    num_ele = int(fc[0]) - 1
    num_face = int(fc[1])
    face_node, face_bf = get_tet_face(num_face)
    idx = elements[num_ele] - 1
    x, y, z = nodes[idx, 0], nodes[idx, 1], nodes[idx, 2]
    pairs = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
    l = np.array([np.linalg.norm(nodes[idx[a]] - nodes[idx[b]]) for a, b in pairs])

    mu = 1.0
    gamma = complex(mode["gamma"][0], mode["gamma"][1])
    power = mode["powerCoef"]
    normal = np.array(mode["normal"], dtype=float)
    et_coef = np.array(mode["Et_re"]) + 1j * np.array(mode["Et_im"])
    ez_coef = np.array(mode["Ez_re"]) + 1j * np.array(mode["Ez_im"])
    port_edge = np.array(mode["portEdgeOfFace"][face_idx], dtype=int)
    port_ez = np.array(mode["portNewFaces"][face_idx], dtype=int)

    jac = np.column_stack([x[:3] - x[3], y[:3] - y[3], z[:3] - z[3]])
    inv_jac = np.linalg.inv(jac)
    xx, yy, zz = x[face_node], y[face_node], z[face_node]

    f_jac = np.zeros((3, 3))
    f_jac[0, 0] = -xx[0] + xx[1]
    f_jac[0, 1] = -yy[0] + yy[1]
    f_jac[1, 0] = -xx[0] + xx[2]
    f_jac[1, 1] = -yy[0] + yy[2]
    f_jac[2, 2] = 1.0
    inv_f = np.linalg.inv(f_jac)
    f_jac_s = np.zeros((3, 3))
    inv22 = np.linalg.inv(f_jac[:2, :2])
    f_jac_s[0, 0] = inv22[1, 1]
    f_jac_s[0, 1] = -inv22[1, 0]
    f_jac_s[1, 0] = -inv22[0, 1]
    f_jac_s[1, 1] = inv22[0, 0]
    f_jac_s[2, 2] = 1.0
    f_t_jac = f_jac.T / np.linalg.det(f_jac)
    f_det = abs(np.linalg.det(f_jac))

    f_jac2 = np.zeros((2, 3))
    f_jac2[0, 0] = -xx[0] + xx[1]
    f_jac2[0, 1] = -yy[0] + yy[1]
    f_jac2[1, 0] = -xx[0] + xx[2]
    f_jac2[1, 1] = -yy[0] + yy[2]
    v_jac = np.linalg.inv(jac)

    xft, yft, pft = gauss_tri_order2()
    g = np.zeros(n_edges, dtype=complex)
    st = np.zeros(n_edges, dtype=complex)
    b_edge = np.zeros(n_edges, dtype=complex)
    p = 0.0 + 0.0j
    b1 = 0.0 + 0.0j

    for gp in range(len(pft)):
        f_e = np.zeros(3, dtype=complex)
        f_curl_e = np.zeros(3, dtype=complex)
        for i in range(3):
            et = inv_f @ bf_et(i + 1, xft[gp], yft[gp]) * l[face_bf[i]]
            ez = bf_ez(i + 1, xft[gp], yft[gp])
            curl_et = f_t_jac @ bf_curl_et(i + 1, xft[gp], yft[gp]) * l[face_bf[i]]
            curl_ez = f_jac_s @ bf_curl_ez(i + 1, xft[gp], yft[gp])
            f_e += et * et_coef[port_edge[i]] + ez * ez_coef[port_ez[i]]
            f_curl_e += curl_et * et_coef[port_edge[i]] + curl_ez * ez_coef[port_ez[i]]

        rot = np.array([f_e[1], -f_e[0], 0.0])
        if is_input:
            f_n0 = f_e * power
            f_curl_n0 = (f_curl_e + gamma * rot) * power
            f_n1 = f_e
            f_curl_n1 = f_curl_e - gamma * rot
        else:
            f_n2 = f_e
            f_curl_n2 = f_curl_e + gamma * rot
        f_w = f_e

        ref = np.array([xft[gp], yft[gp]]) @ f_jac2 + np.array([xx[0], yy[0], zz[0]])
        ref3 = ref @ v_jac - np.array([x[3], y[3], z[3]]) @ v_jac

        if is_input:
            for i in range(3):
                bf = inv_jac @ bf_edge(face_bf[i] + 1, ref3[0], ref3[1], ref3[2]) * l[face_bf[i]]
                edge_id = int(eoe[num_ele, face_bf[i]])
                g[edge_id] += pft[gp] * f_det * (f_w[0] * bf[0] + f_w[1] * bf[1])
                temp = mu * np.cross(normal, f_curl_n1)
                st[edge_id] += pft[gp] * f_det * np.vdot(bf, temp)
                temp0 = mu * np.cross(normal, f_curl_n0)
                b_edge[edge_id] += pft[gp] * f_det * np.vdot(bf, temp0)
            p += pft[gp] * f_det * (f_w[0] * f_n1[0] + f_w[1] * f_n1[1])
            b1 += pft[gp] * f_det * (f_w[0] * f_n0[0] + f_w[1] * f_n0[1])
        else:
            for i in range(3):
                bf = inv_jac @ bf_edge(face_bf[i] + 1, ref3[0], ref3[1], ref3[2]) * l[face_bf[i]]
                edge_id = int(eoe[num_ele, face_bf[i]])
                g[edge_id] += pft[gp] * f_det * (f_w[0] * bf[0] + f_w[1] * bf[1])
                temp = mu * np.cross(normal, f_curl_n2)
                st[edge_id] += pft[gp] * f_det * np.vdot(bf, temp)
            p += pft[gp] * f_det * (f_w[0] * f_n2[0] + f_w[1] * f_n2[1])

    return st, g, b_edge, p, b1


from scipy.sparse import bmat


def block_append_input(a: sparse.csr_matrix, b: np.ndarray, s: np.ndarray, g: np.ndarray, p: complex, b1: complex):
    n = b.size
    s_col = sparse.csr_matrix(s.reshape(-1, 1))
    g_row = sparse.csr_matrix(g.reshape(1, -1))
    p_corner = sparse.csr_matrix(np.array([[-p]]))
    a2 = bmat([[a, s_col], [g_row, p_corner]], format="csr")
    b2 = np.concatenate([-b, [b1]])
    return a2, b2


def block_append_output(a: sparse.csr_matrix, b: np.ndarray, t: np.ndarray, g: np.ndarray, p: complex):
    n = b.size
    t_col = sparse.csr_matrix(t.reshape(-1, 1))
    g_row = sparse.csr_matrix(g.reshape(1, -1))
    p_corner = sparse.csr_matrix(np.array([[-p]]))
    a2 = bmat([[a, t_col], [g_row, p_corner]], format="csr")
    b2 = np.concatenate([b, [0.0]])
    return a2, b2


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument(
        "--export",
        type=Path,
        default=None,
        help="export reduced A/b (COO) to this directory",
    )
    args = ap.parse_args()

    root = Path(__file__).resolve().parents[2]
    rel = root / "3D opticsfem-master/port/Release"
    with h5py.File(next(root.rglob("MeshData2x.mat")), "r") as f:
        nodes = np.asarray(f["Mesh/Nodes"]).T
        elements = np.asarray(f["Mesh/Elements"]).T.astype(int)
        faces = np.asarray(f["Mesh/Faces"]).T.astype(int)
        faces_index = np.asarray(f["Mesh/FacesIndex"]).reshape(-1).astype(int)
        domains = np.asarray(f["Mesh/Domains"]).reshape(-1).astype(int)

    modes = json.loads(rel.joinpath("port_modes_fem4.json").read_text())
    _, eoe = build_edges(elements)

    a = assemble_wave_matlab(nodes, elements, domains)
    b = np.zeros(a.shape[0], dtype=complex)

    n_edge = int(eoe.max()) + 1

    for mode, is_in in [(modes["input"], True), (modes["output"], False)]:
        st = np.zeros(a.shape[0], dtype=complex)
        g = np.zeros(a.shape[0], dtype=complex)
        bedge = np.zeros(n_edge, dtype=complex)
        p = 0.0 + 0.0j
        b1 = 0.0 + 0.0j
        for fi in range(len(mode["facesConn"])):
            s1, g1, be1, p1, b1e = assemble_port_face(nodes, elements, eoe, mode, fi, is_in)
            st[:n_edge] += s1
            g[:n_edge] += g1
            if is_in:
                bedge += be1
                b1 += b1e
            p += p1
        if is_in:
            b[:n_edge] += bedge
            a, b = block_append_input(a, b, st[: a.shape[0]], g[: a.shape[0]], p, b1)
            print(f"after input |b|={np.linalg.norm(b):.6g} b1={b1:.6g}")
        else:
            a, b = block_append_output(a, b, st[: a.shape[0]], g[: a.shape[0]], p)
            print(f"after output |b|={np.linalg.norm(b):.6g}")

    pec_faces = faces[np.isin(faces_index, [1, 2, 5, 24])]
    pec_pairs = set()
    for tri in pec_faces:
        for a0, b0 in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])]:
            pec_pairs.add(tuple(sorted((int(a0), int(b0)))))
    edge_u, _ = build_edges(elements)
    pec_idx = {i for i, e in enumerate(edge_u) if tuple(e) in pec_pairs}
    free = [i for i in range(a.shape[0]) if i not in pec_idx]
    a = a.tocsr()[free, :][:, free]
    b = b[free]

    export_dir = args.export
    if export_dir is None:
        export_dir = root / "三维matlab代码/2023-2-端口激励问题(四面体网格)/OutFile_fem4_ab"
    export_ab_coo(a, b, export_dir)

    x = spsolve(a, b)
    pcoef = modes["input"]["powerCoef"]
    e1, e2 = x[-2], x[-1]
    print(f"Python S11={e1/pcoef} S21={e2/pcoef}")
    ref = next(root.rglob("OutFile_fem4/S_params.txt"))
    vals = ref.read_text().strip().splitlines()[1].split()
    print(f"MATLAB S11={complex(float(vals[0]), float(vals[1]))} S21={complex(float(vals[2]), float(vals[3]))}")
    cpp = rel / "OutFile_fem4" / "S_params.txt"
    print("C++", cpp.read_text().strip())
    return 0


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