import type { CreateRuleFunction, createRuleFactory } from "./createRuleFactory.js";
import type { PureScopedStyleSheet } from "./createStylesheet.js";
import type { CreateVarsFunctionReturnStyleObject } from "./createVarFactory.js";

type StyledVNode<T> = ((props: T, ...args: any[]) => any) & {
    defaultProps?: T;
    displayName?: string;
    ruleSelector?: string;
    ruleClass?: string;
};

type Tagged<P extends Record<string, any> = object> = StyledVNode<P>;

/**
 * When using this in your app, please make sure to make the `JSX` namespaces available through
 * a package like `@types/react` to avoid the following error:
 *
 * > The inferred type of 'XXXXXXXXXXXXX' cannot be named without a reference to
 */
type CreateJsxFunction = <
    TagName extends keyof JSX.IntrinsicElements,
    P extends Record<string, any> = object,
    Attributes = JSX.LibraryManagedAttributes<TagName, JSX.IntrinsicElements[TagName]>,
>(
    tag: TagName,
    propertiesOrRule: Parameters<CreateRuleFunction>[0] | ReturnType<CreateRuleFunction>,
    attributes?: Attributes & {
        modifyProps?: (props: Attributes) => void;
    },
) => [
    Tagged<JSX.LibraryManagedAttributes<TagName, JSX.IntrinsicElements[TagName]> & P>,
    ...ReturnType<CreateRuleFunction>,
];

/**
 * When using this in your app, please make sure to make the `JSX` namespaces available through
 * a package like `@types/react` to avoid the following error:
 *
 * > The inferred type of 'XXXXXXXXXXXXX' cannot be named without a reference to
 */
type CreateJsxControlFunction = <
    TagName extends keyof JSX.IntrinsicElements,
    Styled extends CreateVarsFunctionReturnStyleObject<any>,
    JsxParametersOfStyled = Styled extends CreateVarsFunctionReturnStyleObject<infer Y> ? Y : never,
    P extends Record<string, any> = object,
    Attributes = JSX.LibraryManagedAttributes<TagName, JSX.IntrinsicElements[TagName]>,
>(
    tag: TagName,
    control: [Styled, string, any, string[], ...any[]],
    attributes?: Attributes & {
        modifyProps?: (props: Attributes, controls: Parameters<Styled>[0]) => void;
    },
) => Tagged<JSX.LibraryManagedAttributes<TagName, JSX.IntrinsicElements[TagName]> & P & JsxParametersOfStyled>;

const createJsxFactory = (
    { settings: { createElement, forwardRef } }: PureScopedStyleSheet,
    { rule: ruleFn }: ReturnType<typeof createRuleFactory>,
): {
    jsx: CreateJsxFunction;
    jsxControl: CreateJsxControlFunction;
} => {
    const createJsx: CreateJsxFunction = (tag, propertiesOrRule, attributes) => {
        if (!createElement) {
            throw new Error("No createElement function passed.");
        }

        let ruleSelector: string;
        let ruleClass: string;
        if (Array.isArray(propertiesOrRule)) {
            [ruleSelector, ruleClass] = propertiesOrRule;
        } else {
            const [createdRuleSelector, createdRuleClass] = ruleFn(propertiesOrRule);
            ruleSelector = createdRuleSelector;
            ruleClass = createdRuleClass;
        }
        const hasForwardRef = typeof forwardRef === "function";
        const jsxFn: Tagged<JSX.LibraryManagedAttributes<"div", JSX.IntrinsicElements["div"]>> = (
            { children, className, ...props },
            ref,
        ) => {
            const useClassName = [ruleClass, className].filter(Boolean);
            const { modifyProps, ...restAttributes } = attributes || {};

            const useProps = {
                className: useClassName.join(" "),
                ...(hasForwardRef ? { ref } : {}),
                ...restAttributes,
                ...props,
            };
            modifyProps?.(useProps as any);

            return createElement(tag, useProps, children);
        };

        const finalJsxFn = (hasForwardRef ? forwardRef(jsxFn as any) : jsxFn) as any;
        finalJsxFn.ruleSelector = ruleSelector;
        finalJsxFn.ruleClass = ruleClass;

        return [finalJsxFn, ruleSelector, ruleClass];
    };

    const createJsxControl: CreateJsxControlFunction = (tag, [styled, rule, , keys], attributes) => {
        const { modifyProps, ...restAttributes } = attributes || {};
        const [jsxFn] = createJsx(tag, [undefined, rule], {
            ...restAttributes,
            modifyProps: (props: any) => {
                props.style = {
                    ...styled(props),
                    ...(props.style || {}),
                };

                // Remove customizable control properties from the rendered DOM
                const controlValues: Record<string, any> = {};
                for (const key of keys) {
                    controlValues[key] = props[key];
                    delete props[key];
                }

                modifyProps?.(props, controlValues);
            },
        });

        return jsxFn as any;
    };

    return {
        jsx: createJsx,
        jsxControl: createJsxControl,
    };
};

export { type CreateJsxFunction, type CreateJsxControlFunction, createJsxFactory };
