"""Parameter scaler classes"""

"""
Copyright (c) 2016-2020, EPFL/Blue Brain Project

 This file is part of BluePyOpt <https://github.com/BlueBrain/BluePyOpt>

 This library is free software; you can redistribute it and/or modify it under
 the terms of the GNU Lesser General Public License version 3.0 as published
 by the Free Software Foundation.

 This library 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 this library; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""

# pylint: disable=W0511

import string

from bluepyopt.ephys.base import BaseEPhys
from bluepyopt.ephys.parameterscalers.acc_iexpr import generate_acc_scale_iexpr
from bluepyopt.ephys.serializer import DictMixin
from bluepyopt.ephys.morphologies import ArbFileMorphology

FLOAT_FORMAT = '%.17g'


def format_float(value):
    """Return formatted float string"""
    return FLOAT_FORMAT % value


class MissingFormatDict(dict):

    """Extend dict for string formatting with missing values"""

    def __missing__(self, key):  # pylint: disable=R0201
        """Return string with format key for missing keys"""
        return '{' + key + '}'


class ParameterScaler(BaseEPhys):

    """Parameter scalers"""
    pass

# TODO get rid of the 'segment' here


class NrnSegmentLinearScaler(ParameterScaler, DictMixin):

    """Linear scaler"""
    SERIALIZED_FIELDS = ('name', 'comment', 'multiplier', 'offset', )

    def __init__(
            self,
            name=None,
            multiplier=1.0,
            offset=0.0,
            comment=''):
        """Constructor

        Args:
            name (str): name of this object
            multiplier (float): slope of the linear scaler
            offset (float): intercept of the linear scaler
        """

        super(NrnSegmentLinearScaler, self).__init__(name, comment)
        self.multiplier = multiplier
        self.offset = offset

    def scale(self, values, segment=None, sim=None):  # pylint: disable=W0613
        """Scale a value based on a segment"""
        if isinstance(values, dict):
            value = values["value"]
        else:
            value = values
        return self.multiplier * value + self.offset

    def __str__(self):
        """String representation"""

        return '%s * value + %s' % (self.multiplier, self.offset)


class NrnSegmentSectionDistanceScaler(ParameterScaler, DictMixin):

    """Scaler based on distance from soma"""
    SERIALIZED_FIELDS = ('name', 'comment', 'distribution',
                         "distribution", "dist_param_names",
                         "ref_sec", "ref_location",)

    def __init__(
            self,
            name=None,
            distribution=None,
            comment='',
            dist_param_names=None,
            ref_section='soma[0]',
            ref_location=0,):
        """Constructor

        Args:
            name (str): name of this object
            distribution (str): distribution of parameter dependent on distance
                from soma. string can contain `distance` and/or `value` as
                placeholders for the distance to the soma and parameter value
                respectivily
            dist_param_names (list): list of names of parameters that
                parametrise the distribution. These names will become
                attributes of this object.
                The distribution string should contain these names, and they
                will be replaced by values of the corresponding attributes
            ref_section (str): string with name of reference section to
                compute distance (e.g. "soma[0]", "dend[2]")
            ref_location (float): location along the soma used as origin
                from which to compute the distances. Expressed as a fraction
                (between 0.0 and 1.0).
        """

        super(NrnSegmentSectionDistanceScaler, self).__init__(name, comment)
        self.distribution = distribution

        self.dist_param_names = dist_param_names
        self.ref_location = ref_location
        self.ref_section = ref_section

        if not (0.0 <= self.ref_location <= 1.0):
            raise ValueError("ref_location must be between 0 and 1.")

        if self.dist_param_names is not None:
            for dist_param_name in self.dist_param_names:
                if dist_param_name not in self.distribution:
                    raise ValueError(
                        'NrnSegmentSectionDistanceScaler: "{%s}" '
                        'missing from distribution string "%s"' %
                        (dist_param_name, distribution))
                setattr(self, dist_param_name, None)

    @property
    def inst_distribution(self):
        """The instantiated distribution"""

        dist_dict = MissingFormatDict()

        if self.dist_param_names is not None:
            for dist_param_name in self.dist_param_names:
                dist_param_value = getattr(self, dist_param_name)
                if dist_param_value is None:
                    raise ValueError("NrnSegmentSomaDistanceScaler: %s "
                                     "was uninitialised" % dist_param_name)
                dist_dict[dist_param_name] = dist_param_value

        # Use this special formatting to bypass missing keys
        return string.Formatter().vformat(self.distribution, (), dist_dict)

    def eval_dist(self, values, distance):
        """Create the final dist string"""

        scale_dict = {}
        if isinstance(values, dict):
            for k, v in values.items():
                scale_dict[k] = format_float(v)
        else:
            scale_dict["value"] = format_float(values)
        scale_dict["distance"] = format_float(distance)

        return self.inst_distribution.format(**scale_dict)

    def scale(self, values, segment, sim=None):
        """Scale a value based on a segment"""

        # find section
        target_sec = None
        for sec in segment.sec.wholetree():
            if "." in sec.name():  # deal with templates
                sec_name = sec.name().split(".")[1]
            else:
                sec_name = sec.name()
            if self.ref_section in sec_name:
                target_sec = sec
                break

        if target_sec is None:
            raise Exception(f"Could not find section {self.ref_section} "
                            f"in section list")

        # Initialise origin
        sim.neuron.h.distance(0, self.ref_location, sec=target_sec)

        distance = sim.neuron.h.distance(1, segment.x, sec=segment.sec)

        # Find something to generalise this
        import math  # pylint:disable=W0611 #NOQA

        # This eval is unsafe (but is it ever dangerous ?)
        # pylint: disable=W0123
        return eval(self.eval_dist(values, distance))

    def acc_scale_iexpr(self, value, constant_formatter=format_float):
        """Generate Arbor scale iexpr for a given value"""
        raise ValueError(
            "Parameter scaling based on general Neuron segment/section"
            " distances not supported in Arbor.")

    def __str__(self):
        """String representation"""

        return self.distribution


class NrnSegmentSomaDistanceScaler(NrnSegmentSectionDistanceScaler,
                                   ParameterScaler, DictMixin):

    """Scaler based on distance from soma"""
    SERIALIZED_FIELDS = ('name', 'comment', 'distribution', )

    def __init__(
            self,
            name=None,
            distribution=None,
            comment='',
            dist_param_names=None,
            soma_ref_location=0.5):
        """Constructor
        Args:
            name (str): name of this object
            distribution (str): distribution of parameter dependent on distance
                from soma. string can contain `distance` and/or `value` as
                placeholders for the distance to the soma and parameter value
                respectivily
            dist_param_names (list): list of names of parameters that
                parametrise the distribution. These names will become
                attributes of this object.
                The distribution string should contain these names, and they
                will be replaced by values of the corresponding attributes
            soma_ref_location (float): location along the soma used as origin
                from which to compute the distances. Expressed as a fraction
                (between 0.0 and 1.0).
        """

        super(NrnSegmentSomaDistanceScaler, self).__init__(
            name, distribution, comment, dist_param_names,
            ref_section='soma[0]', ref_location=soma_ref_location)

    def acc_scale_iexpr(self, value, constant_formatter=format_float):
        """Generate Arbor scale iexpr for a given value"""

        iexpr = self.inst_distribution

        variables = dict(
            value=value,
            distance='(distance %s)' %  # could be a ctor param if required
            ArbFileMorphology.region_labels['somatic'].ref
        )
        return generate_acc_scale_iexpr(iexpr, variables, constant_formatter)
