import hex2rgb from '~/converters/hex2rgb';
import { extractAlphaFromHex, normalizeAlpha } from '~/modules/alpha';
import { COLOR_KEYS, MESSAGES } from '~/modules/constants';
import { invariant } from '~/modules/invariant';
import { hasValidMatches, isHex, isString } from '~/modules/validators';

import { ColorModelKey, PlainObject } from '~/types';

// Regex components for parsing CSS color strings
const MODEL = '(rgb|hsl|oklab|oklch)a?';
const SEP = '(?:\\s*[,/]\\s*|\\s+)';
// Accepts: numbers, percentages, 'none', and angle units (deg, grad, rad, turn)
const VALUE = '(none|[\\d%.-]+(?:deg|grad|rad|turn)?)';

const colorRegex = new RegExp(
  `${MODEL}\\s*\\(\\s*${VALUE}${SEP}${VALUE}${SEP}${VALUE}(?:${SEP}${VALUE})?\\s*\\)`,
  'i',
);

export type ExtractColorPartsReturn = {
  alpha?: number;
  model: ColorModelKey;
} & PlainObject<number>;

/**
 * Convert angle value with units to degrees
 */
function parseAngle(value: string): number {
  const number_ = parseFloat(value);
  let result: number;

  if (value.endsWith('grad')) {
    result = number_ * 0.9; // 400grad = 360deg
  } else if (value.endsWith('rad')) {
    result = number_ * (180 / Math.PI); // radians to degrees
  } else if (value.endsWith('turn')) {
    result = number_ * 360; // 1turn = 360deg
  } else {
    // 'deg' suffix or unitless - return as-is
    result = number_;
  }

  // Round to 5 decimal places for consistency
  return Math.round(result * 100000) / 100000;
}

/**
 * Extract the color parts from a CSS color string.
 * Hex colors are supported via conversion.
 *
 * @param input - The CSS color string.
 * @returns An object with the color model and component values.
 */
export default function extractColorParts(input: string): ExtractColorPartsReturn {
  invariant(isString(input), MESSAGES.inputString);

  if (isHex(input)) {
    const keys = COLOR_KEYS.rgb;
    const { r, g, b } = hex2rgb(input);
    const alpha = extractAlphaFromHex(input);

    return {
      model: 'rgb' as ColorModelKey,
      [keys[0]]: r,
      [keys[1]]: g,
      [keys[2]]: b,
      alpha: alpha < 1 ? alpha : undefined,
    } as ExtractColorPartsReturn;
  }

  const matches = colorRegex.exec(input);

  invariant(hasValidMatches(matches), MESSAGES.invalidCSS);

  let rawAlpha = 1;

  if (matches[5]) {
    rawAlpha = matches[5] === 'none' ? 0 : parseFloat(matches[5]);
  }

  const model = matches[1] as ColorModelKey;
  const keys = COLOR_KEYS[model];
  const alpha = normalizeAlpha(rawAlpha);

  // Parse values and convert percentages/angles for color models
  const parseValue = (value: string, index: number): number => {
    // Handle 'none' keyword - treated as 0 for rendering (CSS Color Level 4)
    if (value === 'none') {
      return 0;
    }

    // Handle hue values with angle units (HSL index 0, OkLCH index 2)
    const isHue = (model === 'hsl' && index === 0) || (model === 'oklch' && index === 2);

    if (isHue) {
      return parseAngle(value);
    }

    const parsedValue = parseFloat(value);
    const isPercent = value.includes('%');

    if (!isPercent) {
      return parsedValue;
    }

    // Convert percentages based on color model and position
    if (model === 'oklch') {
      // oklch: L (0-100), C (0-0.4), H (0-360)
      if (index === 1) {
        // Chroma: 100% = 0.4
        return (parsedValue * 0.4) / 100;
      }
      // oklab: L (0-100), a (-0.4 to 0.4), b (-0.4 to 0.4)
    } else if (model === 'oklab' && (index === 1 || index === 2)) {
      // a/b: ±100% = ±0.4
      return (parsedValue * 0.4) / 100;
    }

    return parsedValue;
  };

  const values = [parseValue(matches[2], 0), parseValue(matches[3], 1), parseValue(matches[4], 2)];

  // Validate ranges for oklab/oklch after percentage conversion
  if (model === 'oklab') {
    // a and b must be in range -0.4 to 0.4
    invariant(values[1] >= -0.4 && values[1] <= 0.4, MESSAGES.invalidRange);
    invariant(values[2] >= -0.4 && values[2] <= 0.4, MESSAGES.invalidRange);
  } else if (model === 'oklch') {
    // chroma must be in range 0 to 0.4
    invariant(values[1] >= 0 && values[1] <= 0.4, MESSAGES.invalidRange);
  }

  return {
    model,
    [keys[0]]: values[0],
    [keys[1]]: values[1],
    [keys[2]]: values[2],
    alpha: alpha < 1 ? alpha : undefined,
  } as ExtractColorPartsReturn;
}
