import { parseColor } from './color'
import { parseBoxShadowValue } from './parseBoxShadowValue'
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'

let cssFunctions = ['min', 'max', 'clamp', 'calc']

// Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types

function isCSSFunction(value) {
  return cssFunctions.some((fn) => new RegExp(`^${fn}\\(.*\\)`).test(value))
}

const placeholder = '--tw-placeholder'
const placeholderRe = new RegExp(placeholder, 'g')

// This is not a data type, but rather a function that can normalize the
// correct values.
export function normalize(value, isRoot = true) {
  if (value.startsWith('--')) {
    return `var(${value})`
  }

  // Keep raw strings if it starts with `url(`
  if (value.includes('url(')) {
    return value
      .split(/(url\(.*?\))/g)
      .filter(Boolean)
      .map((part) => {
        if (/^url\(.*?\)$/.test(part)) {
          return part
        }

        return normalize(part, false)
      })
      .join('')
  }

  // Convert `_` to ` `, except for escaped underscores `\_`
  value = value
    .replace(
      /([^\\])_+/g,
      (fullMatch, characterBefore) => characterBefore + ' '.repeat(fullMatch.length - 1)
    )
    .replace(/^_/g, ' ')
    .replace(/\\_/g, '_')

  // Remove leftover whitespace
  if (isRoot) {
    value = value.trim()
  }

  value = normalizeMathOperatorSpacing(value)

  return value
}

/**
 * Add spaces around operators inside math functions
 * like calc() that do not follow an operator or '('.
 *
 * @param {string} value
 * @returns {string}
 */
function normalizeMathOperatorSpacing(value) {
  return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
    let vars = []

    return match
      .replace(/var\((--.+?)[,)]/g, (match, g1) => {
        vars.push(g1)
        return match.replace(g1, placeholder)
      })
      .replace(/(-?\d*\.?\d(?!\b-\d.+[,)](?![^+\-/*])\D)(?:%|[a-z]+)?|\))([+\-/*])/g, '$1 $2 ')
      .replace(placeholderRe, () => vars.shift())
  })
}

export function url(value) {
  return value.startsWith('url(')
}

export function number(value) {
  return !isNaN(Number(value)) || isCSSFunction(value)
}

export function percentage(value) {
  return (value.endsWith('%') && number(value.slice(0, -1))) || isCSSFunction(value)
}

// Please refer to MDN when updating this list:
// https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units
// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Container_Queries#container_query_length_units
let lengthUnits = [
  'cm',
  'mm',
  'Q',
  'in',
  'pc',
  'pt',
  'px',
  'em',
  'ex',
  'ch',
  'rem',
  'lh',
  'rlh',
  'vw',
  'vh',
  'vmin',
  'vmax',
  'vb',
  'vi',
  'svw',
  'svh',
  'lvw',
  'lvh',
  'dvw',
  'dvh',
  'cqw',
  'cqh',
  'cqi',
  'cqb',
  'cqmin',
  'cqmax',
]
let lengthUnitsPattern = `(?:${lengthUnits.join('|')})`
export function length(value) {
  return (
    value === '0' ||
    new RegExp(`^[+-]?[0-9]*\.?[0-9]+(?:[eE][+-]?[0-9]+)?${lengthUnitsPattern}$`).test(value) ||
    isCSSFunction(value)
  )
}

let lineWidths = new Set(['thin', 'medium', 'thick'])
export function lineWidth(value) {
  return lineWidths.has(value)
}

export function shadow(value) {
  let parsedShadows = parseBoxShadowValue(normalize(value))

  for (let parsedShadow of parsedShadows) {
    if (!parsedShadow.valid) {
      return false
    }
  }

  return true
}

export function color(value) {
  let colors = 0

  let result = splitAtTopLevelOnly(value, '_').every((part) => {
    part = normalize(part)

    if (part.startsWith('var(')) return true
    if (parseColor(part, { loose: true }) !== null) return colors++, true

    return false
  })

  if (!result) return false
  return colors > 0
}

export function image(value) {
  let images = 0
  let result = splitAtTopLevelOnly(value, ',').every((part) => {
    part = normalize(part)

    if (part.startsWith('var(')) return true
    if (
      url(part) ||
      gradient(part) ||
      ['element(', 'image(', 'cross-fade(', 'image-set('].some((fn) => part.startsWith(fn))
    ) {
      images++
      return true
    }

    return false
  })

  if (!result) return false
  return images > 0
}

let gradientTypes = new Set([
  'conic-gradient',
  'linear-gradient',
  'radial-gradient',
  'repeating-conic-gradient',
  'repeating-linear-gradient',
  'repeating-radial-gradient',
])
export function gradient(value) {
  value = normalize(value)

  for (let type of gradientTypes) {
    if (value.startsWith(`${type}(`)) {
      return true
    }
  }
  return false
}

let validPositions = new Set(['center', 'top', 'right', 'bottom', 'left'])
export function position(value) {
  let positions = 0
  let result = splitAtTopLevelOnly(value, '_').every((part) => {
    part = normalize(part)

    if (part.startsWith('var(')) return true
    if (validPositions.has(part) || length(part) || percentage(part)) {
      positions++
      return true
    }

    return false
  })

  if (!result) return false
  return positions > 0
}

export function familyName(value) {
  let fonts = 0
  let result = splitAtTopLevelOnly(value, ',').every((part) => {
    part = normalize(part)

    if (part.startsWith('var(')) return true

    // If it contains spaces, then it should be quoted
    if (part.includes(' ')) {
      if (!/(['"])([^"']+)\1/g.test(part)) {
        return false
      }
    }

    // If it starts with a number, it's invalid
    if (/^\d/g.test(part)) {
      return false
    }

    fonts++

    return true
  })

  if (!result) return false
  return fonts > 0
}

let genericNames = new Set([
  'serif',
  'sans-serif',
  'monospace',
  'cursive',
  'fantasy',
  'system-ui',
  'ui-serif',
  'ui-sans-serif',
  'ui-monospace',
  'ui-rounded',
  'math',
  'emoji',
  'fangsong',
])
export function genericName(value) {
  return genericNames.has(value)
}

let absoluteSizes = new Set([
  'xx-small',
  'x-small',
  'small',
  'medium',
  'large',
  'x-large',
  'x-large',
  'xxx-large',
])
export function absoluteSize(value) {
  return absoluteSizes.has(value)
}

let relativeSizes = new Set(['larger', 'smaller'])
export function relativeSize(value) {
  return relativeSizes.has(value)
}
