# Default event loop for Simpleline application.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020  Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline.  If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#

from threading import Lock

from simpleline.event_loop import AbstractEventLoop, ExitMainLoop
from simpleline.event_loop.event_queue import EventQueue
from simpleline.event_loop.signals import ExceptionSignal
from simpleline.logging import get_simpleline_logger

log = get_simpleline_logger()

__all__ = ["MainLoop"]


class MainLoop(AbstractEventLoop):
    """Default main event loop for the Simpleline.

    This event loop can be replaced by your event loop by implementing
    `simpleline.event_loop.AbstractEventLoop` class.
    """

    def __init__(self):
        super().__init__()
        self._active_queue = EventQueue()
        self._event_queues = [self._active_queue]
        self._lock = Lock()

    def register_signal_source(self, signal_source):
        """Register source of signal for actual event queue.

        :param signal_source: Source for future signals.
        :type signal_source: `simpleline.render.ui_screen.UIScreen`.
        """
        super().register_signal_source(signal_source)
        self._active_queue.add_source(signal_source)

    def run(self):
        """This methods starts the application.

        Do not use self.mainloop() directly as run() handles all the required exceptions
        needed to keep nested mainloop working.
        """
        super().run()
        self._run_loop = True

        try:
            self._mainloop()
        except ExitMainLoop:
            pass

        log.debug("Main loop ended. Running callback if set.")

        if self._quit_callback:
            cb = self._quit_callback.callback
            cb(self._quit_callback.args)

    def force_quit(self):
        """Force quit all running event loops.

        Kill all loop including inner loops (modal window).
        None of the Simpleline events will be processed anymore.
        """
        super().force_quit()
        self._event_queues.clear()
        self._run_loop = False

    def execute_new_loop(self, signal):
        """Starts the new event loop and pass `signal` in it.

        This is required for processing a modal screens.

        :param signal: Signal passed to the new event loop.
        :type signal: The `AbstractSignal` based class.
        """
        super().execute_new_loop(signal)

        if self._force_quit:
            return

        self._active_queue = EventQueue()

        # TODO: Remove when python3-astroid 1.5.3 will be in Fedora
        # pylint: disable=not-context-manager
        with self._lock:
            self._event_queues.append(self._active_queue)

        self.enqueue_signal(signal)
        self._mainloop()
        log.debug("Inner loop is closed")

    def close_loop(self):
        """Close active event loop.

        Close an event loop created by the `execute_new_loop()` method.
        """
        super().close_loop()
        self.process_signals()

        # TODO: Remove when python3-astroid 1.5.3 will be in Fedora
        # pylint: disable=not-context-manager
        with self._lock:
            self._event_queues.pop()
            try:
                self._active_queue = self._event_queues[-1]
            except IndexError:
                log.error("No more event queues to work with!")
                raise ExitMainLoop()  # pylint: disable=raise-missing-from

        self._run_loop = False

    def enqueue_signal(self, signal):
        """Enqueue new event for processing.

        Enqueue signal to the most inner queue (nearest to the active queue) where
        the `signal.source` belongs.
        If it belongs nowhere enqueue it to the active one.

        This method is thread safe.

        :param signal: Event which you want to add to the event queue for processing.
        :type signal: Instance based on AbstractEvent class.
        """
        if self._force_quit:
            return

        super().enqueue_signal(signal)
        # TODO: Remove when python3-astroid 1.5.3 will be in Fedora
        # pylint: disable=not-context-manager
        with self._lock:
            for queue in reversed(self._event_queues):
                if queue.enqueue_if_source_belongs(signal, signal.source):
                    return

        self._active_queue.enqueue(signal)

    def _mainloop(self):
        """Single mainloop. Do not use directly, start the application using run()."""
        # run infinite loop
        # this will always wait on input processing or similar so it should not busy waiting
        while self._run_loop:
            self._process_signals_loop()

        if not self._force_quit:
            # set back to True to leave outer loop working
            self._run_loop = True

    def process_signals(self, return_after=None):
        """This method processes incoming async messages.

        Process signals en-queued by the `self.enqueue_signal()` method. Call handlers
        registered to the signals by the `self.register_signal_handler()` method.

        When `return_after` is specified then wait to the point when this signal is processed.
        NO warranty that this method will return immediately after the signal was processed!

        Without `return_after` parameter this method will return after all queued signals
        with the highest priority will be processed.

        The method is NOT thread safe!

        :param return_after: Wait on this signal to be processed.
        :type return_after: Class of the signal.
        """
        super().process_signals(return_after)
        if return_after is not None:
            self._process_signals_with_return(return_after)
        else:
            self._process_signals_iteration()

    def _process_signals_with_return(self, return_after):
        """Process signals until the return_after signal was processed.

        Or the loop quited.
        """
        # get unique ID when waiting for the signal
        unique_id = self._register_wait_on_signal(return_after)

        while self._run_loop:
            signal = self._active_queue.get()

            # do the signal processing (call handlers)
            self._process_signal(signal)

            # was our signal processed if yes, return this method
            if self._check_if_signal_processed(return_after, unique_id):
                return

    def _process_signals_iteration(self):
        """Process queued signal and then return."""
        priority = None

        while not self._active_queue.empty() and self._run_loop:
            if priority is None:
                # take first signal to find out the highest priority in queue
                signal = self._active_queue.get()
                priority = signal.priority
            else:
                # get signal with this priority only
                signal = self._active_queue.get_top_event_if_priority(priority)

            # Signal with this priority is not available anymore
            if signal is None:
                return

            self._process_signal(signal)

    def _process_signals_loop(self):
        """Process signal until the event loop quited."""
        while self._run_loop:
            signal = self._active_queue.get()
            self._process_signal(signal)

    def _process_signal(self, signal):
        log.debug("Processing signal %s", signal)

        self._mark_signal_processed(signal)

        if type(signal) in self._handlers: # pylint: disable=unidiomatic-typecheck
            for handler_data in self._handlers[type(signal)]:
                try:
                    handler_data.callback(signal, handler_data.data)
                except ExitMainLoop:  # pylint: disable=try-except-raise
                    raise
                except Exception:  # pylint: disable=broad-except
                    self.enqueue_signal(ExceptionSignal(self))
        elif isinstance(signal, ExceptionSignal):
            self.kill_app_with_traceback(signal)
