import atexit
import inspect
import math
import sys
import traceback
import types

from PySide6 import QtCore, QtWidgets

from .. import core


NO_RESULT = object()
class Promise:
    """This class makes it easier to schedule follow-up operations after a
    background task finishes.
    """
    def __init__(self, result=NO_RESULT):
        assert not hasattr(result, "connect"), "Don't pass Qt signals"
        self.callbacks = []
        if isinstance(result, Promise):
            self.result = NO_RESULT
            result.then(self.set_result)
        else:
            self.result = result

    def set_result(self, result):
        assert self.result is NO_RESULT and result is not NO_RESULT
        self.result = result
        callbacks = self.callbacks
        self.callbacks = []
        for callback in callbacks:
            self.invoke_promise_callback(callback)

    def then(self, callback):
        if self.result is not NO_RESULT:
            self.invoke_promise_callback(callback)
        else:
            self.callbacks.append(callback)
        return self

    def invoke_promise_callback(self, callback):
        """Invoke a callback, passing in `self.result` if the callback accepts
        it and skipping it if not.  This lets us use signal/slot-like semantics
        where you don't have accept all available arguments.
        """
        params = list(inspect.signature(callback).parameters.values())

        positional_params = [
            param for param in params if param.kind in (
                inspect.Parameter.POSITIONAL_ONLY,
                inspect.Parameter.POSITIONAL_OR_KEYWORD)]
        has_varargs = any(
            param.kind == inspect.Parameter.VAR_POSITIONAL for param in params)
        required_positional = [
            param for param in positional_params
            if param.default is inspect._empty]

        # If there are no positional args and no *args, ignore `result`.
        if not positional_params and not has_varargs:
            return callback()

        if len(required_positional) > 1:
            raise TypeError(
                "Promise.then callback must accept 0 or 1 positional arguments")

        return callback(self.result)


class BackgroundTaskRunner(QtCore.QObject):
    started = QtCore.Signal()
    progress_percentage = QtCore.Signal(int)
    progress_message = QtCore.Signal(str)
    append_status = QtCore.Signal(str, bool)
    error = QtCore.Signal(str)
    finished = QtCore.Signal(object)

    def __init__(self, core_app, parent=None):
        super().__init__(parent)
        self.core_app = core_app
        self.background_worker = None
        self.background_thread = None
        self.background_worker_has_finished = False
        self.background_result = NO_RESULT
        self.background_promise = None

        # If we're running a background operation and Houdini is closed, we
        # want to shut the thread down cleanly or Houdini will crash.  Note
        # that Houdini doesn't support app.aboutToQuit so we use atexit.
        atexit.register(self.on_exit_houdini)


    def ensure_on_main_thread(self):
        app = QtWidgets.QApplication.instance()
        assert app is not None
        assert QtCore.QThread.currentThread() is app.thread()

    def run_generator_in_background(self, generator):
        self.ensure_on_main_thread()

        # If it was a regular function we'll receive its return value instead
        # of a generator.
        if not isinstance(generator, types.GeneratorType):
            return Promise(generator)

        # It's important that we set is_running_in_background before evaluating
        # the first part of the generator in this thread, in case that first
        # part calls something else that tries to run in the background.
        self.core_app.is_running_in_background = True

        # Run the first part in the foreground.  If the function returns
        # before it yields then we'll get a StopIteration right away.
        try:
            next(generator)
        except StopIteration as e:
            self.core_app.is_running_in_background = False
            return Promise(e.value)
        except:
            self.core_app.is_running_in_background = False
            raise

        # Now run the rest in a background thread.
        self.background_thread = QtCore.QThread(self)
        self.background_result = NO_RESULT
        self.background_worker_has_finished = False

        stack_trace = traceback.format_stack(sys._getframe().f_back)
        self.background_worker = BackgroundWorker(
            self.core_app, generator, stack_trace)
        self.background_worker.moveToThread(self.background_thread)
        self.background_thread.started.connect(self.background_worker.run)

        # Forward the background worker's signals to our signals for the
        # UI to consume.
        self.background_worker.progress_percentage.connect(
            self.progress_percentage)
        self.background_worker.progress_message.connect(self.progress_message)
        self.background_worker.append_status.connect(self.append_status)
        self.background_worker.error.connect(self.error)

        # We need to be very careful not to drop references to the background
        # worker and thread if the thread has events still queued on it when
        # it quits, or Houdini can crash.  So, we follow a very precise
        # shutdown sequence.
        self.background_worker.finished.connect(
            self.on_background_worker_finished)
        self.background_worker.finished.connect(self.background_thread.quit)
        self.background_worker.finished.connect(
            self.background_worker.deleteLater)

        self.background_thread.finished.connect(
            self.on_background_thread_finished)
        self.background_thread.finished.connect(
            self.background_thread.deleteLater)

        self.started.emit()

        # Start the thread and block until the loop is done.
        self.background_thread.start()
        self.background_promise = Promise()
        return self.background_promise

    @QtCore.Slot(object)
    def on_background_worker_finished(self, result):
        # When the worker finishes we store the result, and we wait for the
        # thread to finish before doing the rest of the work.
        self.ensure_on_main_thread()
        assert self.background_result is NO_RESULT
        self.background_result = result
        self.background_worker_has_finished = True

    @QtCore.Slot()
    def on_background_thread_finished(self):
        self.ensure_on_main_thread()
        assert self.background_worker_has_finished
        self.background_worker_has_finished = False

        result = self.background_result
        self.background_result= NO_RESULT

        self.core_app.is_running_in_background = False

        # Capture references before we clear them, so any accidental use
        # after this point is limited to this function.
        promise = self.background_promise
        self.background_promise = None

        worker = self.background_worker
        thread = self.background_thread
        self.background_thread = None
        self.background_worker = None

        # At this point the native thread has exited and the worker has been
        # deleteLater'd.  We can now resolve the promise.
        if promise is not None:
            promise.set_result(result)

        self.finished.emit(result)

    def on_cancel_background_work(self):
        if self.background_worker:
            self.background_worker.was_cancelled = True
        self.progress_message.emit("")

    def on_exit_houdini(self):
        if (not hasattr(self, "background_thread") or
                self.background_thread is None or
                not self.background_thread.isRunning()):
            return

        # Pretend the cancel button was pressed and wait for it to be
        # processed, with a maximum wait time.
        self.on_cancel_background_work()

        loop = QtCore.QEventLoop(self)
        self.background_thread.finished.connect(loop.quit)
        QtCore.QTimer.singleShot(5000, loop.quit)
        loop.exec()


class BackgroundWorker(QtCore.QObject):
    progress_percentage = QtCore.Signal(int)
    progress_message = QtCore.Signal(str)
    append_status = QtCore.Signal(str, bool)
    error = QtCore.Signal(str)
    finished = QtCore.Signal(object)

    def __init__(
            self, core_app, generator, main_thread_stack_trace,
            parent=None):
        super().__init__(parent)
        self.core_app = core_app
        self.generator = generator
        self.main_thread_stack_trace = main_thread_stack_trace
        self.was_cancelled = False

    def set_progress_fraction(self, core_app, fraction, prefix="", suffix=""):
        self.progress_percentage.emit(int(math.ceil(fraction * 100)))
        percent_str = f"{(100 * fraction):1.1f}"
        message = (prefix + (" " if prefix and suffix else "") + suffix +
            f": [{percent_str}%]")
        self.progress_message.emit(message)

    def erase_progress_bar(self, core_app=None):
        self.progress_message.emit("")

    def print_info(self, message, word_wrap=True):
        self.append_status.emit(message, word_wrap)

    @QtCore.Slot()
    def run(self):
        result = None
        try:
            with self.core_app.override_hooks() as hooks:
                hooks.set_progress_fraction = self.set_progress_fraction
                hooks.erase_progress_bar = self.erase_progress_bar
                hooks.print_info = self.print_info
                hooks.check_if_was_cancelled = lambda: self.was_cancelled

                try:
                    while True:
                        result = next(self.generator)
                except StopIteration as e:
                    result = e.value
                except core.Error as e:
                    self.core_app.toplevel_call_succeeded = False
                    self.erase_progress_bar()
                    self.print_info(str(e))
        except Exception as e:
            self.core_app.toplevel_call_succeeded = False
            combined_trace = "".join(
                ["An error occurred:\n"] +
                [f"{e}\nTraceback (most recent call last):\n"] +
                self.main_thread_stack_trace +
                traceback.format_tb(e.__traceback__))
            self.error.emit(combined_trace)
        finally:
            self.erase_progress_bar()
            self.finished.emit(result)
