// /* eslint-disable no-octal */
// /* eslint-disable no-mixed-operators */
import { uuidv4 } from '@firebase/util';
import { isEmpty } from '../../utils/obj/isEmpty';
import { values } from '../../utils/obj/values';
import { ThemeColors } from '../../styles/defaults/themes.interface';
import { Color } from '../Color';
import { Palette } from '../Palette';
import { ensureAllLowerCase, StringUtils } from './string.utils';
import { makeFirstLetterUppercase } from './styles.utils';
import { ApphouseRGBColorFormat, colorsByName } from '../../utils/color/names';
import { ColorDefinition, HslaColor, RgbaColor } from './color.interface';
import { PaletteType } from '../palette.interface';

export const WHITE = 'rgba(255,255,255,1)';
export const BLACK = 'rgba(0,0,0,1)';
export const WHITE_ = 'rgba(255, 255, 255, 1)';
export const BLACK_ = 'rgba(0, 0, 0, 1)';

export function rgba2hex(color: string) {
  let a;

  let rgb = color
    .replace(/\s/g, '')
    .match(/^rgba?\((\d+),(\d+),(\d+),?([^,\s)]+)?/i);
  let alpha = ((rgb && rgb[4]) || '').trim();
  //@ts-ignore
  let hex = rgb
    ? //@ts-ignore
      (rgb[1] | (1 << 8)).toString(16).slice(1) +
      //@ts-ignore
      (rgb[2] | (1 << 8)).toString(16).slice(1) +
      //@ts-ignore
      (rgb[3] | (1 << 8)).toString(16).slice(1)
    : color;

  if (alpha !== '') {
    a = alpha;
  } else {
    a = 1;
  }
  // multiply before convert to HEX
  //@ts-ignore
  a = ((a * 255) | (1 << 8)).toString(16).slice(1);
  hex = hex + a;

  return hex;
}

export const ensureHexColor = (color: string): string | undefined => {
  let str = color;
  if (!str) {
    return undefined;
  }
  if (Array.isArray(str)) {
    return `linear-gradient(90deg, ${str[0]} 0%, ${str[1]} 100%)`;
  }
  if (str.indexOf('!important') >= 0) {
    str = str.replace('!important', '');
  }

  if (str.startsWith('#')) {
    if (str.length === 4) {
      const color = str.split('#');
      const threeDigitColor = color.join('');
      const updatedColor = `#${threeDigitColor}${threeDigitColor}`;
      return updatedColor;
    }
    return str;
  } else if (str.startsWith('rgba')) {
    return rgba2hex(str);
  } else if (str.startsWith('hsla') || str.startsWith('hsl')) {
    return hslStringToRgba(str);
  } else if (str.startsWith('rgb')) {
    return rgba2hex(str);
  } else if (str.startsWith('linear-gradient')) {
    return str;
  } else if (str.length === 8) {
    // potential hex color
  } else {
    const name = makeFirstLetterUppercase(str);
    const potentialColor = colorsByName[name];
    if (potentialColor) {
      return potentialColor;
    }
    // other color
  }

  return undefined;
};
export const fromHexStringToRgbaObject = (
  color: string
): RgbaColor | undefined => {
  const rgbString = ColorUtils.toRgbaStringFromHex(color);
  return ColorUtils.toRgbaObjectFromRgbaString(rgbString);
};

export const fromColorStringToRgbaObject = (
  color: string
): RgbaColor | undefined => {
  if (!color) {
    return undefined;
  }
  if (color.startsWith('#')) {
    const rgbString = ColorUtils.toRgbaStringFromHex(color);
    return ColorUtils.toRgbaObjectFromRgbaString(rgbString);
  } else {
    return ColorUtils.toRgbaObjectFromRgbaString(color);
  }
};

export const fromColorToRgbaString = (color: Color) => {
  return ColorUtils.toRgbaStringFromRgbaObject(color.color.rgb);
};

/**
 * Convert palette into consumable objects with simple key value pairs
 * with only the rgba string
 * @param palette
 * @returns
 */
export const objectifyPaletteColorsFlat = (
  palette: Palette
): Record<string, string> => {
  const obj: any = {};
  Object.keys(palette.colors).forEach((key) => {
    // create keys
    const color = palette.colors[key];

    obj[color.id] = fromColorToRgbaString(color);
  });
  return obj;
};

export const toColorsObjectFlat = (palettes: Record<string, Palette>) => {
  const colors: Record<string, Record<string, string>> = {};
  values(palettes).forEach((palette) => {
    if (!colors[palette.id]) {
      colors[palette.id] = objectifyPaletteColorsFlat(palette);
    }
  });
  return colors;
};

export const toColorsObjectFull = (palettes: Record<string, Palette>) => {
  const colors: Record<string, PaletteType> = {};
  Object.keys(palettes).forEach((paletteId) => {
    if (!colors[paletteId]) {
      if (!isEmpty(palettes[paletteId])) {
        colors[paletteId] = palettes[paletteId].objectify;
      }
    }
  });
  return colors;
};

export const toApphouseColors = (palette: Palette): ThemeColors => {
  const palettes: Record<string, Palette> = {};
  palettes[palette.id] = palette;
  // TODO: ensure colors is of theme tokens type
  const themeColors: any = toColorsObjectFlat(palettes);
  let colors: any;

  const colorsForMode = themeColors[palette.mode];
  if (colorsForMode) {
    colors = colorsForMode[palette.id];
  }
  return colors as ThemeColors;
};

export function getOpacity(str: string): number {
  if (str) {
    const tmp = str.split(',')[3];
    if (tmp) {
      const val = parseFloat(tmp);
      if (!isNaN(val) && val < 1) {
        return val;
      }
    }
  }
  return 1;
}

export const getColorFromColorString = (
  color: string,
  key: string
): Color | undefined => {
  if (typeof color === 'string') {
    // user may be attempting to add a raw color
    // let's normalize it
    let isHex = false;
    let rgb = ColorUtils.toRgbaObjectFromRgbaString(color);
    if (!rgb) {
      // let's try hex
      rgb = fromHexStringToRgbaObject(color);
      if (rgb) {
        isHex = true;
      }
    }
    if (rgb) {
      return new Color({
        title: key,
        id: key,
        color: {
          hex: isHex ? ensureFullHex(color) || color : rgba2hex(color),
          rgb
        }
      });
    }
  }
  return undefined;
};

export function ensureFullHex(hex: string) {
  const color = ensureHexColor(hex);
  let updatedColor = color;
  if (color && color.startsWith('#')) {
    if (color.length === 4) {
      const hexColor = color.split('#')[1];
      if (hexColor.length === 3) {
        updatedColor =
          hexColor[0] +
          hexColor[0] +
          hexColor[1] +
          hexColor[1] +
          hexColor[2] +
          hexColor[2];
      }
      return `#${updatedColor}`;
    }
  }

  return updatedColor;
}

/**
 *
 * @param rgbaString in the format 'rgba(r,g,b,a)'
 * @returns
 */
export function rbgaStringToHex(rgbaString: string): string | undefined {
  const rgba = ColorUtils.toRgbaObjectFromRgbaString(rgbaString);
  if (rgba) {
    const { a } = rgba;
    if (a === 1) {
      return ColorUtils.toHexFromRgbaObject(rgba);
    } else {
      return rgbaString;
    }
  }

  return rgbaString;
}

export const hslStringToRgba = (hsl: string): string | undefined => {
  if (hsl.startsWith('hsla')) {
    // need to convert to
    const hslColor = hsl
      .replace('hsla(', '')
      .replace(')', '')
      .split(',')
      .map((v) => parseFloat(v));
    // hsla color
    const h = hslColor[0];
    const s = hslColor[1];
    const l = hslColor[2];
    const a = hslColor[3];
    const rgba = hslToRgba({ h, s, l, a });
    return rbgaStringToHex(`rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${a})`);
  } else if (hsl.startsWith('hsl')) {
    const hslColor = hsl
      .replace('hsl(', '')
      .replace(')', '')
      .split(',')
      .map((v) => parseFloat(v));

    const h = hslColor[0];
    const s = hslColor[1];
    const l = hslColor[2];
    const a = 1;
    const rgba = hslToRgba({ h, s, l, a });
    return rbgaStringToHex(`rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${a})`);
  }
  return '';
};
export const hslToRgba = (hsl: HslaColor): RgbaColor => {
  let { h, s, l } = hsl;

  // IMPORTANT if s and l between 0,1 remove the next two lines:
  s /= 100;
  l /= 100;

  const k = (n: number) => (n + h / 30) % 12;
  const a = s * Math.min(l, 1 - l);
  const f = (n: number) =>
    l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
  return {
    r: Math.round(255 * f(0)),
    g: Math.round(255 * f(8)),
    b: Math.round(255 * f(4)),
    a: 1
  };
};

// /**** FROM HERE TO BOTTOM. OK */
export const getNameForColor = (color: string): string => {
  if (!color) {
    return uuidv4();
  }

  return color;
};

export const getColorId = (color: string): string => {
  let name = color;
  return name;
};

export const sortColorsByRgb = (
  sortBy: 'r' | 'g' | 'b',
  colors: ApphouseRGBColorFormat[]
): ApphouseRGBColorFormat[] => {
  return colors.slice().sort((a, b) => {
    if (a[sortBy] < b[sortBy]) {
      return 1;
    }
    if (a[sortBy] > b[sortBy]) {
      return -1;
    }
    return 0;
  });
};

export function moveCursorToEndAndFocus(inputId: string) {
  const input: HTMLTextAreaElement = document?.getElementById(
    inputId
  ) as HTMLTextAreaElement;

  if (input) {
    const inputLength = input.value.length;
    input.setSelectionRange(inputLength, inputLength);
    input.focus();
  }
}
export default class ColorUtils {
  /**
   * Get the complementary color for a given color
   * A complementary color is a direct opposite of a color on the color wheel
   * @param color a color in string format
   * @returns the complementary color in #hex format
   */
  static getComplementaryColor = (color: string): string => {
    const rgb = ColorUtils.toRgbaObjectFromColorString(color);
    if (rgb) {
      const { r, g, b } = rgb;
      return ColorUtils.toHexFromRgbaObject({
        r: 255 - r,
        g: 255 - g,
        b: 255 - b,
        a: 1
      });
    }
    return color;
  };

  /**
   * Get the a significantly lighter/darker shade of a color
   * @param color a color in string format
   * @returns
   */
  static getInverseColor = (color: string): string => {
    const shades = ColorUtils.getSurfaceColors(10, color);
    const inverse = shades[shades.length - 1];

    // if (inverse === BLACK_) {
    //   // went too far
    //   inverse = shades[shades.length - 2];
    // } else if (inverse === WHITE_) {
    //   // went too far
    //   inverse = shades[shades.length - 2];
    // }

    return inverse;
  };

  /**
   * A function to that creates shaded or tinted colors
   * based on a single color. The way it decides on tinted vs
   * shaded is based how close the surface color is to "white" or
   * "black". If it is closer to "white", it creates shades and if its
   * closer to "black" it creates tints
   * @param variants number of variants to create
   * @param color a color used for a background
   * @returns an array of colors
   */
  static getSurfaceColors = (variants: number, color: string): string[] => {
    // we check what would its foreground color be based on color
    const foregroundColor = ColorUtils.getColorForeground(color);
    const rgba = ColorUtils.toRgbaObjectFromColorString(foregroundColor);
    if (rgba && rgba?.r === 0) {
      // this means that foreground color is black
      // which means the given color is more on the lighter side
      // we get color shades (the colors will come from lighter to darker)
      return ColorUtils.getColorShades(color, variants, 0.3);
    }
    // The foreground color is white.
    // The given color is more on the darker side
    // We get color tints (the colors will come from darker to lighter)
    return ColorUtils.getColorTints(color, variants, 0.4);
  };

  /**
   * Pair background colors with their respective foreground colors
   * @param colors a list of colors to be paired
   * @returns a list of matching colors, Color[][] where the first color
   * in the pair is the background and the second color is the foreground
   */
  static getPairedColors = (colors: Color[]): Color[][] => {
    const onColor: { [wantsToMatchWith: string]: Color[] } =
      ColorUtils.getOnColors(colors);
    const matchingPairs: Color[][] = [];

    Object.keys(onColor).forEach((backgroundColorId) => {
      const matchingColors = onColor[backgroundColorId as any];

      const onColors = matchingColors.filter(
        (c) =>
          c.id.toLocaleLowerCase() !== backgroundColorId.toLocaleLowerCase()
      );
      const pairWith = colors.filter(
        (c) =>
          c.id.toLocaleLowerCase() === backgroundColorId.toLocaleLowerCase()
      );

      onColors.forEach((color) => {
        //Order is important here, the first color is the background and the second color is the foreground
        matchingPairs.push([pairWith[0], color]);
      });
    });

    return matchingPairs;
  };

  /**
   * Get onColors based on a list of colors
   * @param colors a list of colors to extract the onColors from
   * @returns a hashed object containing a list of colors with the key being the color the onColor would pair with
   */
  static getOnColors = (
    colors: Color[]
  ): { [wantsToMatchWith: string]: Color[] } => {
    const onColor: { [wantsToMatchWith: string]: Color[] } = {};

    colors.forEach((color) => {
      const lowercaseId = ensureAllLowerCase(color.id);
      if (lowercaseId.startsWith('on')) {
        // if color has "_" it supposedly means it is a variant/shade or tint from the main color.
        // let's match that with the appropriate color
        const id = lowercaseId.split('_')[0];
        const wantsToMatchWith = id.replace('on', '');
        if (!onColor[wantsToMatchWith]) {
          onColor[wantsToMatchWith] = [color];
        } else {
          onColor[wantsToMatchWith] = [...onColor[wantsToMatchWith], color];
        }
      }
    });

    return onColor;
  };

  /**
   * Convert rgba string to rgba object
   * @param color color in rgba() string format
   * @returns Rgba Object
   */
  static toRgbaObjectFromRgbaString = (
    color: string
  ): RgbaColor | undefined => {
    if (color.startsWith('rgba')) {
      const m = color.match(/(\d+){1}/g);

      if (m) {
        return {
          r: parseInt(m[0]),
          g: parseInt(m[1]),
          b: parseInt(m[2]),
          a: getOpacity(color)
        };
      }

      return undefined;
    }
    if (color.startsWith('rgb')) {
      const m = color.match(/(\d+){1}/g);

      if (m) {
        return {
          r: parseInt(m[0]),
          g: parseInt(m[1]),
          b: parseInt(m[2]),
          a: 1
        };
      }
    }
  };
  /**
   * Helper function to transform a hex color into RGB numbers into a comma
   * separated string. Example: #FFFFFF will return '255,255,255'
   * @param hex color string in hex
   * @returns string in the format 'number, number, number'
   */
  static toRgbValues = (hex: string): string => {
    if (typeof hex !== 'string') {
      console.warn(
        'attempted to convert a non-string hex color to an rgba string',
        hex
      );
      return hex;
    }
    if (hex.startsWith('rgba')) {
      // already an rgb string, let's just split it
      const rgba = hex.replace('rgba(', '').replace(')', '');
      // drop the alpha value
      return rgba.split(',').slice(0, 3).join(',');
    }

    if (hex.startsWith('rgb')) {
      // already an rgb string, let's just split it
      return hex.replace('rgb(', '').replace(')', '');
    }
    let hexColor = hex.replace(/^#/, '');
    if (hexColor.length === 3) {
      hexColor = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
    }
    const num = parseInt(hexColor, 16);
    return `${num >> 16}, ${(num >> 8) & 255}, ${num & 255}`;
  };

  /**
   * Convert hex color to Rgba string
   * @param hex hex color
   * @returns rgba string color
   */
  static toRgbaStringFromHex = (hex: string): string => {
    if (typeof hex !== 'string') {
      // throw new TypeError("Expected a string");
      console.warn(
        'attempted to convert a non-string hex color to an rgba string',
        hex
      );
      return hex;
    }
    if (hex.startsWith('rgb')) {
      // already an rgba color
      return hex;
    }
    const hexColor = ensureFullHex(hex);

    if (!hexColor) {
      return BLACK;
    }
    const color = hexColor.split('#')[1];

    const num = parseInt(color, 16);

    return `rgba(${num >> 16}, ${(num >> 8) & 255}, ${num & 255}, 1)`;
  };

  /**
   * Get white or black foreground color based on a background color.
   * (it not always works when the color has opacity < 1)
   * @param backgroundColor the background color you want the foreground color for
   * @returns the foreground color with enough contrast from the original color
   * it will be either white or black
   */
  static getColorForeground = (backgroundColor?: string): string => {
    if (!backgroundColor) {
      console.warn(
        'Returned default #ffffff for color foreground, no background color found'
      );
      return '#FFFFFF';
    }

    // we need to find out which color is the brightest and which color is the lightest
    const rgb = ColorUtils.toRgbaObjectFromColorString(backgroundColor);
    if (rgb && rgb.r === 255 && rgb.g === 255 && rgb.b === 255) {
      return BLACK;
    }
    if (rgb && rgb.r === 0 && rgb.g === 0 && rgb.b === 0) {
      return WHITE;
    }
    const c2 = ColorUtils.getContrastRatio('#FFFFFF', backgroundColor);

    return c2 && c2 > 3 ? WHITE : BLACK;
  };

  /**
   * Convert a color name from theme.dark.colorname to color name
   * Strips out the "colorname" from theme.dark.colorname
   * @param colorNameToken a string in the format of theme.colorname or theme.dark.colorname
   * @returns the last part of the string after the last dot
   */
  static getColorTitleFromTokenString = (colorNameToken: string): string => {
    const colorName = colorNameToken.split('.');
    return colorName[colorName.length - 1];
  };

  /**
   * Converts an RGBA oject to a rgba string
   * @param rgba RgbaColor
   * @returns string rgba color in the format rgba(r, g, b, a)
   */
  static toRgbaStringFromRgbaObject = (
    rgba?: RgbaColor
  ): string | undefined => {
    if (!rgba) {
      // console.warn("no rgba color");
      return undefined;
    }
    try {
      const { r, g, b, a } = rgba;
      const hasRGBColors = r >= 0 && g >= 0 && b >= 0 ? true : false;
      if (!hasRGBColors) {
        console.warn('missing r,g,b values');
        return undefined;
      }
      const parse = (v: number) => Math.round(v);
      if (hasRGBColors && (!a || a === 1)) {
        return `rgba(${parse(r)}, ${parse(g)}, ${parse(b)}, 1)`;
      }
      return `rgba(${parse(r)}, ${parse(g)}, ${parse(b)}, ${a.toFixed(1)})`;
    } catch (error) {
      return undefined;
    }
  };

  /**
   * Convert string color to rgba object
   * @param color color string to be converted, it can be a hex or rgba color
   * @returns
   */
  static toRgbaObjectFromColorString = (
    color: string
  ): RgbaColor | undefined => {
    if (!color) {
      return undefined;
    }
    if (color.startsWith('#')) {
      const rgbString = ColorUtils.toRgbaStringFromHex(color);
      return ColorUtils.toRgbaObjectFromRgbaString(rgbString);
    } else if (color.startsWith('rgb')) {
      return ColorUtils.toRgbaObjectFromRgbaString(color);
    } else if (color.startsWith('hsl')) {
      return ColorUtils.toRgbaObjectFromHslaString(color);
    }
  };

  static toHslaObjectFromHslaString = (hsla: string) => {
    const hslaArray = hsla.replace('hsla(', '').replace(')', '').split(',');
    const h = parseFloat(hslaArray[0]);
    const s = parseFloat(hslaArray[1]);
    const l = parseFloat(hslaArray[2]);
    const a = parseFloat(hslaArray[3]);
    return { h, s, l, a };
  };

  static toRgbaObjectFromHslaString = (hsla: string): RgbaColor | undefined => {
    if (!hsla) {
      return undefined;
    }
    if (!hsla.startsWith('hsl')) {
      console.warn('not a hsla color');
      return undefined;
    }
    const hslaObj = ColorUtils.toHslaObjectFromHslaString(hsla);
    const rgba = hslToRgba(hslaObj);
    return rgba;
  };

  static toHslaObjectFromHex = (hex: string): HslaColor => {
    const rgbaString = ColorUtils.toRgbaStringFromHex(hex);
    const rgb = ColorUtils.toRgbaObjectFromRgbaString(rgbaString);
    if (!rgb) {
      return { h: 0, s: 0, l: 0, a: 1 };
    }
    const { r, g, b } = rgb;
    const max = Math.max(r, g, b);
    const min = Math.min(r, g, b);
    const l = (max + min) / 2;
    let h;
    let s;
    if (max === min) {
      h = 0;
      s = 0;
    } else {
      const d = max - min;
      s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
      switch (max) {
        case r:
          h = (g - b) / d + (g < b ? 6 : 0);
          break;
        case g:
          h = (b - r) / d + 2;
          break;
        case b:
          h = (r - g) / d + 4;
          break;
      }
      // h && h /= 6;
    }
    return { h: h || 0, s, l, a: 1 };
  };

  static toHslaFromColorString = (colorStr: string): HslaColor => {
    if (colorStr.startsWith('#')) {
      // color is a hex color
      // should convert hex to hsla
      return ColorUtils.toHslaObjectFromHex(colorStr);
    } else if (colorStr.startsWith('rgb')) {
      // color is a hex color
      // should convert hex to hsla
      return ColorUtils.toHslaObjectFromHslaString(colorStr);
    } else if (colorStr.startsWith('hsl')) {
      // color is a hex color
      // should convert hex to hsla
      return ColorUtils.toHslaObjectFromHslaString(colorStr);
    }
    return {
      h: 0,
      s: 0,
      l: 0,
      a: 0
    };
  };

  static toColorDefinitionFromColorString = (
    colorStr: string
  ): ColorDefinition => {
    return {
      hex: ColorUtils.toHexFromColorString(colorStr) || '',
      rgb: ColorUtils.toRgbaObjectFromColorString(colorStr),
      hsl: ColorUtils.toHslaFromColorString(colorStr)
    };
  };

  static toHexFromColorString = (color: string) => {
    if (!color) {
      return undefined;
    }
    if (color.startsWith('#')) {
      return color;
    }
    if (color.startsWith('rgb')) {
      const rgba = ColorUtils.toRgbaObjectFromRgbaString(color);
      return rgba && ColorUtils.toHexFromRgbaObject(rgba);
    } else if (color.startsWith('hsl')) {
      const rgba = ColorUtils.toRgbaObjectFromHslaString(color);
      return rgba && ColorUtils.toHexFromRgbaObject(rgba);
    }
  };

  static toHexFromRgbaObject = (rgbaObject: RgbaColor) => {
    const { r, g, b } = rgbaObject;
    return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
  };

  /**
   * Get color shades from a color
   * A shade is produced by "darkening" a hue or "adding black"
   * @param color the hex or rgb string color you want shades from
   * @param variations the number of shades
   * @param multiplier how close together you want the color shades to be (the lower the number the closer)
   * @returns an array of color strings with shaded variations of the original color
   */
  static getColorShades = (
    color: string,
    variations: number,
    multiplier: number = 2
  ): string[] => {
    const colorShades: string[] = [];
    const colorObject = ColorUtils.toRgbaObjectFromColorString(color);
    if (!colorObject) {
      return colorShades;
    } else {
      const { r, g, b } = colorObject;
      //
      const step = (1 / variations) * multiplier;
      for (let i = 0; i < variations; i++) {
        const shade = ColorUtils.toRgbaStringFromRgbaObject({
          r: r * (1 - step * i),
          g: g * (1 - step * i),
          b: b * (1 - step * i),
          a: 1
        });
        if (shade) {
          colorShades.push(shade);
        }
      }
    }
    return colorShades;
  };

  /**
   * Get color tints from a color
   * A tint is produced by "lightening" a hue or "adding white"
   * @param color the hex or rgb string color you want shades from
   * @param variations the number of shades
   * @param multiplier how close together you want the color tints to be (the lower the number the closer)
   * @returns an array of color strings with shaded variations of the original color
   */
  static getColorTints = (
    color: string,
    variations: number,
    multiplier: number = 1
  ): string[] => {
    const colorTints: string[] = [];
    const colorObject = ColorUtils.toRgbaObjectFromColorString(color);
    if (!colorObject) {
      return colorTints;
    } else {
      const { r, g, b } = colorObject;
      //
      const step = (1 / variations) * multiplier;
      for (let i = 0; i < variations; i++) {
        const tint = ColorUtils.toRgbaStringFromRgbaObject({
          r: r + (255 - r) * (step * i),
          g: g + (255 - g) * (step * i),
          b: b + (255 - b) * (step * i),
          a: 1
        });
        if (tint) {
          colorTints.push(tint);
        }
      }
    }
    return colorTints;
  };

  /**
   * Calculate contrast ratio number for colors
   * @param colors Array of 2 colors, the first color being the background and second the foreground
   * @returns the contrast ratio number
   */
  static getContrastRatioForColors = (colors: Color[]): number | undefined => {
    // The latest accessibility guidelines (e.g., WCAG 2.0 1.4.3) require that text
    // (and images of text) provide adequate contrast for people who have visual impairments.
    // Contrast is measured using a formula that gives a ratio ranging from 1:1
    // (no contrast, e.g., black text on a black background) to 21:1 (maximum contrast,
    //  e.g., black text on a white background). Using this formula, the requirements are:
    // 3:1 - minimum contrast for "large scale" text (18 pt or 14 pt bold, or larger) under WCAG 2.0 1.4.3 (Level AA)
    // 4.5:1 - minimum contrast for regular sized text under WCAG 2.0 1.4.3 (Level AA)
    // 7:1 - "enhanced" contrast for regular sized text under WCAG 2.0 1.4.6 (Level AAA)
    // FORMULA
    // contrastRatio = (L1 + 0.05) / (L2 + 0.05), where:
    // L1 is the relative luminance of the lighter of the colors, and
    // L2 is the relative luminance of the darker of the colors.
    if (colors.length < 2) {
      return undefined;
    }
    const background = colors[0];
    const foreground = colors[1];
    if (!background || !foreground) {
      return undefined;
    }
    const backgroundColor = background.color?.hex;
    const foregroundColor = foreground.color?.hex;

    let darkestColor = backgroundColor;
    let lightestColor = foregroundColor;
    // we need to find out which color is the brightest and which color is the lightest
    const b = ColorUtils.getContrastRatio('#FFFFFF', backgroundColor);
    const f = ColorUtils.getContrastRatio('#FFFFFF', foregroundColor);

    if (f && b && lightestColor && darkestColor) {
      // foreground color is darker
      if (f > b) {
        darkestColor = foregroundColor;
        lightestColor = backgroundColor;
      }

      const contrastRatio = ColorUtils.getContrastRatio(
        lightestColor,
        darkestColor
      );
      if (contrastRatio) {
        return parseFloat(contrastRatio.toFixed(1));
      }
    }
    return undefined;
  };

  // Calculating a Contrast Ratio
  // Contrast ratios can range from 1 to 21 (commonly written 1:1 to 21:1).
  // (L1 + 0.05) / (L2 + 0.05), whereby:
  // L1 is the relative luminance of the lighter of the colors, and
  // L2 is the relative luminance of the darker of the colors.

  /**
   * Function to calculate the contrast ratio between two colors.
   * @param lightestColor
   * @param darkestColor
   * @returns
   */
  static getContrastRatio = (
    lightestColor: string,
    darkestColor: string
  ): number | undefined => {
    const L1 = ColorUtils.getLuminance(lightestColor);
    const L2 = ColorUtils.getLuminance(darkestColor);

    let contrastRatio = 0;
    if (L1 !== undefined && L2 !== undefined && L1 >= 0 && L2 >= 0) {
      const ratio = (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
      contrastRatio = parseFloat(ratio.toFixed(2));
    }
    return contrastRatio;
  };

  /**
   * Calculate brightness value by RGB or HEX color.
   * @param color (String) The color value in RGB or HEX (for example: #000000 || #000 || rgb(0,0,0) || rgba(0,0,0,0))
   * @returns (Number) The brightness value (dark) 0 ... 255 (light)
   */
  static getLuminance = (color: string): number | undefined => {
    /**
     * relative luminance
     * The relative brightness of any point in a colorspace, normalized to 0 for
     * darkest black and 1 for lightest white

      Note 1: For the sRGB colorspace, the relative luminance of a color is defined
      as L = 0.2126 * R + 0.7152 * G + 0.0722 * B where R, G and B are defined as:

      if RsRGB <= 0.03928 then R = RsRGB/12.92 else R = ((RsRGB+0.055)/1.055) ^ 2.4

      if GsRGB <= 0.03928 then G = GsRGB/12.92 else G = ((GsRGB+0.055)/1.055) ^ 2.4

      if BsRGB <= 0.03928 then B = BsRGB/12.92 else B = ((BsRGB+0.055)/1.055) ^ 2.4
     */
    const rgba = ColorUtils.toRgbaObjectFromColorString(color);
    if (rgba) {
      const { r, g, b } = rgba;
      // const luminance = (r * 299 + g * 587 + b * 114) / 1000;
      const luminance =
        0.2126 * ColorUtils.getsRGB(r) +
        0.7152 * ColorUtils.getsRGB(g) +
        0.0722 * ColorUtils.getsRGB(b);

      return luminance;
    }
    return undefined;
  };

  /**
   *
   * @param c a number from 0 to 255
   * @returns srgb equivalent
   */
  static getsRGB = (c: number) => {
    c = c / 255;
    c = c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
    return c;
  };

  static getMatchingColorPair = (
    color: Color,
    paletteColors: Color[]
  ): Color[] => {
    if (!color || !paletteColors || paletteColors.length === 0) {
      return [];
    }
    if (!color.id) {
      return [];
    }
    if (color.id.startsWith('on')) {
      const matchingColors: Color[] = [];
      // color is foreground, matching pair is background
      paletteColors.forEach((pColor) => {
        const matchingKey = color.id.replace('on', '');
        const key =
          StringUtils.makeFirstLetterLowercase(matchingKey).split('_')[0];
        if (pColor.id === key) {
          matchingColors.push(pColor);
        }
      });
      return matchingColors;
    } else {
      // color is background, matching pair is foreground
      const matchingColors: Color[] = [];
      // color is foreground, matching pair is background
      paletteColors.forEach((pColor) => {
        const matchingKey = `on${StringUtils.makeFirstLetterUppercase(
          color.id
        )}`;
        if (pColor.id.split('_')[0] === matchingKey) {
          matchingColors.push(pColor);
        }
      });
      return matchingColors;
    }
  };
}
