import type { Inspect, Options } from './types.js'

const ansiColors = {
  bold: ['1', '22'],
  dim: ['2', '22'],
  italic: ['3', '23'],
  underline: ['4', '24'],
  // 5 & 6 are blinking
  inverse: ['7', '27'],
  hidden: ['8', '28'],
  strike: ['9', '29'],
  // 10-20 are fonts
  // 21-29 are resets for 1-9
  black: ['30', '39'],
  red: ['31', '39'],
  green: ['32', '39'],
  yellow: ['33', '39'],
  blue: ['34', '39'],
  magenta: ['35', '39'],
  cyan: ['36', '39'],
  white: ['37', '39'],

  brightblack: ['30;1', '39'],
  brightred: ['31;1', '39'],
  brightgreen: ['32;1', '39'],
  brightyellow: ['33;1', '39'],
  brightblue: ['34;1', '39'],
  brightmagenta: ['35;1', '39'],
  brightcyan: ['36;1', '39'],
  brightwhite: ['37;1', '39'],

  grey: ['90', '39'],
} as const

const styles: Record<string, keyof typeof ansiColors> = {
  special: 'cyan',
  number: 'yellow',
  bigint: 'yellow',
  boolean: 'yellow',
  undefined: 'grey',
  null: 'bold',
  string: 'green',
  symbol: 'green',
  date: 'magenta',
  regexp: 'red',
} as const

export const truncator = '…'

type AnsiValue = typeof ansiColors[keyof typeof ansiColors]

function colorise<S extends string>(value: S, styleType: string): string {
  const color: AnsiValue | undefined =
    ansiColors[styles[styleType]] || ansiColors[styleType as keyof typeof ansiColors] || ''
  if (!color) {
    return String(value) as S
  }
  return `\u001b[${color[0]}m${String(value) as S}\u001b[${color[1]}m`
}

export function normaliseOptions(
  {
    showHidden = false,
    depth = 2,
    colors = false,
    customInspect = true,
    showProxy = false,
    maxArrayLength = Infinity,
    breakLength = Infinity,
    seen = [],
    // eslint-disable-next-line no-shadow
    truncate = Infinity,
    stylize = String,
  }: Partial<Options> = {},
  inspect: Inspect
): Options {
  const options = {
    showHidden: Boolean(showHidden),
    depth: Number(depth),
    colors: Boolean(colors),
    customInspect: Boolean(customInspect),
    showProxy: Boolean(showProxy),
    maxArrayLength: Number(maxArrayLength),
    breakLength: Number(breakLength),
    truncate: Number(truncate),
    seen,
    inspect,
    stylize,
  }
  if (options.colors) {
    options.stylize = colorise
  }
  return options
}

function isHighSurrogate(char: string): boolean {
  return char >= '\ud800' && char <= '\udbff'
}

export function truncate(string: string | number, length: number, tail: typeof truncator = truncator) {
  string = String(string)
  const tailLength = tail.length
  const stringLength = string.length
  if (tailLength > length && stringLength > tailLength) {
    return tail
  }
  if (stringLength > length && stringLength > tailLength) {
    let end = length - tailLength
    if (end > 0 && isHighSurrogate(string[end - 1])) {
      end = end - 1
    }
    return `${string.slice(0, end)}${tail}`
  }
  return string
}

// eslint-disable-next-line complexity
export function inspectList(
  list: ArrayLike<unknown>,
  options: Options,
  inspectItem?: Inspect,
  separator = ', '
): string {
  inspectItem = inspectItem || options.inspect
  const size = list.length
  if (size === 0) return ''
  const originalLength = options.truncate
  let output = ''
  let peek = ''
  let truncated = ''
  for (let i = 0; i < size; i += 1) {
    const last = i + 1 === list.length
    const secondToLast = i + 2 === list.length
    truncated = `${truncator}(${list.length - i})`
    const value = list[i]

    // If there is more than one remaining we need to account for a separator of `, `
    options.truncate = originalLength - output.length - (last ? 0 : separator.length)
    const string = peek || inspectItem(value, options) + (last ? '' : separator)
    const nextLength = output.length + string.length
    const truncatedLength = nextLength + truncated.length

    // If this is the last element, and adding it would
    // take us over length, but adding the truncator wouldn't - then break now
    if (last && nextLength > originalLength && output.length + truncated.length <= originalLength) {
      break
    }

    // If this isn't the last or second to last element to scan,
    // but the string is already over length then break here
    if (!last && !secondToLast && truncatedLength > originalLength) {
      break
    }

    // Peek at the next string to determine if we should
    // break early before adding this item to the output
    peek = last ? '' : inspectItem(list[i + 1], options) + (secondToLast ? '' : separator)

    // If we have one element left, but this element and
    // the next takes over length, the break early
    if (!last && secondToLast && truncatedLength > originalLength && nextLength + peek.length > originalLength) {
      break
    }

    output += string

    // If the next element takes us to length -
    // but there are more after that, then we should truncate now
    if (!last && !secondToLast && nextLength + peek.length >= originalLength) {
      truncated = `${truncator}(${list.length - i - 1})`
      break
    }

    truncated = ''
  }
  return `${output}${truncated}`
}

function quoteComplexKey(key: string): string {
  if (key.match(/^[a-zA-Z_][a-zA-Z_0-9]*$/)) {
    return key
  }
  return JSON.stringify(key)
    .replace(/'/g, "\\'")
    .replace(/\\"/g, '"')
    .replace(/(^"|"$)/g, "'")
}

export function inspectProperty([key, value]: [unknown, unknown], options: Options): string {
  options.truncate -= 2
  if (typeof key === 'string') {
    key = quoteComplexKey(key)
  } else if (typeof key !== 'number') {
    key = `[${options.inspect(key, options)}]`
  }
  options.truncate -= (key as string).length
  value = options.inspect(value, options)
  return `${key}: ${value}`
}
