"""
PML implementations

#TODO discussion of PMLs
#TODO cpml documentation

"""
# TODO retest pmls!

from typing import Callable, Sequence, Any
from copy import deepcopy
import numpy
from numpy.typing import NDArray, DTypeLike

from ..fdmath import fdfield_t, dx_lists_t
from ..fdmath.functional import deriv_forward, deriv_back


__author__ = 'Jan Petykiewicz'


def cpml_params(
        axis: int,
        polarity: int,
        dt: float,
        thickness: int = 8,
        ln_R_per_layer: float = -1.6,
        epsilon_eff: float = 1,
        mu_eff: float = 1,
        m: float = 3.5,
        ma: float = 1,
        cfs_alpha: float = 0,
        ) -> dict[str, Any]:

    if axis not in range(3):
        raise Exception('Invalid axis: {}'.format(axis))

    if polarity not in (-1, 1):
        raise Exception('Invalid polarity: {}'.format(polarity))

    if thickness <= 2:
        raise Exception('It would be wise to have a pml with 4+ cells of thickness')

    if epsilon_eff <= 0:
        raise Exception('epsilon_eff must be positive')

    sigma_max = -ln_R_per_layer / 2 * (m + 1)
    kappa_max = numpy.sqrt(epsilon_eff * mu_eff)
    alpha_max = cfs_alpha

    xe = numpy.arange(1, thickness + 1, dtype=float)        # TODO: pass in dtype?
    xh = numpy.arange(1, thickness + 1, dtype=float)
    if polarity > 0:
        xe -= 0.5
    elif polarity < 0:
        xh -= 0.5
        xe = xe[::-1]
        xh = xh[::-1]
    else:
        raise Exception('Bad polarity!')

    expand_slice_l: list[Any] = [None, None, None]
    expand_slice_l[axis] = slice(None)
    expand_slice = tuple(expand_slice_l)

    def par(x: NDArray[numpy.float64]) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]]:
        scaling = (x / thickness) ** m
        sigma = scaling * sigma_max
        kappa = 1 + scaling * (kappa_max - 1)
        alpha = ((1 - x / thickness) ** ma) * alpha_max
        p0 = numpy.exp(-(sigma / kappa + alpha) * dt)
        p1 = sigma / (sigma + kappa * alpha) * (p0 - 1)
        p2 = 1 / kappa
        return p0[expand_slice], p1[expand_slice], p2[expand_slice]

    p0e, p1e, p2e = par(xe)
    p0h, p1h, p2h = par(xh)

    region_list = [slice(None), slice(None), slice(None)]
    if polarity < 0:
        region_list[axis] = slice(None, thickness)
    elif polarity > 0:
        region_list[axis] = slice(-thickness, None)
    else:
        raise Exception('Bad polarity!')
    region = tuple(region_list)

    return {
        'param_e': (p0e, p1e, p2e),
        'param_h': (p0h, p1h, p2h),
        'region': region,
        }


def updates_with_cpml(
        cpml_params: Sequence[Sequence[dict[str, Any] | None]],
        dt: float,
        dxes: dx_lists_t,
        epsilon: fdfield_t,
        *,
        dtype: DTypeLike = numpy.float32,
        ) -> tuple[Callable[[fdfield_t, fdfield_t, fdfield_t], None],
                   Callable[[fdfield_t, fdfield_t, fdfield_t], None]]:

    Dfx, Dfy, Dfz = deriv_forward(dxes[1])
    Dbx, Dby, Dbz = deriv_back(dxes[1])

    psi_E: list[list[tuple[Any, Any]]] = [[(None, None) for _ in range(2)] for _ in range(3)]
    psi_H: list[list[tuple[Any, Any]]] = deepcopy(psi_E)
    params_E: list[list[tuple[Any, Any, Any, Any]]] = [[(None, None, None, None) for _ in range(2)] for _ in range(3)]
    params_H: list[list[tuple[Any, Any, Any, Any]]] = deepcopy(params_E)

    for axis in range(3):
        for pp, polarity in enumerate((-1, 1)):
            cpml_param = cpml_params[axis][pp]
            if cpml_param is None:
                psi_E[axis][pp] = (None, None)
                psi_H[axis][pp] = (None, None)
                continue

            region = cpml_param['region']
            region_shape = epsilon[0][region].shape

            psi_E[axis][pp] = (
                numpy.zeros(region_shape, dtype=dtype),
                numpy.zeros(region_shape, dtype=dtype),
                )
            psi_H[axis][pp] = (
                numpy.zeros(region_shape, dtype=dtype),
                numpy.zeros(region_shape, dtype=dtype),
                )
            params_E[axis][pp] = cpml_param['param_e'] + (region,)
            params_H[axis][pp] = cpml_param['param_h'] + (region,)

    pE = numpy.empty_like(epsilon, dtype=dtype)
    pH = numpy.empty_like(epsilon, dtype=dtype)

    def update_E(
            e: fdfield_t,
            h: fdfield_t,
            epsilon: fdfield_t,
            ) -> None:
        dyHx = Dby(h[0])
        dzHx = Dbz(h[0])
        dxHy = Dbx(h[1])
        dzHy = Dbz(h[1])
        dxHz = Dbx(h[2])
        dyHz = Dby(h[2])

        dH = ((dxHy, dxHz),
              (dyHx, dyHz),
              (dzHx, dzHy))

        pE.fill(0)

        for axis in range(3):
            se = (-1, 1, -1)[axis]
            transverse = numpy.delete(range(3), axis)
            u, v = transverse
            dHu, dHv = dH[axis]

            for pp in range(2):
                psi_Eu, psi_Ev = psi_E[axis][pp]

                if psi_Eu is None:
                    # No pml in this direction
                    continue

                p0e, p1e, p2e, region = params_E[axis][pp]

                dHu[region] *= p2e
                dHv[region] *= p2e
                psi_Eu *= p0e
                psi_Ev *= p0e
                psi_Eu += p1e * dHv[region]    # note reversed u,v mapping
                psi_Ev += p1e * dHu[region]
                pE[u][region] += +se * psi_Eu
                pE[v][region] += -se * psi_Ev

        e[0] += dt / epsilon[0] * (dyHz - dzHy + pE[0])
        e[1] += dt / epsilon[1] * (dzHx - dxHz + pE[1])
        e[2] += dt / epsilon[2] * (dxHy - dyHx + pE[2])

    def update_H(
            e: fdfield_t,
            h: fdfield_t,
            mu: fdfield_t = numpy.ones(3),
            ) -> None:
        dyEx = Dfy(e[0])
        dzEx = Dfz(e[0])
        dxEy = Dfx(e[1])
        dzEy = Dfz(e[1])
        dxEz = Dfx(e[2])
        dyEz = Dfy(e[2])

        dE = ((dxEy, dxEz),
              (dyEx, dyEz),
              (dzEx, dzEy))

        pH.fill(0)

        for axis in range(3):
            se = (-1, 1, -1)[axis]
            transverse = numpy.delete(range(3), axis)
            u, v = transverse
            dEu, dEv = dE[axis]

            for pp in range(2):
                psi_Hu, psi_Hv = psi_H[axis][pp]

                if psi_Hu is None:
                    # No pml here
                    continue

                p0h, p1h, p2h, region = params_H[axis][pp]

                dEu[region] *= p2h      # modifies d_E_
                dEv[region] *= p2h
                psi_Hu *= p0h
                psi_Hv *= p0h
                psi_Hu += p1h * dEv[region]    # note reversed u,v mapping
                psi_Hv += p1h * dEu[region]
                pH[u][region] += +se * psi_Hu
                pH[v][region] += -se * psi_Hv

        h[0] -= dt / mu[0] * (dyEz - dzEy + pH[0])
        h[1] -= dt / mu[1] * (dzEx - dxEz + pH[1])
        h[2] -= dt / mu[2] * (dxEy - dyEx + pH[2])
    return update_E, update_H
