import copy
import inspect
import logging
import warnings
from typing import List, Callable, Union, MutableSequence

from ..mixin import InstanceRegistry, ClassProperty


logger = logging.getLogger(__name__)


class Action(InstanceRegistry):
    UUID_PREFIX = 'action-'

    def __init__(self,
                 action: Callable,
                 *default_args,
                 **default_kwargs,
                 ):
        """
        Class for storing and tracking functions and methods to be executed.

        :param action: action method to execute when called
        :param default_args: default arguments for the action
        :param default_kwargs: default kwargs for the action
        """
        # todo consider implementing action removal
        if self.action_is_registered(action) is True:
            raise ValueError(
                f'The action "{action}" is already registered, use {self.__class__.__name__}.register_action instead'
            )
        InstanceRegistry.__init__(self)
        self.action = action
        self.__doc__ = inspect.getdoc(action)
        self.signature = inspect.signature(action)
        self.default_args = default_args
        self.logger = logger.getChild(f'{self.__class__.__name__}.{self.name}')
        fn_kwargs = {
            name: param.default
            for name, param in self.signature.parameters.items() if param.default is not inspect._empty
        }
        fn_kwargs.update(default_kwargs)
        self.default_kwargs = fn_kwargs

    def __repr__(self):
        return f'{self.__class__.__name__}({self.action.__name__})'

    def __call__(self, *args, **kwargs):
        self.logger.debug(f'calling {self.name} with args {args} and kwargs {kwargs}')
        return self.action(*args, **kwargs)

    @property
    def name(self) -> str:
        """action method name"""
        return self.action.__name__

    @property
    def return_type(self):
        """the return type of the action"""
        if self.signature.return_annotation is inspect._empty:
            return None
        return self.signature.return_annotation

    @classmethod
    def action_is_registered(cls,
                             action: Union[str, Callable, "Action"]
                             ) -> bool:
        """
        Checks whether the provided action is registered as a Action instance.

        :param action: action to check
        :return: whether action is registered
        """
        for instance in cls.class_registry:
            if any([
                instance.name == action,
                instance == action,
                instance.action == action,
            ]):
                return True
        return False

    @classmethod
    def register_action(cls,
                        action: Union[str, Callable, "Action"],
                        ) -> "Action":
        """
        Registers a new instance or returns an existing instance if the action is already registered.

        :param action: action method
        :return: Action instance
        """
        # todo consider logging that a new action has been registered
        # catch class instance
        if isinstance(action, Action):
            return action
        elif hasattr(action, 'action') and isinstance(action.action, Action):
            return action.action
        # search for action name
        elif type(action) is str:
            for instance in cls.class_registry:
                if instance.name == action:
                    return instance
            # if no action matched, raise
            raise ValueError(f'The action "{action}" is not registered')
        else:
            # check for preexisting actions
            for instance in cls.class_registry:
                if instance.name == action.__name__:
                    return instance
            # if not found, create and return
            return cls(
                action
            )

    @classmethod
    def register_actions(cls, *actions: Union[str, Callable, "Action"]) -> List["Action"]:
        """
        Registers multiple actions at once

        :param actions: action methods to be registered
        :return: list of actions which were registered
        """
        return [
            cls.register_action(action) for action in actions
        ]

    @classmethod
    def get_registered_actions(cls) -> List["Action"]:
        """returns a list of registered Action instances"""
        warnings.warn(
            'get_registered_actions has been deprecated, retrieve registered_actions directly',
            DeprecationWarning,
            stacklevel=2,
        )
        return cls.registered_actions

    @ClassProperty
    def registered_actions(cls) -> List["Action"]:
        """a list of registered Action instances"""
        return cls.class_registry

    @classmethod
    def get_registered_action_names(cls) -> List[str]:
        """returns a list of the names of the registered Scheduler Actions"""
        warnings.warn(
            'get_registered_action_names has been deprecated, retrieve registered_action_names directly',
            DeprecationWarning,
            stacklevel=2,
        )
        return cls.registered_action_names

    @ClassProperty
    def registered_action_names(cls) -> List[str]:
        """a list of the names of the registered Actions"""
        return [action.name for action in cls.registered_actions]

    @property
    def returns_bool(self) -> bool:
        """whether the action return type bool"""
        return self.return_type is bool


class EvalAction(Action):
    def __init__(self,
                 action: Callable,
                 *default_args,
                 **default_kwargs,
                 ):
        """
        Subclass of an Action where the action must return a bool. Note that this class does not enforce this, but
        warns the user if a non-bool type hint was detected.

        :param action: action method to execute when called
        :param default_args: default arguments for the action
        :param default_kwargs: default kwargs for the action
        """
        super().__init__(
            action=action,
            *default_args,
            **default_kwargs,
         )
        if self.return_type is not bool:
            warnings.warn(f'The action "{self.name}" provided as an EvalAction does not have a bool return type')


class ActionList(MutableSequence):
    def __init__(self, *actions: Action):
        """
        A manager for a list of Actions.

        :param actions: actions to sequence
        """
        self._action_list: List[Action] = []
        for action in actions:
            self.append(action)

    def __repr__(self):
        return f'{self.__class__.__name__}[{", ".join([str(action) for action in self._action_list])}]'

    @staticmethod
    def _ensure_type(action: Union[str, Action]) -> Action:
        """Ensures that the provided action is of the correct type for the list"""
        return Action.register_action(action)

    def __getitem__(self, item) -> Action:
        return self._action_list[item]

    def __setitem__(self, key, value: Action):
        value = self._ensure_type(value)
        self._action_list[key] = value

    def __delitem__(self, key):
        del self._action_list[key]

    def __len__(self):
        return len(self._action_list)

    def __copy__(self):
        # provides a copy of the class while preventing mutation of the actions in the instance itself
        return self.__class__(
            *[copy.copy(action) for action in self]
        )

    def insert(self, index: int, action: Action) -> None:
        """
        Inserts the provided action into the Action list

        :param index: index to insert at
        :param action: action object to insert
        :return:
        """
        action = self._ensure_type(action)
        self._action_list.insert(
            index,
            action,
        )