import pc from 'picocolors'
import type { Arg } from '../../utils/args'
import { UI, header, highlight, indent, println, wordWrap } from '../../utils/renderer'

export function help({
  invalid,
  usage,
  options,
}: {
  invalid?: string
  usage?: string[]
  options?: Arg
}) {
  // Available terminal width
  let width = process.stdout.columns

  // Render header
  println(header())

  // Render the invalid command
  if (invalid) {
    println()
    println(`${pc.dim('Invalid command:')} ${invalid}`)
  }

  // Render usage
  if (usage && usage.length > 0) {
    println()
    println(pc.dim('Usage:'))
    for (let [idx, example] of usage.entries()) {
      // Split the usage example into the command and its options. This allows
      // us to wrap the options based on the available width of the terminal.
      let command = example.slice(0, example.indexOf('['))
      let options = example.slice(example.indexOf('['))

      // Make the options dimmed, to make them stand out less than the command
      // itself.
      options = options.replace(/\[.*?\]/g, (option) => pc.dim(option))

      // The space between the command and the options.
      let space = 1

      // Wrap the options based on the available width of the terminal.
      let lines = wordWrap(options, width - UI.indent - command.length - space)

      // Print an empty line between the usage examples if we need to split due
      // to width constraints. This ensures that the usage examples are visually
      // separated.
      //
      // E.g.: when enough space is available
      //
      // ```
      //   Usage:
      //     tailwindcss build [--input input.css] [--output output.css] [--watch] [options...]
      //     tailwindcss other [--watch] [options...]
      // ```
      //
      // E.g.: when not enough space is available
      //
      // ```
      //   Usage:
      //     tailwindcss build [--input input.css] [--output output.css]
      //                       [--watch] [options...]
      //
      //     tailwindcss other [--watch] [options...]
      // ```
      if (lines.length > 1 && idx !== 0) {
        println()
      }

      // Print the usage examples based on available width of the terminal.
      //
      // E.g.: when enough space is available
      //
      // ```
      //   Usage:
      //     tailwindcss [--input input.css] [--output output.css] [--watch] [options...]
      // ```
      //
      // E.g.: when not enough space is available
      //
      // ```
      //   Usage:
      //     tailwindcss [--input input.css] [--output output.css]
      //                 [--watch] [options...]
      // ```
      //
      // > Note how the second line is indented to align with the first line.
      println(indent(`${command}${lines.shift()}`))
      for (let line of lines) {
        println(indent(line, command.length))
      }
    }
  }

  // Render options
  if (options) {
    // Track the max alias length, this is used to indent the options that don't
    // have an alias such that everything is aligned properly.
    let maxAliasLength = 0
    for (let { alias } of Object.values(options)) {
      if (alias) {
        maxAliasLength = Math.max(maxAliasLength, alias.length)
      }
    }

    // The option strings, which are the combination of the `alias` and the
    // `flag`, with the correct spacing.
    let optionStrings: string[] = []

    // Track the max option length, which is the longest combination of an
    // `alias` followed by `, ` and followed by the `flag`.
    let maxOptionLength = 0

    for (let [flag, { alias, values }] of Object.entries(options)) {
      if (values?.length) {
        flag += `[=${values.join(', ')}]`
      }

      // The option string, which is the combination of the alias and the flag
      // but already properly indented based on the other aliases to ensure
      // everything is aligned properly.
      let option = [
        alias ? `${alias.padStart(maxAliasLength)}` : alias,
        alias ? flag : ' '.repeat(maxAliasLength + 2 /* `, `.length */) + flag,
      ]
        .filter(Boolean)
        .join(', ')

      optionStrings.push(option)
      maxOptionLength = Math.max(maxOptionLength, option.length)
    }

    println()
    println(pc.dim('Options:'))

    // The minimum amount of dots between the option and the description.
    let minimumGap = 8

    for (let { description, default: defaultValue = null } of Object.values(options)) {
      // The option to render
      let option = optionStrings.shift() as string

      // The amount of dots to show between the option and the description.
      let dotCount = minimumGap + (maxOptionLength - option.length)

      // To account for the space before and after the dots.
      let spaces = 2

      // The available width remaining for the description.
      let availableWidth = width - option.length - dotCount - spaces - UI.indent

      // Wrap the description and the default value (if present), based on the
      // available width.
      let lines = wordWrap(
        defaultValue !== null
          ? `${description} ${pc.dim(`[default:\u202F${highlight(`${defaultValue}`)}]`)}`
          : description,
        availableWidth,
      )

      // Print the option, the spacer dots and the start of the description.
      println(
        indent(`${pc.blue(option)} ${pc.dim(pc.gray('\u00B7')).repeat(dotCount)} ${lines.shift()}`),
      )

      // Print the remaining lines of the description, indenting them to align
      // with the start of the description.
      for (let line of lines) {
        println(indent(`${' '.repeat(option.length + dotCount + spaces)}${line}`))
      }
    }
  }
}
