"""
Platform specific process customization.
"""
import logging
import subprocess
import sys
import threading
from contextlib import contextmanager
from typing import Optional, Sequence, Callable

logger = logging.getLogger(__name__)


class AbstractNativeProcessAdapter(object):
    @classmethod
    @contextmanager
    def start(
        cls,
        process_type: str,
        py_shutdown_fn: Callable[[bool], Optional[bool]],
        restart_argv: Optional[Sequence[str]] = None,
    ):
        """
        Start and return a thread that will run necessary platform specific integrations and will live as long as
        the main work thread. In case this thread stops due to an error, the process shall exit.

        The Event yielded may be used to signal faster cleanup to shutdown requests (at logoff for example).

        :param process_type: string
        :param py_shutdown_fn: A callable that shall signal the work thread to exit. When the shutdown is initiated
            by the user this shall return True to notify this layer that the restart shall be unregistered from the
            manager. The argument is True if the shutdown is for session ending, False fo all other reasons.
        :param restart_argv:
        """
        work_end = threading.Event()

        def _wait():
            work_end.wait()

        native_thread = threading.Thread(target=cls._start, args=(process_type, _wait, py_shutdown_fn, restart_argv))
        native_thread.start()
        try:
            yield work_end
        finally:
            work_end.set()
            native_thread.join()

    @classmethod
    def _start(
        cls,
        process_type: str,
        py_loop_fn: Callable[[], None],
        py_shutdown_fn: Callable[[bool], bool],
        restart_argv: Optional[Sequence[str]],
    ):
        try:
            self = cls(process_type, py_shutdown_fn)
            self._run(py_loop_fn, restart_argv)
        except Exception:
            logger.exception("Native process adapter error")
            raise
        finally:
            # Ensure work thread is notified to shut down with this one in case of an error.
            py_shutdown_fn(False)  # NOTE: session_end is N/A at this point anyways

    def __init__(self, process_type: str, py_shutdown_fn: Callable[[bool], bool]):
        self._process_type = process_type  # informative
        self._py_shutdown_fn = py_shutdown_fn

    def _run(self, py_loop_fn: Callable[[], None], restart_argv: Optional[Sequence[str]] = None):
        raise NotImplementedError()


if sys.platform.startswith("win"):
    from ctypes import windll

    import win32api
    import win32con
    import win32gui

    class WindowsProcessAdapter(AbstractNativeProcessAdapter):
        """
        Integrate with Windows session handling by using a hidden window and the Windows messages event loop
        in parallel with the primary work put into a thread.
        """

        BLOCK_WAIT_FOR_EXIT_READY = 30.0

        def __init__(self, process_type: str, py_shutdown_fn: Callable[[bool], bool]):
            AbstractNativeProcessAdapter.__init__(self, process_type, py_shutdown_fn)
            self._exit_ready = threading.Event()
            self._wnd_class = win32gui.WNDCLASS()
            self._wnd_class.lpszClassName = f"ClarabotNano{process_type.title()}WClass"
            message_map = {
                win32con.WM_QUERYENDSESSION: self._query_session_end_event_handler,
                win32con.WM_ENDSESSION: self._session_end_event_handler,
                win32con.WM_CLOSE: self._close_event_handler,
                # NOTE: The win32con.WM_QUIT is handled normally, which means it will stop the message loop processing.
            }
            self._wnd_class.lpfnWndProc = message_map
            self._reg_wnd_class = win32gui.RegisterClass(self._wnd_class)
            self._wnd_handle = win32gui.CreateWindowEx(
                win32con.WS_EX_LEFT,
                self._reg_wnd_class,
                f"ClarabotNano{process_type.title()}W",
                0,
                0,
                0,
                win32con.CW_USEDEFAULT,
                win32con.CW_USEDEFAULT,
                0,  # NOTE: The win32con.HWND_MESSAGE value would prevent the broadcast messages from being caught.
                0,
                None,
                None,
            )
            if not self._wnd_handle:
                raise Exception(f"Failed to create {process_type} window: {win32api.GetLastError()}")
            win32gui.ShowWindow(self._wnd_handle, win32con.SW_HIDE)
            # This is necessary to retain console ctrl event handling in case we have one.
            win32api.SetConsoleCtrlHandler(self._console_events_handler, True)

        def _console_events_handler(self, evt):
            if evt in (
                win32con.CTRL_C_EVENT,
                win32con.CTRL_BREAK_EVENT,
                win32con.CTRL_LOGOFF_EVENT,
                win32con.CTRL_SHUTDOWN_EVENT,
            ):
                self._py_shutdown_fn(evt in (win32con.CTRL_LOGOFF_EVENT, win32con.CTRL_SHUTDOWN_EVENT))
                self._exit_ready.wait(self.BLOCK_WAIT_FOR_EXIT_READY)
                return True
            return False

        def _query_session_end_event_handler(self, hwnd, msg, wparam, lparam):
            assert msg == win32con.WM_QUERYENDSESSION
            # NOTE: Returning False or delaying will only block the shutdown of this process if there is at least
            # one visible window for it.
            return True  # agree to terminate

        def _session_end_event_handler(self, hwnd, msg, wparam, lparam):
            assert msg == win32con.WM_ENDSESSION
            if wparam:
                # The lparam can be examined to see what kind of shutdown is happening. For example
                # ENDSESSION_CLOSEAPP means that the Restart Manager is trying to coordinate a
                # logoff/shutdown shutdown or just wants this process gone.
                self._py_shutdown_fn(True)  # Shutdown quickly.
                self._exit_ready.wait(self.BLOCK_WAIT_FOR_EXIT_READY)
            return 0

        def _close_event_handler(self, hwnd, msg, wparam, lparam):
            assert msg == win32con.WM_CLOSE
            # This is received when the user explicitly wants to close the process rather than doing it through
            # the control GUI. Such may happen by "taskkill /pid <pid>" for example. This is not the usual way
            # that we shut down, but we should support it regardless.
            # NOTE: It is ambiguous if this is explicitly done by the user, after WM_ENDSESSION the system also
            # sends the WM_CLOSE message to processes according to the docs.
            self._py_shutdown_fn(False)
            self._exit_ready.wait(self.BLOCK_WAIT_FOR_EXIT_READY)
            return 0

        def _run(self, py_loop_fn: Callable[[], None], restart_argv: Optional[Sequence[str]] = None):
            if self._exit_ready.is_set():
                raise Exception(f"Can't start {self._process_type} with exit_ready set.")
            main_thread_id = threading.get_native_id()
            if restart_argv is not None:
                restart_argv = list(restart_argv)[1:]  # Restart Manager needs the args without the executable.
                restart_argv = subprocess.list2cmdline(restart_argv)  # Restart Manager needs the arguments as string.
                res = windll.kernel32.RegisterApplicationRestart(restart_argv, 0)
                if res != 0:
                    logger.warning("Failed to register application for restart: %s %r", res, restart_argv)

            def _py_loop_wrapper():
                """
                Stop the windows event loop if the python thread exits and propagate errors.
                """
                try:
                    py_loop_fn()
                except Exception:
                    logger.exception("Native process waiter error")
                    raise
                finally:
                    if not self._exit_ready.is_set():
                        self._exit_ready.set()
                    win32gui.PostThreadMessage(main_thread_id, win32con.WM_QUIT, 0, 0)

            py_waiter = threading.Thread(target=_py_loop_wrapper)
            py_waiter.start()
            win32gui.PumpMessages()  # main loop until WM_QUIT
            explicit_user_request = self._py_shutdown_fn(False)  # should be noop/query if already called
            self._exit_ready.wait(self.BLOCK_WAIT_FOR_EXIT_READY)  # necessary if the message loop stops first
            if restart_argv is not None and explicit_user_request:  # Shutdown by explicit user request.
                res = windll.kernel32.UnregisterApplicationRestart()
                if res != 0:
                    logger.warning("Failed to unregister application restart: %s", res)
            py_waiter.join()

    NativeProcessAdapter = WindowsProcessAdapter  # alias for current impl

# TODO: A launchd plist file could be automatically created for the running user on darwin... should this be in
#  the configuration wizard / UI?
#  https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html

else:

    class NoopProcessAdapter(AbstractNativeProcessAdapter):
        def __init__(self, process_type: str, py_shutdown_fn: Callable[[bool], bool]):
            AbstractNativeProcessAdapter.__init__(self, process_type, py_shutdown_fn)

        def _run(self, py_loop_fn: Callable[[], None], restart_argv: Optional[Sequence[str]] = None):
            py_loop_fn()

    NativeProcessAdapter = NoopProcessAdapter  # alias for current impl
