"""
@package pmsco.calculators.edac
Garcia de Abajo EDAC program interface.

@author Matthias Muntwiler

@copyright (c) 2015-18 by Paul Scherrer Institut @n
Licensed under the Apache License, Version 2.0 (the "License"); @n
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at
  http://www.apache.org/licenses/LICENSE-2.0
"""

import logging
import numpy as np
import os

import pmsco.calculators.calculator as calculator
import pmsco.data as md
import pmsco.cluster as mc
from pmsco.helpers import BraceMessage as BMsg

logger = logging.getLogger(__name__)

try:
    import edac
except (ImportError, ModuleNotFoundError) as e:
    edac = None
    logger.critical("Error importing the edac package.", exc_info=e)


class EdacCalculator(calculator.Calculator):
    def write_input_file(self, params, scan, filepath):
        """
        write parameters to an EDAC input file

        EDAC will calculate results on a rectangular grid.
        the grid is constructed from the limits of the scan coordinates
        and the number of positions in each respective dimension.
        to avoid any confusion, the input scan should be rectangular with equidistant steps.

        the following scans are supported:
        (energy), (energy, theta), (energy, phi), (energy, alpha), (theta, phi) holo.
        except for the holo scan, each scan dimension must be linear.
        the holo scan is translated to a rectangular (theta, phi) scan
        where theta is copied
        and phi is replaced by a linear scan from the minimum to the maximum phi at 1 degree steps.
        the scan type is detected from the scan file.

        if alpha is defined, theta is implicitly set to normal emission! (to be generalized)

        @param params: a pmsco.project.CalculatorParams object with all necessary values except cluster and output files set.

        @param scan: a pmsco.project.Scan() object describing the experimental scanning scheme.

        @param filepath: (str) name and path of the file to be created.

        @return dictionary of created files {filename: category}
        """
        files = {}

        with open(filepath, "wt", encoding="latin1") as f:
            f.write("verbose off\n")
            f.write("cluster input {0}\n".format(params.cluster_file))
            f.write("emitters {0:d} l(A)\n".format(len(params.emitters)))
            for em in params.emitters:
                f.write("{0:f} {1:f} {2:f} {3:d}\n".format(*em))

            en = scan.energies + params.work_function
            en_min = en.min()
            en_max = en.max()
            if en.shape[0] <= 1:
                en_num = 1
            else:
                de = np.diff(en)
                de = de[de >= 0.01]
                de = de.min()
                en_num = int(round((en_max - en_min) / de)) + 1
                if en_num != en.shape[0]:
                    logger.warning("energy scan length mismatch: EDAC {0}, scan {1}".format(en_num, en.shape[0]))
                assert en_num < en.shape[0] * 10, \
                    "linearization of energy scan causes excessive oversampling {0}/{1}".format(en_num, en.shape[0])
            f.write("emission energy E(eV) {en0:f} {en1:f} {nen:d}\n".format(en0=en_min, en1=en_max, nen=en_num))

            if params.fixed_cluster:
                th = scan.alphas
                ph = np.remainder(scan.phis + 90.0, 360.0)
                f.write("fixed cluster\n")
                if np.abs(scan.thetas).max() > 0.0:
                    logger.warning("theta angle implicitly set to zero due to alpha scan.")
            else:
                th = np.unique(scan.thetas)
                ph = scan.phis
                f.write("movable cluster\n")

            th_min = th.min()
            th_max = th.max()
            if th.shape[0] <= 1:
                th_num = 1
            else:
                dt = np.diff(th)
                dt = dt[dt >= 0.1]
                dt = dt.min()
                if ph.shape[0] > 1:
                    # hemispherical scan
                    if th_min < 0:
                        th_min = max(th_min - dt, -90.0)
                    else:
                        th_min = max(th_min - dt, 0.0)
                    if th_max > 0:
                        th_max = min(th_max + dt, 90.0)
                    else:
                        th_max = min(th_max + dt, 0.0)
                th_num = int(round((th_max - th_min) / dt)) + 1
                assert th_num < th.shape[0] * 10, \
                    "linearization of theta scan causes excessive oversampling {0}/{1}".format(th_num, th.shape[0])

            f.write("beta {0}\n".format(params.polar_incidence_angle))
            f.write("incidence {0} {1}\n".format(params.polar_incidence_angle, params.azimuthal_incidence_angle))
            f.write("emission angle theta {th0:f} {th1:f} {nth:d}\n".format(th0=th_min, th1=th_max, nth=th_num))

            ph_min = ph.min()
            ph_max = ph.max()
            if th.shape[0] <= 1:
                # azimuthal scan
                ph_num = ph.shape[0]
            elif ph.shape[0] <= 1:
                # polar scan
                ph_num = 1
            else:
                # hemispherical scan
                dp = np.diff(ph)
                dp = dp[dp >= 0.1]
                dp = dp.min()
                ph_min = max(ph_min - dp, 0.0)
                ph_max = min(ph_max + dp, 360.0)
                dt = (th_max - th_min) / (th_num - 1)
                dp = min(dp, dt)
                ph_num = int(round((ph_max - ph_min) / dp)) + 1
                assert ph_num < ph.shape[0] * 10, \
                    "linearization of phi scan causes excessive oversampling {0}/{1}".format(ph_num, ph.shape[0])

            f.write("emission angle phi {ph0:f} {ph1:f} {nph:d}\n".format(ph0=ph_min, ph1=ph_max, nph=ph_num))

            f.write("initial state {0}\n".format(params.initial_state))
            polarizations = {'H': 'LPx', 'V': 'LPy', 'L': 'LCP', 'R': 'RCP'}
            f.write("polarization {0}\n".format(polarizations[params.polarization]))

            scatterers = ["scatterer {at} {fi}\n".format(at=at, fi=fi)
                          for (at, fi) in params.phase_files.items()
                          if os.path.isfile(fi)]
            rme = ["rmat {fi}\n".format(fi=fi)
                   for (at, fi) in params.rme_files.items()
                   if at == params.emitters[0][3] and os.path.isfile(fi)] or \
                  ["rmat inline 1 regular1 {l0} {pv} {pd} {mv} {md}\n".format(l0=params.l_init,
                   pv=params.rme_plus_value, pd=params.rme_plus_shift,
                   mv=params.rme_minus_value, md=params.rme_minus_shift)]
            if scatterers and rme:
                for scat in scatterers:
                    f.write(scat)
                f.write(rme[0])
            else:
                f.write("muffin-tin\n")

            f.write("V0 E(eV) {0:f}\n".format(params.inner_potential))
            f.write("cluster surface l(A) {0:f}\n".format(params.z_surface))
            f.write("imfp SD-UC\n")
            f.write("temperature {0:f} {1:f}\n".format(params.experiment_temperature, params.debye_temperature))
            f.write("iteration recursion\n")
            f.write("dmax l(A) {0:f}\n".format(params.dmax))
            f.write("lmax {0:d}\n".format(params.lmax))
            f.write("orders {0:d} ".format(len(params.orders)))
            f.write(" ".join(format(order, "d") for order in params.orders) + "\n")
            f.write("emission angle window {0:F}\n".format(params.angular_resolution / 2.0))

            # scattering factor output (see project.CalculatorParams.phase_output_classes)
            if params.phase_output_classes is not None:
                fn = "{0}.clu".format(params.output_file)
                f.write("cluster output l(A) {fn}\n".format(fn=fn))
                files[fn] = "output"
                try:
                    cls = (cl for cl in params.phase_output_classes)
                except TypeError:
                    cls = range(params.phase_output_classes)
                for cl in cls:
                    fn = "{of}.{cl}.scat".format(cl=cl, of=params.output_file)
                    f.write("scan scatterer {cl} phase-shifts {fn}\n".format(cl=cl, fn=fn))
                    files[fn] = "output"

            f.write("scan pd {0}\n".format(params.output_file))
            files[params.output_file] = "output"
            f.write("end\n")

        return files

    def run(self, params, cluster, scan, output_file):
        """
        run EDAC with the given parameters and cluster.

        @param params: a pmsco.project.CalculatorParams object with all necessary values except cluster and output files set.

        @param cluster: a pmsco.cluster.Cluster(format=FMT_EDAC) object with all atom positions set.

        @param scan: a pmsco.project.Scan() object describing the experimental scanning scheme.

        @param output_file: base name for all intermediate and output files

        @return: (str, dict) result_file, and dictionary of created files {filename: category}
        """

        # set up scan
        params.fixed_cluster = 'a' in scan.mode

        # generate file names
        base_filename = output_file
        clu_filename = base_filename + ".clu"
        out_filename = base_filename + ".out"
        par_filename = base_filename + ".par"
        dat_filename = out_filename
        if params.fixed_cluster:
            etpi_filename = base_filename + ".etpai"
            dtype = md.DTYPE_ETPAI
        else:
            etpi_filename = base_filename + ".etpi"
            dtype = md.DTYPE_ETPI

        # fix EDAC particularities
        params.cluster_file = clu_filename
        params.output_file = out_filename
        params.data_file = dat_filename
        params.emitters = cluster.get_emitters(['x', 'y', 'z', 'c'])

        # save parameter files
        logger.debug("writing cluster file %s", clu_filename)
        cluster.save_to_file(clu_filename, fmt=mc.FMT_EDAC)
        logger.debug("writing input file %s", par_filename)
        files = self.write_input_file(params, scan, par_filename)

        # run EDAC
        logger.info("calling EDAC with input file %s", par_filename)
        edac.run_script(par_filename)

        # load results and save in ETPI or ETPAI format
        logger.debug("importing data from output file %s", dat_filename)
        result_etpi = md.load_edac_pd(dat_filename, energy=scan.energies[0] + params.work_function,
            theta=scan.thetas[0], phi=scan.phis[0],
            fixed_cluster=params.fixed_cluster)
        result_etpi['e'] -= params.work_function

        if 't' in scan.mode and 'p' in scan.mode:
            dest_tpi = np.zeros(scan.raw_data.shape, dtype)
            dest_tpi['t'] = scan.thetas
            dest_tpi['p'] = scan.phis
            result_etpi = md.interpolate_hemi_scan(result_etpi, dest_tpi)

        if params.fixed_cluster:
            expected_shape = max(scan.energies.shape[0], 1) * max(scan.alphas.shape[0], 1)
        else:
            expected_shape = max(scan.energies.shape[0], 1) * max(scan.phis.shape[0], scan.thetas.shape[0], 1)
        if result_etpi.shape[0] != expected_shape:
            logger.warning(BMsg("possible scan length mismatch: EDAC result: {result}, expected: {expected}",
                              result=result_etpi.shape[0], expected=expected_shape))

        logger.debug("save result to file %s", etpi_filename)
        md.save_data(etpi_filename, result_etpi)

        files[clu_filename] = 'input'
        files[par_filename] = 'input'
        files[dat_filename] = 'output'
        files[etpi_filename] = 'region'

        return etpi_filename, files
