/**
 * Combine the two given colors according to alpha blending.
 */
function blendColors(fgRGBA: number[], bgRGBA: number[]): number[] {
  // eslint-disable-next-line prefer-destructuring
  const alpha = fgRGBA[3]
  return [
    (1 - alpha) * bgRGBA[0] + alpha * fgRGBA[0],
    (1 - alpha) * bgRGBA[1] + alpha * fgRGBA[1],
    (1 - alpha) * bgRGBA[2] + alpha * fgRGBA[2],
    alpha + bgRGBA[3] * (1 - alpha),
  ]
}

export function rgbaToHsla([r, g, b, a]: number[]): number[] {
  const max = Math.max(r, g, b)
  const min = Math.min(r, g, b)
  const diff = max - min
  const sum = max + min

  let h
  if (min === max) {
    h = 0
  } else if (r === max) {
    h = (((1 / 6) * (g - b)) / diff + 1) % 1
  } else if (g === max) {
    h = ((1 / 6) * (b - r)) / diff + 1 / 3
  } else {
    h = ((1 / 6) * (r - g)) / diff + 2 / 3
  }

  const l = 0.5 * sum

  let s
  if (l === 0) {
    s = 0
  } else if (l === 1) {
    s = 0
  } else if (l <= 0.5) {
    s = diff / sum
  } else {
    s = diff / (2 - sum)
  }

  return [h, s, l, a]
}

/**
 * Calculate the luminance of this color using the WCAG algorithm.
 * See http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
 */
export function luminance([rSRGB, gSRGB, bSRGB]: number[]): number {
  const r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow((rSRGB + 0.055) / 1.055, 2.4)
  const g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow((gSRGB + 0.055) / 1.055, 2.4)
  const b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow((bSRGB + 0.055) / 1.055, 2.4)

  return 0.2126 * r + 0.7152 * g + 0.0722 * b
}

/**
 * Calculate the contrast ratio between a foreground and a background color.
 * Returns the ratio to 1, for example for two two colors with a contrast ratio of 21:1, this function will return 21.
 * See http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
 */
export function contrastRatio(fgRGBA: number[], bgRGBA: number[]): number {
  const blendedFg = blendColors(fgRGBA, bgRGBA)
  const fgLuminance = luminance(blendedFg)
  const bgLuminance = luminance(bgRGBA)
  const contrastRatio =
    (Math.max(fgLuminance, bgLuminance) + 0.05) /
    (Math.min(fgLuminance, bgLuminance) + 0.05)
  return contrastRatio
}

// Constants for basic APCA version.
// See https://github.com/Myndex/SAPC-APCA
const mainTRC = 2.4
const normBgExp = 0.55
const normFgExp = 0.58
const revBgExp = 0.62
const revFgExp = 0.57
const blkThrs = 0.03
const blkClmp = 1.45
const scaleBoW = 1.25
const scaleWoB = 1.25
const deltaLuminanceMin = 0.0005
const loConThresh = 0.078
const loConFactor = 12.82051282051282
const loConOffset = 0.06
const loClip = 0.001

/**
 * Calculate relative luminance of a color.
 * See https://github.com/Myndex/SAPC-APCA
 */
function luminanceAPCA([rSRGB, gSRGB, bSRGB]: number[]): number {
  const r = Math.pow(rSRGB, mainTRC)
  const g = Math.pow(gSRGB, mainTRC)
  const b = Math.pow(bSRGB, mainTRC)

  return 0.2126729 * r + 0.7151522 * g + 0.072175 * b
}

/**
 * Calculate the contrast ratio between a foreground and a background color.
 * Returns the percentage of the predicted visual contrast.
 * See https://github.com/Myndex/SAPC-APCA
 */
export function contrastRatioAPCA(fgRGBA: number[], bgRGBA: number[]): number {
  return contrastRatioByLuminanceAPCA(luminanceAPCA(fgRGBA), luminanceAPCA(bgRGBA))
}

function clampLuminance(value: number): number {
  return value > blkThrs ? value : value + Math.pow(blkThrs - value, blkClmp)
}

function contrastRatioByLuminanceAPCA(fgLuminance: number, bgLuminance: number): number {
  fgLuminance = clampLuminance(fgLuminance)
  bgLuminance = clampLuminance(bgLuminance)
  if (Math.abs(fgLuminance - bgLuminance) < deltaLuminanceMin) {
    return 0
  }
  let result = 0
  if (bgLuminance >= fgLuminance) {
    // Black text on white.
    result =
      (Math.pow(bgLuminance, normBgExp) - Math.pow(fgLuminance, normFgExp)) * scaleBoW
    result =
      result < loClip
        ? 0
        : result < loConThresh
        ? result - result * loConFactor * loConOffset
        : result - loConOffset
  } else {
    // White text on black.
    result =
      (Math.pow(bgLuminance, revBgExp) - Math.pow(fgLuminance, revFgExp)) * scaleWoB
    result =
      result > -loClip
        ? 0
        : result > -loConThresh
        ? result - result * loConFactor * loConOffset
        : result + loConOffset
  }
  return result * 100
}

// clang-format off
const contrastAPCALookupTable = [
  // See https://github.com/Myndex/SAPC-APCA
  // font size in px | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 weights
  [12, -1, -1, -1, -1, 100, 90, 80, -1, -1],
  [14, -1, -1, -1, 100, 90, 80, 60, 60, -1],
  [16, -1, -1, 100, 90, 80, 60, 55, 50, 50],
  [18, -1, -1, 90, 80, 60, 55, 50, 40, 40],
  [24, -1, 100, 80, 60, 55, 50, 40, 38, 35],
  [30, -1, 90, 70, 55, 50, 40, 38, 35, 40],
  [36, -1, 80, 60, 50, 40, 38, 35, 30, 25],
  [48, 100, 70, 55, 40, 38, 35, 30, 25, 20],
  [60, 90, 60, 50, 38, 35, 30, 25, 20, 20],
  [72, 80, 55, 40, 35, 30, 25, 20, 20, 20],
  [96, 70, 50, 35, 30, 25, 20, 20, 20, 20],
  [120, 60, 40, 30, 25, 20, 20, 20, 20, 20],
]
// clang-format on

contrastAPCALookupTable.reverse()

export function getAPCAThreshold(fontSize: string, fontWeight: string): number | null {
  const size = parseFloat(fontSize.replace("px", ""))
  const weight = parseFloat(fontWeight)

  // Go over the table backwards to find the first matching font size and then the weight.
  // Fonts larger than 96px, use the thresholds for 96px.
  // Fonts smaller than 12px, don't get any threshold meaning the font size needs to be increased.
  for (const [rowSize, ...rowWeights] of contrastAPCALookupTable) {
    if (size >= rowSize) {
      for (const [idx, keywordWeight] of [
        900, 800, 700, 600, 500, 400, 300, 200, 100,
      ].entries()) {
        if (weight >= keywordWeight) {
          const threshold = rowWeights[rowWeights.length - 1 - idx]
          return threshold === -1 ? null : threshold
        }
      }
    }
  }

  return null
}

function isLargeFont(fontSize: string, fontWeight: string): boolean {
  const boldWeights = ["bold", "bolder", "600", "700", "800", "900"]

  const fontSizePx = parseFloat(fontSize.replace("px", ""))
  const isBold = boldWeights.indexOf(fontWeight) !== -1

  const fontSizePt = (fontSizePx * 72) / 96
  if (isBold) {
    return fontSizePt >= 14
  }
  return fontSizePt >= 18
}

const contrastThresholds = {
  largeFont: { aa: 3.0, aaa: 4.5 },
  normalFont: { aa: 4.5, aaa: 7.0 },
}

export function getContrastThreshold(
  fontSize: string,
  fontWeight: string,
): {
  aa: number
  aaa: number
} {
  if (isLargeFont(fontSize, fontWeight)) {
    return contrastThresholds.largeFont
  }
  return contrastThresholds.normalFont
}
