import weakref
import numpy

from amuse.units import nbody_system
from amuse.units import generic_unit_system
from amuse.units import quantities
from amuse.units.core import IncompatibleUnitsException
from amuse.units.quantities import is_quantity
from amuse.support import exceptions

from amuse.support.core import OrderedDictionary


class Parameters(object):
    __name__ = "Parameters"

    def __init__(self, definitions, instance):
        object.__setattr__(self, "_instance", weakref.ref(instance))
        object.__setattr__(self, "_definitions", definitions)
        object.__setattr__(
            self, "_mapping_from_name_to_definition", OrderedDictionary()
        )
        object.__setattr__(self, "_mapping_from_name_to_parameter", OrderedDictionary())

        self.update()

    def update(self):
        for x in self._definitions:
            self._mapping_from_name_to_definition[x.name] = x

        if len(self._definitions) != len(self._mapping_from_name_to_definition):
            raise Exception("Duplicate parameters detected")

    def __getattr__(self, name):
        # if name.startswith('__'):
        #    return object.__getattribute__(self, name)
        if not name in self._mapping_from_name_to_definition:
            raise exceptions.CoreException(
                f"tried to get unknown parameter '{name}' "
                f"for a '{type(self._instance()).__name__}' object"
            )
        self._instance().before_get_parameter()

        return self.get_parameter(name).get_value()

    def __setattr__(self, name, value):
        if not name in self._mapping_from_name_to_definition:
            # ~ print "Did you mean to set one of these parameters?\n", \
            # ~ "\n  ".join(self._mapping_from_name_to_definition.keys())
            raise exceptions.CoreException(
                f"tried to set unknown parameter '{name}' for a "
                f"'{type(self._instance()).__name__}' object"
            )

        self._instance().before_set_parameter()

        return self.get_parameter(name).set_value(value)

    def __getitem__(self, name):
        return self.get_parameter(name)

    def names(self):
        return list(self._mapping_from_name_to_definition.keys())

    def set_defaults(self):
        self._instance().before_set_parameter()

        for name in self.names():
            parameter = self.get_parameter(name)
            parameter.set_default_value()

    def __dir__(self):
        result = []
        result.extend(dir(type(self)))
        result.extend(self.names())
        return result

    def get_default_value_for(self, name):
        if not name in self._mapping_from_name_to_definition:
            raise exceptions.CoreException(
                f"tried to get default value of unknown parameter '{name}'"
                f"for a '{type(self._instance()).__name__}' object"
            )

        definition = self._mapping_from_name_to_definition[name]
        return definition.get_default_value(self)

    def __str__(self):
        output = ""

        for name in sorted(self.names()):
            output += name + ": "
            output += str(getattr(self, name))
            if self.get_parameter(name).is_readonly():
                output += "  (read only)"
            output += "\n"

        return output

    def get_parameter(self, name):
        if not name in self._mapping_from_name_to_definition:
            raise exceptions.AmuseException(
                "{0!r} not defined as parameter".format(name)
            )

        if not name in self._mapping_from_name_to_parameter:
            definition = self._mapping_from_name_to_definition[name]
            self._mapping_from_name_to_parameter[name] = Parameter(definition, self)

        return self._mapping_from_name_to_parameter[name]

    def iter_parameters(self):
        for name in self.names():
            yield self.get_parameter(name)

    __iter__ = iter_parameters

    def send_cached_parameters_to_code(self):
        cached_parameters = [
            x for x in self.iter_parameters() if x.definition.is_cached()
        ]
        for x in cached_parameters:
            if not x.is_set:
                x.set_default_value()

        functions = OrderedDictionary()
        for x in cached_parameters:
            definition = x.definition
            if not definition.functionname in functions:
                functions[definition.functionname] = []
            functions[definition.functionname].append(x)

        for functionname, parameters in functions.items():
            object = self._instance()
            method = getattr(object, functionname)
            keyword_arguments = {}
            for parameter in parameters:
                keyword_arguments[
                    parameter.definition.parameter_name
                ] = parameter.get_cached_value()
            errorcode = method(**keyword_arguments)

    def send_not_set_parameters_to_code(self):
        parameters = [x for x in self.iter_parameters() if x.must_set_to_default()]
        for x in parameters:
            x.set_default_value()

    def check_defaults(self):
        for x in self.iter_parameters():
            default_value = self.get_default_value_for(x.definition.name)
            try:
                value = x.get_value()
            except:
                print("could not get value for:", x.definition.name, default_value)
                continue
            print(x.definition.name, value, default_value)
            if not value == default_value:
                print(
                    "!default value is not equal to value in code: {0}".format(
                        x.definition.name
                    )
                )

    def copy(self):
        mapping_from_name_to_value = {}
        for name in self.names():
            mapping_from_name_to_value[name] = getattr(self, name)

        return ParametersMemento(mapping_from_name_to_value)

    def reset_from_memento(self, memento):
        for name in memento.names():
            if not name in self._mapping_from_name_to_definition:
                raise exceptions.CoreException(
                    f"tried to set unknown parameter '{name}' for a "
                    f"'{type(self._instance()).__name__}' object"
                )

            if self.get_parameter(name).is_readonly():
                if not getattr(memento, name) == getattr(self, name):
                    raise exceptions.CoreException(
                        f"tried to change read-only parameter '{name}' for a "
                        f"'{type(self._instance()).__name__}' object"
                    )
            else:
                setattr(self, name, getattr(memento, name))

    def has_writable_parameter(self, name):
        if not name in self._mapping_from_name_to_definition:
            return False
        return not self.get_parameter(name).is_readonly()


class ParametersMemento(object):
    __name__ = "Parameters"

    def __init__(self, mapping_from_name_to_value=None):
        if mapping_from_name_to_value is None:
            mapping_from_name_to_value = {}

        object.__setattr__(
            self, "_mapping_from_name_to_value", mapping_from_name_to_value
        )

    def __getstate__(self):
        return self.__dict__

    def __setstate__(self, state):
        object.__setattr__(self, "__dict__", state)

    def __getattr__(self, name):
        if not name in self._mapping_from_name_to_value:
            raise exceptions.CoreException(f"tried to get unknown parameter '{name}'")

        return self._mapping_from_name_to_value[name]

    def __setattr__(self, name, value):
        if not name in self._mapping_from_name_to_value:
            raise exceptions.CoreException(f"tried to set unknown parameter '{name}'")

        self._mapping_from_name_to_value[name] = value

    def names(self):
        return list(self._mapping_from_name_to_value.keys())

    def set_defaults(self):
        pass

    def __dir__(self):
        result = []
        result.extend(dir(type(self)))
        result.extend(self.names())
        return result

    def get_default_value_for(self, name):
        if not name in self._mapping_from_name_to_value:
            raise exceptions.CoreException(
                f"tried to get default value of unknown parameter '{name}'"
            )

        raise exceptions.CoreException(
            "tried to get default value, for a parameter in a parameters memento"
        )

    def __str__(self):
        output = ""

        for name in sorted(self.names()):
            output += name + ": "
            output += str(getattr(self, name)) + "\n"

        return output


def new_parameters_instance_with_docs(definitions, instance):
    class _ParametersMetaclass(type):
        def _get_doc(self):
            output = "Parameters: \n"
            for parameter_definition in definitions:
                output += parameter_definition.name + "\n\n"
                output += "    " + parameter_definition.description
                output += (
                    " (default value:"
                    + str(parameter_definition.default_value)
                    + ")\n\n"
                )
            return output

        __doc__ = property(_get_doc)

    class ParametersWithDocs(Parameters, metaclass=_ParametersMetaclass):
        def _get_doc(self):
            output = "Parameters: \n"
            for parameter_definition in definitions:
                output += parameter_definition.name + "\n\n"
                output += "    " + parameter_definition.description
                output += (
                    " (default value:"
                    + str(self.get_default_value_for(parameter_definition.name))
                    + ")\n\n"
                )
            return output

        __doc__ = property(_get_doc)

    return ParametersWithDocs(definitions, instance)


def new_parameters_with_units_converted_instance_with_docs(original, converter):
    class _ParametersMetaclass(type):
        def _convert_from_target_to_source_if_needed(value):
            if isinstance(value, bool) or isinstance(
                value, quantities.NonNumericQuantity
            ):
                return value
            else:
                return converter.from_target_to_source(value)

        def _get_doc(self):
            output = "Parameters: \n"
            for parameter_definition in original._definitions:
                value = parameter_definition.default_value
                if not isinstance(value, bool) and not isinstance(
                    value, quantities.NonNumericQuantity
                ):
                    value = converter.from_target_to_source(value)
                output += parameter_definition.name + "\n\n"
                output += "    " + parameter_definition.description
                output += " (default value:" + str(value) + ")\n\n"
            return output

        __doc__ = property(_get_doc)

    class ParametersWithDocs(
        ParametersWithUnitsConverted, metaclass=_ParametersMetaclass
    ):
        def _get_doc(self):
            output = "Parameters: \n"
            for parameter_definition in original._definitions:
                output += parameter_definition.name + "\n\n"
                output += "    " + parameter_definition.description
                output += (
                    " (default value:"
                    + str(self.get_default_value_for(parameter_definition.name))
                    + ")\n\n"
                )
            return output

        __doc__ = property(_get_doc)

    return ParametersWithDocs(original, converter)


class ParametersWithUnitsConverted(object):
    def __init__(self, original, converter):
        object.__setattr__(self, "_original", original)
        object.__setattr__(self, "_converter", converter)

    def __getattr__(self, name):
        return self.convert_from_target_to_source_if_needed(
            getattr(self._original, name)
        )

    def __setattr__(self, name, value):
        if not name in self._original._mapping_from_name_to_definition:
            raise exceptions.CoreException(
                f"Could not set unknown parameter '{name}' for a "
                f"'{type(self._original()).__name__}' object"
            )

        try:
            setattr(self._original, name, self._converter.from_source_to_target(value))
        except IncompatibleUnitsException as ex:
            setattr(self._original, name, value)

    def names(self):
        return self._original.names()

    def set_defaults(self):
        self._original.set_defaults()

    def __dir__(self):
        return dir(self._original)

    def convert_from_target_to_source_if_needed(self, value):
        if isinstance(value, bool) or isinstance(value, quantities.NonNumericQuantity):
            return value
        else:
            return self._converter.from_target_to_source(value)

    def get_default_value_for(self, name):
        return self.convert_from_target_to_source_if_needed(
            self._original.get_default_value_for(name)
        )

    def __str__(self):
        output = ""

        for name in sorted(self.names()):
            output += name + ": "
            output += str(getattr(self, name))
            output += " default: " + str(self.get_default_value_for(name))
            output += "\n"

        return output

    def check_defaults(self):
        for x in self.iter_parameters():
            default_value = self.get_default_value_for(x.definition.name)
            try:
                value = x.get_value()
            except:
                print("could not get value for:", x.definition.name, default_value)
                continue
            print(x.definition.name, value, default_value)
            if not value == default_value:
                print(
                    "default value is not equal to value in code: {0}".format(
                        x.definition.name
                    )
                )


class AbstractParameterDefinition(object):
    def __init__(self, name, description):
        self.name = name
        self.description = description

    def get_default_value(self, parameterset):
        return self.default_value

    def get_value(self, parameter, object):
        raise exceptions.AmuseException("not implemented")

    def set_value(self, parameter, object, quantity):
        raise exceptions.AmuseException("not implemented")

    def set_default_value(self, parameter, object):
        pass

    def is_readonly(self):
        return False

    def is_cached(self):
        return False

    def must_set_to_default_if_not_set(self):
        return True


class AliasParameterDefinition(AbstractParameterDefinition):
    def __init__(self, name, aliased_name, description, alias_set=None):
        AbstractParameterDefinition.__init__(self, name, description)
        self.aliased_name = aliased_name
        self.alias_set = alias_set
        self.default_value = None

    def get_default_value(self, parameter_set):
        return parameter_set.get_parameter(
            self.aliased_name
        ).definition.get_default_value(parameter_set)

    def get_value(self, parameter, object):
        if self.alias_set:
            parameter_set = getattr(object, self.alias_set)
        else:
            parameter_set = parameter.parameter_set
        return getattr(parameter_set, self.aliased_name)

    def set_value(self, parameter, object, quantity):
        if self.alias_set:
            parameter_set = getattr(object, self.alias_set)
        else:
            parameter_set = parameter.parameter_set
        return setattr(parameter_set, self.aliased_name, quantity)

    def set_default_value(self, parameter, object):
        pass

    def is_readonly(self):
        return False

    def is_cached(self):
        return False

    def must_set_to_default_if_not_set(self):
        return False


class ParameterDefinition(AbstractParameterDefinition):
    def __init__(self, name, description, default_value, must_set_before_get=False):
        AbstractParameterDefinition.__init__(self, name, description)
        self.default_value = default_value
        self.must_set_before_get = must_set_before_get

    def get_value(self, parameter, object):
        raise NotImplementedError()

    def set_value(self, parameter, object, quantity):
        raise NotImplementedError()

    def set_default_value(self, parameter, object):
        if self.default_value is None:
            return None

        if self.is_readonly():
            return None

        self.set_value(parameter, object, self.default_value)

    def is_readonly(self):
        return False

    def is_cached(self):
        return False


class InterfaceParameterDefinition(ParameterDefinition):
    def __init__(self, name, description, default_value, state_guard=None):
        AbstractParameterDefinition.__init__(self, name, description)
        self.default_value = default_value
        self.must_set_before_get = False
        self.value = default_value
        self.state_guard = state_guard

    def get_value(self, parameter, object):
        try:
            x = self.value.copy()
        except:
            x = self.value
        return x

    def set_value(self, parameter, object, quantity):
        try:
            self.value = quantity.copy()
        except:
            self.value = quantity
        if self.state_guard:
            getattr(object, self.state_guard)()

    def must_set_to_default_if_not_set(self):
        return False


class ParameterException(AttributeError):
    template = (
        "Could not {0} value for parameter '{1}' of a '{2}' object, got errorcode <{3}>"
    )

    def __init__(self, object, parameter_name, errorcode, is_get):
        AttributeError.__init__(
            self,
            self.template.format(
                "get" if is_get else "set",
                parameter_name,
                type(object).__name__,
                errorcode,
            ),
        )
        self.errorcode = errorcode
        self.parameter_name = parameter_name


class ModuleMethodParameterDefinition(ParameterDefinition):
    def __init__(
        self,
        get_method,
        set_method,
        name,
        description,
        default_value=None,
        must_set_before_get=False,
    ):
        ParameterDefinition.__init__(
            self, name, description, default_value, must_set_before_get
        )
        self.get_method = get_method
        self.set_method = set_method
        self.stored_value = None

    def get_value(self, parameter, object):
        if self.must_set_before_get and not parameter.is_set:
            self.set_default_value(parameter, object)

        if self.get_method is None:
            return self.stored_value
        else:
            return getattr(object, self.get_method)()

    def set_value(self, parameter, object, quantity):
        # if self.unit.is_non_numeric() or len(self.unit.base) == 0:
        #    if not isinstance(quantity, quantities.Quantity):
        #        quantity = quantity | self.unit

        if self.set_method is None:
            raise exceptions.CoreException(
                f"Could not set value for parameter '{self.name}' of a "
                f"'{type(object).__name__}' object, parameter is read-only"
            )
        getattr(object, self.set_method)(quantity)

        if self.get_method is None:
            self.stored_value = quantity

        parameter.is_set = True

    def is_readonly(self):
        return self.set_method is None


class ModuleBooleanParameterDefinition(ModuleMethodParameterDefinition):
    def __init__(self, *args, **kwargs):
        ModuleMethodParameterDefinition.__init__(self, *args, **kwargs)

    def get_value(self, parameter, object):
        return (
            True
            if ModuleMethodParameterDefinition.get_value(self, parameter, object)
            else False
        )

    def set_value(self, parameter, object, bool):
        return ModuleMethodParameterDefinition.set_value(
            self, parameter, object, 1 if bool else 0
        )


class ModuleCachingParameterDefinition(ParameterDefinition):
    def __init__(
        self, functionname, parameter_name, name, description, default_value=None
    ):
        ParameterDefinition.__init__(
            self, name, description, default_value, must_set_before_get=True
        )
        self.stored_value = None
        self.parameter_name = parameter_name
        self.functionname = functionname

    def get_value(self, parameter, object):
        if self.must_set_before_get and not parameter.is_set:
            self.set_default_value(parameter, object)

        return parameter.cached_value

    def set_value(self, parameter, object, quantity):
        if is_quantity(self.default_value):
            unit = self.default_value.unit
            if unit.is_non_numeric() or len(unit.base) == 0:
                if not is_quantity(quantity):
                    quantity = quantity | unit

        parameter.cached_value = quantity

        parameter.is_set = True

    def is_cached(self):
        return True


class Parameter(object):
    def __init__(self, definition, parameter_set):
        self.parameter_set = parameter_set
        self.definition = definition
        self.is_set = False

    def get_value(self):
        return self.definition.get_value(self, self.parameter_set._instance())

    def set_value(self, quantity):
        return self.definition.set_value(self, self.parameter_set._instance(), quantity)

    def set_default_value(self):
        self.definition.set_default_value(self, self.parameter_set._instance())

    def is_readonly(self):
        return self.definition.is_readonly()

    def must_set_to_default(self):
        if self.definition.is_cached():
            return False

        return (not self.is_set) and self.definition.must_set_to_default_if_not_set()

    def get_cached_value(self):
        return self.definition.get_value(self, self.parameter_set._instance())

    @property
    def name(self):
        return self.definition.name

    @property
    def description(self):
        return self.definition.description

    @property
    def value(self):
        return self.get_value()

    def __str__(self):
        return self.name


class ModuleVectorMethodParameterDefinition(ModuleMethodParameterDefinition):
    def get_value(self, parameter, object):
        if self.must_set_before_get and not parameter.is_set:
            self.set_default_value(parameter, object)

        if self.get_method is None:
            return self.stored_value
        else:
            list_of_scalars = getattr(object, self.get_method)()
            result = quantities.AdaptingVectorQuantity()
            result.extend(list_of_scalars)
            return result.copy()

    def set_value(self, parameter, object, vector_quantity):
        if self.set_method is None:
            raise exceptions.CoreException(
                f"Could not set value for parameter '{self.name}' of a "
                f"'{type(object).__name__}' object, parameter is read-only"
            )

        getattr(object, self.set_method)(*vector_quantity)

        if self.get_method is None:
            self.stored_value = vector_quantity

        parameter.is_set = True

    def is_readonly(self):
        return self.set_method is None


class VectorParameterDefinition(AbstractParameterDefinition):
    def __init__(self, name, description, names_of_parameters, default_value):
        AbstractParameterDefinition.__init__(self, name, description)
        self.names_of_parameters = names_of_parameters
        self.default_value = default_value

    def get_value(self, parameter, object):
        all_parameters = parameter.parameter_set
        result = []
        unit = None
        for name in self.names_of_parameters:
            parameter = all_parameters.get_parameter(name)
            element = parameter.get_value()
            if unit is None:
                if is_quantity(element):
                    unit = element.unit

            if not unit is None:
                result.append(element.value_in(unit))
            else:
                result.append(element)

        if not unit is None:
            return unit.new_quantity(result)
        else:
            return numpy.asarray(result)

    def set_value(self, parameter, object, quantity):
        all_parameters = parameter.parameter_set
        for index, name in enumerate(self.names_of_parameters):
            parameter = all_parameters.get_parameter(name)
            parameter.set_value(quantity[index])

    def must_set_to_default_if_not_set(self):
        return False

    def get_unit(self, parameter):
        result = None
        all_parameters = parameter.parameter_set
        for index, name in enumerate(self.names_of_parameters):
            parameter = all_parameters.get_parameter(name)
            if hasattr(parameter.definition, "unit"):
                result = parameter.definition.unit
        return result


# to do: higher dimensional array parameters
class ModuleArrayParameterDefinition(ParameterDefinition):
    def __init__(self, get_method, set_method, range_method, name, description):
        ParameterDefinition.__init__(self, name, description, None, False)
        self.get_method = get_method
        self.set_method = set_method
        self.range_method = range_method
        self.stored_value = None

    def get_value(self, parameter, object):
        if self.get_method is None:
            return self.stored_value
        else:
            irange = getattr(object, self.range_method)()
            index = numpy.arange(irange[0], irange[1] + 1)
            return getattr(object, self.get_method)(index)

    def set_value(self, parameter, object, quantity):
        if self.set_method is None:
            raise exceptions.CoreException(
                "Could not set value for parameter '{self.name}' of a "
                "'{type(object).__name__}' object, parameter is read-only"
            )

        irange = getattr(object, self.range_method)()
        index = numpy.arange(irange[0], irange[1] + 1)
        getattr(object, self.set_method)(index, quantity)

        if self.get_method is None:
            self.stored_value = quantity

        parameter.is_set = True

    def is_readonly(self):
        return self.set_method is None
