#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import abc
from contextlib import contextmanager

from poppy.core.conf import settings
from poppy.core.db.connector import Connector
from poppy.core.db.dry_runner import DryRunner
from poppy.core.generic.cache import CachedProperty
from poppy.core.logger import logger

__all__ = ["BaseTarget", ]


class BaseTargetException(Exception):
    pass


class BaseTargetMeta(type):
    """
    The BaseTargetMeta metaclass allows to retrieve an existing instance of the target (using the 'target_id') and
    update it, or to create a new one if the pipeline does not yet contain the 'target_id'
    """

    def __call__(self, target_id, pipeline, *args, many=False, **kwargs):
        """

        :param self: the target class (BaseTarget, FileTarget, etc.)
        :param target_id: the target id
        :param pipeline: the pipeline instance
        :param args: target args
        :param many: the many flag indicating a multi-target instance
        :param kwargs: target kwargs
        :return:
        """
        if target_id in pipeline.targets:
            target = pipeline.targets[target_id]

            # check the compatibility of the target classes
            if type(target) != self:
                raise BaseTargetException(f"Same target_id '{target_id}' was use with two different target classes "
                                          f"('{type(target)}' and '{self}')")

            # check compatibility for multi-target instances
            if target._is_multi_target != many:
                raise BaseTargetException(f"Error in target definitions for target_id '{target_id}'. The target has "
                                          f"been defined as both multi and simple target. Check the 'many' keyword of "
                                          f"your target declarations")

            target.update(target_id, pipeline, *args, many=many, **kwargs)

            return target
        else:
            target = self.__new__(self, target_id, pipeline, *args, many=many, **kwargs)
            target.__init__(target_id, pipeline, *args, many=many, **kwargs)
            pipeline.targets[target_id] = target
            return target


class BaseTarget(metaclass=BaseTargetMeta):
    """
    Base class for the targets of the tasks, which are the input/output used and
    generated by the task. A wrapper is used to easily define the I/O of each
    task while keeping the job of managing it easy, by hiding the database
    representation, the existence or not of the I/O etc.
    """
    PENDING = "Pending"
    STOPPED = "Terminated"
    STARTED = "InProgress"
    EMPTY = "Empty"
    STATE = {PENDING, STOPPED, STARTED, EMPTY}

    OK = "OK"
    WARNING = "WARNING"
    ERROR = "ERROR"
    STATUS = {OK, WARNING, ERROR}

    class TargetEmpty(Exception):
        """
        Exception to indicate that the target is empty.
        """

    def __iter__(self):
        """
        Generate the target instances for multi-target inputs/outputs (many=True)
        """
        raise NotImplementedError(f"The method '__iter__' of the class '{self.__class__.__name__}' is not implemented")

    def update(self, *args, **kwargs):
        """
        Update the target instance with the given args/kwargs

        The update method should prevents conflict (same args defined multiple time) for multiple target initialization
        (for example with input and output declarations)

        Note: the update method is automatically called during instantiation of already existing target instances
        (instance with the same target_id).

        :return:
        """
        pass

    def __init__(self, target_id, pipeline, *args, many=False, **kwargs):
        """
        Initialize a POPPy target.

        :param target_id: Identifier of the target
        :param target_version: Version of the target (01 by default)
        :param target_file: If target is a file, the path to the file
        """

        # store a flag indicating if the the target represent one or more file
        self._is_multi_target = many

        self._pipeline = pipeline

        # the connection object used to communicate with the database
        self.db = Connector.manager[settings.MAIN_DATABASE]

        self._state = self.PENDING
        self._status = self.OK
        self.is_empty = False

        # store the identifier of the target
        self.id = target_id

        # call the update function to handle merge conflicts
        self.update(*args, **kwargs)

    def link(self, target_id):
        """
        Replace the 'target_id' instance by the current one in the pipeline object

        :param target_id:
        :return:
        """

        self._pipeline.targets[target_id] = self

    def exists(self):
        """
        Returns ``True`` if the target exists and ``False`` otherwise.
        """
        pass

    def state(self, value):
        """
        To change the state of the target with the given provided argument.
        """
        # check that the value is accepted
        if value not in self.STATE:
            raise ValueError(
                "The state of a target must be set to {0}".format(self.STATE)
            )

        # store the state
        self._state = value

        # call internal method to change the state of a target
        self.target_changed()

    def status(self, value):
        """
        To change the status of the target.
        """
        # check that the value is accepted
        if value not in self.STATUS:
            raise ValueError(
                "The status of a target must be set to " +
                "{0}".format(self.STATUS)
            )

        # store the status
        self._status = value

        # indicate the status changed
        self.target_changed()

    @DryRunner.dry_run
    def target_changed(self):
        """
        Callback used when target changed.
        """
        pass

    def ok(self):
        """
        Set to ok the status
        """
        self.status(self.OK)

    def warning(self):
        """
        Set the status to warning.
        """
        self.status(self.WARNING)

    def error(self):
        """
        Set the status to error.
        """
        self.status(self.ERROR)

    def pending(self):
        """
        Set the state to pending.
        """
        self.state(self.PENDING)

    def terminated(self):
        """
        Set the state to terminated.
        """
        self.state(self.STOPPED)

    def progress(self):
        """
        Set the state to in progress.
        """
        self.state(self.STARTED)

    def empty(self):
        """
        Set the state to empty.
        """
        self.is_empty = True
        self.state(self.EMPTY)

    @abc.abstractmethod
    def open(self, *args, **kwargs):
        """
        A wrapper around the open function of the file.
        """
        pass

    @contextmanager
    def activate(self):
        """
        To use the target as a wrapper around other files type that cannot be
        opened as usual.
        """
        # mark as pending and ok
        self.ok()
        self.pending()

        # try to do the things inside the context
        try:
            # mark as in progress
            self.progress()
            yield
        except self.TargetEmpty as e:
            self.empty()
        except Exception as e:
            self.error()
            self.terminated()
            raise e
        else:
            # mark as terminated
            self.terminated()

    @CachedProperty
    def description(self):
        """Given the target id, get the target target description from the database."""
        return self.db.get_target(self.id)

    def __repr__(self):
        return self.id

    @classmethod
    def output(cls, identifier, *args, **kwargs):
        """
        Define a new output target for a given class

        :param identifier: the target identifier
        :return: the class wrapper
        """

        def wrapper(TaskClass):
            TaskClass.add_output(target_class=cls, identifier=identifier, *args, **kwargs)
            return TaskClass

        return wrapper

    @classmethod
    def input(cls, identifier, *args, **kwargs):
        """
        Define a new input target for a given class

        :param identifier: the target identifier
        :return: the class wrapper
        """

        def wrapper(TaskClass):
            TaskClass.add_input(target_class=cls, identifier=identifier, *args, **kwargs)
            return TaskClass

        return wrapper
