import Color from "color";

import { CLASSPREFIX as eccgui, COLORMINDISTANCE } from "../../configuration/constants";

import { colorCalculateDistance } from "./colorCalculateDistance";
import CssCustomProperties from "./CssCustomProperties";

type ColorOrFalse = Color | false;
export type ColorWeight = 100 | 300 | 500 | 700 | 900;
export type PaletteGroup = "identity" | "semantic" | "layout" | "extra";

export interface getEnabledColorsProps {
    /** Specify the palette groups used to define the set of colors. */
    includePaletteGroup?: PaletteGroup[];
    /** Use only some weights of a color tint. */
    includeColorWeight?: ColorWeight[];
    /** Only keep colors in the stack with a minimal color distance to all other colors. */
    minimalColorDistance?: number;
    /** Extend color stack by values generated by mixing tints of the same weight, e.g. `yellow100` with `purple100`. */
    // includeMixedColors?: boolean;
}

const getEnabledColorsFromPaletteCache = new Map<string, Color[]>();
const getEnabledColorPropertiesFromPaletteCache = new Map<string, [string, string][]>();

export function getEnabledColorsFromPalette(props: getEnabledColorsProps): Color[] {
    const configId = JSON.stringify({
        includePaletteGroup: props.includePaletteGroup,
        includeColorWeight: props.includeColorWeight,
    });

    if (getEnabledColorsFromPaletteCache.has(configId)) {
        return getEnabledColorsFromPaletteCache.get(configId)!;
    }

    const colorPropertiesFromPalette = Object.values(getEnabledColorPropertiesFromPalette(props));

    getEnabledColorsFromPaletteCache.set(
        configId,
        colorPropertiesFromPalette.map((color) => {
            return Color(color[1]);
        })
    );

    return getEnabledColorsFromPaletteCache.get(configId)!;
}

export function getEnabledColorPropertiesFromPalette({
    includePaletteGroup = ["layout"],
    includeColorWeight = [100, 300, 500, 700, 900],
    // (planned for later): includeMixedColors = false,
    minimalColorDistance = COLORMINDISTANCE,
}: getEnabledColorsProps): [string, string][] {
    const configId = JSON.stringify({
        includePaletteGroup,
        includeColorWeight,
    });

    if (getEnabledColorPropertiesFromPaletteCache.has(configId)) {
        return getEnabledColorPropertiesFromPaletteCache.get(configId)!;
    }

    const colorsFromPalette = new CssCustomProperties({
        selectorText: `:root`,
        filterName: (name: string) => {
            if (!name.includes(`--${eccgui}-color-palette-`)) {
                // only allow custom properties created for the palette
                return false;
            }
            // test for correct group and weight of the palette color
            const tint = name.substring(`--${eccgui}-color-palette-`.length).split("-");
            const group = tint[0] as PaletteGroup;
            const weight = parseInt(tint[2], 10) as ColorWeight;
            return includePaletteGroup.includes(group) && includeColorWeight.includes(weight);
        },
        removeDashPrefix: true,
        returnObject: true,
    }).customProperties();

    const colorsFromPaletteValues = Object.entries(colorsFromPalette) as [string, string][];

    const colorsFromPaletteWithEnoughDistance =
        minimalColorDistance > 0
            ? colorsFromPaletteValues.reduce((enoughDistance: [string, string][], color: [string, string]) => {
                  if (enoughDistance.includes(color)) {
                      return enoughDistance.filter((checkColor) => {
                          const distance = colorCalculateDistance({ color1: color[1], color2: checkColor[1] });
                          return checkColor === color || (distance && minimalColorDistance <= distance);
                      });
                  } else {
                      return enoughDistance;
                  }
              }, colorsFromPaletteValues)
            : colorsFromPaletteValues;

    getEnabledColorPropertiesFromPaletteCache.set(configId, colorsFromPaletteWithEnoughDistance);

    return getEnabledColorPropertiesFromPaletteCache.get(configId)!;
}

function getColorcode(text: string): ColorOrFalse {
    try {
        return Color(text);
    } catch {
        return false;
    }
}

interface textToColorOptions {
    /** Stack of colors that are allowed to be returned. */
    enabledColors: Color[] | "all" | getEnabledColorsProps;
    /** Return input text if it represents a valid color string, e.g. `#000` or `black`. */
    returnValidColorsDirectly: boolean;
}

interface textToColorProps {
    text: string;
    options?: textToColorOptions;
}

/**
 * Map a text string to a color.
 * It always returns the same color for a text as long as the options stay the same.
 * It returns `false` in case there are no colors defined to chose from.
 */
export function textToColorHash({
    text,
    options = {
        enabledColors: getEnabledColorsFromPalette({}),
        returnValidColorsDirectly: false,
    },
}: textToColorProps): string | false {
    let color = getColorcode(text);

    if (options.returnValidColorsDirectly && color) {
        // return color code for text because it was a valid color string
        return color.hex().toString();
    }

    if (!color) {
        color = getColorcode(stringToHexColorHash(text)) as Color;
    }

    if (options.enabledColors === "all" && color) {
        // all colors are allowed as return value
        return color.hex().toString();
    }

    let enabledColors = [] as Color[];

    if (Array.isArray(options.enabledColors)) {
        enabledColors = options.enabledColors;
    } else {
        enabledColors = getEnabledColorsFromPalette(options.enabledColors as getEnabledColorsProps);
    }

    if (enabledColors.length === 0) {
        // eslint-disable-next-line no-console
        console.warn("textToColorHash functionaliy need enabledColors list with at least 1 color.");
        return false;
    }

    return nearestColorNeighbour(color, enabledColors as Color[])
        .hex()
        .toString();
}

function stringToIntegerHash(inputString: string): number {
    /* this function is idempotent, meaning it retrieves the same result for the same input
    no matter how many times it's called */
    // Convert the string to a hash code
    let hashCode = 0;
    for (let i = 0; i < inputString.length; i++) {
        hashCode = (hashCode << 5) - hashCode + inputString.charCodeAt(i);
        hashCode &= hashCode; // Convert to 32bit integer
    }
    return hashCode;
}

function integerToHexColor(number: number): string {
    // Convert the hash code to a positive number (32unsigned)
    const hash = Math.abs(number + Math.pow(31, 2));
    // Convert the number to a hex color (excluding white)
    const hexColor = "#" + (hash % 0xffffff).toString(16).padStart(6, "0");
    return hexColor;
}

function stringToHexColorHash(inputString: string): string {
    const integerHash = stringToIntegerHash(inputString);
    return integerToHexColor(integerHash);
}

function nearestColorNeighbour(color: Color, enabledColors: Color[]): Color {
    const nearestNeighbour = enabledColors.reduce(
        (nearestColor, enabledColorsItem) => {
            const distance = colorCalculateDistance({
                color1: color,
                color2: enabledColorsItem,
            });
            return distance && distance < nearestColor.distance
                ? {
                      distance,
                      color: enabledColorsItem,
                  }
                : nearestColor;
        },
        {
            distance: Number.POSITIVE_INFINITY,
            color: enabledColors[0],
        }
    );
    return nearestNeighbour.color;
}
