var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _AsyncHook_debug, _AsyncHook_hookId, _AsyncHook_invalidate, _AsyncHook_ctx;
import { UIEventScope } from '@devvit/protos';
import { CIRCUIT_BREAKER_MSG } from '@devvit/shared-types/CircuitBreaker.js';
import { StringUtil } from '@devvit/shared-types/StringUtil.js';
import isEqual from 'lodash.isequal';
import { registerHook } from './BlocksHandler.js';
/**
 * This tries to save an error into the state.  If the error is a circuit breaker, it will throw the error instead,
 * as those errors are not meant to be saved in state.
 *
 * @param e  -- an original error type
 * @returns A JSONValue that can be saved in states
 */
export function toSerializableErrorOrCircuitBreak(e) {
    // This is a little side-effecty, so open to suggestions on how to improve.
    if (e instanceof Error) {
        if (e.message === CIRCUIT_BREAKER_MSG) {
            throw e;
        }
        console.error(e);
        return { message: e.message, details: e.stack ?? '' };
    }
    else {
        console.error(e);
        return { message: 'Unknown error', details: StringUtil.caughtToString(e) };
    }
}
class AsyncHook {
    constructor(initializer, options, params) {
        _AsyncHook_debug.set(this, void 0);
        _AsyncHook_hookId.set(this, void 0);
        _AsyncHook_invalidate.set(this, void 0);
        _AsyncHook_ctx.set(this, void 0);
        __classPrivateFieldSet(this, _AsyncHook_debug, !!params.context.devvitContext.debug.useAsync, "f");
        if (__classPrivateFieldGet(this, _AsyncHook_debug, "f"))
            console.debug('[useAsync] v1', options);
        this.state = { data: null, load_state: 'initial', error: null, depends: null };
        __classPrivateFieldSet(this, _AsyncHook_hookId, params.hookId, "f");
        this.initializer = initializer;
        __classPrivateFieldSet(this, _AsyncHook_invalidate, params.invalidate, "f");
        __classPrivateFieldSet(this, _AsyncHook_ctx, params.context, "f");
        this.localDepends = options.depends ?? null;
    }
    /**
     * After we look at our state, we need to decide if we need to dispatch a request to load the data.
     */
    onStateLoaded() {
        if (this.state.load_state === 'disabled') {
            return;
        }
        if (__classPrivateFieldGet(this, _AsyncHook_debug, "f"))
            console.debug('[useAsync] async onLoad ', __classPrivateFieldGet(this, _AsyncHook_hookId, "f"), this.state);
        if (__classPrivateFieldGet(this, _AsyncHook_debug, "f"))
            console.debug('[useAsync] async onLoad have ', this.localDepends, 'and', this.state.depends);
        if (!isEqual(this.localDepends, this.state.depends) || this.state.load_state === 'initial') {
            if (__classPrivateFieldGet(this, _AsyncHook_debug, "f"))
                console.debug(`[useAsync] attempting to resolve for hookId`, __classPrivateFieldGet(this, _AsyncHook_hookId, "f"));
            this.state.load_state = 'loading';
            this.state.depends = this.localDepends;
            __classPrivateFieldGet(this, _AsyncHook_invalidate, "f").call(this);
            const requeueEvent = {
                scope: UIEventScope.ALL,
                hook: __classPrivateFieldGet(this, _AsyncHook_hookId, "f"),
                async: true,
                asyncRequest: {
                    requestId: __classPrivateFieldGet(this, _AsyncHook_hookId, "f") + '-' + JSON.stringify(this.state.depends),
                },
            };
            if (__classPrivateFieldGet(this, _AsyncHook_debug, "f"))
                console.debug('[useAsync] onLoad requeue');
            __classPrivateFieldGet(this, _AsyncHook_ctx, "f").addToRequeueEvents(requeueEvent);
        }
    }
    async onUIEvent(event, context) {
        /**
         * Requests and responses are both handled here. If we have a request, we need to load the data
         * and then send a response.  If we have a response, we need to update our state and re-render.
         *
         * This is a very event-driven way to handle state, but it's the only way to handle async state.
         */
        if (event.asyncRequest) {
            const asyncResponse = { requestId: event.asyncRequest.requestId };
            try {
                asyncResponse.data = {
                    value: await this.initializer(),
                };
            }
            catch (e) {
                asyncResponse.error = toSerializableErrorOrCircuitBreak(e);
            }
            const requeueEvent = {
                scope: UIEventScope.ALL,
                asyncResponse: asyncResponse,
                hook: __classPrivateFieldGet(this, _AsyncHook_hookId, "f"),
            };
            if (__classPrivateFieldGet(this, _AsyncHook_debug, "f"))
                console.debug('[useAsync] onReq requeue');
            context.addToRequeueEvents(requeueEvent);
        }
        else if (event.asyncResponse) {
            const anticipatedRequestId = __classPrivateFieldGet(this, _AsyncHook_hookId, "f") + '-' + JSON.stringify(this.state.depends);
            if (event.asyncResponse.requestId === anticipatedRequestId) {
                this.state = {
                    ...this.state,
                    data: event.asyncResponse.data?.value ?? null,
                    error: event.asyncResponse.error ?? null,
                    load_state: event.asyncResponse.error ? 'error' : 'loaded',
                };
                __classPrivateFieldGet(this, _AsyncHook_invalidate, "f").call(this);
            }
            else {
                if (__classPrivateFieldGet(this, _AsyncHook_debug, "f"))
                    console.debug('[useAsync] onResp skip, stale event', event.asyncResponse.requestId, ' !== ', anticipatedRequestId);
            }
        }
        else {
            throw new Error('Unknown event type');
        }
    }
}
_AsyncHook_debug = new WeakMap(), _AsyncHook_hookId = new WeakMap(), _AsyncHook_invalidate = new WeakMap(), _AsyncHook_ctx = new WeakMap();
/**
 * This is the preferred way to handle async state in Devvit.
 *
 * @param initializer -- any async function that returns a JSONValue
 * @returns UseAsyncResult<S>
 */
export function useAsync(initializer, options = {}) {
    const hook = registerHook({
        namespace: 'useAsync',
        initializer: (params) => {
            return new AsyncHook(initializer, options, params);
        },
    });
    return {
        data: hook.state.data,
        error: hook.state.error,
        loading: hook.state.load_state === 'loading',
    };
}
