import { FASTElementDefinition, Observable, } from "@microsoft/fast-element";
import { FoundationElementRegistry, } from "@microsoft/fast-foundation";
const reservedReactProperties = new Set([
    "children",
    "localName",
    "ref",
    "style",
    "className",
]);
const emptyProps = Object.freeze(Object.create(null));
const DEFAULT_CACHE_NAME = "_default";
// This will be a two levels cache Map<type, Map<name, ReactWrapper>>
// to distinguish components of same type but different tag name.
// Default name: '_default'
const wrappersCache = new Map();
// There are 2 kinds of refs and there's no built in React API to set one.
function setRef(ref, value) {
    if (typeof ref === "function") {
        ref(value);
    }
    else {
        ref.current = value;
    }
}
function getTagName(type, config) {
    if (!config.name) {
        /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
        const definition = FASTElementDefinition.forType(type);
        if (definition) {
            config.name = definition.name;
        }
        else {
            throw new Error("React wrappers must wrap a FASTElement or be configured with a name.");
        }
    }
    return config.name;
}
function getElementEvents(config) {
    return config.events || (config.events = {});
}
function keyIsValid(type, config, name) {
    if (reservedReactProperties.has(name)) {
        console.warn(`${getTagName(type, config)} contains property ${name} which is a React ` +
            `reserved property. It will be used by React and not set on ` +
            `the element.`);
        return false;
    }
    return true;
}
function getElementKeys(type, config) {
    if (!config.keys) {
        if (config.properties) {
            config.keys = new Set(config.properties.concat(Object.keys(getElementEvents(config))));
        }
        else {
            const keys = new Set(Object.keys(getElementEvents(config)));
            const accessors = Observable.getAccessors(type.prototype);
            if (accessors.length > 0) {
                for (const a of accessors) {
                    if (keyIsValid(type, config, a.name)) {
                        keys.add(a.name);
                    }
                }
            }
            else {
                for (const p in type.prototype) {
                    if (!(p in HTMLElement.prototype) && keyIsValid(type, config, p)) {
                        keys.add(p);
                    }
                }
            }
            config.keys = keys;
        }
    }
    return config.keys;
}
/**
 * @param React - The React module, typically imported from the `react` npm
 * package
 * @param designSystem - A design system to register the components with.
 * @public
 */
export function provideReactWrapper(React, designSystem) {
    let registrations = [];
    const registry = {
        register(container, ...rest) {
            registrations.forEach(x => x.register(container, ...rest));
            registrations = [];
        },
    };
    function wrap(type, config = {}) {
        var _a, _b;
        if (type instanceof FoundationElementRegistry) {
            if (designSystem) {
                designSystem.register(type);
            }
            else {
                registrations.push(type);
            }
            type = type.type;
        }
        const cachedCandidates = wrappersCache.get(type);
        if (cachedCandidates) {
            const cachedWrapper = cachedCandidates.get((_a = config.name) !== null && _a !== void 0 ? _a : DEFAULT_CACHE_NAME);
            if (cachedWrapper) {
                return cachedWrapper;
            }
        }
        class ReactComponent extends React.Component {
            constructor() {
                super(...arguments);
                this._element = null;
            }
            _updateElement(oldProps) {
                const element = this._element;
                if (element === null) {
                    return;
                }
                const currentProps = this.props;
                const previousProps = oldProps || emptyProps;
                const events = getElementEvents(config);
                for (const key in this._elementProps) {
                    const newValue = currentProps[key];
                    const event = events[key];
                    if (event === undefined) {
                        element[key] = newValue;
                    }
                    else {
                        const oldValue = previousProps[key];
                        if (newValue === oldValue) {
                            continue;
                        }
                        if (oldValue !== undefined) {
                            element.removeEventListener(event, oldValue);
                        }
                        if (newValue !== undefined) {
                            element.addEventListener(event, newValue);
                        }
                    }
                }
            }
            componentDidMount() {
                this._updateElement();
            }
            componentDidUpdate(old) {
                this._updateElement(old);
            }
            render() {
                // Since refs only get fulfilled once, pass a new one if the user's
                // ref changed. This allows refs to be fulfilled as expected, going from
                // having a value to null.
                const userRef = this.props.__forwardedRef;
                if (this._ref === undefined || this._userRef !== userRef) {
                    this._ref = (value) => {
                        if (this._element === null) {
                            this._element = value;
                        }
                        if (userRef !== null) {
                            setRef(userRef, value);
                        }
                        this._userRef = userRef;
                    };
                }
                // Filter class properties and pass the remaining attributes to React.
                // This allows attributes to use framework rules
                // for setting attributes and render correctly under SSR.
                const newReactProps = { ref: this._ref };
                const newElementProps = (this._elementProps = {});
                const elementKeys = getElementKeys(type, config);
                const currentProps = this.props;
                for (const k in currentProps) {
                    const v = currentProps[k];
                    if (elementKeys.has(k)) {
                        newElementProps[k] = v;
                    }
                    else {
                        // React does *not* handle `className` for custom elements so
                        // coerce it to `class` so it's handled correctly.
                        newReactProps[k === "className" ? "class" : k] = v;
                    }
                }
                return React.createElement(getTagName(type, config), newReactProps);
            }
        }
        const reactComponent = React.forwardRef((props, ref) => React.createElement(ReactComponent, Object.assign(Object.assign({}, props), { __forwardedRef: ref }), props === null || props === void 0 ? void 0 : props.children));
        if (!wrappersCache.has(type)) {
            wrappersCache.set(type, new Map());
        }
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        wrappersCache.get(type).set((_b = config.name) !== null && _b !== void 0 ? _b : DEFAULT_CACHE_NAME, reactComponent);
        return reactComponent;
    }
    return { wrap, registry };
}
