// TYPES -----------------------------------------------------------------------

export type Color = Hex | Rgb | Hsl

export type Hex = `#${string}`

export type Rgb = { r: number; g: number; b: number; a?: number }

export type Hsl = { h: number; s: number; l: number; a?: number }

// CONVERSIONS -----------------------------------------------------------------

export const toRgb = (color: Color) => {
  if (typeof color === 'object' && 'h' in color) {
    return hslToRgb(color)
  }

  if (typeof color === 'string') {
    return hexToRgb(color)
  }

  return color
}

export const toHex = (color: Color) => {
  if (typeof color === 'object' && 'r' in color) {
    return rgbToHex(color)
  }

  if (typeof color === 'object' && 'h' in color) {
    return hslToHex(color)
  }

  return color
}

export const toHsl = (color: Color) => {
  if (typeof color === 'object' && 'r' in color) {
    return rgbToHsl(color)
  }

  if (typeof color === 'string') {
    return hexToHsl(color)
  }

  return color
}

export const toCss = (color: Color) => {
  if (typeof color === 'object' && 'r' in color) {
    return color.a ? `rgb(${color.r}, ${color.g}, ${color.b}, ${color.a})` : `rgb(${color.r}, ${color.g}, ${color.b})`
  }

  if (typeof color === 'object' && 'h' in color) {
    return color.a
      ? `hsl(${color.h}, ${color.s}%, ${color.l}% / ${color.a})`
      : `hsl(${color.h}, ${color.s}%, ${color.l}%)`
  }

  return color
}

export const toShades = (lightShade: Color, darkShade: Color, totalShades: number): Color[] => {
  if (totalShades < 2) {
    totalShades = 2
  }

  const lightRgb = toRgb(lightShade)
  const darkRgb = toRgb(darkShade)

  const shades: Color[] = []
  for (let i = 0; i < totalShades; i++) {
    const ratio = i / (totalShades - 1) // Calculate the interpolation ratio
    const interpolatedRgb = {
      r: Math.round(lightRgb.r + ratio * (darkRgb.r - lightRgb.r)),
      g: Math.round(lightRgb.g + ratio * (darkRgb.g - lightRgb.g)),
      b: Math.round(lightRgb.b + ratio * (darkRgb.b - lightRgb.b)),
    } satisfies Rgb

    shades.push(toHex(interpolatedRgb))
  }

  return shades
}

// ACCESSIBILITY ---------------------------------------------------------------

export const luminance = (color: Color) => {
  let { r, g, b } = toRgb(color)

  const r8bit = r / 255
  const g8bit = g / 255
  const b8bit = b / 255

  r = r8bit <= 0.03928 ? r8bit / 12.92 : Math.pow((r8bit + 0.055) / 1.055, 2.4)
  g = g8bit <= 0.03928 ? g8bit / 12.92 : Math.pow((g8bit + 0.055) / 1.055, 2.4)
  b = b8bit <= 0.03928 ? b8bit / 12.92 : Math.pow((b8bit + 0.055) / 1.055, 2.4)

  return Number((0.2126 * r + 0.7152 * g + 0.0722 * b).toPrecision(2))
}

export const contrast = (l1: number, l2: number) => (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05)

export const isAccessible = (
  bg: Color,
  fg: Color,
  size: 'sm' | 'lg' = 'sm',
  criteria: 'aa' | 'aaa' = 'aaa',
): boolean => {
  const bgRgb = toRgb(bg)
  const fgRgb = toRgb(fg)

  const bgLuminance = luminance(bgRgb)
  const fgLuminance = luminance(fgRgb)

  const contrastRatio = contrast(bgLuminance, fgLuminance)

  if (criteria === 'aa' && size === 'sm') {
    return contrastRatio >= 4.5
  } else if (criteria === 'aa' && size === 'lg') {
    return contrastRatio >= 3
  } else if (criteria === 'aaa' && size === 'sm') {
    return contrastRatio >= 7
  } else if (criteria === 'aaa' && size === 'lg') {
    return contrastRatio >= 4.5
  }

  return false
}

export const toContrastColor = (background: Color): Color => {
  const bgLuminance = luminance(background)

  console.log(background, bgLuminance)

  const whiteContrast = contrast(bgLuminance, luminance('#ffffff'))
  const blackContrast = contrast(bgLuminance, luminance('#000000'))

  return whiteContrast >= blackContrast ? '#ffffff' : '#000000'
}

// GUARDS ----------------------------------------------------------------------

export const isCssVariable = (color: unknown): color is `--${string}` =>
  typeof color === 'string' && color.startsWith('--')

export const isHexColor = (color: unknown): color is `#${string}` => typeof color === 'string' && color.startsWith('#')

// INTERNAL CONVERSIONS --------------------------------------------------------

const hexToRgb = (hex: string) => {
  const re = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i
  const match = re.exec(hex)

  if (!match) {
    throw new Error(`Invalid hex color: ${hex}`)
  }

  return {
    r: Number.parseInt(match[1], 16),
    g: Number.parseInt(match[2], 16),
    b: Number.parseInt(match[3], 16),
  }
}

const hexToHsl = (hex: string) => rgbToHsl(hexToRgb(hex))

const hslToRgb = ({ h, s, l, a }: Hsl) => {
  // The HSL format CSS uses is degrees and percentages, like hsl(124, 60%, 39%).
  // The algorithm below expects these values to be normalized to the range 0 - 1.
  h /= 360
  s /= 100
  l /= 100

  let r, g, b

  if (s === 0) {
    r = g = b = l
  } else {
    const q = l < 0.5 ? l * (1 + s) : l + s - l * s
    const p = 2 * l - q

    r = hueToRgb(p, q, h + 1 / 3)
    g = hueToRgb(p, q, h)
    b = hueToRgb(p, q, h - 1 / 3)
  }

  return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255), a }
}

const hueToRgb = (p: number, q: number, t: number) => {
  if (t < 0) {
    t += 1
  }
  if (t > 1) {
    t -= 1
  }
  if (t < 1 / 6) {
    return p + (q - p) * 6 * t
  }
  if (t < 1 / 2) {
    return q
  }
  if (t < 2 / 3) {
    return p + (q - p) * (2 / 3 - t) * 6
  }

  return p
}

const hslToHex = ({ h, s, l, a }: Hsl) => rgbToHex(hslToRgb({ h, s, l, a }))

const rgbToHsl = ({ r, g, b, a }: Rgb) => {
  r /= 255
  g /= 255
  b /= 255

  const max = Math.max(r, g, b)
  const min = Math.min(r, g, b)

  let h = 0
  let s = 0
  const l = (max + min) / 2

  if (max !== min) {
    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)) / 6
        break
      case g:
        h = ((b - r) / d + 2) / 6
        break
      case b:
        h = ((r - g) / d + 4) / 6
        break
    }
  }

  return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100), a }
}

const rgbToHex = ({ r, g, b }: Rgb) => {
  // @todo: Add support for alpha channel.
  return ('#' + ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1)) as Hex
}

//

export const generateShades = (baseColor: string, count: number): Hex[] => {
  const shades: Hex[] = []

  // Parse the HSL string to extract h, s, and l values
  const hslRe = /hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/
  const hslMatch = hslRe.exec(baseColor)
  if (!hslMatch) {
    throw new Error('Invalid HSL color format. Expected "hsl(h, s%, l%)".')
  }

  const h = Number.parseInt(hslMatch[1], 10) // Hue
  const s = Number.parseInt(hslMatch[2], 10) // Saturation
  const l = Number.parseInt(hslMatch[3], 10) // Lightness

  for (let i = 0; i < count; i++) {
    const lightnessStep = i / (count - 1) // Calculate the lightness step
    const adjustedLightness = Math.max(0, Math.min(100, l * (1 - lightnessStep))) // Adjust lightness

    const shadeHsl: Hsl = { h, s, l: adjustedLightness } // Create the new HSL shade
    const shadeRgb = hslToRgb(shadeHsl) // Convert the HSL shade back to RGB
    shades.push(rgbToHex(shadeRgb)) // Convert the RGB shade to Hex and add to the array
  }

  return shades
}
