"""
Module for handling parameters in the context of SKF optimisation for DFTB via SKPAR.
The following assumptions are made:

    * whatever optimiser is used, it is unaware of the actual meaning of 
      the parameters
    * from the perspective of the optimiser, parameters are just a list of values.
    * the user must create an OrderedDict of parameter name:value pairs; 
      the OrderedDict eliminates automatically duplicate definitions of parameters,
      yielding the minimum possible number of degrees of freedom for the optimiser
    * the OrderedDictionary is built by parsing an skdefs.template file that
      contains parameter-strings (conveying the name, initial value and range).
    * skdefs.template is parsed by functions looking for a given format from which
      parameter-strings are extracted
    * the Class Parameter is initialised from a parameter-string.
    * a reduced skdefs.template is obtained, by removing the initial value and range
      from the input skdefs.template which is ready for creating the final skdefs
      file by string.format(dict(zip(ParNames,Parvalues)) substitution.
"""
import logging
import os.path
from skpar.core.utils import get_logger

DEFAULT_PARAMETER_FILE='current.par'

module_logger = get_logger('skpar.parameters')

def get_parameters(spec):
    """
    """
    params=[]
    for pardef in spec:
        # module_logger.debug(pardef)
        try:
            (pname, pdef), = pardef.items()
        except AttributeError:
            # pardef is cannot be itemised => assume just pname
            pname = pardef
            pdef  = ""
        try:
            parstring = " ".join([pname,] + pdef.split())
        except AttributeError:
            # pdef turns out to not be a string
            try:
                # assume it is a list of floats
                parstring = " ".join([pname,] + ["{}".format(v) for v in pdef])
            except TypeError:
                # pdef not iterable; assume it is a single float
                parstring = " ".join([pname, str(pdef)])
        # module_logger.debug(parstring)
        newpar = Parameter(parstring)
        # module_logger.debug(newpar)
        params.append(newpar)
    return params

class Parameter(object):
    """
    A parameter object, that is initialised from a string.

    ParmaeterName InitialValue MinValue Maxvalue [ParameterType]
    ParmaeterName MinValue Maxvalue [ParameterType]
    ParmaeterName InitialValue [ParameterType]
    ParmaeterName [ParameterType]

    ParameterName must be alphanumeric, allowing _ too.
    Iinit/Min/MaxValue can be integer or float
    ParameterType is optional (float by default), and indicated 
    by either 'i'(int) or 'f'(float)
    White space separation is mandatory.
    """
    # permitted parameter types
    typedict = {'i': int, 'f': float}

    def __init_from_kwargs(self, name, **kwargs):
        """
        Look for one obligatory argument -- name, and 
        the rest is optional.
        """
        self.name = name
        for key in ['value', 'minv', 'maxv']:
            _val = kwargs.get(key, None)
            setattr(self, key, _val)

    def __init_from_string(self, parameterstring):
        """
        assume S is a string of one of the following forms:
        ParName initial min max partype
        ParName min max partype
        ParName initial partype
        ParName partype
        Permit only 'i' or 'f' for partype and make it optional
        """
        # take away spaces, check format consistency and get type
        assert isinstance(parameterstring, str)
        words = parameterstring.split()
        #module_logger.debug(len(words))
        if len(words) > 1:
            if words[-1] in list(Parameter.typedict.keys()):
                self.ptype = Parameter.typedict[words[-1]]
                words = words[:-1]
            else:
                self.ptype = float
            # extract data, converting values to appropriate type
            self.name = words[0]
            # the conversion from words to float to type allows one to do
            # %(name,1.0,2.0,3.0)i; otherwise map(int,'3.0') gives valueerror
            floats = list(map(float, words[1:]))
            try:
                self.value, self.minv, self.maxv = list(map(self.ptype, floats))
            except ValueError:
                try:
                    self.minv, self.maxv = list(map(self.ptype, floats))
                    self.value = 0.
                except ValueError:
                    # note at this stage value is a single item, not a list
                    self.value = list(map(self.ptype, floats))[0]
                    self.minv = None
                    self.maxv = None
                except:
                    print ("Parameter string not understood: {}".format(parstring))
            except:
                print ("Parameter string not understood: {}".format(parstring))
        else:
            self.ptype = float
            self.name  = words[0]
            self.value = None
            self.minv  = None
            self.maxv  = None

    def __init__(self, string, **kwargs):
        """wrapper init method

        The whole thing became patchy. It needs a careful revision.
        """
        if string.split() == [string,] and kwargs:
            self.__init_from_kwargs(string, **kwargs)
        else:
            self.__init_from_string(string)

    def __repr__(self):
        return "Parameter {name} = {val}, range=[{minv},{maxv}]". \
            format(name=self.name, val=self.value, minv=self.minv, maxv=self.maxv)


def substitute_template(parameters, parnames, templatefile, resultfile):
    """Substitute a template with actual parameter values.

    Args:
        parameters (list): Parameter list, items being either floats or objects
            with .value and .name attributes.
        parnames (list): If `parameters` is a list of floats, then 
            parnames is the list of corresponding names.
        templatefile (str): Name of template file with substitution patterns.
        resultfile (str): Name of file to contain the substituted result.
    """
    with open(templatefile, 'r') as fd:
        template = fd.read()
    try:
        pardict = dict([(p.name, p.value) for p in parameters])
    except AttributeError:
        pardict = dict([(name, val)
                        for (name, val) in zip(parnames, parameters)])
    updated = update_template(template, pardict)
    with open(resultfile, 'w') as fd:
        fd.write(updated)


def update_template(template, pardict):
    """Makes variable substitution in a template.

    Args:
        template (str): Template with old style Python format strings.
        pardict (dict): Dictionary of parameters to substitute.
    
    Returns:
        str: String with substituted content.
    """
    return template % pardict
    

def update_parameters(workroot, templates, parameters, parnames):
    """Update relevant templates with new parameter values.

    Args:
        workroot (str): Root working directory, template names are relative
            to this directory.
        templates (list): List of ascii filenames containing placeholders
            for inserting parameter values. The place holders must follow
            the old string formatting of Python: $(ParameterName)ParameterType.
        parameters (list): Either a list of floats, or a list of objects
            (each having .value and .name attributes)
        parnames (list): If `parameters` is a list of floats, then 
            parnames is the list of corresponding names.
    """
    logger = module_logger

    # Overwrite parnames with the names of the parameter objects, if available
    try:
        parnames = [p.name  for p in parameters]
    except AttributeError:
        # parameters is a list of floats
        assert all([isinstance(p, (float, int)) for p in parameters]), "{}".format(parameters)
        # test consistency if parnames is given (may be None)
        if parnames:
            assert len(parameters) == len(parnames)
    except TypeError:
        assert parameters is None
        parnames = None

    # Prepare the values to write
    try:
        parvalues = [p.value for p in parameters]
    except AttributeError:
        parvalues = parameters
    except TypeError:
        assert parameters is None
        parvalues = None

    # Udpate (fill in) template files
    if templates:
        for ftempl in templates:
            assert parnames is not None
            fin = os.path.normpath(os.path.join(workroot, ftempl))
            path, templname = os.path.split(fin)
            name = templname.replace('template.', '')
            fout = os.path.join(path, name)
            substitute_template(parvalues, parnames, fin, fout)
