# installation_tasks.py
# Container classes for running of installation tasks.
#
# Copyright (C) 2016  Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
import time

from dasbus.error import DBusError
from pyanaconda.core.signal import Signal
from pyanaconda.errors import errorHandler, ERROR_RAISE
from pyanaconda.modules.common.task import sync_run_task
from pyanaconda.anaconda_loggers import get_module_logger

log = get_module_logger(__name__)


class BaseTask(object):
    """A base class for Task and TaskQueue.

    It holds shared methods, properties and signals.
    """

    def __init__(self, name):
        self._name = name
        self._parent = None
        self._elapsed_time = None
        self.started = Signal()
        self.completed = Signal()

    @property
    def name(self):
        """Task name.

        :returns: task name
        :rtype: str
        """
        return self._name

    @property
    def elapsed_time(self):
        """Elapsed time since the task has been started in milliseconds."""
        return self._elapsed_time

    @property
    def summary(self):
        """A description of the task - to be overridden by subclasses."""
        raise NotImplementedError

    def set_parent(self, queue):
        """Set the parent task queue."""
        self._parent = queue

    def start(self):
        """Start the task."""
        # trigger the "started" signal
        self.started.emit(self)

        # run the task
        start_timestamp = time.time()

        self._run()

        done_timestamp = time.time()
        self._elapsed_time = done_timestamp - start_timestamp

        # trigger the "completed" signal
        self.completed.emit(self)

    def _run(self):
        """Run the task - to be overridden by sub-classes."""
        raise NotImplementedError


class TaskQueue(BaseTask):
    """TaskQueue represents a queue of TaskQueues or Tasks.

    TaskQueues and Tasks can be mixed in a single TaskQueue.
    """

    def __init__(self, name, status_message=None):
        super().__init__(name)
        self._status_message = status_message
        # the list backing this TaskQueue instance
        self._queue = []
        # triggered if a TaskQueue contained in this one was started/completed
        self.queue_started = Signal()
        self.queue_completed = Signal()
        # triggered when a task is started
        self.task_started = Signal()
        self.task_completed = Signal()

    @property
    def status_message(self):
        """A status message describing the Queue is trying to achieve.

        Eq. "Converting all foo into bar."

        The current main usecase is to set the ProgressHub status message when
        a TaskQueue is started.

        :returns: a status message
        :rtype: str
        """
        return self._status_message

    @property
    def items(self):
        """Task and queues contained in this queue.

        :return: a list of tasks and queues
        """
        return self._queue

    @property
    def nested_items(self):
        """Tasks and queues contained in this and all nested task queues.

        :return: a list of tasks and queues
        """
        items = []

        for item in self._queue:
            items.append(item)

            if isinstance(item, TaskQueue):
                items.extend(item.nested_items)

        return items

    @property
    def queue_count(self):
        """Returns number of TaskQueues contained in this and all nested TaskQueues.

        :returns: number of queues
        :rtype: int
        """
        return len([i for i in self.nested_items if isinstance(i, TaskQueue)])

    @property
    def task_count(self):
        """Returns number of tasks contained in this and all nested TaskQueues.

        :returns: number of tasks
        :rtype: int
        """
        return len([i for i in self.nested_items if not isinstance(i, TaskQueue)])

    @property
    def summary(self):
        """Return a multi-line summary of the contents of the task queue.

        :returns: summary of task queue contents
        :rtype: str
        """
        if self._parent is None:
            message = "Top-level task queue: %s\n" % self.name
            # this is the top-level queue, so add some "global" stats
            message += "Number of task queues: %d\n" % self.queue_count
            message += "Number of tasks: %d\n" % self.task_count
            message += "Task & task group listing:\n"
        else:
            message = "Task queue: %s\n" % self.name

        for item in self._queue:
            for line in item.summary.splitlines():
                message += " %s\n" % line

        return message.strip()

    def _run(self):
        """Run the task queue."""
        for item in self._queue:
            # start the item (TaskQueue/Task)
            item.start()

    # implement the Python list "interface" and make sure parent is always
    # set to a correct value
    def append(self, item):
        item.started.connect(self.task_started.emit)
        item.completed.connect(self.task_completed.emit)

        if isinstance(item, TaskQueue):
            # connect own start/completion signal to parents queue start/completion signal
            item.started.connect(self.queue_started.emit)
            item.completed.connect(self.queue_completed.emit)

            # propagate start/completion signals from nested queues/tasks
            item.queue_started.connect(self.queue_started.emit)
            item.queue_completed.connect(self.queue_completed.emit)

        self._queue.append(item)
        item.set_parent(self)

    def append_dbus_tasks(self, service_id, dbus_tasks):
        """Append DBus Tasks from a module to the TaskQueue.

        :param service_id: DBusServiceIdentifier instance corresponding to an Anaconda DBus module
        :param dbus_tasks: list of DBus Tasks paths
        """
        for dbus_task_path in dbus_tasks:
            task_proxy = service_id.get_proxy(dbus_task_path)
            self.append(DBusTask(task_proxy))


class Task(BaseTask):
    """Task is a wrapper for a single installation related task.

    It has a name and a callable (called task), which is the actual task to execute.
    Arguments and keywoard arguments for the callable can be suplied by using
    the task_args and task_kwargs options.
    """

    def __init__(self, task_name, task_cb, task_args=None, task_kwargs=None):
        super().__init__(task_name)
        self._task_cb = task_cb
        self._task_args = task_args or []
        self._task_kwargs = task_kwargs or dict()

    @property
    def summary(self):
        """A description of the task.

        :returns: a single line describing the task
        :rtype: str
        """
        return "Task: %s" % self.name

    def _run(self):
        """Runs the task (callable) assigned to this Task class instance."""
        try:
            # Run the task.
            self._task_cb(*self._task_args, **self._task_kwargs)
        except Exception as e:  # pylint: disable=broad-except
            # Handle an error.
            if errorHandler.cb(e) == ERROR_RAISE:
                raise


class DBusTask(BaseTask):
    """Wrapper for a DBus installation task."""

    def __init__(self, task_proxy):
        """Create a new task.

        :param task_proxy: a DBus proxy of the task
        """
        super().__init__(task_proxy.Name)
        self._task_proxy = task_proxy

    @property
    def summary(self):
        """A description of the task.

        :returns: a single line describing the task
        :rtype: str
        """
        return "Task: %s" % self.name

    def _run(self):
        """Run the DBus task."""
        try:
            # Report the progress messages.
            self._task_proxy.ProgressChanged.connect(self._progress_cb)

            # Run the task.
            sync_run_task(self._task_proxy)
        except DBusError as e:
            # Handle a remote error.
            if errorHandler.cb(e) == ERROR_RAISE:
                raise
        finally:
            # Disconnect from the signal.
            self._task_proxy.ProgressChanged.disconnect()

    def _progress_cb(self, step, message):
        """Callback for task progress reporting."""
        log.info(message)
