"""A parent poller for unix."""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.

try:
    import ctypes
except ImportError:
    ctypes = None  # type:ignore[assignment]
import os
import platform
import signal
import time
import warnings
from _thread import interrupt_main  # Py 3
from threading import Thread

from traitlets.log import get_logger


class ParentPollerUnix(Thread):
    """A Unix-specific daemon thread that terminates the program immediately
    when the parent process no longer exists.
    """

    def __init__(self, parent_pid=0):
        """Initialize the poller.

        Parameters
        ----------
        parent_handle : int, optional
            If provided, the program will terminate immediately when
            process parent is no longer this original parent.
        """
        super().__init__()
        self.parent_pid = parent_pid
        self.daemon = True

    def run(self):
        """Run the poller."""
        # We cannot use os.waitpid because it works only for child processes.
        from errno import EINTR

        # before start, check if the passed-in parent pid is valid
        original_ppid = os.getppid()
        if original_ppid != self.parent_pid:
            self.parent_pid = 0

        get_logger().debug(
            "%s: poll for parent change with original parent pid=%d",
            type(self).__name__,
            self.parent_pid,
        )

        while True:
            try:
                ppid = os.getppid()
                parent_is_init = not self.parent_pid and ppid == 1
                parent_has_changed = self.parent_pid and ppid != self.parent_pid
                if parent_is_init or parent_has_changed:
                    get_logger().warning("Parent appears to have exited, shutting down.")
                    os._exit(1)
                time.sleep(1.0)
            except OSError as e:
                if e.errno == EINTR:
                    continue
                raise


class ParentPollerWindows(Thread):
    """A Windows-specific daemon thread that listens for a special event that
    signals an interrupt and, optionally, terminates the program immediately
    when the parent process no longer exists.
    """

    def __init__(self, interrupt_handle=None, parent_handle=None):
        """Create the poller. At least one of the optional parameters must be
        provided.

        Parameters
        ----------
        interrupt_handle : HANDLE (int), optional
            If provided, the program will generate a Ctrl+C event when this
            handle is signaled.
        parent_handle : HANDLE (int), optional
            If provided, the program will terminate immediately when this
            handle is signaled.
        """
        assert interrupt_handle or parent_handle
        super().__init__()
        if ctypes is None:
            msg = "ParentPollerWindows requires ctypes"  # type:ignore[unreachable]
            raise ImportError(msg)
        self.daemon = True
        self.interrupt_handle = interrupt_handle
        self.parent_handle = parent_handle

    def run(self):
        """Run the poll loop. This method never returns."""
        try:
            from _winapi import INFINITE, WAIT_OBJECT_0  # type:ignore[attr-defined]
        except ImportError:
            from _subprocess import INFINITE, WAIT_OBJECT_0

        # Build the list of handle to listen on.
        handles = []
        if self.interrupt_handle:
            handles.append(self.interrupt_handle)
        if self.parent_handle:
            handles.append(self.parent_handle)
        arch = platform.architecture()[0]
        c_int = ctypes.c_int64 if arch.startswith("64") else ctypes.c_int

        # Listen forever.
        while True:
            result = ctypes.windll.kernel32.WaitForMultipleObjects(  # type:ignore[attr-defined]
                len(handles),  # nCount
                (c_int * len(handles))(*handles),  # lpHandles
                False,  # bWaitAll
                INFINITE,
            )  # dwMilliseconds

            if WAIT_OBJECT_0 <= result < len(handles):
                handle = handles[result - WAIT_OBJECT_0]

                if handle == self.interrupt_handle:
                    # check if signal handler is callable
                    # to avoid 'int not callable' error (Python issue #23395)
                    if callable(signal.getsignal(signal.SIGINT)):
                        interrupt_main()

                elif handle == self.parent_handle:
                    get_logger().warning("Parent appears to have exited, shutting down.")
                    os._exit(1)
            elif result < 0:
                # wait failed, just give up and stop polling.
                warnings.warn(
                    """Parent poll failed.  If the frontend dies,
                the kernel may be left running.  Please let us know
                about your system (bitness, Python, etc.) at
                ipython-dev@scipy.org""",
                    stacklevel=2,
                )
                return
