import { COLOR_KEYS, MESSAGES } from '~/modules/constants';
import { invariant } from '~/modules/invariant';
import { isHSL, isLAB, isLCH, isNumber, isPlainObject, isRGB } from '~/modules/validators';

import { ColorModel, ColorModelKey, ConverterParameters, LAB, LCH } from '~/types';

/**
 * Clamp a value between a min and max.
 *
 * @param value - The value to clamp.
 * @param min - The minimum value (default: 0).
 * @param max - The maximum value (default: 100).
 * @returns The clamped value.
 */
export function clamp(value: number, min = 0, max = 100): number {
  return Math.min(Math.max(value, min), max);
}

/**
 * Constrain the degrees between 0 and 360.
 *
 * @param input - The base degrees value.
 * @param amount - The amount to add to the degrees.
 * @returns The constrained degrees value (0-360).
 */
export function constrainDegrees(input: number, amount: number): number {
  invariant(isNumber(input), MESSAGES.inputNumber);

  return (((input + amount) % 360) + 360) % 360;
}

/**
 * Normalize OkLab/OkLCH lightness from percentage (0-100) to 0-1 range.
 */
export function normalizeOkLightness<T extends { l: number }>(color: T): T {
  if (color.l > 1) {
    return { ...color, l: parseFloat((color.l / 100).toPrecision(15)) };
  }

  return color;
}

/**
 * Parse the input parameters for converters.
 *
 * @param input - The converter parameters (object or tuple).
 * @param model - The target color model.
 * @returns The parsed color model object.
 */
export function parseInput<T extends ColorModel>(
  input: ConverterParameters<T>,
  model: ColorModelKey,
): T {
  const keys = COLOR_KEYS[model];
  const validator = {
    hsl: isHSL,
    oklab: isLAB,
    oklch: isLCH,
    rgb: isRGB,
  };

  invariant(isPlainObject(input) || Array.isArray(input), MESSAGES.invalid);

  const value = Array.isArray(input)
    ? ({ [keys[0]]: input[0], [keys[1]]: input[1], [keys[2]]: input[2] } as unknown as T)
    : input;

  invariant(validator[model](value), `${MESSAGES.invalidColor}: ${model}`);

  return value;
}

/**
 * Restrict the values to a certain number of digits.
 * When precision is undefined, returns input unchanged (no rounding).
 *
 * @param input - The LAB or LCH color model.
 * @param precision - The number of significant digits. Undefined = no rounding.
 * @param forcePrecision - Whether to use decimal places (true) or significant digits (false).
 * @returns The color model with restricted values.
 */
export function restrictValues<T extends LAB | LCH>(
  input: T,
  precision?: number,
  forcePrecision = true,
): T {
  if (precision == null) {
    return input;
  }

  const output = new Map(Object.entries(input));

  for (const [key, value] of output.entries()) {
    output.set(key, round(value, precision, forcePrecision));
  }

  return Object.fromEntries(output) as T;
}

/**
 * Round decimal numbers.
 *
 * @param input - The number to round.
 * @param precision - The number of digits (default: 2).
 * @param forcePrecision - When true, rounds to N decimal places. When false, rounds to N significant digits.
 * @returns The rounded number.
 */
export function round(input: number, precision = 2, forcePrecision = true): number {
  if (!isNumber(input) || input === 0) {
    return 0;
  }

  if (forcePrecision) {
    const factor = 10 ** precision;

    return Math.round(input * factor) / factor;
  }

  // Significant digits mode (matches color.js toPrecision behavior):
  // For |n| >= 1: N significant digits. For |n| < 1: N decimal places.
  const integer = Math.trunc(input);
  let digits = 0;

  if (integer) {
    digits = Math.floor(Math.log10(Math.abs(integer))) + 1;
  }

  const factor = 10 ** (precision - digits);

  return Math.floor(input * factor + 0.5) / factor;
}

/**
 * Log a warning in development mode.
 *
 * @param message - The warning message to log.
 */
export function warn(message: string): void {
  if (process.env.NODE_ENV !== 'production') {
    // eslint-disable-next-line no-console
    console.warn(`[colorizr] ${message}`);
  }
}

/**
 * Pre-computed step keys for each step count (3-20).
 *
 * Based on Tailwind CSS color scale conventions:
 * - Standard keys: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950
 * - 500 is typically the "base" color
 * - Lower numbers = lighter, higher numbers = darker (in light mode)
 *
 * Keys are symmetrically distributed to maintain visual balance.
 */
const STEP_KEYS: Record<number, number[]> = {
  3: [100, 500, 900],
  4: [100, 400, 600, 900],
  5: [100, 300, 500, 700, 900],
  6: [100, 200, 400, 600, 800, 900],
  7: [100, 200, 400, 500, 600, 800, 900],
  8: [100, 200, 300, 500, 600, 700, 800, 900],
  9: [100, 200, 300, 400, 500, 600, 700, 800, 900],
  10: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
  11: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950],
  12: [50, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900, 950],
  13: [50, 100, 150, 200, 300, 400, 500, 600, 700, 800, 850, 900, 950],
  14: [50, 100, 150, 200, 250, 300, 400, 500, 600, 700, 800, 850, 900, 950],
  15: [50, 100, 150, 200, 250, 300, 400, 500, 600, 700, 750, 800, 850, 900, 950],
  16: [50, 100, 150, 200, 250, 300, 350, 400, 500, 600, 700, 750, 800, 850, 900, 950],
  17: [50, 100, 150, 200, 250, 300, 350, 400, 500, 600, 650, 700, 750, 800, 850, 900, 950],
  18: [50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 600, 650, 700, 750, 800, 850, 900, 950],
  19: [
    50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950,
  ],
  20: [
    50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950,
    1000,
  ],
};

/**
 * Get the step keys for a given step count.
 *
 * @param steps - The number of steps (clamped to 3-20).
 * @returns The array of step keys.
 */
export function getScaleStepKeys(steps: number): number[] {
  const value = clamp(Math.round(steps), 3, 20);

  return STEP_KEYS[value];
}
