"""
@package projects.common.clusters.crystals
cluster generators for some common bulk crystals

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

@copyright (c) 2015-19 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
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import math
import numpy as np
import os.path
import periodictable as pt
import logging

import pmsco.cluster as cluster
import pmsco.dispatch as dispatch
import pmsco.project as project
from pmsco.helpers import BraceMessage as BMsg

logger = logging.getLogger(__name__)


class ZincblendeCluster(cluster.ClusterGenerator):
    def __init__(self, proj):
        super(ZincblendeCluster, self).__init__(proj)
        self.atomtype1 = 30
        self.atomtype2 = 16
        self.bulk_lattice = 1.0
        self.surface = (1, 1, 1)

    @classmethod
    def check(cls, outfilename=None, model_dict=None, domain_dict=None):
        """
        function to test and debug the cluster generator.

        to use this function, you don't need to import or initialize anything but the class.
        though the project class is used internally, the result does not depend on any project settings.

        @param outfilename: name of output file for the cluster (XYZ format).
            the file is written to the same directory where this module is located.
            if empty or None, no file is written.

        @param model_dict: dictionary of model parameters to override the default values.

        @param domain_dict: dictionary of domain parameters to override the default values.

        @return: @ref pmsco.cluster.Cluster object
        """
        proj = project.Project()
        dom = project.ModelSpace()
        dom.add_param('dlat', 10.)
        dom.add_param('rmax', 5.0)
        if model_dict:
            dom.start.update(model_dict)

        try:
            proj.domains[0].update({'zrot': 0.})
        except IndexError:
            proj.add_domain({'zrot': 0.})
        if domain_dict:
            proj.domains[0].update(domain_dict)
        proj.add_scan("", 'C', '1s')

        clu_gen = cls(proj)
        index = dispatch.CalcID(0, 0, 0, -1, -1)
        clu = clu_gen.create_cluster(dom.start, index)

        if outfilename:
            project_dir = os.path.dirname(os.path.abspath(__file__))
            outfilepath = os.path.join(project_dir, outfilename)
            clu.save_to_file(outfilepath, fmt=cluster.FMT_XYZ, comment="{0} {1} {2}".format(cls, index, str(dom.start)))

        return clu

    def count_emitters(self, model, index):
        return 1

    def create_cluster(self, model, index):
        """
        calculate a specific set of atom positions given the optimizable parameters.

        @param model  (dict)          optimizable parameters
            @arg    model['dlat']     bulk lattice constant in Angstrom
            @arg    model['rmax']     cluster radius
            @arg    model['phi']      azimuthal rotation angle in degrees

        @param dom (dict)             domain
            @arg    dom['term']       surface termination
        """
        clu = cluster.Cluster()
        clu.comment = "{0} {1}".format(self.__class__, index)
        clu.set_rmax(model['rmax'])
        a_lat = model['dlat']
        dom = self.project.domains[index]
        try:
            term = int(dom['term'])
        except ValueError:
            term = pt.elements.symbol(dom['term'].strip().number)

        if self.surface == (0, 0, 1):
            # identity matrix
            m = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
        elif self.surface == (1, 1, 1):
            # this will map the [111] direction onto the z-axis
            m1 = np.array([1, -1, 0]) * math.sqrt(1/2)
            m2 = np.array([0.5, 0.5, -1]) * math.sqrt(2/3)
            m3 = np.array([1, 1, 1]) * math.sqrt(1/3)
            m = np.array([m1, m2, m3])
        else:
            raise ValueError("unsupported surface specification")

        # lattice vectors
        a1 = np.matmul(m, np.array((1.0, 0.0, 0.0)) * a_lat)
        a2 = np.matmul(m, np.array((0.0, 1.0, 0.0)) * a_lat)
        a3 = np.matmul(m, np.array((0.0, 0.0, 1.0)) * a_lat)

        # basis
        b1 = [np.array((0.0, 0.0, 0.0)), (a2 + a3) / 2, (a3 + a1) / 2, (a1 + a2) / 2]
        if term == self.atomtype1:
            d1 = np.array((0, 0, 0))
            d2 = (a1 + a2 + a3) / 4
        else:
            d1 = -(a1 + a2 + a3) / 4
            d2 = np.array((0, 0, 0))
        for b in b1:
            clu.add_bulk(self.atomtype1, b + d1, a1, a2, a3)
            clu.add_bulk(self.atomtype2, b + d2, a1, a2, a3)

        return clu
