"""
@package pmsco.projects.demo.molecule
scattering calculation project for single molecules or coordinate files from other programs

the atomic positions are read from a molecule (.xyz) file.
emitters are selected by chemical element symbol or by an additional molecule file (emitter file)
that contains only those atoms of the cluster file which are inequivalent emitters.

cluster, emitters, initial state and kinetic energy are specified on the command line.
there are no structural parameters.

example 1: molecule from XYZ file
---------------------------------

the cluster file contains all atomic positions necessary for calculating the diffraction pattern.
emitters are selected by chemical element symbol.
the cluster is not trimmed.
normal emission is along the z-axis.


example 2: periodic structure from external program (e.g. Vesta)
----------------------------------------------------------------

the cluster file contains the unit cells spanned by at least 3 unit vectors in the surface plane.
the emitter file contains only one unit cell as a sub-set of the cluster file.
emitters can be narrowed down further by chemical element symbol.
an rmax parameter can be specified to trim the cluster.
normal emission is along the z-axis.


@author Matthias Muntwiler, matthias.muntwiler@psi.ch

@copyright (c) 2015-20 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 math
import numpy as np
from pathlib import Path
import periodictable as pt
import logging

# noinspection PyUnresolvedReferences
from pmsco.calculators.calculator import InternalAtomicCalculator
# noinspection PyUnresolvedReferences
from pmsco.calculators.edac import EdacCalculator
# noinspection PyUnresolvedReferences
from pmsco.calculators.phagen.runner import PhagenCalculator
import pmsco.cluster as cluster
import pmsco.data
# noinspection PyUnresolvedReferences
import pmsco.elements.bindingenergy
from pmsco.helpers import BraceMessage as BMsg
import pmsco.project

logger = logging.getLogger(__name__)


class MoleculeFileCluster(cluster.ClusterGenerator):
    """
    cluster generator based on external file.

    work in progress.
    """
    def __init__(self, project):
        super(MoleculeFileCluster, self).__init__(project)
        self.base_cluster = None
        self.emitter_cluster = None

    def get_base_cluster(self):
        """
        return the project-defined atom coordinates, load from file if necessary.

        the file path is set in self.project.cluster_file.
        the file must be in XYZ (.xyz) or PMSCO cluster (.clu) format (cf. pmsco.cluster module).

        @return: Cluster object (also referenced by self.base_cluster)
        """
        if self.base_cluster is None:
            clu = cluster.Cluster()
            clu.set_rmax(np.inf)
            p = Path(self.project.cluster_file)
            ext = p.suffix
            if ext == ".xyz":
                fmt = cluster.FMT_XYZ
            elif ext == ".clu":
                fmt = cluster.FMT_PMSCO
            else:
                raise ValueError(f"unknown cluster file extension {ext}")
            clu.load_from_file(self.project.cluster_file, fmt=fmt)
            self.base_cluster = clu

        return self.base_cluster

    def get_emitter_cluster(self):
        """
        return the project-defined emitter coordinates, load from file if necessary.

        the file path is set in self.project.emitter_file.
        if the file path is None, the method loads the base cluster self.project.cluster_file.
        the file must be in XYZ (.xyz) or PMSCO cluster (.clu) format (cf. pmsco.cluster module).

        @return: Cluster object (also referenced by self.emitter_cluster).
            None if no emitter file was specified.
        """
        if self.emitter_cluster is None:
            clu = cluster.Cluster()
            clu.set_rmax(np.inf)
            try:
                p = Path(self.project.emitter_file)
            except TypeError:
                p = Path(self.project.cluster_file)
            ext = p.suffix
            if ext == ".xyz":
                fmt = cluster.FMT_XYZ
            elif ext == ".clu":
                fmt = cluster.FMT_PMSCO
            else:
                raise ValueError(f"unknown cluster file extension {ext}")
            clu.load_from_file(self.project.emitter_file, fmt=fmt)
            self.emitter_cluster = clu

        return self.emitter_cluster

    def count_emitters(self, model, index):
        """
        count the number of emitter configurations.

        the method creates the full cluster and counts the emitters.

        @param model: model parameters.
        @param index: scan and domain are used by the create_cluster() method,
            emit decides whether the method returns the number of configurations (-1),
            or the number of emitters in the specified configuration (>= 0).
        @return: number of emitter configurations.
        """
        if index.emit == -1:
            clu = self.get_emitter_cluster()
            sel_emit = clu.data['s'] == self.project.scans[index.scan].emitter
            return np.sum(sel_emit)
        elif index.emit >= 0:
            return 1
        else:
            raise ValueError(f"invalid emitter index {index.emit}")

    def create_cluster(self, model, index):
        """
        import a cluster from a coordinate file (XYZ format).

        the method does the following:
        - load the cluster file specified by self.cluster_file.
        - trim the cluster according to model['rmax'].
        - mark the 6 nitrogen atoms at the center of the trimer as emitters.

        @param model: rmax is the trim radius of the cluster in units of the surface lattice constant.

        @param index (named tuple CalcID) calculation index.
            this method uses the domain index to look up domain parameters in
            `pmsco.project.Project.domains`.
            `index.emit` selects whether a single-emitter (>= 0) or all-emitter cluster (== -1) is returned.

        @return pmsco.cluster.Cluster object
        """
        clu = cluster.Cluster()
        clu.copy_from(self.get_base_cluster())
        clu.comment = f"{self.__class__}, {index}"
        dom = self.project.domains[index.domain]

        # emitter selection
        ems = cluster.Cluster()
        ems.copy_from(self.get_emitter_cluster())
        ems.set_rmax(model['rmax'] + 0.1)
        ems.trim_cylinder(clu.rmax, clu.rmax)

        idx_emit = np.where(ems.data['s'] == self.project.scans[index.scan].emitter)
        assert isinstance(idx_emit, tuple)
        idx_emit = idx_emit[0]
        if index.emit >= 0:
            idx_emit = idx_emit[index.emit]
            origin = ems.get_position(idx_emit)
            clu.translate(-origin)
            clu.data['e'] = 0
            clu.set_emitter(pos=np.array((0.0, 0.0, 0.0)))
        else:
            for idx in idx_emit:
                clu.set_emitter(pos=ems.get_position(idx))

        # rotation
        if 'xrot' in model:
            clu.rotate_x(model['xrot'])
        elif 'xrot' in dom:
            clu.rotate_x(dom['xrot'])
        if 'yrot' in model:
            clu.rotate_y(model['yrot'])
        elif 'yrot' in dom:
            clu.rotate_y(dom['yrot'])
        if 'zrot' in model:
            clu.rotate_z(model['zrot'])
        elif 'zrot' in dom:
            clu.rotate_z(dom['zrot'])

        # trim
        clu.set_rmax(model['rmax'] + 0.1)
        clu.trim_paraboloid(clu.rmax, -clu.rmax)

        logger.info(f"cluster for calculation {index}: "
                    f"{clu.get_atom_count()} atoms, {clu.get_emitter_count()} emitters")

        return clu


class MoleculeProject(pmsco.project.Project):
    """
    general molecule project.

    the following model parameters are used:

    @arg `model['zsurf']`   : position of surface above molecule (angstrom)
    @arg `model['Texp']`    : experimental temperature (K)
    @arg `model['Tdeb']`    : debye temperature (K)
    @arg `model['V0']`      : inner potential (eV)
    @arg `model['rmax']`    : cluster radius (angstrom)
    @arg `model['ares']`    : angular resolution (degrees, FWHM)
    @arg `model['distm']`   : dmax for EDAC (angstrom)

    the following domain parameters are used.
    they can also be specified as model parameters.

    @arg `'xrot'`           : rotation about x-axis (applied first) (deg)
    @arg `'yrot'`           : rotation about y-axis (applied after x) (deg)
    @arg `'zrot'`           : rotation about z-axis (applied after x and y) (deg)

    the project parameters are:

    @arg `cluster_file`    : name of cluster file of template molecule.
                              default: "dpdi-trimer.xyz"
    """
    def __init__(self):
        """
        initialize a project instance
        """
        super(MoleculeProject, self).__init__()
        self.model_space = pmsco.project.ModelSpace()
        self.cluster_file = "demo-cluster.xyz"
        self.emitter_file = None
        self.cluster_generator = MoleculeFileCluster(self)
        self.atomic_scattering_factory = PhagenCalculator
        self.multiple_scattering_factory = EdacCalculator
        self.phase_files = {}
        self.rme_files = {}

    def validate(self):
        """
        Validate project parameters

        Resolve paths of cluster and emitter files after calling the inherited method.

        @return: None
        """

        super().validate()
        self.cluster_file = self.directories.resolve_path(self.cluster_file)
        self.emitter_file = self.directories.resolve_path(self.emitter_file)
        logger.warning(f"cluster_file: {self.cluster_file}")
        logger.warning(f"emitter_file: {self.emitter_file}")

    def create_params(self, model, index):
        """
        set a specific set of parameters given the optimizable parameters.

        @param model: (dict) optimization parameters
            this method requires zsurf, V0, Texp, Tdeb, ares and distm.

        @param index (named tuple CalcID) calculation index.
            this method formats the index into the comment line.
        """
        params = pmsco.project.CalculatorParams()

        params.title = "molecule demo"
        params.comment = f"{self.__class__} {index}"
        params.cluster_file = ""
        params.output_file = ""
        initial_state = self.scans[index.scan].initial_state
        params.initial_state = initial_state
        emitter = self.scans[index.scan].emitter
        params.binding_energy = pt.elements.symbol(emitter).binding_energy[initial_state]
        params.polarization = "H"
        params.z_surface = model['zsurf']
        params.inner_potential = model['V0']
        params.work_function = 4.5
        params.polar_incidence_angle = 60.0
        params.azimuthal_incidence_angle = 0.0
        params.angular_resolution = model['ares']
        params.experiment_temperature = model['Texp']
        params.debye_temperature = model['Tdeb']

        if self.phase_files:
            state = emitter + initial_state
            try:
                params.phase_files = self.phase_files[state]
            except KeyError:
                params.phase_files = {}
                logger.warning("no phase files found for {} - using default calculator".format(state))

        params.rme_files = {}
        params.rme_minus_value = 0.1
        params.rme_minus_shift = 0.0
        params.rme_plus_value = 1.0
        params.rme_plus_shift = 0.0

        # edac_interface only
        params.emitters = []
        params.lmax = 15
        params.dmax = model['distm']
        params.orders = [20]

        return params

    def create_model_space(self):
        """
        define the range of model parameters.

        see the class description for a list of parameters.
        """

        return self.model_space
