/* eslint-disable @typescript-eslint/no-unused-vars */
/**
 * The base class for models.
 *
 * Models wrap a logic related to manipulating model data, querying for data from the API,
 * retry logic, cashing, refreshing of data, offline storage, pooling endpoints for new data, etc.
 *
 * The application should use these models in order to centralize object manipulation.
 *
 * Note that the preferred way to get data from the model is through calling the `debounce` method.
 * This ensures that multiple changes to the model properties will result in a single query after
 * the `debounceTimeout` period. If you are not plannig to use the debouncer, make sure to call the
 * `resolveTaskPromise` method after the operation completes to properly manage the `taskComplete` promise.
 *
 * @template T The underlying domain model.
 */
export class Model extends EventTarget {
    /**
     * The underlying raw object.
     * Do not modify properties of the object directly.
     * Use one of the methods provided by the model.
     * Direct changes will not be reflected in the UI
     * and may cause issues when processing the object.
     */
    raw;
    /**
     * The timeout for the query debouncer.
     * When any property change this is the time the element will wait
     * until the actual query is made.
     */
    debounceTimeout = 100;
    /**
     * A reference to the current debouncer.
     */
    debouncerValue;
    /**
     * Map of debounce timers keyed by argument signature for parameter-aware debouncing.
     */
    #debouncedTimers = new Map();
    /**
     * Default limit for list queries.
     */
    limit = 30;
    /**
     * A flag that helps to determine whether the `taskComplete` is setup.
     */
    #hasPendingTaskPromise = false;
    /**
     * A hidden value for the `taskComplete` getter.
     */
    #taskComplete;
    /**
     * The resolver to call when the debounced task completes.
     */
    #taskResolver;
    /**
     * @type A promise resolved when the debounced task finished.
     */
    get taskComplete() {
        return this.#taskComplete;
    }
    /**
     * A getter for the raw value.
     */
    get value() {
        return this.raw;
    }
    /**
     * The number of retry attempts for operations.
     */
    retryCount = 3;
    /**
     * @param obj The source object to use.
     */
    constructor(obj) {
        super();
        this.raw = obj;
        this.#setUpdatePromise();
    }
    /**
     * Creates a new instance of the data. It often invokes API call.
     * This is what happens after the user triggers "create new" flow.
     *
     * After this method is calls, the `raw` object is set.
     *
     * @param data The partial data to create.
     * @returns The created object.
     */
    create(data) {
        throw new Error(`Not implemented.`);
    }
    /**
     * Debounces the given callback for the `debounceTimeout` period.
     * Multiple calls with the same arguments will reset the timeout for that argument set.
     * Calls with different arguments will each have their own debounce timer.
     * Only the last call for each unique set of arguments is executed.
     * @param callback The function to execute.
     * @param args Optional arguments to pass to the callback and use for debounce comparison.
     */
    debounce(callback, ...args) {
        const argsKey = this.#getArgsKey(args);
        // Clear the existing timer for this argument set if it exists
        const existingTimer = this.#debouncedTimers.get(argsKey);
        if (existingTimer) {
            clearTimeout(existingTimer);
        }
        // Set up a new promise if there isn't one
        if (!this.#hasPendingTaskPromise) {
            this.#setUpdatePromise();
        }
        // Create a new timer for this argument set
        const timer = setTimeout(async () => {
            this.#debouncedTimers.delete(argsKey);
            try {
                await callback.call(this, ...args);
            }
            catch (e) {
                const err = e;
                this.notifyError(err);
                // I don't think we want to re-throw here as it would be unhandled
                // throw err
            }
            finally {
                this.resolveTaskPromise();
            }
        }, this.debounceTimeout);
        this.#debouncedTimers.set(argsKey, timer);
    }
    /**
     * Converts arguments array to a JSON string key for use in the debounce timer map.
     */
    #getArgsKey(args) {
        try {
            return JSON.stringify(args);
        }
        catch {
            return 'error';
        }
    }
    notifyError(error) {
        this.dispatchEvent(new CustomEvent('error', {
            detail: error,
        }));
    }
    #setUpdatePromise() {
        this.#taskComplete = new Promise((resolve) => {
            this.#taskResolver = resolve;
            this.#hasPendingTaskPromise = true;
        });
    }
    resolveTaskPromise() {
        if (!this.#hasPendingTaskPromise) {
            return;
        }
        this.#hasPendingTaskPromise = false;
        const resolver = this.#taskResolver;
        this.#taskResolver = undefined;
        if (resolver) {
            resolver();
        }
    }
}
//# sourceMappingURL=Model.js.map