import { dispatchVariableUpdateEvent } from "./createComputedFactory.js";
import { CSS_VALUE_FALSE } from "./createUtilsFactory.js";
import { STYLESHEET_ELEMENT_CREATED_EVENT } from "./events.js";
import { hexToRgb } from "./utils/hexToRgb.js";
import { slugify } from "./utils/slugify.js";

import type { PureScopedStyleSheet } from "./createStylesheet.js";

type VarValue = string | number | boolean;

/**
 * @param suffix Due to performance reasons and the amount of times this function is called, the suffix is
 * not automatically slugged. Do this on the top level where you receive the suffix.
 */
type CreateVariableNameFunction = (suffix?: string) => string;

/**
 * -----------------------------------------------------
 *
 * VarConsumer: Access single variable name
 *
 * -----------------------------------------------------
 */

type RawVarConsumer<PossibleMapValues = boolean> = {
    [key in PossibleMapValues extends string
        ? PossibleMapValues
        : never | PossibleMapValues extends number
          ? number
          : never]: string;
};

type Exact<T, U> = T extends U ? (U extends T ? T : never) : never;

/**
 * Consume variable with `var()` CSS functionality or just return the variable name.
 *
 * It also includes an update functionality.
 */
type VarConsumer<
    Value extends VarValue | VarValue[] = string,
    PossibleMapValues = boolean,
> = RawVarConsumer<PossibleMapValues> &
    (Exact<PossibleMapValues, boolean> extends never
        ? { (consume: false | PossibleMapValues, fallback?: VarValue): string }
        : { (consume?: PossibleMapValues, fallback?: VarValue): string }) & {
        update: (
            value: Value,
            /**
             * Replace values in this CSS string instead of the generated stylesheet.
             * This is mainly used for internal usage to improve object updates.
             */
            css?: string,
        ) => string;
    };

type VarConsumerPossibleMapValue<T> = T extends any[]
    ? number | boolean // An array is also accessible via `true` as it is concatenated through `join(" ")`, helpful for e.g. margins and padding
    : boolean | T extends VarValue
      ? boolean
      : T extends Record<string, unknown>
        ? keyof T
        : boolean;

/**
 * -----------------------------------------------------
 *
 * CreateVarFunction: Create single variable
 *
 * -----------------------------------------------------
 */

/**
 * @param suffix Due to performance reasons and the amount of times this function is called, the suffix is
 * not automatically slugged. Do this on the top level where you receive the suffix.
 */
type CreateVarFunction = <Value extends VarValue | VarValue[], MapFunction extends (value: Value) => any>(
    value: Value,
    mapValue?: MapFunction,
    suffix?: string,
) => VarConsumer<Value, VarConsumerPossibleMapValue<ReturnType<MapFunction>>>;

/**
 * -----------------------------------------------------
 *
 * CreateVarsFunction: Create multiple variables from an object
 *
 * -----------------------------------------------------
 */

type CreateVarsFunctionValue = Record<string, VarValue | VarValue[]>;
type CreateVarsFunctionMapValueFunctions<Value extends CreateVarsFunctionValue> = {
    [P in keyof Value]?: (val: Value[P]) => any;
};
type CreateVarsFunctionReturnConsumers<
    Value extends CreateVarsFunctionValue,
    MapValueFunctions extends CreateVarsFunctionMapValueFunctions<Value>,
> = {
    [P in keyof Value]: VarConsumer<
        Value[P],
        VarConsumerPossibleMapValue<
            MapValueFunctions[P] extends (...args: any[]) => any ? ReturnType<MapValueFunctions[P]> : Value[P]
        >
    >;
};
type CreateVarsFunctionReturnUpdater<Value extends CreateVarsFunctionValue> = (value: Partial<Value>) => string;
type CreateVarsFunctionReturnStyleObject<Value extends CreateVarsFunctionValue> = (value: Partial<Value>) => {
    [P in keyof Value]: string;
};

/**
 * Consume multiple variables with `var()` CSS functionality or just return the variable name.
 *
 * Return arguments array:
 *
 * - Index `0`: All variable consumers to get `var()` CSS functionality or just return the variable name
 * - Index `1`: Updater functionality for the complete variable object.
 * - Index `2`: Create an object prepared for a `style` attribute so you can customize individual rules with expected variables.
 */
type CreateVarsFunction = <
    Value extends CreateVarsFunctionValue,
    MapValueFunctions extends CreateVarsFunctionMapValueFunctions<Value>,
>(
    object: Value,
    mapValue?: MapValueFunctions,
    createSuffix?: boolean,
) => [
    CreateVarsFunctionReturnConsumers<Value, MapValueFunctions>,
    CreateVarsFunctionReturnUpdater<Value>,
    CreateVarsFunctionReturnStyleObject<Value>,
];

const createVarConsumer = <Value extends VarValue | VarValue[]>(
    meta: PureScopedStyleSheet,
    variableName: string,
    mappedValue: any,
    mapValue?: (value: Value) => any,
): VarConsumer<Value> => {
    const { element } = meta;
    const getter = (consume = true, fallback?: VarValue) => {
        const consumable = `${variableName}${["number", "string"].indexOf(typeof consume) > -1 ? `-${consume}` : ""}`;
        const doConsume = ["boolean", "number", "string"].indexOf(typeof consume) > -1 && consume !== false;

        /*if (doConsume && !previousValues.has(consumable)) {
            console.log(consumable, new Error());
            // Currently, coverered through TypeScript
        }*/

        return doConsume ? `var(${consumable}${fallback ? `, ${fallback}` : ""})` : variableName;
    };
    const previousValues = new Map<string, ReturnType<typeof serializeValue>>();

    iterateMappedValue(variableName, mappedValue, (variable, serializedValue, k) => {
        if (k !== undefined) {
            getter[k] = variable;
        }
        previousValues.set(variable, serializedValue);
    });

    getter.update = (value: VarValue, css?: string) => {
        let textContent = css || element.textContent;

        // Is the stylesheet element already initialized so `update` makes no sense here?
        if (!css && !element.textContent) {
            element.addEventListener(STYLESHEET_ELEMENT_CREATED_EVENT, () => getter.update(value), { once: true });
            return textContent;
        }

        let changed = false;

        const mappedValue = executeMapValue(value, mapValue);
        iterateMappedValue(variableName, mappedValue, (k, v) => {
            const previousValue = previousValues.get(k);

            if (previousValue !== v) {
                previousValues.set(k, v);
                textContent = replaceInStyleSheet(textContent, k, v);
                changed = true;
            }
        });

        if (changed) {
            if (!css) {
                element.textContent = textContent;
            }

            meta.varsVal.set(variableName, value);
            dispatchVariableUpdateEvent(variableName, meta);
        }

        return textContent;
    };

    return getter as VarConsumer<Value>;
};

const createVarFactory = (
    meta: PureScopedStyleSheet,
    extendDetached: boolean,
): {
    varName: CreateVariableNameFunction;
    variable: CreateVarFunction;
    vars: CreateVarsFunction;
} => {
    const { className: mainClassName, isExtension, rules, id, element } = meta;

    // Make variables available to this class name
    const variableScopeClassName = isExtension && !extendDetached ? mainClassName.split("-ext")[0] : mainClassName;

    // Create the variables within this "scope", that means, when using `extendDetached` the variable names will be
    // the same accross multiple class names
    const variableScopeName = extendDetached ? id.split("-ext")[0] : id;

    // Make variables also available to pseudo elements as they are not inherited by default
    // See also https://stackoverflow.com/a/72294151/5506547
    /*variableScopeClassName += `,${["before", "after"]
        .map((pseudo) => `${variableScopeClassName} *::${pseudo}`)
        .join(",")}`;*/

    const createVariableName: CreateVariableNameFunction = (suffix) =>
        `--${variableScopeName}-${meta.inc++}${suffix ? `-${suffix}` : ""}`;

    const createVar: CreateVarFunction = (value, mapValue, suffix) => {
        const variableName = createVariableName(suffix);
        meta.varsVal.set(variableName, value);

        rules.set(variableScopeClassName, rules.get(variableScopeClassName) || {});
        const ruleSet = rules.get(variableScopeClassName);
        const mappedValue = executeMapValue(value, mapValue);

        iterateMappedValue(variableName, mappedValue, (k, v) => {
            ruleSet[k] = v;
        });

        return createVarConsumer(meta, variableName, mappedValue, mapValue) as any;
    };

    const createVars: CreateVarsFunction = (object, mapValue, createSuffix = true) => {
        const consumers: Record<string, VarConsumer<any, any>> = {};

        for (const objectKey in object) {
            const value = object[objectKey];
            const mapFunction = mapValue?.[objectKey];
            consumers[objectKey] = createVar(value, mapFunction, createSuffix ? slugify(objectKey) : undefined);
        }

        return [
            consumers as any,
            (newValue) => {
                let { textContent } = element;
                for (const objectKey in newValue) {
                    textContent = consumers[objectKey]?.update(newValue[objectKey], textContent);
                }

                if (textContent !== element.textContent) {
                    element.textContent = textContent;
                }
                return textContent;
            },
            (newValue) => {
                const result: Record<string, string> = {};
                const saveToResult = (k: string, v: string) => {
                    if (k.endsWith("-not")) {
                        throw new Error(
                            `Boolish variable "${k}" cannot be created as style-attribute in your HTML tag as this is not supported by browsers. Alternatively, use a classname and pseudos to toggle styles.`,
                        );
                    }

                    result[k] = v === "" ? CSS_VALUE_FALSE : v;
                };

                for (const objectKey in newValue) {
                    const consumer = consumers[objectKey];

                    if (!consumer) {
                        continue;
                    }

                    const variableName = consumer(false);
                    const mapFunction = mapValue?.[objectKey];

                    iterateMappedValue(variableName, executeMapValue(newValue[objectKey], mapFunction), saveToResult);
                }

                /*let str = objectToCss(result).trim();
                str = str
                    .substr(3, str.length - 4)
                    .trim()
                    .replace(/^\s+/gm, "");*/

                return result as any;
            },
        ];
    };

    return { varName: createVariableName, variable: createVar, vars: createVars };
};

/**
 * -----------------------------------------------------
 *
 * Misc helper functions for the variable factory.
 *
 * -----------------------------------------------------
 */

// See https://regex101.com/r/tQkPtN/1
const replaceInStyleSheet = (css: string, variableName: string, newValue: any) => {
    return css.replace(new RegExp(`^((?:    |      )${variableName}: )(.*)?;$`, "m"), `$1${newValue};`);
};

const executeMapValue = (value: any, fn: (value: any) => any) => {
    // Never touch a `--var` value
    if (typeof value === "string" && value.startsWith("var(--")) {
        return value;
    }

    return fn ? fn(value) : value;
};

const serializeValue = (value: any) => {
    if (typeof value === "boolean") {
        return value ? "initial" : "";
    }
    if (Array.isArray(value)) {
        return value.join(" ");
    }
    return value;
};

const iterateMappedValue = (
    variableName: string,
    value: any,
    callback: (variableName: string, value: string, key?: string | number) => void,
    //previousKeys?: (string | number)[]
) => {
    const keys: Array<string | number> = [];

    // Special case: boolean variables should be automatically be made available as `-not` variables
    // so this can be used with `boolNot`.
    const handleBooleans = (variableName: string, value: any) => {
        if (typeof value === "boolean") {
            callback(`${variableName}-not`, serializeValue(!value));
        }
    };

    // Proxy with value validation
    const useCallback: typeof callback = (variableName, value, key) => {
        // Avoid string literals exposing functions to the resulting property value.
        // Example: width: `calc(${thickness} + 1px)` -> width: calc(function () {
        if (typeof value === "string" && value.indexOf("function () {") > -1) {
            throw new Error(`${variableName} contains a serialized function ("${value}").`);
        }

        callback(variableName, value, key);
    };

    if (Array.isArray(value)) {
        useCallback(variableName, value.map(serializeValue).join(" "));

        // Also make the array values accessible as single property
        for (let i = 0; i < value.length; i++) {
            const arrVarName = `${variableName}-${i}`;
            handleBooleans(arrVarName, value[i]);
            useCallback(arrVarName, serializeValue(value[i]), i);
            keys.push(i);
        }
    } else if (typeof value === "object") {
        for (const objectKey in value) {
            const objVarName = `${variableName}-${slugify(objectKey)}`;
            handleBooleans(objVarName, value[objectKey]);
            useCallback(objVarName, serializeValue(value[objectKey]), objectKey);
            keys.push(objectKey);
        }
    } else {
        handleBooleans(variableName, value);
        useCallback(variableName, serializeValue(value));
    }

    // Reset keys which are no longer present in the mapped value (tbd when needed?!)
    /*if (previousKeys?.length) {
        let difference = previousKeys.filter((x) => !keys.includes(x));
        console.log(difference);
    }*/

    return keys;
};

const mapValueSuffix = (suffix: string) => (value: any) => `${value}${suffix}`;
const mapValueSuffixArray = (suffix: string) => (value: any[]) => value.map((v) => `${v}${suffix}`);
const mapValueSuffixPx = mapValueSuffix("px");
const mapValueSuffixPxArray = mapValueSuffixArray("px");
const mapHex = (hex: string) => {
    const { r, g, b } = hexToRgb(hex);
    return { r, g, b, hex };
};
const mapStringToBoolean =
    <PossibleValues extends string, ConvertValue extends PossibleValues>(
        // Only used for typing
        possibleValues: PossibleValues,
        convertValues: ConvertValue[],
    ) =>
    (
        // This is not necesserely types as it cean lead to the following TypeScript error when reusing the result:
        // > The inferred type of 'XXXXXXXXXXXXX' cannot be named without a reference to
        string: string,
    ): { [P in `is-${Lowercase<ConvertValue>}`]: boolean } & ReturnType<ReturnType<typeof mapStringToIsEmpty>> & {
            val: string;
        } => {
        return {
            ...convertValues.reduce((p, c) => {
                p[`is-${c.toLowerCase()}`] = string === c;
                return p;
            }, {} as any),
            ...mapStringToIsEmpty(false)(string),
        };
    };
const mapStringArrayToBoolean =
    <PossibleValues extends string[], ConvertValue extends PossibleValues[number]>(
        // Only used for typing
        possibleValues: PossibleValues,
        convertValues: ConvertValue[],
    ) =>
    (
        // This is not necesserely types as it cean lead to the following TypeScript error when reusing the result:
        // > The inferred type of 'XXXXXXXXXXXXX' cannot be named without a reference to
        array: string[],
    ): { [P in `has-${Lowercase<ConvertValue>}`]: boolean } => {
        return {
            ...convertValues.reduce((p, c) => {
                p[`has-${c.toLowerCase()}`] = array.indexOf(c) > -1;
                return p;
            }, {} as any),
        };
    };
const mapStringToIsEmpty =
    (encode = true) =>
    (string: string) => {
        const length = string?.length;
        const val = string || "";
        return {
            "is-empty": !length,
            "is-filled": !!length,
            val: encode ? JSON.stringify(val) : val,
        };
    };
const mapIsSet = (string: any) => {
    return {
        "is-set": typeof string !== "undefined",
    };
};
const mapBooleanToReverse = (b: boolean) => ({
    "is-true": b,
    "is-false": !b,
});
const mapIgnore = (_string: any) => '"undefined"';
const mapAll = function <T, Fn>(obj: T, fn: Fn): { [K in keyof T]: Fn } {
    return Object.keys(obj).reduce(
        (c, p) => {
            c[p] = fn;
            return c;
        },
        {} as { [K in keyof T]: Fn },
    );
};

export {
    type VarConsumer,
    type RawVarConsumer,
    type VarValue,
    type VarConsumerPossibleMapValue,
    type CreateVariableNameFunction,
    type CreateVarsFunctionValue,
    type CreateVarsFunctionMapValueFunctions,
    type CreateVarsFunctionReturnConsumers,
    type CreateVarsFunctionReturnUpdater,
    type CreateVarsFunctionReturnStyleObject,
    type CreateVarFunction,
    type CreateVarsFunction,
    createVarFactory,
    mapValueSuffix,
    mapValueSuffixArray,
    mapValueSuffixPx,
    mapValueSuffixPxArray,
    mapHex,
    mapStringToBoolean,
    mapStringArrayToBoolean,
    mapStringToIsEmpty,
    mapIsSet,
    mapBooleanToReverse,
    mapAll,
    mapIgnore,
};
