"""Generic event classes which can be used in multiple contexts.

The different events differ in their timing structure and whether they
are nested or not:
"""

from __future__ import annotations

import bisect
import copy
import types
import typing

from mutwo import events
from mutwo import parameters
from mutwo.utilities import constants
from mutwo.utilities import decorators
from mutwo.utilities import tools


__all__ = ("SimpleEvent", "SequentialEvent", "SimultaneousEvent")


class SimpleEvent(events.abc.Event):
    """Event-Object which doesn't contain other Event-Objects (the node or leaf).

    :param new_duration: The duration of the ``SimpleEvent``. This can be any number.
        The unit of the duration is up to the interpretation of the user and the
        respective converter routine that will be used.

    **Example:**

    >>> from mutwo.events import basic
    >>> simple_event = basic.SimpleEvent(2)
    >>> print(simple_event)
    SimpleEvent(duration = 2)
    """

    def __init__(self, new_duration: parameters.abc.DurationType):
        self.duration = new_duration

    # ###################################################################### #
    #                           magic methods                                #
    # ###################################################################### #

    def __eq__(self, other: typing.Any) -> bool:
        """Test for checking if two objects are equal."""
        try:
            return self._is_equal(other) and other._is_equal(self)
        except AttributeError:
            return False

    def __repr__(self) -> str:
        attributes = (
            "{} = {}".format(attribute, getattr(self, attribute))
            for attribute in self._parameters_to_print
        )
        return "{}({})".format(type(self).__name__, ", ".join(attributes))

    # ###################################################################### #
    #                           properties                                   #
    # ###################################################################### #

    @property
    def _parameters_to_print(self) -> typing.Tuple[str, ...]:
        """Return tuple of attribute names which shall be printed for repr.
        """
        return self._parameters_to_compare

    @property
    def _parameters_to_compare(self) -> typing.Tuple[str, ...]:
        """Return tuple of attribute names which values define the SimpleEvent.

        The returned attribute names are used for equality check between two
        SimpleEvent objects.
        """
        return tuple(
            attribute
            for attribute in dir(self)
            # no private attributes
            if attribute[0] != "_"
            # no methods
            and not isinstance(getattr(self, attribute), types.MethodType)
        )

    @property
    def duration(self) -> parameters.abc.DurationType:
        return self._duration

    @duration.setter
    def duration(self, new_duration: parameters.abc.DurationType):
        self._duration = new_duration

    # ###################################################################### #
    #                           private methods                              #
    # ###################################################################### #

    def _is_equal(self, other: typing.Any) -> bool:
        """Helper function to inspect if two SimpleEvent objects are equal."""

        for parameter_to_compare in self._parameters_to_compare:
            try:
                # if the assigned values of the specific parameter aren't
                # equal, both objects can't be equal
                if getattr(self, parameter_to_compare) != getattr(
                    other, parameter_to_compare
                ):
                    return False

            # if the other object doesn't know the essential parameter
            # mutwo assumes that both objects can't be equal
            except AttributeError:
                return False

        # if all compared parameters are equal, return True
        return True

    # ###################################################################### #
    #                           public methods                               #
    # ###################################################################### #

    def destructive_copy(self) -> SimpleEvent:
        return copy.deepcopy(self)

    def get_parameter(self, parameter_name: str) -> typing.Any:
        """Return event attribute with the entered name.

        :parameter_name: The name of the attribute that shall be returned.
        :returns: Return the value that has been assigned to the passed
            parameter_name. If an event doesn't posses the asked parameter,
            the method will simply return None.

        **Example:**

        >>> from mutwo.events import basic
        >>> simple_event = basic.SimpleEvent(2)
        >>> simple_event.pitch = 200
        >>> simple_event.get_parameter('pitch')
        200
        >>> simple_event.get_parameter('pitches')
        None
        """
        try:
            return getattr(self, parameter_name)
        except AttributeError:
            return None

    @decorators.add_return_option
    def set_parameter(  # type: ignore
        self,
        parameter_name: str,
        object_or_function: typing.Union[
            typing.Callable[[parameters.abc.Parameter], parameters.abc.Parameter],
            typing.Any,
        ],
        set_unassigned_parameter: bool = True,
    ) -> typing.Optional[SimpleEvent]:
        """Sets event parameter to new value.

        :param parameter_name: The name of the parameter which values shall be changed.
        :param object_or_function: For setting the parameter either a new value can be
            passed directly or a function can be passed. The function gets as an
            argument the previous value that has had been assigned to the respective
            object and has to return a new value that will be assigned to the object.
        :param set_unassigned_parameter: If set to False a new parameter will only be
            assigned to an Event if the Event already has a attribute with the
            respective `parameter_name`. If the Event doesn't know the attribute yet
            and `set_unassigned_parameter` is False, the method call will simply be
            ignored.

        **Example:**

        >>> from mutwo.events import basic
        >>> simple_event = basic.SimpleEvent(2)
        >>> simple_event.set_parameter('duration', lambda old_duration: old_duration * 2)
        >>> simple_event.duration
        4
        >>> simple_event.set_parameter('duration', 3)
        >>> simple_event.duration
        3
        >>> simple_event.set_parameter('unknown_parameter', 10, set_unassigned_parameter=False)  # this will be ignored
        >>> simple_event.unknown_parameter
        AttributeError: 'SimpleEvent' object has no attribute 'unknown_parameter'
        >>> simple_event.set_parameter('unknown_parameter', 10, set_unassigned_parameter=True)  # this will be written
        >>> simple_event.unknown_parameter
        10
        """

        old_parameter = self.get_parameter(parameter_name)
        if set_unassigned_parameter or old_parameter is not None:
            if hasattr(object_or_function, "__call__"):
                new_parameter = object_or_function(old_parameter)
            else:
                new_parameter = object_or_function
            setattr(self, parameter_name, new_parameter)

    @decorators.add_return_option
    def mutate_parameter(  # type: ignore
        self,
        parameter_name: str,
        function: typing.Union[
            typing.Callable[[parameters.abc.Parameter], None], typing.Any
        ],
    ) -> typing.Optional[SimpleEvent]:
        parameter = self.get_parameter(parameter_name)
        if parameter is not None:
            function(parameter)

    @decorators.add_return_option
    def cut_out(  # type: ignore
        self, start: parameters.abc.DurationType, end: parameters.abc.DurationType,
    ) -> typing.Optional[SimpleEvent]:
        self._assert_correct_start_and_end_values(
            start, end, condition=lambda start, end: start < end
        )

        duration = self.duration

        difference_to_duration: constants.Real = 0

        if start > 0:
            difference_to_duration += start
        if end < duration:
            difference_to_duration += duration - end

        try:
            assert difference_to_duration < duration
        except AssertionError:
            message = (
                "Can't cut out SimpleEvent '{}' with duration '{}' from (start = {} to"
                " end = {}).".format(self, duration, start, end)
            )
            raise ValueError(message)

        self.duration -= difference_to_duration

    @decorators.add_return_option
    def cut_off(  # type: ignore
        self, start: parameters.abc.DurationType, end: parameters.abc.DurationType,
    ) -> typing.Optional[SimpleEvent]:
        self._assert_correct_start_and_end_values(start, end)

        duration = self.duration
        if start < duration:
            if end > duration:
                end = duration

            self.duration -= end - start


T = typing.TypeVar("T", bound=events.abc.Event)


class SequentialEvent(events.abc.ComplexEvent, typing.Generic[T]):
    """Event-Object which contains other Events which happen in a linear order."""

    # ###################################################################### #
    #                           properties                                   #
    # ###################################################################### #

    @events.abc.ComplexEvent.duration.getter
    def duration(self) -> parameters.abc.DurationType:
        return sum(event.duration for event in self)

    @property
    def absolute_times(self) -> typing.Tuple[constants.Real, ...]:
        """Return absolute point in time for each event."""

        durations = (event.duration for event in self)
        return tuple(tools.accumulate_from_zero(durations))[:-1]

    @property
    def start_and_end_time_per_event(
        self,
    ) -> typing.Tuple[typing.Tuple[constants.Real, constants.Real], ...]:
        """Return start and end time for each event."""

        durations = (event.duration for event in self)
        absolute_times = tuple(tools.accumulate_from_zero(durations))
        return tuple(zip(absolute_times, absolute_times[1:]))

    # ###################################################################### #
    #                           public methods                               #
    # ###################################################################### #

    def get_event_index_at(self, absolute_time: constants.Real) -> int:
        """Get index of event which is active at the passed absolute_time.

        :param absolute_time: The absolute time where the method shall search
            for the active event.

        **Example:**

        >>> from mutwo.events import basic
        >>> sequential_event = basic.SequentialEvent([basic.SimpleEvent(2), basic.SimpleEvent(3)])
        >>> sequential_event.get_event_index_at(1)
        0
        >>> sequential_event.get_event_index_at(3)
        1
        """

        absolute_times = self.absolute_times
        return bisect.bisect_right(absolute_times, absolute_time) - 1

    def get_event_at(self, absolute_time: constants.Real) -> T:
        """Get event which is active at the passed absolute_time.

        :param absolute_time: The absolute time where the method shall search
            for the active event.

        **Example:**

        >>> from mutwo.events import basic
        >>> sequential_event = basic.SequentialEvent([basic.SimpleEvent(2), basic.SimpleEvent(3)])
        >>> sequential_event.get_event_at(1)
        SimpleEvent(duration = 2)
        >>> sequential_event.get_event_at(3)
        SimpleEvent(duration = 3)
        """

        return self[self.get_event_index_at(absolute_time)]  # type: ignore

    @decorators.add_return_option
    def cut_out(  # type: ignore
        self, start: parameters.abc.DurationType, end: parameters.abc.DurationType,
    ) -> typing.Optional[SequentialEvent[T]]:
        self._assert_correct_start_and_end_values(start, end)

        remove_nth_event = []
        for nth_event, event_start, event in zip(
            range(len(self)), self.absolute_times, self
        ):
            event_duration = event.duration
            event_end = event_start + event_duration

            cut_out_start: constants.Real = 0
            cut_out_end = event_duration

            if event_start < start:
                cut_out_start += start - event_start

            if event_end > end:
                cut_out_end -= event_end - end

            if cut_out_start < cut_out_end:
                event.cut_out(cut_out_start, cut_out_end)
            else:
                remove_nth_event.append(nth_event)

        for nth_event_to_remove in reversed(remove_nth_event):
            del self[nth_event_to_remove]

    @decorators.add_return_option
    def cut_off(  # type: ignore
        self, start: parameters.abc.DurationType, end: parameters.abc.DurationType,
    ) -> typing.Optional[SequentialEvent[T]]:
        cut_off_duration = end - start

        # avoid unnecessary iterations
        if cut_off_duration > 0:

            # collect events which are only active within the
            # cut_off - range
            events_to_delete = []
            for event_index, event_start, event in zip(
                range(len(self)), self.absolute_times, self
            ):
                event_end = event_start + event.duration
                if event_start >= start and event_end <= end:
                    events_to_delete.append(event_index)

                # shorten event which are partly active within the
                # cut_off - range
                elif event_start <= start and event_end >= start:
                    difference_to_event_start = start - event_start
                    event.cut_off(
                        difference_to_event_start,
                        difference_to_event_start + cut_off_duration,
                    )

                elif event_start < end and event_end > end:
                    difference_to_event_start = event_start - start
                    event.cut_off(0, cut_off_duration - difference_to_event_start)

            for index in reversed(events_to_delete):
                del self[index]

    @decorators.add_return_option
    def squash_in(  # type: ignore
        self, start: parameters.abc.DurationType, event_to_squash_in: events.abc.Event
    ) -> typing.Optional[SequentialEvent[T]]:
        self._assert_start_in_range(start)

        cut_off_end = start + event_to_squash_in.duration

        self.cut_off(start, cut_off_end)

        if start == self.duration:
            self.append(event_to_squash_in)

        else:
            absolute_times = self.absolute_times
            active_event_index = self.get_event_index_at(start)
            split_position = start - absolute_times[active_event_index]
            # potentially split event
            if split_position > 6e-14:  # avoid floating point errors
                split_active_event = self[active_event_index].split_at(split_position)
                self[active_event_index] = split_active_event[1]
                self.insert(active_event_index, split_active_event[0])
                active_event_index += 1

            self.insert(active_event_index, event_to_squash_in)


class SimultaneousEvent(events.abc.ComplexEvent, typing.Generic[T]):
    """Event-Object which contains other Event-Objects which happen at the same time."""

    # ###################################################################### #
    #                           properties                                   #
    # ###################################################################### #

    @events.abc.ComplexEvent.duration.getter
    def duration(self) -> parameters.abc.DurationType:
        return max(event.duration for event in self)

    # ###################################################################### #
    #                           public methods                               #
    # ###################################################################### #

    @decorators.add_return_option
    def cut_out(  # type: ignore
        self, start: parameters.abc.DurationType, end: parameters.abc.DurationType,
    ) -> typing.Optional[SimultaneousEvent[T]]:
        self._assert_correct_start_and_end_values(start, end)
        [event.cut_out(start, end) for event in self]

    @decorators.add_return_option
    def cut_off(  # type: ignore
        self, start: parameters.abc.DurationType, end: parameters.abc.DurationType,
    ) -> typing.Optional[SimultaneousEvent[T]]:
        self._assert_correct_start_and_end_values(start, end)
        [event.cut_off(start, end) for event in self]

    @decorators.add_return_option
    def squash_in(  # type: ignore
        self, start: parameters.abc.DurationType, event_to_squash_in: events.abc.Event
    ) -> typing.Optional[SimultaneousEvent[T]]:
        self._assert_start_in_range(start)

        for event in self:
            try:
                event.squash_in(start, event_to_squash_in)  # type: ignore

            # simple events don't have a 'squash_in' method
            except AttributeError:
                message = (
                    "Can't squash '{}' in '{}'. Does the SimultaneousEvent contain"
                    " events that inherit from SimpleEvent? For being able to"
                    " squash in, the SimultaneousEvent needs to contain"
                    " SequentialEvents or SimultaneousEvents.".format(
                        event_to_squash_in, self
                    )
                )
                raise TypeError(message)
