// Extension to make it easier to stream responses back form the server.
// Works like hx-post, but parses the response as a stream of Server-Sent Events
// (SSE). Unlike the normal HTMX SSE extension, this extension assumes that the
// stream is short-lived, as opposed to lasting indefinitely.

// Code was largely copied from various parts of HTMX, so is licensed under the
// same license as HTMX (simplified BSD).

// This file depends on both HTMX and the SSE polyfill that we're using, so it
// should be loaded after both.
(() => {
    // This extension is adapted from the SSE extension in HTMX:
    // https://github.com/bigskysoftware/htmx/blob/master/src/ext/sse.js
    var api;
    htmx.defineExtension("post-streaming", {
        init: internalAPI => {
            api = internalAPI;
        },
        onEvent: (name, evt) => {
            switch (name) {
                case "htmx:beforeCleanupElement":
                    var internalData = api.getInternalData(evt.target)
                    if (internalData.sseEventSource) {
                        internalData.sseEventSource.close();
                    }
                    return;

                // Try to create EventSources when elements are processed
                case "htmx:afterProcessNode":
                    initElement(evt.target);
            }
        }
    });

    function initElement(elt) {
        // Check that this is a hx-post-streaming element
        if (elt == null) {
            return null;
        }
        const submitURL = api.getAttributeValue(elt, "hx-post-streaming");
        if (submitURL == undefined) {
            return null;
        }
        console.debug("Initializing hx-post-streaming element")

        // Figure out when we should trigger an SSE request (e.g. on form
        // submission for a <form>, on click for a <button>, etc.) & add
        // appropriate handlers.
        const internalData = api.getInternalData(elt);
        const triggerSpecs = api.getTriggerSpecs(elt);
        triggerSpecs.forEach(function(triggerSpec) {
            api.addTriggerHandler(elt, triggerSpec, internalData, (elt, evt) => {
                issueStreamingRequest(submitURL, elt, evt)
            })
        });
    }

    function issueStreamingRequest(submitURL, elt, evt) {
        if (!api.bodyContains(elt)) {
            // do not issue requests for elements removed from the DOM
            return;
        }
        var target = api.getTarget(elt);
        if (target == null) {
            api.triggerErrorEvent(elt, 'htmx:targetError', {target: api.getAttributeValue(elt, "hx-target")});
            return;
        }

        // Cancel any old request from this specific element
        const eltData = api.getInternalData(elt);
        if (eltData.sseEventSource) {
            eltData.sseEventSource.close();
        }

        // Parse form data in approximately the same way as issueAjaxRequest
        // in htmx, but minus support for headers and other more advanced
        // features.
        let headers = {
            // URL encoded
            'Content-Type': 'application/x-www-form-urlencoded',
        };
        let results = api.getInputValues(elt, 'post');
        let parameters = results.values;

        eltData.sseEventSource = new SSE(submitURL, {
            method: 'post',
            headers: headers,
            // URL-encode the parameters
            payload: new URLSearchParams(parameters).toString(),
        });

        // When we get an SSE message, we look for a command type in the
        // 'event:' field. The supported commands are:
        //
        // 1. hx-swap:<swap spec>: Uses the provided swap spec to replace
        //    the content of the target element with the SSE message data
        // 2. hx-trigger:<trigger name>,<trigger name>,...: Triggers one
        //    or more htmx events on the element.
        eltData.sseEventSource.onmessage = msg => {
            // event name (msg.type) is semi-colon separated list of commands to process
            // data (msg.data) is the data to process
            let content = msg.data;
            // skip any msg that doesn't start with 'hx-'
            if (!msg.type || !msg.type.trim().startsWith('hx-')) {
                return;
            }
            let [command, commandContent] = msg.type.trim().split(':', 2);
            command = command.toLowerCase();
            switch (command) {
                case 'hx-swap':
                    // this should ignore elt since we have swapOverride taken from the event name
                    let swapSpec = api.getSwapSpecification(elt, commandContent);
                    let settleInfo = api.makeSettleInfo(elt);
                    api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
                    // Handle settle in the same way as SSE extension
                    settleInfo.elts.forEach(elt => {
                        if (elt.classList) {
                            elt.classList.add(htmx.config.settlingClass);
                        }
                        api.triggerEvent(elt, 'htmx:beforeSettle');
                    });
                    if (swapSpec.settleDelay > 0) {
                        setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
                    } else {
                        doSettle(settleInfo)();
                    }
                    break;
                case 'hx-trigger':
                    if (commandContent !== undefined) {
                        commandContent.split(",").forEach(triggerName => {
                            htmx.trigger(elt, triggerName.trim(), {"data": content});
                        });
                    }
                    break;
                default:
                    console.error("Unknown SSE command", command, "in event string", msg.type);
            }
        };

        // When we get an error or the connection closes, we remove the
        // indicator classes so that spinners etc. go away.
        eltData.sseEventSource.addEventListener('error', (e) => {
            console.error("SSE error", e);
            removeRequestIndicatorClasses(indicators);
        });
        eltData.sseEventSource.addEventListener('abort', (e) => {
            console.error("SSE abort", e);
            removeRequestIndicatorClasses(indicators);
        });
        eltData.sseEventSource.addEventListener('load', (e) => {
            // This is the expected end state
            removeRequestIndicatorClasses(indicators);
        });
        var indicators = addRequestIndicatorClasses(elt);

        // Launch request!
        eltData.sseEventSource.stream();

        return null;
    }

    // These are parts of the internal HTMX api copied out of htmx.js
    // because there's no other way to access them.
    // {add,remove}RequestIndicatorClasses is for adding/removing spinners.
    function addRequestIndicatorClasses(elt) {
        // I don't want to copy out everything that findAttributeTarget
        // depends on, so I'm going to use getAttributeValue instead.
        // var indicators = findAttributeTargets(elt, 'hx-indicator');
        let indicators = [];
        let indicatorElem = api.getAttributeValue(elt, 'hx-indicator');
        let nullSelector = '#nothingWithThisNameShouldEverExist.OrElseThisCodeWillBreakHorribly > script > script > script';
        document.querySelectorAll(indicatorElem || nullSelector).forEach(function (indicator) {
            indicators.push(indicator);
        });
        if (indicators.length === 0) {
            indicators = [elt];
        }
        // The rest of this function is unchanged.
        indicators.forEach(ic => {
            var internalData = api.getInternalData(ic);
            internalData.requestCount = (internalData.requestCount || 0) + 1;
            ic.classList["add"].call(ic.classList, htmx.config.requestClass);
        });
        return indicators;
    }

    function removeRequestIndicatorClasses(indicators) {
        indicators.forEach(ic => {
            var internalData = api.getInternalData(ic);
            internalData.requestCount = (internalData.requestCount || 0) - 1;
            if (internalData.requestCount === 0) {
                ic.classList["remove"].call(ic.classList, htmx.config.requestClass);
            }
        });
    }

    // This was copied from the HTMX SSE extension. I don't think our Tensor
    // Trust code requires this 'settle' handling, although IIRC there are some
    // things that will break if you don't have it (e.g. CSS animations).
    function doSettle(settleInfo) {
        return () => {
            settleInfo.tasks.forEach(task => {
                task.call();
            });

            settleInfo.elts.forEach(elt => {
                if (elt.classList) {
                    elt.classList.remove(htmx.config.settlingClass);
                }
                api.triggerEvent(elt, 'htmx:afterSettle');
            });
        };
    }
})();