import { specifySelector } from "./createStylesheet.js";

import type { PureScopedStyleSheet, PureScopedStyleSheetRules } from "./createStylesheet.js";
import type {
    CreateVarsFunctionMapValueFunctions,
    CreateVarsFunctionReturnConsumers,
    CreateVarsFunctionReturnStyleObject,
    CreateVarsFunctionValue,
    VarConsumer,
    VarValue,
    createVarFactory,
} from "./createVarFactory.js";

type CreateClassNameFunction = () => string;

type RuleProperties = PureScopedStyleSheetRules & {
    classNames?: string | string[];
    /**
     * Forces this selector instead of generting a new class name.
     */
    forceSelector?: string | string[];
    /**
     * You can pass multiple pseudos like `:hover`, `:before` or `>div`. It simply gets
     * appended to the created rule class.
     *
     * **Special feature**: You can pass a pseudo `< .my-class` which is similar to `>` in
     * CSS but in reverse order. So, it gets resolved to `.my-class .ad5fdeabb71-0-8`
     */
    pseudos?: Record<string, PureScopedStyleSheetRules>;
};

/**
 * A rule represents a rule in a CSS stylesheet.
 *
 * Return arguments array:
 *
 * - Index `0`: Selector resulting in stylesheet (with leading `.`)
 * - Index `1`: Class name (without leading `.`)
 */
type CreateRuleFunction = (properties: RuleProperties) => [string, string | undefined];

/**
 * Allows you to define a "customizable" set of rules with variables.
 *
 * Return arguments array:
 *
 * - Index `0`: Create an object prepared for a `style` attribute so you can customize individual rules with expected variables.
 * - Index `1`: Return of `create` function to obtain created class names
 * - Index `2`: All variable consumers to get `var()` CSS functionality or just return the variable name
 * - Index `3`: All keys of the passed object
 *
 * **Note**: Currently, `boolIf` and `:after/:before` does currently not support consuming inline style variables
 * due to how inheritance works for custom properties.
 */
type CreateControlFunction = <
    Value extends CreateVarsFunctionValue,
    MapValueFunctions extends CreateVarsFunctionMapValueFunctions<Value>,
    CreateReturn,
>(
    object: Value,
    mapValue: MapValueFunctions,
    /**
     * Create your `rule`'s and return whatever you want (e.g. class names).
     */
    create: (variables: CreateVarsFunctionReturnConsumers<Value, MapValueFunctions>) => CreateReturn,
) => [
    CreateVarsFunctionReturnStyleObject<Value>,
    CreateReturn,
    CreateVarsFunctionReturnConsumers<Value, MapValueFunctions>,
    string[],
];

type CreateVariantFunction = (variables: Array<[VarConsumer, VarValue]>) => [string, string];

const hyphenProperty = (property: string) => {
    const [firstChar] = property;
    if (firstChar.toUpperCase() === firstChar.toLowerCase() || property.indexOf("-") > -1) {
        return property;
    }

    return property.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
};

const hyphenObject = (object: Record<string, string | VarConsumer>) => {
    return Object.keys(object).reduce(
        (previous, currentValue) => {
            let val = object[currentValue];
            val = typeof val === "function" ? val() : val;

            // Avoid string literals exposing functions to the resulting property value.
            // Example: width: `calc(${thickness} + 1px)` -> width: calc(function () {
            if (typeof val === "string" && val.indexOf("function () {") > -1) {
                throw new Error(`${currentValue} contains a serialized function ("${val}").`);
            }

            previous[hyphenProperty(currentValue)] = val;
            return previous;
        },
        {} as Record<string, any>,
    );
};

const createClassNameFactory = (meta: PureScopedStyleSheet, withDot = false) =>
    `${meta.className.substr(withDot ? 0 : 1)}-${meta.inc++}`;

const createRuleFactory = (
    meta: PureScopedStyleSheet,
    { vars: varsFn }: ReturnType<typeof createVarFactory>,
): {
    className: CreateClassNameFunction;
    rule: CreateRuleFunction;
    control: CreateControlFunction;
    variant: CreateVariantFunction;
} => {
    const { id, specifiedIds } = meta;
    const { runPlugin } = meta;
    const createClassName = (withDot?: boolean) => createClassNameFactory(meta, withDot);

    const createRule: CreateRuleFunction = (properties) => {
        runPlugin("modifyRule", properties);
        const { classNames, pseudos, forceSelector, ...rest } = properties;
        const useForceSelector = Array.isArray(forceSelector) ? forceSelector.join(" ") : forceSelector;
        const useClassNames = Array.isArray(classNames) ? classNames : classNames ? classNames.split(" ") : [];
        const selector = useForceSelector || createClassName(true);
        meta.rules.set(specifySelector(id, specifiedIds, selector), hyphenObject(rest));

        if (pseudos) {
            // Allow comma separated selectors to apply the pseudos too
            const selectors = selector.split(",");

            for (const pseudo in pseudos) {
                // Allow comma separated pseudos like `::after,::before`
                const pseudoSplit = pseudo.split(",");
                const ruleSelector = selectors
                    .map((s) =>
                        pseudoSplit.map((p) =>
                            s === p ? undefined : p.startsWith("<") ? `${p.substr(1)}${s}` : `${s}${p}`,
                        ),
                    )
                    .flat()
                    .filter(Boolean)
                    .join(",");
                meta.rules.set(specifySelector(id, specifiedIds, ruleSelector), hyphenObject(pseudos[pseudo]));
            }
        }

        // Build class name
        const className = [selector.substr(1)];
        if (!forceSelector) {
            runPlugin("filterClassName", useClassNames, className[0], meta);
            className.push(...useClassNames);
        }

        return [selector, forceSelector ? undefined : className.join(" ")];
    };

    const createControl: CreateControlFunction = (object, mapValue, create) => {
        const [consumer, , styleObject] = varsFn(object, mapValue, false);
        const result = create(consumer);
        return [styleObject, result, consumer, Object.keys(object)];
    };

    const createVariant: CreateVariantFunction = (variables) => {
        const className = createClassName(true);

        return [
            createRule(
                variables.reduce(
                    (a, [variableConsumer, expression]) => {
                        // Add extra whitespace so `VarConsumer#update` does not update this expression
                        a[` ${variableConsumer(false)}`] = expression;
                        return a;
                    },
                    {
                        forceSelector: `${meta.className}${className}`,
                    } as Parameters<CreateRuleFunction>[0],
                ),
            )[0],
            className.substr(1),
        ];
    };

    return {
        className: createClassName,
        rule: createRule,
        control: createControl,
        variant: createVariant,
    };
};

export {
    type CreateRuleFunction,
    type CreateControlFunction,
    type CreateVariantFunction,
    type CreateClassNameFunction,
    createRuleFactory,
};
