import type { DesignSystem } from './design-system'
import { decodeArbitraryValue } from './utils/decode-arbitrary-value'
import { segment } from './utils/segment'

const COLON = 0x3a
const DASH = 0x2d
const LOWER_A = 0x61
const LOWER_Z = 0x7a

type ArbitraryUtilityValue = {
  kind: 'arbitrary'

  /**
   * ```
   * bg-[color:var(--my-color)]
   *     ^^^^^
   * ```
   */
  dataType: string | null

  /**
   * ```
   * bg-[#0088cc]
   *     ^^^^^^^
   *
   * bg-[var(--my_variable)]
   *     ^^^^^^^^^^^^^^^^^^
   * ```
   */
  value: string
}

export type NamedUtilityValue = {
  kind: 'named'

  /**
   * ```
   * bg-red-500
   *    ^^^^^^^
   *
   * w-1/2
   *   ^
   * ```
   */
  value: string

  /**
   * ```
   * w-1/2
   *   ^^^
   * ```
   */
  fraction: string | null
}

type ArbitraryModifier = {
  kind: 'arbitrary'

  /**
   * ```
   * bg-red-500/[50%]
   *             ^^^
   * ```
   */
  value: string
}

type NamedModifier = {
  kind: 'named'

  /**
   * ```
   * bg-red-500/50
   *            ^^
   * ```
   */
  value: string
}

export type CandidateModifier = ArbitraryModifier | NamedModifier

type ArbitraryVariantValue = {
  kind: 'arbitrary'
  value: string
}

type NamedVariantValue = {
  kind: 'named'
  value: string
}

export type Variant =
  /**
   * Arbitrary variants are variants that take a selector and generate a variant
   * on the fly.
   *
   * E.g.: `[&_p]`
   */
  | {
      kind: 'arbitrary'
      selector: string

      // Whether or not the selector is a relative selector
      // @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors/Selector_structure#relative_selector
      relative: boolean
    }

  /**
   * Static variants are variants that don't take any arguments.
   *
   * E.g.: `hover`
   */
  | {
      kind: 'static'
      root: string
    }

  /**
   * Functional variants are variants that can take an argument. The argument is
   * either a named variant value or an arbitrary variant value.
   *
   * E.g.:
   *
   * - `aria-disabled`
   * - `aria-[disabled]`
   * - `@container-size`          -> @container, with named value `size`
   * - `@container-[inline-size]` -> @container, with arbitrary variant value `inline-size`
   * - `@container`               -> @container, with no value
   */
  | {
      kind: 'functional'
      root: string
      value: ArbitraryVariantValue | NamedVariantValue | null
      modifier: ArbitraryModifier | NamedModifier | null
    }

  /**
   * Compound variants are variants that take another variant as an argument.
   *
   * E.g.:
   *
   * - `has-[&_p]`
   * - `group-*`
   * - `peer-*`
   */
  | {
      kind: 'compound'
      root: string
      modifier: ArbitraryModifier | NamedModifier | null
      variant: Variant
    }

export type Candidate =
  /**
   * Arbitrary candidates are candidates that register utilities on the fly with
   * a property and a value.
   *
   * E.g.:
   *
   * - `[color:red]`
   * - `[color:red]/50`
   * - `[color:red]/50!`
   */
  | {
      kind: 'arbitrary'
      property: string
      value: string
      modifier: ArbitraryModifier | NamedModifier | null
      variants: Variant[]
      important: boolean
      raw: string
    }

  /**
   * Static candidates are candidates that don't take any arguments.
   *
   * E.g.:
   *
   * - `underline`
   * - `box-border`
   */
  | {
      kind: 'static'
      root: string
      variants: Variant[]
      negative: boolean
      important: boolean
      raw: string
    }

  /**
   * Functional candidates are candidates that can take an argument.
   *
   * E.g.:
   *
   * - `bg-red-500`
   * - `bg-[#0088cc]`
   * - `w-1/2`
   */
  | {
      kind: 'functional'
      root: string
      value: ArbitraryUtilityValue | NamedUtilityValue | null
      modifier: ArbitraryModifier | NamedModifier | null
      variants: Variant[]
      negative: boolean
      important: boolean
      raw: string
    }

export function* parseCandidate(input: string, designSystem: DesignSystem): Iterable<Candidate> {
  // hover:focus:underline
  // ^^^^^ ^^^^^^           -> Variants
  //             ^^^^^^^^^  -> Base
  let rawVariants = segment(input, ':')

  // A prefix is a special variant used to prefix all utilities. When present,
  // all utilities must start with that variant which we will then remove from
  // the variant list so no other part of the codebase has to know about it.
  if (designSystem.theme.prefix) {
    if (rawVariants.length === 1) return null
    if (rawVariants[0] !== designSystem.theme.prefix) return null

    rawVariants.shift()
  }

  // Safety: At this point it is safe to use TypeScript's non-null assertion
  // operator because even if the `input` was an empty string, splitting an
  // empty string by `:` will always result in an array with at least one
  // element.
  let base = rawVariants.pop()!

  let parsedCandidateVariants: Variant[] = []

  for (let i = rawVariants.length - 1; i >= 0; --i) {
    let parsedVariant = designSystem.parseVariant(rawVariants[i])
    if (parsedVariant === null) return

    parsedCandidateVariants.push(parsedVariant)
  }

  let important = false
  let negative = false

  // Candidates that end with an exclamation mark are the important version with
  // higher specificity of the non-important candidate, e.g. `mx-4!`.
  if (base[base.length - 1] === '!') {
    important = true
    base = base.slice(0, -1)
  }

  // Legacy syntax with leading `!`, e.g. `!mx-4`.
  else if (base[0] === '!') {
    important = true
    base = base.slice(1)
  }

  // Candidates that start with a dash are the negative versions of another
  // candidate, e.g. `-mx-4`.
  if (base[0] === '-') {
    negative = true
    base = base.slice(1)
  }

  // Check for an exact match of a static utility first as long as it does not
  // look like an arbitrary value.
  if (designSystem.utilities.has(base, 'static') && !base.includes('[')) {
    yield {
      kind: 'static',
      root: base,
      variants: parsedCandidateVariants,
      negative,
      important,
      raw: input,
    }
  }

  // Figure out the new base and the modifier segment if present.
  //
  // E.g.:
  //
  // ```
  // bg-red-500/50
  // ^^^^^^^^^^    -> Base without modifier
  //            ^^ -> Modifier segment
  // ```
  let [baseWithoutModifier, modifierSegment = null, additionalModifier] = segment(base, '/')

  // If there's more than one modifier, the utility is invalid.
  //
  // E.g.:
  //
  // - `bg-red-500/50/50`
  if (additionalModifier) return

  // Arbitrary properties
  if (baseWithoutModifier[0] === '[') {
    // Arbitrary properties should end with a `]`.
    if (baseWithoutModifier[baseWithoutModifier.length - 1] !== ']') return

    // The property part of the arbitrary property can only start with a-z
    // lowercase or a dash `-` in case of vendor prefixes such as `-webkit-`
    // or `-moz-`.
    //
    // Otherwise, it is an invalid candidate, and skip continue parsing.
    let charCode = baseWithoutModifier.charCodeAt(1)
    if (charCode !== DASH && !(charCode >= LOWER_A && charCode <= LOWER_Z)) {
      return
    }

    baseWithoutModifier = baseWithoutModifier.slice(1, -1)

    // Arbitrary properties consist of a property and a value separated by a
    // `:`. If the `:` cannot be found, then it is an invalid candidate, and we
    // can skip continue parsing.
    //
    // Since the property and the value should be separated by a `:`, we can
    // also verify that the colon is not the first or last character in the
    // candidate, because that would make it invalid as well.
    let idx = baseWithoutModifier.indexOf(':')
    if (idx === -1 || idx === 0 || idx === baseWithoutModifier.length - 1) return

    let property = baseWithoutModifier.slice(0, idx)
    let value = decodeArbitraryValue(baseWithoutModifier.slice(idx + 1))

    yield {
      kind: 'arbitrary',
      property,
      value,
      modifier: modifierSegment === null ? null : parseModifier(modifierSegment),
      variants: parsedCandidateVariants,
      important,
      raw: input,
    }

    return
  }

  // The different "versions"" of a candidate that are utilities
  // e.g. `['bg', 'red-500']` and `['bg-red', '500']`
  let roots: Iterable<Root>

  // If the base of the utility ends with a `]`, then we know it's an arbitrary
  // value. This also means that everything before the `[…]` part should be the
  // root of the utility.
  //
  // E.g.:
  //
  // ```
  // bg-[#0088cc]
  // ^^           -> Root
  //    ^^^^^^^^^ -> Arbitrary value
  //
  // bg-red-[#0088cc]
  // ^^^^^^           -> Root
  //        ^^^^^^^^^ -> Arbitrary value
  // ```
  if (baseWithoutModifier[baseWithoutModifier.length - 1] === ']') {
    let idx = baseWithoutModifier.indexOf('-[')
    if (idx === -1) return

    let root = baseWithoutModifier.slice(0, idx)

    // The root of the utility should exist as-is in the utilities map. If not,
    // it's an invalid utility and we can skip continue parsing.
    if (!designSystem.utilities.has(root, 'functional')) return

    let value = baseWithoutModifier.slice(idx + 1)

    roots = [[root, value]]
  }

  // Not an arbitrary value
  else {
    roots = findRoots(baseWithoutModifier, (root: string) => {
      return designSystem.utilities.has(root, 'functional')
    })
  }

  for (let [root, value] of roots) {
    let candidate: Candidate = {
      kind: 'functional',
      root,
      modifier: modifierSegment === null ? null : parseModifier(modifierSegment),
      value: null,
      variants: parsedCandidateVariants,
      negative,
      important,
      raw: input,
    }

    if (value === null) {
      yield candidate
      continue
    }

    {
      let startArbitraryIdx = value.indexOf('[')
      let valueIsArbitrary = startArbitraryIdx !== -1

      if (valueIsArbitrary) {
        let arbitraryValue = value.slice(startArbitraryIdx + 1, -1)

        // Extract an explicit typehint if present, e.g. `bg-[color:var(--my-var)])`
        let typehint = ''
        for (let i = 0; i < arbitraryValue.length; i++) {
          let code = arbitraryValue.charCodeAt(i)

          // If we hit a ":", we're at the end of a typehint.
          if (code === COLON) {
            typehint = arbitraryValue.slice(0, i)
            arbitraryValue = arbitraryValue.slice(i + 1)
            break
          }

          // Keep iterating as long as we've only seen valid typehint characters.
          if (code === DASH || (code >= LOWER_A && code <= LOWER_Z)) {
            continue
          }

          // If we see any other character, there's no typehint so break early.
          break
        }

        candidate.value = {
          kind: 'arbitrary',
          dataType: typehint || null,
          value: decodeArbitraryValue(arbitraryValue),
        }
      } else {
        // Some utilities support fractions as values, e.g. `w-1/2`. Since it's
        // ambiguous whether the slash signals a modifier or not, we store the
        // fraction separately in case the utility matcher is interested in it.
        let fraction =
          modifierSegment === null || candidate.modifier?.kind === 'arbitrary'
            ? null
            : `${value}/${modifierSegment}`

        candidate.value = {
          kind: 'named',
          value,
          fraction,
        }
      }
    }

    yield candidate
  }
}

function parseModifier(modifier: string): CandidateModifier {
  if (modifier[0] === '[' && modifier[modifier.length - 1] === ']') {
    let arbitraryValue = modifier.slice(1, -1)

    return {
      kind: 'arbitrary',
      value: decodeArbitraryValue(arbitraryValue),
    }
  }

  return {
    kind: 'named',
    value: modifier,
  }
}

export function parseVariant(variant: string, designSystem: DesignSystem): Variant | null {
  // Arbitrary variants
  if (variant[0] === '[' && variant[variant.length - 1] === ']') {
    /**
     * TODO: Breaking change
     *
     * @deprecated Arbitrary variants containing at-rules with other selectors
     * are deprecated. Use stacked variants instead.
     *
     * Before:
     *  - `[@media(width>=123px){&:hover}]:`
     *
     * After:
     *  - `[@media(width>=123px)]:[&:hover]:`
     *  - `[@media(width>=123px)]:hover:`
     */
    if (variant[1] === '@' && variant.includes('&')) return null

    let selector = decodeArbitraryValue(variant.slice(1, -1))

    let relative = selector[0] === '>' || selector[0] === '+' || selector[0] === '~'

    // Ensure `&` is always present by wrapping the selector in `&:is(…)`,
    // unless it's a relative selector like `> img`.
    //
    // E.g.:
    //
    // - `[p]:flex`
    if (!relative && selector[0] !== '@' && !selector.includes('&')) {
      selector = `&:is(${selector})`
    }

    return {
      kind: 'arbitrary',
      selector,
      relative,
    }
  }

  // Static, functional and compound variants
  {
    // group-hover/group-name
    // ^^^^^^^^^^^            -> Variant without modifier
    //             ^^^^^^^^^^ -> Modifier
    let [variantWithoutModifier, modifier = null, additionalModifier] = segment(variant, '/')

    // If there's more than one modifier, the variant is invalid.
    //
    // E.g.:
    //
    // - `group-hover/foo/bar`
    if (additionalModifier) return null

    let roots = findRoots(variantWithoutModifier, (root) => {
      return designSystem.variants.has(root)
    })

    for (let [root, value] of roots) {
      switch (designSystem.variants.kind(root)) {
        case 'static': {
          // Static variants do not have a value
          if (value !== null) return null

          // Static variants do not have a modifier
          if (modifier !== null) return null

          return {
            kind: 'static',
            root,
          }
        }

        case 'functional': {
          if (value === null) {
            return {
              kind: 'functional',
              root,
              modifier: modifier === null ? null : parseModifier(modifier),
              value: null,
            }
          }

          if (value[0] === '[' && value[value.length - 1] === ']') {
            return {
              kind: 'functional',
              root,
              modifier: modifier === null ? null : parseModifier(modifier),
              value: {
                kind: 'arbitrary',
                value: decodeArbitraryValue(value.slice(1, -1)),
              },
            }
          }

          return {
            kind: 'functional',
            root,
            modifier: modifier === null ? null : parseModifier(modifier),
            value: { kind: 'named', value },
          }
        }

        case 'compound': {
          if (value === null) return null

          let subVariant = designSystem.parseVariant(value)
          if (subVariant === null) return null

          // These two variants must be compatible when compounded
          if (!designSystem.variants.compoundsWith(root, subVariant)) return null

          return {
            kind: 'compound',
            root,
            modifier: modifier === null ? null : { kind: 'named', value: modifier },
            variant: subVariant,
          }
        }
      }
    }
  }

  return null
}

type Root = [
  // The root of the utility, e.g.: `bg-red-500`
  //                                 ^^
  root: string,

  // The value of the utility, e.g.: `bg-red-500`
  //                                     ^^^^^^^
  value: string | null,
]

function* findRoots(input: string, exists: (input: string) => boolean): Iterable<Root> {
  // If there is an exact match, then that's the root.
  if (exists(input)) {
    yield [input, null]
  }

  // Otherwise test every permutation of the input by iteratively removing
  // everything after the last dash.
  let idx = input.lastIndexOf('-')
  if (idx === -1) {
    // Variants starting with `@` are special because they don't need a `-`
    // after the `@` (E.g.: `@-lg` should be written as `@lg`).
    if (input[0] === '@' && exists('@')) {
      yield ['@', input.slice(1)]
    }
    return
  }

  // Determine the root and value by testing permutations of the incoming input.
  //
  // In case of a candidate like `bg-red-500`, this looks like:
  //
  // `bg-red-500` -> No match
  // `bg-red`     -> No match
  // `bg`         -> Match
  do {
    let maybeRoot = input.slice(0, idx)

    if (exists(maybeRoot)) {
      let root: Root = [maybeRoot, input.slice(idx + 1)]

      // If the leftover value is an empty string, it means that the value is an
      // invalid named value, e.g.: `bg-`. This makes the candidate invalid and we
      // can skip any further parsing.
      if (root[1] === '') break

      yield root
    }

    idx = input.lastIndexOf('-', idx - 1)
  } while (idx > 0)
}