/**
 * @license
 * Copyright 2021 Google LLC
 * SPDX-License-Identifier: BSD-3-Clause
 */
const microtask = Promise.resolve();
/**
 * An implementation of ReactiveControllerHost that is driven by React hooks
 * and `useController()`.
 */
class ReactControllerHost {
    constructor(kickCount, kick) {
        this._controllers = [];
        /* @internal */
        this._updatePending = true;
        /* @internal */
        this._isConnected = false;
        this._kickCount = kickCount;
        this._kick = kick;
        this._updateCompletePromise = new Promise((res, _rej) => {
            this._resolveUpdate = res;
        });
    }
    addController(controller) {
        this._controllers.push(controller);
    }
    removeController(controller) {
        // Note, if the indexOf is -1, the >>> will flip the sign which makes the
        // splice do nothing.
        this._controllers?.splice(this._controllers.indexOf(controller) >>> 0, 1);
    }
    requestUpdate() {
        if (!this._updatePending) {
            this._updatePending = true;
            // Trigger a React update by updating some state
            microtask.then(() => this._kick(++this._kickCount));
        }
    }
    get updateComplete() {
        return this._updateCompletePromise;
    }
    /* @internal */
    _connected() {
        this._isConnected = true;
        this._controllers.forEach((c) => c.hostConnected?.());
    }
    /* @internal */
    _disconnected() {
        this._isConnected = false;
        this._controllers.forEach((c) => c.hostDisconnected?.());
    }
    /* @internal */
    _update() {
        this._controllers.forEach((c) => c.hostUpdate?.());
    }
    /* @internal */
    _updated() {
        this._updatePending = false;
        const resolve = this._resolveUpdate;
        // Create a new updateComplete Promise for the next update,
        // before resolving the current one.
        this._updateCompletePromise = new Promise((res, _rej) => {
            this._resolveUpdate = res;
        });
        this._controllers.forEach((c) => c.hostUpdated?.());
        resolve(this._updatePending);
    }
}
/**
 * Creates and stores a stateful ReactiveController instance and provides it
 * with a ReactiveControllerHost that drives the controller lifecycle.
 *
 * Use this hook to convert a ReactiveController into a React hook.
 *
 * @param React the React module that provides the base hooks. Must provide
 * `useState` and `useLayoutEffect`.
 * @param createController A function that creates a controller instance. This
 * function is given a ReactControllerHost to pass to the controller. The
 * create function is only called once per component.
 */
export const useController = (React, createController) => {
    const { useState, useLayoutEffect } = React;
    // State to force updates of the React component
    const [kickCount, kick] = useState(0);
    // Create and store the controller instance. We use useState() instead of
    // useMemo() because React does not guarantee that it will preserve values
    // created with useMemo().
    // TODO (justinfagnani): since this controller are mutable, this may cause
    // issues such as "shearing" with React concurrent mode. The solution there
    // will likely be to snapshot the controller state with something like
    // `useMutableSource`:
    // https://github.com/reactjs/rfcs/blob/master/text/0147-use-mutable-source.md
    // We can address this when React's concurrent mode is closer to shipping.
    let shouldDisconnect = false;
    const [host] = useState(() => {
        const host = new ReactControllerHost(kickCount, kick);
        const controller = createController(host);
        host._primaryController = controller;
        // Note, calls to `useState` are expected to produce no side effects and in
        // StrictMode this is enforced by not running effects for the first render.
        //
        // This happens in StrictMode:
        // 1. Throw away render: component function runs but does not call effects
        // 2. Real render: component function runs and *does* call effects,
        // 2.a. if first render, run effects and
        // 2.a.1 mount,
        // 2.a.2 unmount,
        // 2.a.3 remount
        // 2b. if not first render, just run effects
        //
        // To preserve update lifecycle ordering and run it before this hook
        // returns, run connected here but schedule and async disconnect (handles
        // lifecycle balance for `(1) Throw away render`).
        // The disconnect is cancelled if the effects actually run (handles
        // `(2.a.1) Real render, mount`).
        host._connected();
        shouldDisconnect = true;
        microtask.then(() => {
            if (shouldDisconnect) {
                host._disconnected();
            }
        });
        return host;
    });
    host._updatePending = true;
    // This effect runs only on mount/unmount of the component (via the empty
    // deps array). If the controller has just been created, it's scheduled
    // a disconnect so that it behaves correctly in StrictMode (see above).
    // The returned callback here disconnects the host when the component is
    // unmounted (handles `(2.a.2) Real render, unmount` above).
    // And finally, if the component is disconnected when the effect runs, we
    // connect it (handles `(2.a.3) Real render, remount`).
    useLayoutEffect(() => {
        shouldDisconnect = false;
        if (!host._isConnected) {
            host._connected();
        }
        return () => host._disconnected();
    }, []);
    // We use useLayoutEffect because we need updated() called synchronously
    // after rendering.
    useLayoutEffect(() => host._updated());
    // TODO (justinfagnani): don't call in SSR
    host._update();
    return host._primaryController;
};
//# sourceMappingURL=use-controller.js.map