/*
 * ****************************************************************************
 * Copyright (c) 2013-2023, PyInstaller Development Team.
 *
 * Distributed under the terms of the GNU General Public License (version 2
 * or later) with exception for distributing the bootloader.
 *
 * The full license is in the file COPYING.txt, distributed with this software.
 *
 * SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
 * ****************************************************************************
 */

/*
 * Handling of Apple Events in macOS windowed (app bundle) mode:
 *  - argv emulation
 *  - event forwarding to child process
 */

#if defined(__APPLE__) && defined(WINDOWED)

#include <Carbon/Carbon.h>  /* AppleEventsT */
#include <ApplicationServices/ApplicationServices.h> /* GetProcessForPID, etc */

#include "pyi_global.h"
#include "pyi_utils.h"
#include "pyi_apple_events.h"


/* Not declared in modern headers but exists in Carbon libs since time immemorial
 * See: https://applescriptlibrary.files.wordpress.com/2013/11/apple-events-programming-guide.pdf */
extern Boolean ConvertEventRefToEventRecord(EventRef inEvent, EventRecord *outEvent);

/*
 * On Mac OS X this converts events from kAEOpenDocuments and kAEGetURL into sys.argv.
 * After startup, it also forwards kAEOpenDocuments and KAEGetURL events at runtime to the child process.
 *
 * TODO: The below can be simplified considerably if re-written in Objective C (e.g. put into pyi_utils_osx.m).
 */


/* Static context structure for keeping track of data */
static struct AppleEventHandlerContext
{
    /* Event handlers for argv-emu / event forwarding */
    Boolean installed;  /* Are handlers installed? */

    EventHandlerUPP upp_handler;  /* UPP for event handler callback */
    AEEventHandlerUPP upp_handler_ae;  /* UPP for AppleEvent handler callback */

    EventHandlerRef handler_ref;  /* Reference to installer event handler */

    /* Deferred/pending event forwarding */
    Boolean has_pending_event;  /* Flag indicating that pending_event is valid */
    unsigned int retry_count;  /* Retry count for send attempts */
    AppleEvent pending_event;  /* Copy of the event */
} _ae_ctx = {
    false,  /* installed */
    NULL,  /* handler */
    NULL,  /* handler_ae */
    NULL,  /* handler_ref */
    false,  /* has_pending_event */
    0,  /* retry count */
    {typeNull, nil},  /* pending event */
};

/* Event types list: used to register handler and to listen for events */
static const EventTypeSpec event_types_ae[] = {
    { kEventClassAppleEvent, kEventAppleEvent },
};


/* Convert a FourCharCode into a string (useful for debug). Returned buffer is a static buffer, so subsequent calls
 * may overwrite the same buffer. */
static const char *CC2Str(FourCharCode code) {
    /* support up to 3 calls on the same debug print line */
    static char bufs[3][5];
    static unsigned int bufsidx = 0;
    char *buf = bufs[bufsidx++ % 3u];
    snprintf(buf, 5, "%c%c%c%c", (code >> 24) & 0xFF, (code >> 16) & 0xFF, (code >> 8) & 0xFF, code & 0xFF);
    /* buffer is guaranteed to be nul terminated here */
    return buf;
}

/* Generic event forwarder -- forwards an event destined for this process to the child process,
 * copying its param object, if any. Parameter `theAppleEvent` may be NULL, in which case a new
 * event is created with the specified class and id (containing 0 params / no param object). */
static OSErr generic_forward_apple_event(const AppleEvent *const theAppleEvent /* NULL ok */,
                                         const AEEventClass eventClass, const AEEventID evtID,
                                         const char *const descStr)
{
    const FourCharCode evtCode = (FourCharCode)evtID;
    OSErr err;
    AppleEvent childEvent;
    AEAddressDesc target;
    DescType actualType = 0, typeCode = typeWildCard;
    char *buf = NULL; /* dynamic buffer to hold copied event param data */
    Size bufSize = 0, actualSize = 0;
    pid_t child_pid;

    VS("LOADER [AppleEvent]: Forwarder called for \"%s\".\n", descStr);

    child_pid = pyi_utils_get_child_pid();
    if (!child_pid) {
        /* Child not up yet -- there is no way to "forward" this before child started!. */
         VS("LOADER [AppleEvent]: Child not up yet (child_pid is 0)\n");
         return errAEEventNotHandled;
    }
    VS("LOADER [AppleEvent]: Forwarding '%s' event.\n", CC2Str(evtCode));
    err = AECreateDesc(typeKernelProcessID, &child_pid, sizeof(child_pid), &target);
    if (err != noErr) {
        OTHERERROR("LOADER [AppleEvent]: Failed to create AEAddressDesc: %d\n", (int)err);
        goto out;
    }
    VS("LOADER [AppleEvent]: Created AEAddressDesc.\n");
    err = AECreateAppleEvent(eventClass, evtID, &target, kAutoGenerateReturnID, kAnyTransactionID,
                             &childEvent);
    if (err != noErr) {
        OTHERERROR("LOADER [AppleEvent]: Failed to create event copy: %d\n", (int)err);
        goto release_desc;
    }
    VS("LOADER [AppleEvent]: Created AppleEvent instance for child process.\n");


    if (!theAppleEvent) {
        /* Calling code wants a new event created from scratch, we do so
         * here and it will have 0 params. Assumption: caller knows that
         * the event type in question normally has 0 params. */
        VS("LOADER [AppleEvent]: New AppleEvent class: '%s' code: '%s'\n",
           CC2Str((FourCharCode)eventClass), CC2Str((FourCharCode)evtID));
    } else {
        err = AESizeOfParam(theAppleEvent, keyDirectObject, &typeCode, &bufSize);
        if (err != noErr) {
            /* No params for this event */
            VS("LOADER [AppleEvent]: Failed to get size of param (error=%d) -- event '%s' may lack params.\n",
                (int)err, CC2Str(evtCode));
        } else  {
            /* This event has a param object, copy it. */

            VS("LOADER [AppleEvent]: Got size of param: %ld\n", (long)bufSize);
            buf = malloc(bufSize);
            if (!buf) {
                /* Failed to allocate buffer! */
                OTHERERROR("LOADER [AppleEvent]: Failed to allocate buffer of size %ld: %s\n",
                           (long)bufSize, strerror(errno));
                goto release_evt;
            }
            VS("LOADER [AppleEvent]: Allocated buffer of size: %ld\n", (long)bufSize);
            VS("LOADER [AppleEvent]: Getting param.\n");
            err = AEGetParamPtr(theAppleEvent, keyDirectObject, typeWildCard,
                                &actualType, buf, bufSize, &actualSize);
            if (err != noErr) {
                OTHERERROR("LOADER [AppleEvent]: Failed to get param data.\n");
                goto release_evt;
            }
            if (actualSize > bufSize) {
                /* From reading the Apple API docs, this should never happen, but it pays
                 * to program defensively here. */
                OTHERERROR("LOADER [AppleEvent]: Got param size=%ld > bufSize=%ld, error!\n",
                           (long)actualSize, (long)bufSize);
                goto release_evt;
            }
            VS("LOADER [AppleEvent]: Got param type=%x ('%s') size=%ld\n",
               (UInt32)actualType, CC2Str((FourCharCode)actualType), (long)actualSize);
            VS("LOADER [AppleEvent]: Putting param.\n");
            err = AEPutParamPtr(&childEvent, keyDirectObject, actualType, buf, actualSize);
            if (err != noErr) {
                OTHERERROR("LOADER [AppleEvent]: Failed to put param data.\n");
                goto release_evt;
            }
        }
    }
    VS("LOADER [AppleEvent]: Sending message...\n");
    err = AESendMessage(&childEvent, NULL, kAENoReply, kAEDefaultTimeout);
    VS("LOADER [AppleEvent]: Handler sent \"%s\" message to child pid %ld.\n", descStr, (long)child_pid);

    /* In a onefile build, we may encounter a race condition between the parent
     * and the child process, because child_pid becomes valid immediately after
     * the process is forked, but the child process may not be able to receive
     * the events yet. In such cases, AESendMessage fails with procNotFound (-600).
     * To accommodate this situation,  we defer the event by storing its copy in our
     * event context structure, so that the caller can re-attempt to send it using
     * the pyi_apple_send_pending_event() function.
     */
    if (err == procNotFound) {
        VS("LOADER [AppleEvent]: Sending failed with procNotFound; storing the pending event...\n");

        err = AEDuplicateDesc(&childEvent, &_ae_ctx.pending_event);
        if (err == noErr) {
            _ae_ctx.retry_count = 0;
            _ae_ctx.has_pending_event = true;
        } else {
            VS("LOADER [AppleEvent]: Failed to copy the pending event: %d\n", (int)err);
            _ae_ctx.has_pending_event = false;
            _ae_ctx.retry_count = 0;
        }
    }

release_evt:
    free(buf);
    AEDisposeDesc(&childEvent);
release_desc:
    AEDisposeDesc(&target);
out:
    return err;
}

static Boolean realloc_checked(void **bufptr, Size size)
{
    void *tmp = realloc(*bufptr, size);
    if (!tmp) {
        OTHERERROR("LOADER [AppleEvents]: Failed to allocate a buffer of size %ld.\n", (long)size);
        return false;
    }
    VS("LOADER [AppleEvents]: (re)allocated a buffer of size %ld\n", (long)size);
    *bufptr = tmp;
    return true;
}

/* Handles apple events 'odoc' and 'GURL', both before and after the child_pid is up, Copying them to argv if child
 * not up yet, or otherwise forwarding them to the child if the child is started. */
static OSErr handle_odoc_GURL_events(const AppleEvent *theAppleEvent, const AEEventID evtID)
{
    const FourCharCode evtCode = (FourCharCode)evtID;
    const Boolean apple_event_is_open_doc = evtID == kAEOpenDocuments;
    const char *const descStr = apple_event_is_open_doc ? "OpenDoc" : "GetURL";

    VS("LOADER [AppleEvent]: %s handler called.\n", descStr);

    if (!pyi_utils_get_child_pid()) {
        /* Child process is not up yet -- so we pick up kAEOpen and/or kAEGetURL events and append them to argv. */

        AEDescList docList;
        OSErr err;
        long index;
        long count = 0;
        char *buf = NULL; /* Dynamic buffer for URL/file path data -- gets realloc'd as we iterate */

        VS("LOADER [AppleEvent ARGV_EMU]: Processing args for forward...\n");

        err = AEGetParamDesc(theAppleEvent, keyDirectObject, typeAEList, &docList);
        if (err != noErr) return err;

        err = AECountItems(&docList, &count);
        if (err != noErr) return err;

        for (index = 1; index <= count; ++index) /* AppleEvent lists are 1-indexed (I guess because of Pascal?) */
        {
            DescType returnedType;
            AEKeyword keywd;
            Size actualSize = 0, bufSize = 0;
            DescType typeCode = typeWildCard;

            err = AESizeOfNthItem(&docList, index, &typeCode, &bufSize);
            if (err != noErr) {
                OTHERERROR("LOADER [AppleEvent ARGV_EMU]: Failed to get size of Nth item %ld, error: %d\n",
                           index, (int)err);
                continue;
            }

            if (!realloc_checked((void **)&buf, bufSize+1)) {
                /* Not enough memory -- very unlikely but if so keep going */
                OTHERERROR("LOADER [AppleEvent ARGV_EMU]: Not enough memory for Nth item %ld, skipping%d\n", index);
                continue;
            }

            err = AEGetNthPtr(&docList, index, apple_event_is_open_doc ? typeFileURL : typeUTF8Text, &keywd,
                              &returnedType, buf, bufSize, &actualSize);
            if (err != noErr) {
                VS("LOADER [AppleEvent ARGV_EMU]: err[%ld] = %d\n", index-1L, (int)err);
            } else if (actualSize > bufSize) {
                /* This should never happen but is here for thoroughness */
                VS("LOADER [AppleEvent ARGV_EMU]: err[%ld]: not enough space in buffer (%ld > %ld)\n",
                   index-1L, (long)actualSize, (long)bufSize);
            } else {
                /* Copied data to buf, now ensure data is a simple file path and then append it to argv_pyi */
                char *tmp_str = NULL;
                Boolean ok;

                buf[actualSize] = 0; /* Ensure NUL-char termination. */
                if (apple_event_is_open_doc) {
                    /* Now, convert file:/// style URLs to an actual filesystem path for argv emu. */
                    CFURLRef url = CFURLCreateWithBytes(NULL, (UInt8 *)buf, actualSize, kCFStringEncodingUTF8,
                                                        NULL);
                    if (url) {
                        CFStringRef path = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
                        ok = false;
                        if (path) {
                            const Size newLen = (Size)CFStringGetMaximumSizeOfFileSystemRepresentation(path);
                            if (realloc_checked((void **)&buf, newLen+1)) {
                                bufSize = newLen;
                                ok = CFStringGetFileSystemRepresentation(path, buf, bufSize);
                                buf[bufSize] = 0; /* Ensure NUL termination */
                            }
                            CFRelease(path); /* free */
                        }
                        CFRelease(url); /* free */
                        if (!ok) {
                            VS("LOADER [AppleEvent ARGV_EMU]: "
                               "Failed to convert file:/// path to POSIX filesystem representation for arg %ld!\n",
                               index);
                            continue;
                        }
                    }
                }
                /* Append URL to argv_pyi array, reallocating as necessary */
                VS("LOADER [AppleEvent ARGV_EMU]: appending '%s' to argv_pyi\n", buf);
                if (pyi_utils_append_to_args(buf) < 0) {
                    OTHERERROR("LOADER [AppleEvent ARGV_EMU]: failed to append to argv_pyi: %s\n",
                               buf, strerror(errno));
                } else {
                    VS("LOADER [AppleEvent ARGV_EMU]: argv entry appended.\n");
                }
            }
        }

        free(buf); /* free of possible-NULL ok */

        err = AEDisposeDesc(&docList);

        return err;
    } /* else ... */

    /* The child process exists.. so we forward events to it */
    return generic_forward_apple_event(theAppleEvent,
                                       apple_event_is_open_doc ? kCoreEventClass : kInternetEventClass,
                                       evtID,
                                       descStr);
}

/* This brings the child process's windows to the foreground when the user double-clicks the
 * app's icon again in the macOS UI. 'rapp' is accepted by us only when the child is
 * already running. */
static OSErr handle_rapp_event(const AppleEvent *const theAppleEvent, const AEEventID evtID)
{
    OSErr err;

    VS("LOADER [AppleEvent]: ReopenApp handler called.\n");

    /* First, forward the 'rapp' event to the child */
    err = generic_forward_apple_event(theAppleEvent, kCoreEventClass, evtID, "ReopenApp");

    if (err == noErr) {
        /* Next, create a new activate ('actv') event. We never receive this event because
         * we have no window, but if we did this event would come next. So we synthesize an
         * event that should normally come for a windowed app, so that the child process
         * is brought to the foreground properly. */
        generic_forward_apple_event(NULL /* create new event with 0 params */,
                                    kAEMiscStandards, kAEActivate, "Activate");
    }

    return err;
}

/* Top-level event handler -- dispatches 'odoc', 'GURL', 'rapp', or 'actv' events. */
static pascal OSErr handle_apple_event(const AppleEvent *theAppleEvent, AppleEvent *reply, SRefCon handlerRefCon)
{
    const FourCharCode evtCode = (FourCharCode)(intptr_t)handlerRefCon;
    const AEEventID evtID = (AEEventID)(intptr_t)handlerRefCon;
    (void)reply; /* unused */

    VS("LOADER [AppleEvent]: %s called with code '%s'.\n", __FUNCTION__, CC2Str(evtCode));

    switch(evtID) {
    case kAEOpenApplication:
        /* Nothing to do here, just make sure we report event as handled. */
        return noErr;
    case kAEOpenDocuments:
    case kAEGetURL:
        return handle_odoc_GURL_events(theAppleEvent, evtID);
    case kAEReopenApplication:
        return handle_rapp_event(theAppleEvent, evtID);
    case kAEActivate:
        /* This is not normally reached since the bootloader process lacks a window, and it
         * turns out macOS never sends this event to processes lacking a window. However,
         * since the Apple API docs are very sparse, this has been left-in here just in case. */
        return generic_forward_apple_event(theAppleEvent, kAEMiscStandards, evtID, "Activate");
    default:
        /* Not 'GURL', 'odoc', 'rapp', or 'actv'  -- this is not reached unless there is a
         * programming error in the code that sets up the handler(s) in pyi_process_apple_events. */
        OTHERERROR("LOADER [AppleEvent]: %s called with unexpected event type '%s'!\n",
                   __FUNCTION__, CC2Str(evtCode));
        return errAEEventNotHandled;
    }
}

/* This function gets installed as the process-wide UPP event handler.
 * It is responsible for dequeuing events and telling Carbon to forward
 * them to our installed handlers. */
static OSStatus evt_handler_proc(EventHandlerCallRef href, EventRef eref, void *data) {
    VS("LOADER [AppleEvent]: App event handler proc called.\n");
    Boolean release = false;
    EventRecord eventRecord;
    OSStatus err;

    /* Events of type kEventAppleEvent must be removed from the queue
     * before being passed to AEProcessAppleEvent. */
    if (IsEventInQueue(GetMainEventQueue(), eref)) {
        /* RemoveEventFromQueue will release the event, which will
         * destroy it if we don't retain it first. */
        VS("LOADER [AppleEvent]: Event was in queue, will release.\n");
        RetainEvent(eref);
        release = true;
        RemoveEventFromQueue(GetMainEventQueue(), eref);
    }
    /* Convert the event ref to the type AEProcessAppleEvent expects. */
    ConvertEventRefToEventRecord(eref, &eventRecord);
    VS("LOADER [AppleEvent]: what=%hu message=%lx ('%s') modifiers=%hu\n",
       eventRecord.what, eventRecord.message, CC2Str((FourCharCode)eventRecord.message), eventRecord.modifiers);
    /* This will end up calling one of the callback functions
     * that we installed in pyi_process_apple_events() */
    err = AEProcessAppleEvent(&eventRecord);
    if (err == errAEEventNotHandled) {
        VS("LOADER [AppleEvent]: Ignored event.\n");
    } else if (err != noErr) {
        VS("LOADER [AppleEvent]: Error processing event: %d\n", (int)err);
    }
    if (release) {
        ReleaseEvent(eref);
    }
    return noErr;
}


/*
 * Install Apple Event handlers. The handlers must be install prior to
 * calling pyi_apple_process_events().
 */
int pyi_apple_install_event_handlers()
{
    OSStatus err;

    /* Already installed; nothing to do */
    if (_ae_ctx.installed) {
        return 0;
    }

    VS("LOADER [AppleEvent]: Installing event handlers...\n");

    /* Allocate UPP (universal procedure pointer) for handler functions */
    _ae_ctx.upp_handler = NewEventHandlerUPP(evt_handler_proc);
    _ae_ctx.upp_handler_ae = NewAEEventHandlerUPP(handle_apple_event);

    /* Register Apple Event handlers */
    /* 'oapp' (open application) */
    err = AEInstallEventHandler(kCoreEventClass, kAEOpenApplication, _ae_ctx.upp_handler_ae, (SRefCon)kAEOpenApplication, false);
    if (err != noErr) {
        goto end;
    }
    /* 'odoc' (open document) */
    err = AEInstallEventHandler(kCoreEventClass, kAEOpenDocuments, _ae_ctx.upp_handler_ae, (SRefCon)kAEOpenDocuments, false);
    if (err != noErr) {
        goto end;
    }
    /* 'GURL' (open url) */
    err = AEInstallEventHandler(kInternetEventClass, kAEGetURL, _ae_ctx.upp_handler_ae, (SRefCon)kAEGetURL, false);
    if (err != noErr) {
        goto end;
    }
    /* 'rapp' (re-open application) */
    err = AEInstallEventHandler(kCoreEventClass, kAEReopenApplication, _ae_ctx.upp_handler_ae, (SRefCon)kAEReopenApplication, false);
    if (err != noErr) {
        goto end;
    }
    /* register 'actv' (activate) */
    err = AEInstallEventHandler(kAEMiscStandards, kAEActivate, _ae_ctx.upp_handler_ae, (SRefCon)kAEActivate, false);
    if (err != noErr) {
        goto end;
    }

    /* Install application event handler */
    err = InstallApplicationEventHandler(_ae_ctx.upp_handler, 1, event_types_ae, NULL, &_ae_ctx.handler_ref);

end:
    if (err != noErr) {
        /* Failed to install one of AE handlers or application event handler.
         * Remove everything. */
        AERemoveEventHandler(kAEMiscStandards, kAEActivate, _ae_ctx.upp_handler_ae, false);
        AERemoveEventHandler(kCoreEventClass, kAEReopenApplication, _ae_ctx.upp_handler_ae, false);
        AERemoveEventHandler(kInternetEventClass, kAEGetURL, _ae_ctx.upp_handler_ae, false);
        AERemoveEventHandler(kCoreEventClass, kAEOpenDocuments, _ae_ctx.upp_handler_ae, false);
        AERemoveEventHandler(kCoreEventClass, kAEOpenApplication, _ae_ctx.upp_handler_ae, false);

        DisposeEventHandlerUPP(_ae_ctx.upp_handler);
        DisposeAEEventHandlerUPP(_ae_ctx.upp_handler_ae);

        OTHERERROR("LOADER [AppleEvent]: Failed to install event handlers!\n");
        return -1;
    }

    VS("LOADER [AppleEvent]: Installed event handlers.\n");
    _ae_ctx.installed = true;
    return 0;
}

/*
 * Uninstall Apple Event handlers.
 */
int pyi_apple_uninstall_event_handlers()
{
    /* Not installed; nothing to do */
    if (!_ae_ctx.installed) {
        return 0;
    }

    VS("LOADER [AppleEvent]: Uninstalling event handlers...\n");

    /* Remove application event handler */
    RemoveEventHandler(_ae_ctx.handler_ref);
    _ae_ctx.handler_ref = NULL;

    /* Remove Apple Event handlers */
    AERemoveEventHandler(kAEMiscStandards, kAEActivate, _ae_ctx.upp_handler_ae, false);
    AERemoveEventHandler(kCoreEventClass, kAEReopenApplication, _ae_ctx.upp_handler_ae, false);
    AERemoveEventHandler(kInternetEventClass, kAEGetURL, _ae_ctx.upp_handler_ae, false);
    AERemoveEventHandler(kCoreEventClass, kAEOpenDocuments, _ae_ctx.upp_handler_ae, false);
    AERemoveEventHandler(kCoreEventClass, kAEOpenApplication, _ae_ctx.upp_handler_ae, false);

    /* Cleanup UPPs */
    DisposeEventHandlerUPP(_ae_ctx.upp_handler);
    DisposeAEEventHandlerUPP(_ae_ctx.upp_handler_ae);

    _ae_ctx.upp_handler = NULL;
    _ae_ctx.upp_handler_ae = NULL;

    _ae_ctx.installed = false;

    VS("LOADER [AppleEvent]: Uninstalled event handlers.\n");

    return 0;
}


/*
 * Apple event message pump; retrieves and processes Apple Events until
 * the specified timeout (in seconds) or an error is reached.
 */
void pyi_apple_process_events(float timeout)
{
    /* No-op if we failed to install event handlers */
    if (!_ae_ctx.installed) {
        return;
    }

    VS("LOADER [AppleEvent]: Processing Apple Events...\n");

    /* Event pump: process events until timeout (in seconds) or error */
    for (;;) {
        OSStatus status;
        EventRef event_ref; /* Event that caused ReceiveNextEvent to return. */

        /* If we have a pending event to forward, stop any further processing. */
        if (pyi_apple_has_pending_event()) {
            VS("LOADER [AppleEvent]: Breaking event loop due to pending event.\n");
            break;
        }

        VS("LOADER [AppleEvent]: Calling ReceiveNextEvent\n");
        status = ReceiveNextEvent(1, event_types_ae, timeout, kEventRemoveFromQueue, &event_ref);

        if (status == eventLoopTimedOutErr) {
            VS("LOADER [AppleEvent]: ReceiveNextEvent timed out\n");
            break;
        } else if (status != 0) {
            VS("LOADER [AppleEvent]: ReceiveNextEvent fetching events failed\n");
            break;
        } else {
            /* We actually pulled an event off the queue, so process it.
               We now 'own' the event_ref and must release it. */
            VS("LOADER [AppleEvent]: ReceiveNextEvent got an EVENT\n");

            VS("LOADER [AppleEvent]: Dispatching event...\n");
            status = SendEventToEventTarget(event_ref, GetEventDispatcherTarget());

            ReleaseEvent(event_ref);
            event_ref = NULL;
            if (status != 0) {
                VS("LOADER [AppleEvent]: processing events failed\n");
                break;
            }
        }
    }

    VS("LOADER [AppleEvent]: Out of the event loop.\n");
}


/*
 * Submit oapp (open application) event to ourselves. This is an attempt
 * to mitigate the issues with some UI frameworks (Tcl/Tk, in particular)
 * that are causes by argv-emu being enabled in onedir mode. In this case,
 * argv-emu swallows initial activation event (usually oapp; or odoc/GURL
 * if launched via file/url open request). This function attempts to
 * mitigate that by submitting a manual oapp event to itself so that the
 * UI framework finds the activation even in the event queue, as if no
 * Apple Event processing took place in the bootloader.
 */
void pyi_apple_submit_oapp_event()
{
    AppleEvent event = {typeNull, nil};
    AEAddressDesc target = {typeNull, nil};
    EventRef event_ref;
    ProcessSerialNumber psn;
    OSErr err;

    VS("LOADER [AppleEvent]: Submitting 'oapp' event...\n");

    // Get PSN via GetCurrentProcess. This function is deprecated, but
    // we cannot use {0, kCurrentProcess} because we need our event
    // to be queued.
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
    err = GetCurrentProcess(&psn);
#ifdef __clang__
#pragma clang diagnostic pop
#endif
    if (err != noErr) {
        OTHERERROR("LOADER [AppleEvent]: Failed to obtain PSN: %d\n", (int)err);
        goto cleanup;
    }

    // Create target address using the PSN, ...
    err = AECreateDesc(typeProcessSerialNumber, &psn, sizeof(psn), &target);
    if (err != noErr) {
        OTHERERROR("LOADER [AppleEvent]: Failed to create AEAddressDesc: %d\n", (int)err);
        goto cleanup;
    }

    // ... create OAPP event, ...
    err = AECreateAppleEvent(kCoreEventClass, kAEOpenApplication, &target, kAutoGenerateReturnID, kAnyTransactionID, &event);
    if (err != noErr) {
        OTHERERROR("LOADER [AppleEvent]: Failed to create OAPP event: %d\n", (int)err);
        goto cleanup;
    }

    // ... and send it
    err = AESendMessage(&event, NULL, kAENoReply, kAEDefaultTimeout);
    if (err != noErr) {
        OTHERERROR("LOADER [AppleEvent]: Failed to send event: %d\n", (int)err);
        goto cleanup;
    } else {
        VS("LOADER [AppleEvent]: Submitted 'oapp' event.\n");
    }

    // Now wait for the event to show up in event queue (this implicitly
    // assumes that no other activation event shows up, but those would
    // also solve the problem we are trying to mitigate).
    VS("LOADER [AppleEvent]: Waiting for 'oapp' event to show up in queue...\n");
    err = ReceiveNextEvent(1, event_types_ae, 10.0, kEventLeaveInQueue, &event_ref);
    if (err != noErr) {
        OTHERERROR("LOADER [AppleEvent]: Timed out while waiting for submitted 'oapp' event to show up in queue!\n");
    } else {
        VS("LOADER [AppleEvent]: Submitted 'oapp' event is available in the queue.\n");
    }

cleanup:
    AEDisposeDesc(&event);
    AEDisposeDesc(&target);

    return;
}


/* Check if we have a pending event that we need to forward. */
int pyi_apple_has_pending_event()
{
    return _ae_ctx.has_pending_event;
}

/* Clean-up the pending event data and status. */
void pyi_apple_cleanup_pending_event()
{
    /* No-op if have no pending event. */
    if (!_ae_ctx.has_pending_event) {
        return;
    }

    /* Dispose event descriptor. */
    AEDisposeDesc(&_ae_ctx.pending_event);

    /* Cleanup state. */
    _ae_ctx.has_pending_event = false;
    _ae_ctx.retry_count = 0;
}

/* Attempt to re-send the pending event after the specified delay (in seconds). */
int pyi_apple_send_pending_event(float delay)
{
    OSErr err;

    /* No-op if have no pending event; signal success. */
    if (!_ae_ctx.has_pending_event) {
        return 0;
    }

    /* Sleep for the specified delay, then attempt to send the event. */
    _ae_ctx.retry_count++;
    VS("LOADER [AppleEvent]: Trying to forward pending event in %f second(s) (attempt %u)\n", delay, _ae_ctx.retry_count);
    usleep(delay*1000000);  /* sec to usec */
    err = AESendMessage(&_ae_ctx.pending_event, NULL, kAENoReply, kAEDefaultTimeout);

    /* If error is procNotFound (again), continue deferring the event. */
    if (err == procNotFound) {
        VS("LOADER [AppleEvent]: Sending failed with procNotFound; deferring event!\n");
        return 1;
    }

    /* Clean-up the pending event. */
    pyi_apple_cleanup_pending_event();

    /* Signal status. */
    if (err == noErr) {
        VS("LOADER [AppleEvent]: Successfully forwarded pending event\n");
        return 0;
    } else {
        VS("LOADER [AppleEvent]: Failed to forward pending event: %d\n", (int)err);
        return -1;
    }
}


#endif /* if defined(__APPLE__) && defined(WINDOWED) */
