/**
 * Minimal SGR parser for terminal snapshots. Turns a captured pane (plain text with
 * ANSI escapes) into styled segments. Colour is reported as an ANSI palette index —
 * the component maps indices to tokens, so no colour value lives here.
 *
 * Non-SGR escape sequences (cursor moves, OSC titles) are stripped: a snapshot is
 * already a rendered screen, so there is nothing left for them to address.
 */

export interface AnsiSegment {
  text: string
  /** ANSI palette index 0-15, or null for the terminal's default foreground. */
  fg: number | null
  bg: number | null
  bold: boolean
  dim: boolean
  italic: boolean
  underline: boolean
  inverse: boolean
}

export type AnsiLine = AnsiSegment[]

interface Style {
  fg: number | null
  bg: number | null
  bold: boolean
  dim: boolean
  italic: boolean
  underline: boolean
  inverse: boolean
}

const RESET: Style = {
  fg: null, bg: null, bold: false, dim: false, italic: false, underline: false, inverse: false,
}

/** CSI (captures params + final byte), OSC up to BEL or ST, or a bare escape. */
const ESCAPE = new RegExp(
  '\\u001b(?:\\[([0-9;:?]*)([@-~])|\\][\\s\\S]*?(?:\\u0007|\\u001b\\\\)|[@-Z\\\\-_])',
  'g',
)

function applySgr(style: Style, params: string): Style {
  const codes = params === '' ? [0] : params.split(';').map((part) => Number(part.split(':')[0] ?? '0'))
  let next = { ...style }
  for (let index = 0; index < codes.length; index += 1) {
    const code = codes[index]!
    if (Number.isNaN(code)) continue
    if (code === 0) next = { ...RESET }
    else if (code === 1) next.bold = true
    else if (code === 2) next.dim = true
    else if (code === 3) next.italic = true
    else if (code === 4) next.underline = true
    else if (code === 7) next.inverse = true
    else if (code === 22) { next.bold = false; next.dim = false }
    else if (code === 23) next.italic = false
    else if (code === 24) next.underline = false
    else if (code === 27) next.inverse = false
    else if (code >= 30 && code <= 37) next.fg = code - 30
    else if (code === 39) next.fg = null
    else if (code >= 40 && code <= 47) next.bg = code - 40
    else if (code === 49) next.bg = null
    else if (code >= 90 && code <= 97) next.fg = code - 90 + 8
    else if (code >= 100 && code <= 107) next.bg = code - 100 + 8
    else if (code === 38 || code === 48) {
      const mode = codes[index + 1]
      // Extended colour outside the 16-colour palette has no token, so it falls back
      // to the default rather than inventing a value.
      const extended = mode === 5 ? codes[index + 2] ?? null : null
      const value = extended !== null && extended >= 0 && extended <= 15 ? extended : null
      if (code === 38) next.fg = value
      else next.bg = value
      index += mode === 5 ? 2 : mode === 2 ? 4 : 1
    }
  }
  return next
}

function pushText(line: AnsiLine, text: string, style: Style): void {
  if (text === '') return
  const last = line[line.length - 1]
  if (last && last.fg === style.fg && last.bg === style.bg && last.bold === style.bold
    && last.dim === style.dim && last.italic === style.italic
    && last.underline === style.underline && last.inverse === style.inverse) {
    last.text += text
    return
  }
  line.push({ text, ...style })
}

export function parseAnsiScreen(input: string): AnsiLine[] {
  const lines: AnsiLine[] = []
  let style: Style = { ...RESET }

  for (const rawLine of input.replace(/\r\n/g, '\n').replace(/\r/g, '').split('\n')) {
    const line: AnsiLine = []
    let cursor = 0
    ESCAPE.lastIndex = 0
    let match: RegExpExecArray | null
    while ((match = ESCAPE.exec(rawLine)) !== null) {
      pushText(line, rawLine.slice(cursor, match.index), style)
      if (match[2] === 'm') style = applySgr(style, match[1] ?? '')
      cursor = match.index + match[0].length
    }
    pushText(line, rawLine.slice(cursor), style)
    lines.push(line)
  }

  return lines
}
