"""
Orange widget base classes to execute Ewoks tasks
"""
import inspect
import logging
from contextlib import contextmanager

from ..orange_version import ORANGE_VERSION

if ORANGE_VERSION == ORANGE_VERSION.oasys_fork:
    from oasys.widgets.widget import OWWidget
    from orangewidget.widget import WidgetMetaClass
    from orangewidget.settings import Setting

    OWBaseWidget = OWWidget
    summarize = None
    PartialSummary = None
elif ORANGE_VERSION == ORANGE_VERSION.henri_fork:
    from Orange.widgets.widget import OWWidget
    from Orange.widgets.widget import WidgetMetaClass
    from Orange.widgets.settings import Setting

    OWBaseWidget = OWWidget
    summarize = None
    PartialSummary = None
else:
    from Orange.widgets.widget import OWWidget
    from Orange.widgets.widget import WidgetMetaClass
    from orangewidget.widget import OWBaseWidget
    from orangewidget.settings import Setting
    from orangewidget.utils.signals import summarize
    from orangewidget.utils.signals import PartialSummary

from ewokscore.variable import Variable
from ewokscore.variable import value_from_transfer
from ewokscore.hashing import UniversalHashable
from ewokscore.hashing import MissingData
from .progress import QProgress
from .taskexecutor import TaskExecutor
from .taskexecutor import ThreadedTaskExecutor
from .taskexecutor_queue import TaskExecutorQueue
from . import owsignals


_logger = logging.getLogger(__name__)


__all__ = [
    "OWEwoksWidgetNoThread",
    "OWEwoksWidgetOneThread",
    "OWEwoksWidgetOneThreadPerRun",
    "OWEwoksWidgetWithTaskStack",
]


MISSING_DATA = UniversalHashable.MISSING_DATA
INVALIDATION_DATA = None  # This means we cannot use `None` as a value


if summarize is not None:

    @summarize.register(Variable)
    def summarize_variable(var: Variable):
        if var.value is MISSING_DATA:
            dtype = MISSING_DATA
        else:
            dtype = type(var.value).__name__
        desc = f"ewoks variable ({dtype})"
        return PartialSummary(desc, desc)

    @summarize.register(object)
    def summarize_object(value: object):
        return PartialSummary(str(type(value)), str(type(value)))


def prepare_OWEwoksWidgetclass(namespace, ewokstaskclass):
    """This needs to be called before signal and setting parsing"""
    namespace["ewokstaskclass"] = ewokstaskclass
    namespace["default_inputs"] = Setting(
        {name: INVALIDATION_DATA for name in ewokstaskclass.input_names()},
        schema_only=True,
    )
    namespace["varinfo"] = Setting(dict(), schema_only=True)
    owsignals.validate_inputs(namespace)
    owsignals.validate_outputs(namespace)


class _OWEwoksWidgetMetaClass(WidgetMetaClass):
    def __new__(metacls, name, bases, attrs, ewokstaskclass=None, **kw):
        if ewokstaskclass:
            prepare_OWEwoksWidgetclass(attrs, ewokstaskclass)
        return super().__new__(metacls, name, bases, attrs, **kw)


# insure compatibility between old orange widget and new
# orangewidget.widget.WidgetMetaClass. This was before split of the two
# projects. Parameter name "openclass" is undefined on the old version
ow_build_opts = {}
if "openclass" in inspect.signature(WidgetMetaClass).parameters:
    ow_build_opts["openclass"] = True


class OWEwoksBaseWidget(OWWidget, metaclass=_OWEwoksWidgetMetaClass, **ow_build_opts):
    """
    Base class to handle boiler plate code to interconnect ewoks and
    orange3
    """

    MISSING_DATA = MISSING_DATA

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__dynamic_inputs = dict()
        self.__task_output_changed_callbacks = {self.task_output_changed}

    @classmethod
    def input_names(cls):
        return cls.ewokstaskclass.input_names()

    @classmethod
    def output_names(cls):
        return cls.ewokstaskclass.output_names()

    def _getTaskArguments(self):
        return {"inputs": self.task_inputs, "varinfo": self.varinfo}

    @staticmethod
    def _get_value(value):
        if isinstance(value, Variable):
            return value.value
        if isinstance(value, MissingData):
            # `Setting` seems to make a copy of MISSING_DATA
            return MISSING_DATA
        return value

    @property
    def defined_default_input_values(self) -> dict:
        # Warning: do not use default_inputs directly because it
        #          messes up MISSING_DATA
        return {
            name: value
            for name, value in self.default_inputs.items()
            if value is not INVALIDATION_DATA
        }

    @property
    def default_input_values(self) -> dict:
        values = {name: MISSING_DATA for name in self.input_names()}
        values.update(self.defined_default_input_values)
        return values

    @property
    def dynamic_input_values(self) -> dict:
        return {k: self._get_value(v) for k, v in self.__dynamic_inputs.items()}

    @property
    def task_inputs(self) -> dict:
        inputs = self.defined_default_input_values
        inputs.update(self.__dynamic_inputs)
        return inputs

    def receiveDynamicInputs(self, name, value):
        if value is INVALIDATION_DATA:
            self.__dynamic_inputs.pop(name, INVALIDATION_DATA)
        else:
            self.__dynamic_inputs[name] = value

    def _get_output_signal(self, ewoksname):
        if ORANGE_VERSION == ORANGE_VERSION.oasys_fork:
            for signal in self.outputs:
                if signal.name == ewoksname:
                    break
            else:
                signal = None
        else:
            signal = getattr(self.Outputs, ewoksname, None)
        if signal is None:
            raise RuntimeError(f"Output signal '{ewoksname}' does not exist")
        return signal

    def trigger_downstream(self):
        if ORANGE_VERSION == ORANGE_VERSION.oasys_fork:
            for ewoksname, var in self.task_outputs.items():
                ewoks_to_orange = owsignals.get_ewoks_to_orange_mapping(
                    type(self), "outputs"
                )
                orangename = ewoks_to_orange.get(ewoksname, ewoksname)
                if var.value is MISSING_DATA:
                    self.send(orangename, INVALIDATION_DATA)  # or channel.invalidate?
                else:
                    self.send(orangename, var)
        else:
            for ewoksname, var in self.task_outputs.items():
                channel = self._get_output_signal(ewoksname)
                if var.value is MISSING_DATA:
                    channel.send(INVALIDATION_DATA)  # or channel.invalidate?
                else:
                    channel.send(var)
        for cb in self.__task_output_changed_callbacks:
            cb()

    @property
    def task_output_changed_callbacks(self) -> set:
        return self.__task_output_changed_callbacks

    def task_output_changed(self):
        pass

    def clear_downstream(self):
        if ORANGE_VERSION == ORANGE_VERSION.oasys_fork:
            for name in self.task_outputs:
                self.send(name, INVALIDATION_DATA)  # or channel.invalidate?
        else:
            for name in self.task_outputs:
                channel = self._get_output_signal(name)
                channel.send(INVALIDATION_DATA)  # or channel.invalidate?

    def defaultInputsHaveChanged(self):
        """Needs to be called when default inputs have changed"""
        self.executeEwoksTask()

    def handleNewSignals(self):
        """Invoked by the workflow signal propagation manager after all
        signals handlers have been called.
        """
        self.executeEwoksTask()

    def executeEwoksTask(self):
        raise NotImplementedError("Base class")

    @property
    def task_outputs(self):
        raise NotImplementedError("Base class")

    @property
    def task_output_values(self):
        return {name: var.value for name, var in self.task_outputs.items()}

    @property
    def task_input_values(self):
        return {
            name: value_from_transfer(var, varinfo=self.varinfo)
            for name, var in self.task_inputs.items()
        }


def is_orange_widget_class(widget_class):
    return issubclass(widget_class, OWBaseWidget)


def is_ewoks_widget_class(widget_class):
    return issubclass(widget_class, OWEwoksBaseWidget)


def is_native_widget_class(widget_class):
    return is_orange_widget_class(widget_class) and not is_ewoks_widget_class(
        widget_class
    )


def is_orange_widget(widget):
    return isinstance(widget, OWBaseWidget)


def is_ewoks_widget(widget):
    return isinstance(widget, OWEwoksBaseWidget)


def is_native_widget(widget_class):
    return is_orange_widget(widget_class) and not is_ewoks_widget(widget_class)


class OWEwoksWidgetNoThread(OWEwoksBaseWidget, **ow_build_opts):
    """Widget which will executeEwoksTask the ewokscore.Task directly"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__taskExecutor = TaskExecutor(self.ewokstaskclass)

    def executeEwoksTask(self):
        self.__taskExecutor.create_task(**self._getTaskArguments())
        try:
            self.__taskExecutor.execute_task()
        except Exception:
            self.clear_downstream()
            raise
        if self.__taskExecutor.succeeded:
            self.trigger_downstream()
        else:
            self.clear_downstream()

    @property
    def task_outputs(self):
        return self.__taskExecutor.output_variables


class _OWEwoksThreadedBaseWidget(OWEwoksBaseWidget, **ow_build_opts):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__taskProgress = QProgress()
        self.__taskProgress.sigProgressChanged.connect(self.progressBarSet)

    def onDeleteWidget(self):
        self.__taskProgress.sigProgressChanged.disconnect(self.progressBarSet)
        self._cleanupTaskExecutor()
        super().onDeleteWidget()

    def _cleanupTaskExecutor(self):
        raise NotImplementedError("Base class")

    @contextmanager
    def _ewoksTaskStartContext(self):
        try:
            self.__ewoksTaskInit()
            yield
        except Exception:
            self.__ewoksTaskFinished()
            raise

    @contextmanager
    def _ewoksTaskFinishedContext(self):
        try:
            yield
        finally:
            self.__ewoksTaskFinished()

    def __ewoksTaskInit(self):
        self.progressBarInit()

    def __ewoksTaskFinished(self):
        self.progressBarFinished()

    def _getTaskArguments(self):
        adict = super()._getTaskArguments()
        adict["progress"] = self.__taskProgress
        return adict


class OWEwoksWidgetOneThread(_OWEwoksThreadedBaseWidget, **ow_build_opts):
    """
    All the processing is done on one thread.
    If a processing is requested when the thread is already running then
    it is refused.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__taskExecutor = ThreadedTaskExecutor(ewokstaskclass=self.ewokstaskclass)
        self.__taskExecutor.finished.connect(self._ewoksTaskFinishedCallback)

    def executeEwoksTask(self):
        if self.__taskExecutor.isRunning():
            _logger.error("A processing is already on going")
            return
        else:
            self.__taskExecutor.create_task(**self._getTaskArguments())
            if self.__taskExecutor.is_ready_to_execute:
                with self._ewoksTaskStartContext():
                    self.__taskExecutor.start()

    @property
    def task_outputs(self):
        return self.__taskExecutor.output_variables

    def _ewoksTaskFinishedCallback(self):
        with self._ewoksTaskFinishedContext():
            if self.__taskExecutor.succeeded:
                self.trigger_downstream()
            else:
                self.clear_downstream()

    def _cleanupTaskExecutor(self):
        self.__taskExecutor.finished.disconnect(self._ewoksTaskFinishedCallback)
        self.__taskExecutor.stop()
        self.__taskExecutor = None


class OWEwoksWidgetOneThreadPerRun(_OWEwoksThreadedBaseWidget, **ow_build_opts):
    """
    Each time a task processing is requested this will create a new thread
    to do the processing.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__taskExecutors = list()
        self.__last_output_variables = dict()

    def executeEwoksTask(self):
        taskExecutor = ThreadedTaskExecutor(ewokstaskclass=self.ewokstaskclass)
        taskExecutor.create_task(**self._getTaskArguments())
        if not taskExecutor.is_ready_to_execute:
            return
        with self.__addTaskExecutor(taskExecutor):
            with self._ewoksTaskStartContext():
                taskExecutor.start()

    @contextmanager
    def __addTaskExecutor(self, taskExecutor):
        self.__disconnectAllTaskExecutors()
        taskExecutor.finished.connect(self._ewoksTaskFinishedCallback)
        self.__taskExecutors.append(taskExecutor)
        try:
            yield
        except Exception:
            taskExecutor.finished.disconnect(self._ewoksTaskFinishedCallback)
            self.__taskExecutors.remove(taskExecutor)
            raise

    def __disconnectAllTaskExecutors(self):
        for taskExecutor in self.__taskExecutors:
            try:
                taskExecutor.finished.disconnect(self._ewoksTaskFinishedCallback)
            except KeyError:
                pass

    def _ewoksTaskFinishedCallback(self):
        with self._ewoksTaskFinishedContext():
            try:
                taskExecutor = self.sender()
                self.__last_output_variables = taskExecutor.output_variables
                if taskExecutor.succeeded:
                    self.trigger_downstream()
                else:
                    self.clear_downstream()
            finally:
                if taskExecutor in self.__taskExecutors:
                    self.__taskExecutors.remove(taskExecutor)

    def _cleanupTaskExecutor(self):
        self.__disconnectAllTaskExecutors()
        for taskExecutor in self.__taskExecutors:
            taskExecutor.quit()
        self.__taskExecutors.clear()

    @property
    def task_outputs(self):
        return self.__last_output_variables


class OWEwoksWidgetWithTaskStack(_OWEwoksThreadedBaseWidget, **ow_build_opts):
    """
    Each time a task processing is requested add it to the FIFO stack.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__taskExecutorQueue = TaskExecutorQueue(ewokstaskclass=self.ewokstaskclass)
        self.__last_output_variables = dict()

    def executeEwoksTask(self):
        with self._ewoksTaskStartContext():
            self.__taskExecutorQueue.add(
                _callbacks=(self._ewoksTaskFinishedCallback,),
                **self._getTaskArguments(),
            )

    @property
    def task_outputs(self):
        return self.__last_output_variables

    def _cleanupTaskExecutor(self):
        self.__taskExecutorQueue.stop()
        self.__taskExecutorQueue = None

    def _ewoksTaskFinishedCallback(self):
        with self._ewoksTaskFinishedContext():
            taskExecutor = self.sender()
            self.__last_output_variables = taskExecutor.output_variables
            if taskExecutor.succeeded:
                self.trigger_downstream()
            else:
                self.clear_downstream()
