#  Copyright (C) 2017 The authors of Py-ChemShell
#
#  This file is part of Py-ChemShell.
#
#  Py-ChemShell is free software: you can redistribute it and/or modify
#  it under the terms of the GNU Lesser General Public License as
#  published by the Free Software Foundation, either version 3 of the
#  License, or (at your option) any later version.
#
#  Py-ChemShell is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public
#  License along with Py-ChemShell.  If not, see
#  <http://www.gnu.org/licenses/>.

from ctypes          import Structure
from ..io            import keyword
from ..objects       import fragment, options, result
from ..dl_py2f       import utils
from ..dl_py2f.utils import miscutils, objutils


# must be <ctypes.Structure> to be ported to Fortran 
class _Interfaces(Structure):
    '''Base class of all interfaces such as QMMM, QM, MM, etc.'''

    _kwargs = {
                'format'      :'general', # YL FIXME 12/03/2024: what is this?
                'frag'        : fragment.Fragment(),
                'kpoints'     : tuple(),
                'options'     : options.Options(),
                'print_level' :'medium',
                'punch'       :'',
                'restart'     : False,
                'sci_notation': True,     # YL 12/03/2024: print float numbers in sicentific notation when generating input scripts (see: self._notation)
                'symmetry'    :'',
                'title'       :'',
              }

    _internals = {
                   '_cell_redundant': True,  # cell definition allows symmetry-redundant centres
                   '_energy'        : True,
                   '_exec_run_shell': False, # run executable with shell=True|False
                   '_gradients'     : False,
                   '_qmmm_embedding': '',
                   '_qmmm_scheme'   : '',
                   '_sys_call_popen': False, # use the popen way (see subprocess.Popen) for a system call to a path
                   '_sys_call_shell': False, # use the literal command for a system call (i.e., subprocess.run(shell=True) or Popen(shell=True))
                   '_potential'     : False,
                   '_recalc_kpoints': True,
                   '_strains'       : False,
                 }

    _init    = '_kwargs', '_internals'
    _inherit = '_init', '_priorities'

    _priorities = ()


    # avoid using __new__()!
    @objutils.init()
    def __init__(self, **kwargs):
        '''Initialise'''

        import chemsh
        from ctypes  import byref, RTLD_LOCAL, DEFAULT_MODE

        setattr(self, 'lib', utils.modutils.getLinkedLib(self.__class__.__name__, chemsh.CHEMSH_LIB_DIR, mode=RTLD_LOCAL))

        # initialise defaults
#        utils.objutils.initAttrs(self)

        # initialise keyword argument
        keyword.parseKeys(self, **kwargs)


    @property
    def frag(self):
        '''Fragment'''

        return self._frag
 

    @frag.setter
#    @miscutils.memusage
    def frag(self, val):
        '''Setter of fragment'''

        self._frag = val
 

    # to make it hashable so to be used as a dict key
    def __hash__(self):
        '''Be hashable'''

        return id(self)


#    def set__all__(self):
#        '''Inherit '__all__' from base class(es)'''
#
#        lbuff = list(super().__self_class__._kwargs.keys())
#        lbuff.extend(super().__thisclass__.__all__)
#        self.__all__ = list(set(lbuff))
#        self.__all__.sort()


    @property
    def _result(self):
        '''Result containing energy and gradients'''

        # associated to task.result
        try:
            return self.__result

        # for example: when a QM/MM task does not initialise task.qmmm.qm._result or task.qmmm.mm._result
        except:
            self.__result = result.Result()
            return self.__result


    @_result.setter
    def _result(self, val):
        '''Setter of result'''

        self.__result = val


#    @property
#    def energy(self):
#        '''Energy'''
#        return self.result.energy
#
#
#    @property
#    def gradients(self):
#        '''Gradients'''
#        return self.result.gradients

    @property
    def natoms(self):
        '''Number of atoms'''
        return self.frag.natoms


    @property
    def title(self):
        '''Job title'''

        if self._title:
            return self._title
        else:
            return self.__class__.__name__


    @title.setter
    def title(self, val):
        '''Setter of title'''

        self._title = val


    @property
    def __all__(self):
        ''''''

        # __self_class__ is the children class (i.e. <class Opt>)
        lbuff = list(super().__self_class__._kwargs.keys())

        # __thisclass__ is the base class (i.e. <class Task>)
        lbuff.extend(super().__thisclass__._kwargs)

        __all__ = list(set(lbuff))

        __all__.sort()

        return __all__


    # FIXME: not in use
    @property
    def _dict(self):
        '''Return an attributes dict of current instance, as <property> attributes are in class __dict__ while not in instance __dict__'''

        for key in self.__class__.__dict__.keys():
            try:
                self.__dict__.update(key=getattr(self, key))
            except:
                pass

        return self.__dict__


#    def _getType(self, keyword):
#        ''''''

    def _setattr(self, attr, val):
        '''Set value to attribute iff the current equals the default'''

        if getattr(self, attr) == self._kwargs[attr]:
            setattr(self, attr, val)

    @property
    def kpoints(self):
        '''K-points sampling'''

# YL FIXME 10/02/2021: lcm is not available until NumPy v1.15
        try:
            from numpy import lcm, count_nonzero
        # temporarily use our own lcm
        except:
            from numpy       import count_nonzero
            from ..base.math import lcm

        # use recommended values if k-points sampling not specified
        if not len(self._kpoints) and count_nonzero(self.frag.cell.consts) and self._recalc_kpoints:

            ia = int(round(self.frag.cell.a))
            ib = int(round(self.frag.cell.b))
            ic = int(round(self.frag.cell.c))
            # NumPy lcm()
            try:
                m = lcm.reduce([ia,ib,ic])
            # use our own lcm()
            except:
                m = lcm(ia,ib,ic)
            self._kpoints = (int(m/ia),int(m/ib),int(m/ic))

        return self._kpoints


    @kpoints.setter
    def kpoints(self, val):
        '''Setter of kpoints'''

        if len(val):
            self._recalc_kpoints = False

        self._kpoints = val


    # YL 23/02/2021: this will allow more options to be passed to the external codes
    @property
    def options(self):
        '''Options'''

        for key, val in self._kwargs.items():

            # skip itself and other attributes
            if key in [ 'frag', 'options' ]:
                continue

            # complement options._kwargs
            self._options._kwargs.setdefault(key, val)

            # sync the value
            setattr(self._options, key, getattr(self, key))

        return self._options


    @options.setter
    def options(self, val):
        '''Setter of options'''

        self._options = val


    @property
    def _hasFrag(self):
        '''Return False if the fragment object of the current interface instance hasn't been initialised and True otherwise'''

        if self.frag.natoms == 1 and all(self.frag.znums==0):
            return False
        else:
            return True


    @property
    def _notation(self):
        '''Return "e" if self.sci_notation is True; "f" otherwise'''

        return {True:'e',False:'f'}.get(self.sci_notation, 'e')



