/**
 * @license
 * Copyright 2018 Google LLC
 * SPDX-License-Identifier: BSD-3-Clause
 */
const NODE_MODE = false;
const DEV_MODE = true;
const reservedReactProperties = new Set([
    'children',
    'localName',
    'ref',
    'style',
    'className',
]);
const listenedEvents = new WeakMap();
/**
 * Adds an event listener for the specified event to the given node. In the
 * React setup, there should only ever be one event listener. Thus, for
 * efficiency only one listener is added and the handler for that listener is
 * updated to point to the given listener function.
 */
const addOrUpdateEventListener = (node, event, listener) => {
    let events = listenedEvents.get(node);
    if (events === undefined) {
        listenedEvents.set(node, (events = new Map()));
    }
    let handler = events.get(event);
    if (listener !== undefined) {
        // If necessary, add listener and track handler
        if (handler === undefined) {
            events.set(event, (handler = { handleEvent: listener }));
            node.addEventListener(event, handler);
            // Otherwise just update the listener with new value
        }
        else {
            handler.handleEvent = listener;
        }
        // Remove listener if one exists and value is undefined
    }
    else if (handler !== undefined) {
        events.delete(event);
        node.removeEventListener(event, handler);
    }
};
/**
 * Sets properties and events on custom elements. These properties and events
 * have been pre-filtered so we know they should apply to the custom element.
 */
const setProperty = (node, name, value, old, events) => {
    const event = events?.[name];
    // Dirty check event value.
    if (event !== undefined) {
        if (value !== old) {
            addOrUpdateEventListener(node, event, value);
        }
        return;
    }
    // But don't dirty check properties; elements are assumed to do this.
    node[name] = value;
    // This block is to replicate React's behavior for attributes of native
    // elements where `undefined` or `null` values result in attributes being
    // removed.
    // https://github.com/facebook/react/blob/899cb95f52cc83ab5ca1eb1e268c909d3f0961e7/packages/react-dom-bindings/src/client/DOMPropertyOperations.js#L107-L141
    //
    // It's only needed here for native HTMLElement properties that reflect
    // attributes of the same name but don't have that behavior like "id" or
    // "draggable".
    if ((value === undefined || value === null) &&
        name in HTMLElement.prototype) {
        node.removeAttribute(name);
    }
};
/**
 * Creates a React component for a custom element. Properties are distinguished
 * from attributes automatically, and events can be configured so they are added
 * to the custom element as event listeners.
 *
 * @param options An options bag containing the parameters needed to generate a
 * wrapped web component.
 *
 * @param options.react The React module, typically imported from the `react`
 * npm package.
 * @param options.tagName The custom element tag name registered via
 * `customElements.define`.
 * @param options.elementClass The custom element class registered via
 * `customElements.define`.
 * @param options.events An object listing events to which the component can
 * listen. The object keys are the event property names passed in via React
 * props and the object values are the names of the corresponding events
 * generated by the custom element. For example, given `{onactivate:
 * 'activate'}` an event function may be passed via the component's `onactivate`
 * prop and will be called when the custom element fires its `activate` event.
 * @param options.displayName A React component display name, used in debugging
 * messages. Default value is inferred from the name of custom element class
 * registered via `customElements.define`.
 */
export const createComponent = ({ react: React, tagName, elementClass, events, displayName, }) => {
    const eventProps = new Set(Object.keys(events ?? {}));
    if (DEV_MODE && !NODE_MODE) {
        for (const p of reservedReactProperties) {
            if (p in elementClass.prototype && !(p in HTMLElement.prototype)) {
                // Note, this effectively warns only for `ref` since the other
                // reserved props are on HTMLElement.prototype. To address this
                // would require crawling down the prototype, which doesn't feel worth
                // it since implementing these properties on an element is extremely
                // rare.
                console.warn(`${tagName} contains property ${p} which is a React reserved ` +
                    `property. It will be used by React and not set on the element.`);
            }
        }
    }
    const ReactComponent = React.forwardRef((props, ref) => {
        const prevElemPropsRef = React.useRef(new Map());
        const elementRef = React.useRef(null);
        // Props to be passed to React.createElement
        const reactProps = {};
        // Props to be set on element with setProperty
        const elementProps = {};
        for (const [k, v] of Object.entries(props)) {
            if (reservedReactProperties.has(k)) {
                // React does *not* handle `className` for custom elements so
                // coerce it to `class` so it's handled correctly.
                reactProps[k === 'className' ? 'class' : k] = v;
                continue;
            }
            if (eventProps.has(k) || k in elementClass.prototype) {
                elementProps[k] = v;
                continue;
            }
            reactProps[k] = v;
        }
        // useLayoutEffect produces warnings during server rendering.
        if (!NODE_MODE) {
            // This one has no dependency array so it'll run on every re-render.
            React.useLayoutEffect(() => {
                if (elementRef.current === null) {
                    return;
                }
                const newElemProps = new Map();
                for (const key in elementProps) {
                    setProperty(elementRef.current, key, props[key], prevElemPropsRef.current.get(key), events);
                    prevElemPropsRef.current.delete(key);
                    newElemProps.set(key, props[key]);
                }
                // "Unset" any props from previous render that no longer exist.
                // Setting to `undefined` seems like the correct thing to "unset"
                // but currently React will set it as `null`.
                // See https://github.com/facebook/react/issues/28203
                for (const [key, value] of prevElemPropsRef.current) {
                    setProperty(elementRef.current, key, undefined, value, events);
                }
                prevElemPropsRef.current = newElemProps;
            });
            // Empty dependency array so this will only run once after first render.
            React.useLayoutEffect(() => {
                elementRef.current?.removeAttribute('defer-hydration');
            }, []);
        }
        if (NODE_MODE) {
            // If component is to be server rendered with `@lit/ssr-react`, pass
            // element properties in a special bag to be set by the server-side
            // element renderer.
            if ((React.createElement.name === 'litPatchedCreateElement' ||
                globalThis.litSsrReactEnabled) &&
                Object.keys(elementProps).length) {
                // This property needs to remain unminified.
                reactProps['_$litProps$'] = elementProps;
            }
        }
        else {
            // Suppress hydration warning for server-rendered attributes.
            // This property needs to remain unminified.
            reactProps['suppressHydrationWarning'] = true;
        }
        return React.createElement(tagName, {
            ...reactProps,
            ref: React.useCallback((node) => {
                elementRef.current = node;
                if (typeof ref === 'function') {
                    ref(node);
                }
                else if (ref !== null) {
                    ref.current = node;
                }
            }, [ref]),
        });
    });
    ReactComponent.displayName = displayName ?? elementClass.name;
    return ReactComponent;
};
//# sourceMappingURL=create-component.js.map