from abc import ABC
from collections.abc import Callable
from typing import Any, Optional

import numpy as np

from osaft import log


class BaseVariable(ABC):
    """ BaseVariable class defining basic methods and attributes shared between
    active and passive attributes.

    This class and its children are based on the Observer Design Pattern.
    In the present context value changes shall be tracked so
    :class:`ActiveVariable` know when their dependencies changed, and they
    need to recompute their underlying value.

    :param name: Name of the variable
    :raises: TypeError if :attr:`name` is of wrong type
    """

    def __init__(self, name: Optional[str] = None) -> None:
        """Constructor method
        """
        self._value = None
        self._variables_to_notify: list[BaseVariable] = []

        if name is None:
            self._name = 'Unnamed'
        elif isinstance(name, str):
            self._name = name
        else:
            raise TypeError(f'name is of type {type(name)} instead of str')

    @property
    def name(self) -> str:
        """Returns name of the variable.

        :setter: sets :attr:`~osaft.variable.BaseVariable._name`
        """
        return self._name

    @property
    def value(self) -> Any:
        """Value of the attribute

        :setter: just defined in :class:`PassiveVariable`
        :getter: returns/computes the value of the instance
        """
        return self._value

    def notify(self) -> None:
        """Notifies all items in :attr:`BaseVariable._variables_to_notify` that
        a dependent value changed. Just the information of a changed dependency
        is passed.
        """
        log.debug(f'Variable {repr(self)} notifies:')
        for att in self._variables_to_notify:
            log.debug(f'attribute: {att}')
            att._change_update_status()

    def _link(self, input_variable: Any) -> None:
        """Links the inputs to self, so it knows who needs to update when
        :attr:`BaseVariable.value` changes.

        :param input_variable:
        """
        log.info(
            f'Variables linked: {str(input_variable)} depends on'
            f' {str(self)}',
        )
        self._variables_to_notify.append(input_variable)

    @staticmethod
    def _check_types(
        inputs: tuple,
    ) -> None:
        """ Checks if elements of :attr:`inputs` are of type
        :class:`BaseVariable`

        :param inputs: iterable to be checked
        :raises: TypeError if :attr:`inputs` is of wrong type
        """
        for item in inputs:
            if not isinstance(item, BaseVariable):
                out = f'{item} is of type {type(item)} instead of BaseVariable'
                raise TypeError(out)


class PassiveVariable(BaseVariable):
    """ Passive variable without any computation dependencies

    :param val: initial value
    :param name: Name of the variable
    """

    def __init__(
        self, val: Any, name: Optional[str] = None,
    ) -> None:
        """Constructor method
        """
        super().__init__(name)
        self.value = val

        # no if clause needed because last in inheritance
        log.debug(f'Creating {self}')

    @BaseVariable.value.setter
    def value(self, value: Any) -> None:
        """
        Checks if :attr:`BaseVariable._value` is already equal to value.
        If not, first :attr:`BaseVariable._value` is set to `value`
        and then all item in :attr:`BaseVariable._variables_to_notify` are
        notified.

        :param value: new value
        """
        has_changed = True
        if self._value is not None:
            try:
                L1 = len(self._value)
            except TypeError:
                L1 = 0
            try:
                L2 = len(value)
            except TypeError:
                L2 = 0

            if L1 == 0 and L2 == 0:
                has_changed = self._value != value
            elif L1 == L2:
                has_changed = not np.alltrue(self._value == value)

        if has_changed:
            log.debug(f'value change of {repr(self)} to {value}')
            self._value = value
            self.notify()

    def __repr__(self) -> str:
        return f'PassiveVariable(name={self.name}, value={self.value})'

    def __str__(self):
        return f'{self.name} = {self.value}'


class ActiveVariable(BaseVariable):
    """ Variables that depended on at least one :class:`.BaseVariable`

    :param functionCall: function signature that defines how
        :attr:`ActiveVariable._value` is computed
        Callable[None, Any]
    :param name: Name of the variable
    """

    def __init__(
        self, functionCall: Callable, name: Optional[str] = None,
    ) -> None:
        """Constructor method
        """
        super().__init__(name)
        self.__compute = functionCall
        self._needs_update = True

        # no if clause needed because last in inheritance
        log.debug(f'Creating {self}')

    @property
    def needs_update(self) -> bool:
        """ Returns if an update is needed for the next value retrieval
        """
        return self._needs_update

    @property
    def value(self) -> Any:
        """Returns the value of the attribute. If
        :attr:`ActiveVariable.needs_update` is true, it calls
        :meth:`ActiveVariable._compute` first and then changes
        :attr:`ActiveVariable._needs_update` to `False` before returning the
        value.
        """
        if self.needs_update:
            self._value = self._compute()
            self._needs_update = False
            log.debug(f'Recomputed: {repr(self)}')
        return self._value

    def _compute(self) -> Any:
        """Executes the assigned routine and outputs its value."""
        log.debug(f'{repr(self)} recomputes its value')
        value = self.__compute()
        return value

    def _change_update_status(self) -> None:
        """Set :attr:`ActiveVariable._needs_update` to `True` and calls
        :meth:`BaseVariable.notify`.
        """
        log.debug(f'{repr(self)} changes _needs_update to True')
        self._needs_update = True
        self.notify()

    def is_computed_by(self, *variables: Any) -> None:
        """ Adds the notification dependencies

        :param variables: variables that are referenced in
            :meth:`~ActiveVariable._compute`
        :raises: TypeError if no arguments are passed
        """
        if len(variables) == 0:
            out = 'is_computed_by() takes at least one positional argument '
            out += '(0 were given)'
            raise TypeError(out)
        self._check_types(variables)
        for variable in variables:
            variable._link(self)

    def __repr__(self) -> str:
        if not self._needs_update:
            return f'ActiveVariable(name={self.name}, value={self.value})'
        else:
            return f'ActiveVariable(name={self.name}, value=N.A.)'

    def __str__(self):
        if not self._needs_update:
            return f'{self.name} = {self.value}'
        else:
            return f'{self.name} = N.A.'


if __name__ == '__main__':
    pass
