"""
Example code for running an OpenCL FDTD simulation

See main() for simulation setup.
"""

import sys
import time
import logging
import pyopencl

import numpy
import lzma
import dill

from opencl_fdtd import Simulation
from masque import Pattern, shapes
import gridlock
import pcgen
import meanas


__author__ = 'Jan Petykiewicz'

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)


def perturbed_l3(a: float, radius: float, **kwargs) -> Pattern:
    """
    Generate a masque.Pattern object containing a perturbed L3 cavity.

    Args:
        a: Lattice constant.
        radius: Hole radius, in units of a (lattice constant).
        hole_dose: Dose for all holes. Default 1.
        trench_dose: Dose for undercut trenches. Default 1.
        hole_layer: Layer for holes. Default (0, 0).
        trench_layer: Layer for undercut trenches. Default (1, 0).
        shifts_a: passed to pcgen.l3_shift
        shifts_r: passed to pcgen.l3_shift
        xy_size: [x, y] number of mirror periods in each direction; total size is
                2 * n + 1 holes in each direction. Default (10, 10).
        perturbed_radius: radius of holes perturbed to form an upwards-driected beam
                (multiplicative factor). Default 1.1.
        trench width: Width of the undercut trenches. Default 1.2e3.

    Returns:
        `masque.Pattern` object containing the L3 design
    """

    default_args = {
        'hole_dose':    1,
        'trench_dose':  1,
        'hole_layer':   0,
        'trench_layer': 1,
        'shifts_a':     (0.15, 0, 0.075),
        'shifts_r':     (1.0, 1.0, 1.0),
        'xy_size':      (10, 10),
        'perturbed_radius': 1.1,
        'trench_width': 1.2e3,
        }
    kwargs = {**default_args, **kwargs}

    xyr = pcgen.l3_shift_perturbed_defect(
        mirror_dims=kwargs['xy_size'],
        perturbed_radius=kwargs['perturbed_radius'],
        shifts_a=kwargs['shifts_a'],
        shifts_r=kwargs['shifts_r'],
        )
    xyr *= a
    xyr[:, 2] *= radius

    pat = Pattern()
    pat.name = f'L3p-a{a:g}r{radius:g}rp{kwargs["perturbed_radius"]:g}'
    pat.shapes += [shapes.Circle(radius=r, offset=(x, y),
                                 dose=kwargs['hole_dose'],
                                 layer=kwargs['hole_layer'])
                   for x, y, r in xyr]

    maxes = numpy.max(numpy.fabs(xyr), axis=0)
    pat.shapes += [
        shapes.Polygon.rectangle(
            lx=(2 * maxes[0]),
            ly=kwargs['trench_width'],
            offset=(0, s * (maxes[1] + a + kwargs['trench_width'] / 2)),
            dose=kwargs['trench_dose'],
            layer=kwargs['trench_layer'],
            )
        for s in (-1, 1)]
    return pat


def main():
    max_t = 4000            # number of timesteps

    dx = 25                 # discretization (nm/cell)
    pml_thickness = 8       # (number of cells)

    wl = 1550               # Excitation wavelength and fwhm
    dwl = 200

    # Device design parameters
    xy_size = numpy.array([10, 10])
    a = 430
    r = 0.285
    th = 170

    # refractive indices
    n_slab = 3.408  # InGaAsP(80, 50) @ 1550nm
    n_air = 1.0   # air

    # Half-dimensions of the simulation grid
#    xy_max = (xy_size + 1) * a * [1, numpy.sqrt(3)/2]
#    z_max = 1.6 * a
#    xyz_max = numpy.hstack((xy_max, z_max)) + pml_thickness * dx
#
#    # Coordinates of the edges of the cells.
#    #  The fdtd package can only do square grids at the moment.
#    half_edge_coords = [numpy.arange(dx/2, m + dx, step=dx) for m in xyz_max]
#    edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords]
    edge_coords = [numpy.arange(-100.5, 101), numpy.arange(-1, 1), numpy.arange(-100.5, 101)]
#    edge_coords = [numpy.arange(-100.5, 101), numpy.arange(-100.5, 101), numpy.arange(-1, 1)]

    # #### Create the grid, mask, and draw the device ####
    grid = gridlock.Grid(edge_coords)
    epsilon = grid.allocate(n_air**2)
#    grid.draw_slab(epsilon,
#                   surface_normal=2,
#                   center=[0, 0, 0],
#                   thickness=th,
#                   eps=n_slab**2)
#    mask = perturbed_l3(a, r)
#
#    grid.draw_polygons(epsilon,
#                       surface_normal=2,
#                       center=[0, 0, 0],
#                       thickness=2 * th,
#                       eps=n_air**2,
#                       polygons=mask.as_polygons())

    logger.info(f'grid shape: {grid.shape}')
    # #### Create the simulation grid ####
#    pmls = [{'axis': a, 'polarity': p, 'thickness': pml_thickness}
#            for a in 'xyz' for p in 'np']
    pmls = [{'axis': a, 'polarity': p, 'thickness': pml_thickness}
            for a in 'xz' for p in 'np']
    #bloch = [{'axis': a, 'real': 1, 'imag': 0} for a in 'x']
    bloch = []
    sim = Simulation(epsilon, do_poynting=True, pmls=pmls, bloch_boundaries=bloch)

    # Source parameters and function
    w = 2 * numpy.pi * dx / wl
    fwhm = dwl * w * w / (2 * numpy.pi * dx)
    alpha = (fwhm ** 2) / 8 * numpy.log(2)
    delay = 7/numpy.sqrt(2 * alpha)

    def field_source(i):
        t0 = i * sim.dt - delay
        return numpy.sin(w * t0) * numpy.exp(-alpha * t0**2)

    with open('sources.c', 'w') as f:
        f.write(sim.sources['E'])
        f.write('\n====================H======================\n')
        f.write(sim.sources['H'])
        if sim.update_S:
            f.write('\n=====================S=====================\n')
            f.write(sim.sources['S'])
        if bloch:
            f.write('\n=====================F=====================\n')
            f.write(sim.sources['F'])
            f.write('\n=====================G=====================\n')
            f.write(sim.sources['G'])


    planes = numpy.empty((max_t, 4))
    planes2 = numpy.empty((max_t, 4))
    Ectr = numpy.empty(max_t)
    u = numpy.empty(max_t)
    ui = numpy.empty(max_t)
    # #### Run a bunch of iterations ####
    # event = sim.whatever([prev_event]) indicates that sim.whatever should be queued
    #  immediately and run once prev_event is finished.
    start = time.perf_counter()
    for t in range(max_t):
        e = sim.update_E([])
#        if bloch:
#            e = sim.update_F([e])
        e.wait()

        ind = numpy.ravel_multi_index(tuple(grid.shape//2), dims=grid.shape, order='C') + numpy.prod(grid.shape)
#        sim.E[ind] += field_source(t)
        if t == 2:
            sim.E[ind] += 1e6

        h_old = sim.H.copy()

        e = sim.update_H([])
#        if bloch:
#            e = sim.update_G([e])
        e.wait()

        S = sim.S.get().reshape(epsilon.shape) * sim.dt * dx * dx *dx
        m = 30
        planes[t] = (
            S[0][+pml_thickness+2, :, pml_thickness+3:-pml_thickness-3].sum(),
            S[0][-pml_thickness-2, :, pml_thickness+3:-pml_thickness-3].sum(),
            S[2][pml_thickness+2:-pml_thickness-2, :, +pml_thickness+2].sum(),
            S[2][pml_thickness+2:-pml_thickness-2, :, -pml_thickness-2].sum(),
            )
        planes2[t] = (
            S[0][grid.shape[0]//2-1, 0, grid.shape[2]//2].sum(),
            S[0][grid.shape[0]//2  , 0, grid.shape[2]//2].sum(),
            S[2][grid.shape[0]//2  , 0, grid.shape[2]//2-1].sum(),
            S[2][grid.shape[0]//2  , 0, grid.shape[2]//2].sum(),
            )

#        planes[t] = (
#            S[0][+pml_thickness+m, pml_thickness+m+1:-pml_thickness-m, :].sum(),
#            S[0][-pml_thickness-m, pml_thickness+m+1:-pml_thickness-m, :].sum(),
#            S[1][pml_thickness+1+m:-pml_thickness-m, +pml_thickness+m, :].sum(),
#            S[1][pml_thickness+1+m:-pml_thickness-m, -pml_thickness-m, :].sum(),
#            )
#        planes2[t] = (
#            S[0][grid.shape[0]//2-1, grid.shape[1]//2  , 0].sum(),
#            S[0][grid.shape[0]//2  , grid.shape[1]//2  , 0].sum(),
#            S[1][grid.shape[0]//2  , grid.shape[1]//2-1, 0].sum(),
#            S[1][grid.shape[0]//2  , grid.shape[1]//2  , 0].sum(),
#            )
        Ectr[t] = sim.E[ind].get()
        u[t] = pyopencl.array.sum(sim.E * sim.E * sim.eps + h_old * sim.H).get() * dx * dx * dx
        ui[t] = (sim.E * sim.E * sim.eps + h_old * sim.H).reshape(epsilon.shape).get()[:, pml_thickness+m:-pml_thickness-m, :,
                                                                                            pml_thickness+m:-pml_thickness-m].sum() * dx * dx * dx
#        ui[t] = (sim.E * sim.E * sim.eps + h_old * sim.H).reshape(epsilon.shape).get()[:, pml_thickness+m:-pml_thickness-m,
#                                                                                            pml_thickness+m:-pml_thickness-m, :].sum() * dx * dx * dx

        if t % 100 == 0:
            avg = (t + 1) / (time.perf_counter() - start)
            logger.info(f'iteration {t}: average {avg} iterations per sec')
            sys.stdout.flush()

    with lzma.open('saved_simulation', 'wb') as f:
        def unvec(f):
            return meanas.fdmath.unvec(f, grid.shape)
        d = {
            'grid': grid,
            'epsilon': epsilon,
            'E': unvec(sim.E.get()),
            'H': unvec(sim.H.get()),
            'dt': sim.dt,
            'dx': dx,
            'planes': planes,
            'planes2': planes2,
            'Ectr': Ectr,
            'u': u,
            'ui': ui,
            }
        if sim.S is not None:
            d['S'] = unvec(sim.S.get())
        dill.dump(d, f)


if __name__ == '__main__':
    main()
