"""PEP 3156 event loop based on CoreFoundation."""

import contextvars
import inspect
import sys
import threading
import warnings
from asyncio import (
    coroutines,
    events,
    tasks,
    unix_events,
)
from ctypes import CFUNCTYPE, POINTER, Structure, c_double, c_int, c_ulong, c_void_p

from .api import ObjCClass, objc_const
from .runtime import load_library, objc_id
from .types import CFIndex

if sys.version_info < (3, 14):
    from asyncio import (
        AbstractEventLoopPolicy,
        DefaultEventLoopPolicy,
        SafeChildWatcher,
        set_event_loop_policy,
    )
elif sys.version_info < (3, 16):
    # Python 3.14 finalized the deprecation of SafeChildWatcher. There's no
    # replacement API; the feature can be removed.
    #
    # Python 3.14 also started the deprecation of event loop policies, to be
    # finalized in Python 3.16; there was some symbol renaming to assist in
    # making the deprecation visible. See
    # https://github.com/python/cpython/issues/127949 for details.
    from asyncio import (
        _AbstractEventLoopPolicy as AbstractEventLoopPolicy,
        _DefaultEventLoopPolicy as DefaultEventLoopPolicy,
    )

__all__ = [
    "EventLoopPolicy",
    "CocoaLifecycle",
    "RubiconEventLoop",
    "iOSLifecycle",
]


###########################################################################
# CoreFoundation types and constants needed for async handlers
###########################################################################

libcf = load_library("CoreFoundation")

CFAllocatorRef = objc_id
kCFAllocatorDefault = None

CFDataRef = objc_id
CFOptionFlags = c_ulong
CFStringRef = objc_id
CFTypeRef = objc_id

CFRunLoopRef = objc_id
CFRunLoopMode = CFStringRef
CFRunLoopSourceRef = objc_id

CFRunLoopTimerRef = objc_id
CFRunLoopTimerCallBack = CFUNCTYPE(None, CFRunLoopTimerRef, c_void_p)

CFSocketRef = objc_id
CFSocketCallbackType = c_int
CFSocketCallback = CFUNCTYPE(
    None, CFSocketRef, CFSocketCallbackType, CFDataRef, c_void_p, c_void_p
)
CFSocketNativeHandle = c_int

CFTimeInterval = c_double
CFAbsoluteTime = CFTimeInterval


class CFRunLoopTimerContext(Structure):
    _fields_ = [
        # CFStringRef (*copyDescription)(const void *info)
        ("copyDescription", CFUNCTYPE(CFStringRef, c_void_p)),
        ("info", c_void_p),
        # void (*release)(const void *info)
        ("release", CFUNCTYPE(None, c_void_p)),
        # const void *(*retain)(const void *info)
        ("retain", CFUNCTYPE(None, c_void_p)),
        ("version", CFIndex),
    ]


kCFRunLoopCommonModes = objc_const(libcf, "kCFRunLoopCommonModes")

kCFSocketNoCallBack = 0
kCFSocketReadCallBack = 1
kCFSocketAcceptCallBack = 2
kCFSocketDataCallBack = 3
kCFSocketConnectCallBack = 4
kCFSocketWriteCallBack = 8

kCFSocketAutomaticallyReenableReadCallBack = 1
kCFSocketAutomaticallyReenableWriteCallBack = 8

NSRunLoop = ObjCClass("NSRunLoop")

###########################################################################
# CoreFoundation methods for async handlers
###########################################################################


class CFSocketContext(Structure):
    _fields_ = [
        # CFStringRef (*copyDescription)(const void *info)
        ("copyDescription", CFUNCTYPE(CFStringRef, c_void_p)),
        ("info", c_void_p),
        # void (*release)(const void *info)
        ("release", CFUNCTYPE(None, c_void_p)),
        # const void *(*retain)(const void *info)
        ("retain", CFUNCTYPE(None, c_void_p)),
        ("version", CFIndex),
    ]


libcf.CFAbsoluteTimeGetCurrent.restype = CFAbsoluteTime
libcf.CFAbsoluteTimeGetCurrent.argtypes = []

libcf.CFRelease.restype = CFTypeRef
libcf.CFRelease.argtypes = [CFTypeRef]

libcf.CFRetain.restype = CFTypeRef
libcf.CFRetain.argtypes = [CFTypeRef]

libcf.CFRunLoopAddSource.restype = None
libcf.CFRunLoopAddSource.argtypes = [CFRunLoopRef, CFRunLoopSourceRef, CFRunLoopMode]

libcf.CFRunLoopAddTimer.restype = None
libcf.CFRunLoopAddTimer.argtypes = [CFRunLoopRef, CFRunLoopTimerRef, CFRunLoopMode]

libcf.CFRunLoopGetMain.restype = CFRunLoopRef
libcf.CFRunLoopGetMain.argtypes = []

libcf.CFRunLoopGetCurrent.restype = CFRunLoopRef
libcf.CFRunLoopGetCurrent.argtypes = []

libcf.CFRunLoopRemoveSource.restype = None
libcf.CFRunLoopRemoveSource.argtypes = [CFRunLoopRef, CFRunLoopSourceRef, CFRunLoopMode]

libcf.CFRunLoopRemoveTimer.restype = None
libcf.CFRunLoopRemoveTimer.argtypes = [CFRunLoopRef, CFRunLoopTimerRef, CFRunLoopMode]

libcf.CFRunLoopRun.restype = None
libcf.CFRunLoopRun.argtypes = []

libcf.CFRunLoopStop.restype = None
libcf.CFRunLoopStop.argtypes = [CFRunLoopRef]

libcf.CFRunLoopTimerCreate.restype = CFRunLoopTimerRef
libcf.CFRunLoopTimerCreate.argtypes = [
    CFAllocatorRef,
    CFAbsoluteTime,
    CFTimeInterval,
    CFOptionFlags,
    CFIndex,
    CFRunLoopTimerCallBack,
    POINTER(CFRunLoopTimerContext),
]

libcf.CFSocketCreateRunLoopSource.restype = CFRunLoopSourceRef
libcf.CFSocketCreateRunLoopSource.argtypes = [CFAllocatorRef, CFSocketRef, CFIndex]

libcf.CFSocketCreateWithNative.restype = CFSocketRef
libcf.CFSocketCreateWithNative.argtypes = [
    CFAllocatorRef,
    CFSocketNativeHandle,
    CFOptionFlags,
    CFSocketCallback,
    POINTER(CFSocketContext),
]

libcf.CFSocketDisableCallBacks.restype = None
libcf.CFSocketDisableCallBacks.argtypes = [CFSocketRef, CFOptionFlags]

libcf.CFSocketEnableCallBacks.restype = None
libcf.CFSocketEnableCallBacks.argtypes = [CFSocketRef, CFOptionFlags]

libcf.CFSocketInvalidate.restype = None
libcf.CFSocketInvalidate.argtypes = [CFSocketRef]

libcf.CFSocketSetSocketFlags.restype = None
libcf.CFSocketSetSocketFlags.argtypes = [CFSocketRef, CFOptionFlags]


###########################################################################
# CoreFoundation types needed for async handlers
###########################################################################


class CFTimerHandle(events.TimerHandle):
    def _cf_timer_callback(self, callback, args):
        # Create a CF-compatible callback for a timer event
        def cf_timer_callback(cftimer, extra):
            callback(*args)
            # Deregister the callback after it has been performed.
            self._loop._timers.discard(self)

        return CFRunLoopTimerCallBack(cf_timer_callback)

    def __init__(self, *, loop, timeout, callback, args):
        super().__init__(
            libcf.CFAbsoluteTimeGetCurrent() + timeout,
            self._cf_timer_callback(callback, args),
            None,
            loop,
        )

        self._timeout = timeout

        # Retain a reference to the Handle
        self._loop._timers.add(self)

        # Create the timer event, and add it to the run loop.
        self._timer = libcf.CFRunLoopTimerCreate(
            kCFAllocatorDefault,
            self._when,
            0,  # interval
            0,  # flags
            0,  # order
            self._callback,  # callout
            None,  # context
        )

        libcf.CFRunLoopAddTimer(
            self._loop._cfrunloop, self._timer, kCFRunLoopCommonModes
        )

    def cancel(self):
        """Cancel the Timer handle."""
        super().cancel()
        libcf.CFRunLoopRemoveTimer(
            self._loop._cfrunloop, self._timer, kCFRunLoopCommonModes
        )
        self._loop._timers.discard(self)


class CFSocketHandle(events.Handle):
    # Create a CF-compatible callback for a source event
    def _cf_socket_callback(
        self, cfSocket, callbackType, ignoredAddress, ignoredData, context
    ):
        if self._fd not in self._loop._sockets:
            # Spurious notifications seem to be generated sometimes if you
            # CFSocketDisableCallBacks in the middle of an event.  I don't know
            # about this FD any more, so let's get rid of it.
            libcf.CFRunLoopRemoveSource(
                self._loop._cfrunloop, self._src, kCFRunLoopCommonModes
            )
            self._src = None
            return

        if callbackType == kCFSocketReadCallBack and self._reader:
            callback, args = self._reader
        elif callbackType == kCFSocketWriteCallBack and self._writer:
            callback, args = self._writer
        else:
            callback = None

        if callback:
            callback(*args)

    def __init__(self, *, loop, fd):
        """Register a file descriptor with the CFRunLoop, or modify its state
        so that it's listening for both notifications (read and write) rather
        than just one; used to implement add_reader and add_writer."""
        super().__init__(CFSocketCallback(self._cf_socket_callback), None, loop)

        # Retain a reference to the Handle
        self._loop._sockets[fd] = self
        self._reader = None
        self._writer = None

        self._fd = fd
        self._cf_socket = libcf.CFSocketCreateWithNative(
            kCFAllocatorDefault,
            self._fd,
            kCFSocketReadCallBack | kCFSocketWriteCallBack | kCFSocketConnectCallBack,
            self._callback,
            None,
        )
        libcf.CFSocketSetSocketFlags(
            self._cf_socket,
            kCFSocketAutomaticallyReenableReadCallBack
            | kCFSocketAutomaticallyReenableWriteCallBack,
            # # This extra flag is to ensure that CF doesn't (destructively,
            # # because destructively is the only way to do it) retrieve
            # # SO_ERROR
            # 1 << 6
        )
        self._src = libcf.CFSocketCreateRunLoopSource(
            kCFAllocatorDefault, self._cf_socket, 0
        )
        libcf.CFRunLoopAddSource(
            self._loop._cfrunloop, self._src, kCFRunLoopCommonModes
        )
        libcf.CFSocketDisableCallBacks(
            self._cf_socket,
            kCFSocketReadCallBack | kCFSocketWriteCallBack | kCFSocketConnectCallBack,
        )

    def enable_read(self, callback, args):
        """Add a callback for read activity on the socket."""
        libcf.CFSocketEnableCallBacks(self._cf_socket, kCFSocketReadCallBack)
        self._reader = (callback, args)

    def disable_read(self):
        """Remove the callback for read activity on the socket."""
        libcf.CFSocketDisableCallBacks(self._cf_socket, kCFSocketReadCallBack)
        self._reader = None
        self.cancel()

    def enable_write(self, callback, args):
        """Add a callback for write activity on the socket."""
        libcf.CFSocketEnableCallBacks(self._cf_socket, kCFSocketWriteCallBack)
        self._writer = (callback, args)

    def disable_write(self):
        """Remove the callback for write activity on the socket."""
        libcf.CFSocketDisableCallBacks(self._cf_socket, kCFSocketWriteCallBack)
        self._writer = None
        self.cancel()

    def cancel(self):
        """(Potentially) cancel the socket handle.

        A socket handle can have both reader and writer components; a call to cancel a
        socket handle will only be successful if *both* the reader and writer component
        have been disabled. If either is still active, cancel() will be a no-op.
        """
        if self._reader is None and self._writer is None and self._src:
            super().cancel()
            del self._loop._sockets[self._fd]

            libcf.CFRunLoopRemoveSource(
                self._loop._cfrunloop, self._src, kCFRunLoopCommonModes
            )
            libcf.CFSocketInvalidate(self._cf_socket)


def context_callback(context, callback):
    if context is None:
        context = contextvars.copy_context()

    def _callback(*args):
        context.run(callback, *args)

    return _callback


class CFEventLoop(unix_events.SelectorEventLoop):
    def __init__(self, lifecycle=None):
        self._lifecycle = lifecycle
        self._cfrunloop = libcf.CFRetain(libcf.CFRunLoopGetCurrent())
        self._running = False

        self._timers = set()
        self._accept_futures = {}
        self._sockets = {}

        super().__init__()

    def __del__(self):
        libcf.CFRelease(self._cfrunloop)
        super().__del__()

    def _add_reader(self, fd, callback, *args):
        try:
            handle = self._sockets[fd]
        except KeyError:
            handle = CFSocketHandle(loop=self, fd=fd)
            self._sockets[fd] = handle

        handle.enable_read(callback, args)

    def add_reader(self, fd, callback, *args):
        """Add a reader callback.

        Method is a direct call through to _add_reader to reflect an
        internal implementation detail added in Python3.5.
        """
        self._add_reader(fd, callback, *args)

    def _remove_reader(self, fd):
        try:
            self._sockets[fd].disable_read()
            return True
        except KeyError:
            return False

    def remove_reader(self, fd):
        """Remove a reader callback.

        Method is a direct call through to _remove_reader to reflect an
        internal implementation detail added in Python3.5.
        """
        self._remove_reader(fd)

    def _add_writer(self, fd, callback, *args):
        try:
            handle = self._sockets[fd]
        except KeyError:
            handle = CFSocketHandle(loop=self, fd=fd)
            self._sockets[fd] = handle

        handle.enable_write(callback, args)

    def add_writer(self, fd, callback, *args):
        """Add a writer callback.

        Method is a direct call through to _add_writer to reflect an
        internal implementation detail added in Python3.5.
        """
        self._add_writer(fd, callback, *args)

    def _remove_writer(self, fd):
        try:
            self._sockets[fd].disable_write()
            return True
        except KeyError:
            return False

    def remove_writer(self, fd):
        """Remove a writer callback.

        Method is a direct call through to _remove_writer to reflect an
        internal implementation detail added in Python3.5.
        """
        self._remove_writer(fd)

    ######################################################################
    # Lifecycle and execution
    ######################################################################
    def _check_not_coroutine(self, callback, name):
        """Check whether the given callback is a coroutine or not."""
        if coroutines.iscoroutine(callback) or inspect.iscoroutinefunction(callback):
            raise TypeError(f"coroutines cannot be used with {name}()")

    def is_running(self):
        """Returns True if the event loop is running."""
        return self._running

    def run(self):
        """Internal implementation of run using the CoreFoundation event
        loop."""
        recursive = self.is_running()
        if (
            not recursive
            and hasattr(events, "_get_running_loop")
            and events._get_running_loop()
        ):
            raise RuntimeError(
                "Cannot run the event loop while another loop is running"
            )

        if not recursive:
            self._running = True
            if hasattr(events, "_set_running_loop"):
                events._set_running_loop(self)

        try:
            self._lifecycle.start()
        finally:
            if not recursive:
                self._running = False
                if hasattr(events, "_set_running_loop"):
                    events._set_running_loop(None)

    def run_until_complete(self, future, **kw):
        """Run until the Future is done.

        If the argument is a coroutine, it is wrapped in a Task.

        WARNING: It would be disastrous to call run_until_complete()
        with the same coroutine twice -- it would wrap it in two
        different Tasks and that can't be good.

        Return the Future's result, or raise its exception.
        """

        def stop(f):
            self.stop()

        future = tasks.ensure_future(future, loop=self)
        future.add_done_callback(stop)
        try:
            self.run_forever(**kw)
        finally:
            future.remove_done_callback(stop)

        if not future.done():
            raise RuntimeError("Event loop stopped before Future completed.")

        return future.result()

    def run_forever(self, lifecycle=None):
        """Run until stop() is called."""
        if not self._lifecycle:
            self._set_lifecycle(
                lifecycle if lifecycle else CFLifecycle(self._cfrunloop)
            )

        if self.is_running():
            raise RuntimeError(
                "Recursively calling run_forever is forbidden. "
                "To recursively run the event loop, call run()."
            )

        try:
            self.run()
        finally:
            self.stop()

    def run_forever_cooperatively(self, lifecycle=None):
        """A non-blocking version of :meth:`run_forever`.

        This may seem like nonsense; however, an iOS app is not expected to
        invoke a blocking "main event loop" method. As a result, we need to
        be able to *start* Python event loop handling, but then return control
        to the main app to start the actual event loop.

        The implementation is effectively all the parts of a call to
        :meth:`run_forever()`, but without any of the shutdown/cleanup logic.
        """
        if not self._lifecycle:
            self._set_lifecycle(
                lifecycle if lifecycle else CFLifecycle(self._cfrunloop)
            )

        if self.is_running():
            raise RuntimeError(
                "Recursively calling run_forever is forbidden. "
                "To recursively run the event loop, call run()."
            )

        self._running = True
        if hasattr(events, "_set_running_loop"):
            events._set_running_loop(self)

        # Start the lifecycle, but invoke it as a deferred event on the event
        # loop. iOSLifeCycle.start() starts the NSRunLoop; this ensures
        # that a full NSRunLoop is running, not just one that responds to
        # iOS events. See #228 for the sort of behavior that occurs on threads
        # if the NSRunLoop isn't started.
        self.call_soon(self._lifecycle.start)

    def call_soon(self, callback, *args, context=None):
        """Arrange for a callback to be called as soon as possible.

        This operates as a FIFO queue: callbacks are called in the
        order in which they are registered.  Each callback will be
        called exactly once.

        Any positional arguments after the callback will be passed to
        the callback when it is called.
        """
        self._check_not_coroutine(callback, "call_soon")

        return CFTimerHandle(
            loop=self,
            timeout=0,
            callback=context_callback(context, callback),
            args=args,
        )

    call_soon_threadsafe = call_soon

    def call_later(self, delay, callback, *args, context=None):
        """Arrange for a callback to be called at a given time.

        Return a Handle: an opaque object with a cancel() method that
        can be used to cancel the call.

        The delay can be an int or float, expressed in seconds.  It is
        always relative to the current time.

        Each callback will be called exactly once.  If two callbacks
        are scheduled for exactly the same time, it undefined which
        will be called first.

        Any positional arguments after the callback will be passed to
        the callback when it is called.
        """
        self._check_not_coroutine(callback, "call_later")

        return CFTimerHandle(
            loop=self,
            timeout=delay,
            callback=context_callback(context, callback),
            args=args,
        )

    def call_at(self, when, callback, *args, context=None):
        """Like call_later(), but uses an absolute time.

        Absolute time corresponds to the event loop's time() method.
        """
        self._check_not_coroutine(callback, "call_at")

        return CFTimerHandle(
            loop=self,
            timeout=when - self.time(),
            callback=context_callback(context, callback),
            args=args,
        )

    def time(self):
        """Return the time according to the event loop's clock.

        This is a float expressed in seconds since an epoch, but the
        epoch, precision, accuracy and drift are unspecified and may
        differ per event loop.
        """
        return libcf.CFAbsoluteTimeGetCurrent()

    def stop(self):
        """Stop running the event loop.

        Every callback already scheduled will still run.  This simply
        informs run_forever to stop looping after a complete iteration.
        """
        super().stop()
        self._lifecycle.stop()

    def close(self):
        """Close the event loop.

        This clears the queues and shuts down the executor,
        but does not wait for the executor to finish.

        The event loop must not be running.
        """
        while self._accept_futures:
            future = self._accept_futures.pop()
            future.cancel()

        while self._timers:
            handler = self._timers.pop()
            handler.cancel()

        super().close()

    def _set_lifecycle(self, lifecycle):
        """Set the application lifecycle that is controlling this loop."""
        if self._lifecycle is not None:
            raise ValueError("Lifecycle is already set")
        if self.is_running():
            raise RuntimeError(
                "You can't set a lifecycle on a loop that's already running."
            )
        self._lifecycle = lifecycle
        if sys.version_info < (3, 14):
            self._policy._lifecycle = lifecycle

    def _add_callback(self, handle):
        """Add a callback to be invoked ASAP.

        The inherited behavior uses a self-pipe to wake up the event loop
        in a thread-safe fashion, which causes the logic in run_once() to
        empty the list of handlers that are awaiting invocation.

        CFEventLoop doesn't use run_once(), so adding handlers to
        self._ready results in handlers that aren't invoked. Instead, we
        create a 0-interval timer to invoke the callback as soon as
        possible.
        """
        if handle._cancelled:
            return
        self.call_soon(handle._callback, *handle._args)


if sys.version_info < (3, 16):

    class EventLoopPolicy(AbstractEventLoopPolicy):
        """Rubicon event loop policy.

        In this policy, each thread has its own event loop. However, we only
        automatically create an event loop by default for the main thread; other
        threads by default have no event loop.

        **DEPRECATED** - Python 3.14 deprecated the concept of manually creating
        EventLoopPolicies. Create and use a ``RubiconEventLoop`` instance instead of
        installing an event loop policy and calling ``asyncio.new_event_loop()``.
        """

        def __init__(self):
            warnings.warn(
                "Custom EventLoopPolicy instances have been deprecated by Python 3.14. "
                "Create and use a `RubiconEventLoop` instance directly instead of "
                "installing an event loop policy and calling `asyncio.new_event_loop()`.",
                DeprecationWarning,
                stacklevel=2,
            )

            self._lifecycle = None
            self._default_loop = None
            if sys.version_info < (3, 14):
                self._watcher_lock = threading.Lock()
                self._watcher = None
            self._policy = DefaultEventLoopPolicy()
            self._policy.new_event_loop = self.new_event_loop
            self.get_event_loop = self._policy.get_event_loop
            self.set_event_loop = self._policy.set_event_loop

        def new_event_loop(self):
            """Create a new event loop and return it."""
            if (
                not self._default_loop
                and threading.current_thread() == threading.main_thread()
            ):
                loop = self.get_default_loop()
            else:
                loop = CFEventLoop(self._lifecycle)
            loop._policy = self

            return loop

        def get_default_loop(self):
            """Get the default event loop."""
            if not self._default_loop:
                self._default_loop = self._new_default_loop()
            return self._default_loop

        def _new_default_loop(self):
            loop = CFEventLoop(self._lifecycle)
            loop._policy = self
            return loop

        if sys.version_info < (3, 14):

            def _init_watcher(self):
                with events._lock:
                    if self._watcher is None:  # pragma: no branch
                        self._watcher = SafeChildWatcher()
                        if threading.current_thread() == threading.main_thread():
                            self._watcher.attach_loop(self._default_loop)

            def get_child_watcher(self):
                """Get the watcher for child processes.

                If not yet set, a :class:`~asyncio.SafeChildWatcher` object is
                automatically created.

                .. note::
                    Child watcher support was removed in Python 3.14
                """
                if self._watcher is None:
                    self._init_watcher()

                return self._watcher

            def set_child_watcher(self, watcher):
                """Set the watcher for child processes.

                .. note::
                    Child watcher support was removed in Python 3.14
                """
                if self._watcher is not None:
                    self._watcher.close()

                self._watcher = watcher


if sys.version_info < (3, 14):

    def RubiconEventLoop():
        """Create a new Rubicon CFEventLoop instance."""
        # If they're using RubiconEventLoop(), they've done the necessary adaptation.
        with warnings.catch_warnings():
            warnings.filterwarnings(
                "ignore",
                message=r"^Custom EventLoopPolicy instances have been deprecated by Python 3.14",
                category=DeprecationWarning,
            )
            policy = EventLoopPolicy()
        set_event_loop_policy(policy)
        return policy.new_event_loop()

else:
    RubiconEventLoop = CFEventLoop


class CFLifecycle:
    """A lifecycle manager for raw CoreFoundation apps."""

    def __init__(self, cfrunloop):
        self._cfrunloop = cfrunloop

    def start(self):
        libcf.CFRunLoopRun()

    def stop(self):
        libcf.CFRunLoopStop(self._cfrunloop)


class CocoaLifecycle:
    """A life cycle manager for Cocoa (``NSApplication``) apps."""

    def __init__(self, application):
        self._application = application

    def start(self):
        self._application.run()

    def stop(self):
        self._application.terminate(None)


class iOSLifecycle:
    """A life cycle manager for iOS (``UIApplication``) apps."""

    def start(self):
        NSRunLoop.currentRunLoop.run()

    def stop(self):
        libcf.CFRunLoopStop(libcf.CFRunLoopGetMain())
