#!/usr/bin/env python3
# -*- coding: utf-8 -*-
__copyright__ = """ This code is licensed under the 3-clause BSD license.
Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group.
See LICENSE.txt for details.
"""
"""
Provides the StatusManager class.
"""

import typing
import operator
import abc
from PySide2.QtCore import QObject
from functools import wraps

if typing.TYPE_CHECKING:
    Signal = typing.Any
else:
    from PySide2.QtCore import Signal

T = typing.TypeVar("T")
S = typing.TypeVar("S")


class Status(typing.Generic[T]):
    """
    Manages a single value and notifies when this value is changed.
    """

    def __init__(self, value_type: typing.Type[T]) -> None:
        self.__communicator = self.create_communicator(value_type)

    @abc.abstractmethod
    def get(self) -> T:
        """
        Returns the current status.
        """

    class BaseCommunicator(QObject):

        def __init__(self, signal_type: typing.Type[T]) -> None:
            super().__init__()
            self.signal_type = signal_type

        def emit_signal(self, value: T) -> None:
            if not isinstance(value, self.signal_type):
                raise TypeError(f"Expected {self.signal_type}, got {type(value).__name__}")
            self.signal.emit(value)

    def create_communicator(self, signal_type: typing.Type[T]):
        class DynamicCommunicator(self.BaseCommunicator):  # type: ignore
            signal = Signal(signal_type)

            def __init__(self) -> None:
                super().__init__(signal_type)

        return DynamicCommunicator()

    @property
    def changed_signal(self) -> Signal:
        """
        Returns the signal used to signal changes.
        """
        return self.__communicator.signal

    @property
    def value(self) -> T:
        """
        Returns the current status.
        """
        return self.get()


class WriteableStatus(Status[T]):
    """
    Manages a single value and notifies when this value is changed.
    """

    @abc.abstractmethod
    def set(self, value: T) -> None:
        """
        Returns the current status.
        """

    @property
    def value(self) -> T:
        """
        Returns the current status.
        """
        return self.get()

    @value.setter
    def value(self, value: T) -> None:
        """
        Sets the current status.
        """
        self.set(value)


class StatusManager(WriteableStatus[T]):
    """
    Stores and manages a single value and notifies when this value is changed.
    """

    def __init__(self, value: T):
        super().__init__(type(value))
        self.__value = value

    def get(self) -> T:
        """
        Returns the contained value.
        """
        return self.__value

    def set(self, value: T) -> None:
        """
        Sets the contained value. Notifies on change.
        """
        if value == self.__value:
            return
        self.__value = value
        self.changed_signal.emit(self.__value)


class TransformedStatusManager(Status[T]):
    """
    Manages a value generated by a transform applied to Status instances.
    """

    def __init__(
        self,
        transform: typing.Callable[..., T],
        *args: Status[typing.Any],
        **kwargs: Status[typing.Any],
    ):
        self.__apply_transform: typing.Callable[[], T] = lambda: transform(
            *[arg.value for arg in args],
            **{key: value.value for key, value in kwargs.items()},
        )
        super().__init__(type(self.__apply_transform()))
        self.__value = StatusManager(self.__apply_transform())
        self.__value.changed_signal.connect(
            lambda: self.changed_signal.emit(self.__value.value)
        )
        for arg in args:
            arg.changed_signal.connect(self.__update)
        for _, value in kwargs.items():
            value.changed_signal.connect(self.__update)

    def get(self) -> T:
        """
        Returns the contained value.
        """
        return self.__value.value

    def __update(self) -> None:
        """
        Updates the contained value.
        """
        setattr(self.__value, "value", self.__apply_transform())


def lift(transform: typing.Callable[..., T]) -> typing.Callable[..., Status[T]]:
    """
    Converts a function `transform` to a function on Status instances.
    """

    @wraps(transform)
    def transformed(
        *args: typing.Any, **kwargs: typing.Any
    ) -> TransformedStatusManager[T]:
        return TransformedStatusManager[T](transform, *args, **kwargs)

    return transformed


and_ = lift(operator.and_)
or_ = lift(operator.or_)


class TransformedWriteableStatusManager(typing.Generic[S, T], WriteableStatus[T]):
    """
    Manages a writeable value generated by a transform applied to Status instances.
    """

    def __init__(
        self,
        transform: typing.Callable[[S], T],
        inverse: typing.Callable[[T], S],
        argument: WriteableStatus[S],
    ):
        self.__apply_transform: typing.Callable[[], T] = lambda: transform(
            argument.value
        )
        super().__init__(type(self.__apply_transform()))
        self.__apply_inverse: typing.Callable[[T], None] = lambda s: setattr(
            argument, "value", inverse(s)
        )
        self.__value = StatusManager(self.__apply_transform())
        self.__value.changed_signal.connect(
            lambda: self.changed_signal.emit(self.__value.value)
        )
        argument.changed_signal.connect(self.__update)

    def get(self) -> T:
        """
        Returns the contained value.
        """
        return self.__value.value

    def set(self, value: T) -> None:
        """
        Sets the contained value.
        """
        self.__apply_inverse(value)

    def __update(self) -> None:
        """
        Updates the contained value.
        """
        setattr(self.__value, "value", self.__apply_transform())


def lift_writeable(
    transform: typing.Callable[[S], T], inverse: typing.Callable[[T], S]
) -> typing.Callable[[WriteableStatus[S]], WriteableStatus[T]]:
    """
    Converts a pair function `transform`, `inverse` to a function on
    WriteableStatus instances.
    """

    def transformed(
        argument: WriteableStatus[S],
    ) -> TransformedWriteableStatusManager[S, T]:
        return TransformedWriteableStatusManager[S, T](transform, inverse, argument)

    return transformed


not_ = lift_writeable(operator.not_, operator.not_)


class AdaptedStatusManager(Status[T]):
    """
    Manages a value generated using the provided functions.
    """

    def __init__(
        self, pull: typing.Callable[[], T], signal: Signal,
    ):
        super().__init__(type(pull()))
        self.__pull = pull

        signal.connect(lambda: self.changed_signal.emit(pull()))

    def get(self) -> T:
        """
        Returns the value returned by pull.
        """
        return self.__pull()


def as_status(pull: typing.Callable[[], T], signal: Signal) -> AdaptedStatusManager[T]:
    """
    Adapts a triple of functions to a Status.
    """
    return AdaptedStatusManager[T](pull=pull, signal=signal)


class AdaptedWriteableStatusManager(WriteableStatus[T]):
    """
    Manages a writeable value generated using the provided functions.
    """

    def __init__(
        self,
        pull: typing.Callable[[], T],
        push: typing.Callable[[T], None],
        signal: Signal,
    ):
        super().__init__(type(pull()))
        self.__pull = pull
        self.__push = push

        signal.connect(lambda: self.changed_signal.emit(pull()))

    def get(self) -> T:
        """
        Returns the value returned by pull.
        """
        return self.__pull()

    def set(self, value: T) -> None:
        """
        Sets the contained value using push.
        """
        self.__push(value)


def as_writeable_status(
    pull: typing.Callable[[], T], push: typing.Callable[[T], None], signal: Signal
) -> AdaptedWriteableStatusManager[T]:
    """
    Adapts a triple of functions to a WriteableStatus.
    """
    return AdaptedWriteableStatusManager[T](pull=pull, push=push, signal=signal)
