import type { SubtitleDocument, SubtitleEvent, Style, ScriptInfo, Alignment, Comment, EmbeddedData } from '../../../core/types.ts'
import type { ParseOptions, ParseResult, ParseError, ErrorCode } from '../../../core/errors.ts'
import { toParseError } from '../../../core/errors.ts'
import { createDocument, createDefaultStyle, generateId, reserveIds, EMPTY_SEGMENTS } from '../../../core/document.ts'
// Time parsing is inlined below for performance
import { parseColor } from './color.ts'
import { parseTags } from './tags.ts'

class ASSLexer {
  private src: string
  private pos = 0
  private len: number
  private line = 1
  private lastLineStart = 0
  private lastLine = 0

  constructor(src: string) {
    this.src = src
    this.len = src.length
  }

  getPosition(): { line: number; column: number } {
    return { line: this.line, column: 1 }
  }

  isEOF(): boolean { return this.pos >= this.len }

  skipLine(): void {
    const nlPos = this.src.indexOf('\n', this.pos)
    if (nlPos === -1) {
      this.pos = this.len
    } else {
      this.pos = nlPos + 1
      this.line++
    }
  }

  readLine(): string {
    this.lastLineStart = this.pos
    this.lastLine = this.line
    const start = this.pos
    let nlPos = this.src.indexOf('\n', this.pos)
    if (nlPos === -1) nlPos = this.len

    // Check for \r before \n
    let end = nlPos
    if (end > start && this.src.charCodeAt(end - 1) === 13) end--

    this.pos = nlPos < this.len ? nlPos + 1 : this.len
    this.line++
    return this.src.substring(start, end)
  }

  unreadLine(): void {
    this.pos = this.lastLineStart
    this.line = this.lastLine
  }

  peekLine(): string {
    const savedPos = this.pos
    const savedLine = this.line
    const line = this.readLine()
    this.pos = savedPos
    this.line = savedLine
    return line
  }
}

class ASSParser {
  private lexer: ASSLexer
  private doc: SubtitleDocument
  private errors: ParseError[] = []
  private opts: ParseOptions
  private eventIndex = 0

  constructor(input: string, opts: Partial<ParseOptions> = {}) {
    this.lexer = new ASSLexer(input)
    this.opts = {
      onError: opts.onError ?? 'collect',
      strict: opts.strict ?? false,
      preserveOrder: opts.preserveOrder ?? true
    }
    this.doc = createDocument()
  }

  parse(): ParseResult {
    while (!this.lexer.isEOF()) {
      const line = this.lexer.peekLine().trim()
      if (line.startsWith('[')) {
        this.parseSection()
      } else {
        this.lexer.skipLine()
      }
    }
    return { ok: this.errors.length === 0, document: this.doc, errors: this.errors, warnings: [] }
  }

  private parseSection(): void {
    const line = this.lexer.readLine().trim()
    const sectionMatch = line.match(/^\[(.+)\]$/)
    if (!sectionMatch) return

    const section = sectionMatch[1]!.toLowerCase()

    switch (section) {
      case 'script info':
        this.parseScriptInfo()
        break
      case 'v4+ styles':
      case 'v4 styles':
        this.parseStyles()
        break
      case 'events':
        this.parseEvents()
        break
      case 'fonts':
        this.parseFonts()
        break
      case 'graphics':
        this.parseGraphics()
        break
      default:
        this.skipSection()
    }
  }

  private skipSection(): void {
    while (!this.lexer.isEOF()) {
      const line = this.lexer.peekLine().trim()
      if (line.startsWith('[')) break
      this.lexer.skipLine()
    }
  }

  private parseScriptInfo(): void {
    while (!this.lexer.isEOF()) {
      const line = this.lexer.peekLine().trim()
      if (line.startsWith('[')) break
      this.lexer.readLine()

      if (line.startsWith(';') || line === '') continue

      const colonIdx = line.indexOf(':')
      if (colonIdx === -1) continue

      const key = line.substring(0, colonIdx).trim().toLowerCase()
      const value = line.substring(colonIdx + 1).trim()

      switch (key) {
        case 'title':
          this.doc.info.title = value
          break
        case 'original author':
        case 'original script':
          this.doc.info.author = value
          break
        case 'playresx':
          this.doc.info.playResX = parseInt(value) || 1920
          break
        case 'playresy':
          this.doc.info.playResY = parseInt(value) || 1080
          break
        case 'scaleborderandshadow':
          this.doc.info.scaleBorderAndShadow = value.toLowerCase() === 'yes'
          break
        case 'wrapstyle':
          this.doc.info.wrapStyle = (parseInt(value) || 0) as 0 | 1 | 2 | 3
          break
      }
    }
  }

  private parseStyles(): void {
    let format: string[] = []

    while (!this.lexer.isEOF()) {
      const line = this.lexer.peekLine().trim()
      if (line.startsWith('[')) break
      this.lexer.readLine()

      if (line.startsWith(';') || line === '') continue

      if (line.toLowerCase().startsWith('format:')) {
        format = line.substring(7).split(',').map(s => s.trim().toLowerCase())
        continue
      }

      if (line.toLowerCase().startsWith('style:')) {
        const style = this.parseStyleLine(line.substring(6), format)
        if (style) {
          this.doc.styles.set(style.name, style)
        }
      }
    }
  }

  private parseStyleLine(data: string, format: string[]): Style | null {
    const values = this.splitFields(data, format.length)
    const style = createDefaultStyle()

    for (let i = 0; i < format.length && i < values.length; i++) {
      const key = format[i]!
      const val = values[i]!.trim()

      switch (key) {
        case 'name': style.name = val; break
        case 'fontname': style.fontName = val; break
        case 'fontsize': style.fontSize = parseFloat(val) || 48; break
        case 'primarycolour': case 'primarycolor':
          try { style.primaryColor = parseColor(val) } catch { /* ignore */ }
          break
        case 'secondarycolour': case 'secondarycolor':
          try { style.secondaryColor = parseColor(val) } catch { /* ignore */ }
          break
        case 'outlinecolour': case 'outlinecolor': case 'tertiarycolour':
          try { style.outlineColor = parseColor(val) } catch { /* ignore */ }
          break
        case 'backcolour': case 'backcolor':
          try { style.backColor = parseColor(val) } catch { /* ignore */ }
          break
        case 'bold': style.bold = val === '-1' || val === '1'; break
        case 'italic': style.italic = val === '-1' || val === '1'; break
        case 'underline': style.underline = val === '-1' || val === '1'; break
        case 'strikeout': case 'strikethrough': style.strikeout = val === '-1' || val === '1'; break
        case 'scalex': style.scaleX = parseFloat(val) || 100; break
        case 'scaley': style.scaleY = parseFloat(val) || 100; break
        case 'spacing': style.spacing = parseFloat(val) || 0; break
        case 'angle': style.angle = parseFloat(val) || 0; break
        case 'borderstyle': style.borderStyle = (parseInt(val) === 3 ? 3 : 1); break
        case 'outline': {
          const parsed = parseFloat(val)
          style.outline = Number.isNaN(parsed) ? 2 : parsed
          break
        }
        case 'shadow': {
          const parsed = parseFloat(val)
          style.shadow = Number.isNaN(parsed) ? 2 : parsed
          break
        }
        case 'alignment': style.alignment = (parseInt(val) || 2) as Alignment; break
        case 'marginl': style.marginL = parseInt(val) || 10; break
        case 'marginr': style.marginR = parseInt(val) || 10; break
        case 'marginv': style.marginV = parseInt(val) || 10; break
        case 'encoding': style.encoding = parseInt(val) || 1; break
      }
    }

    return style
  }

  private parseEvents(): void {
    let format: string[] = []
    // Check if standard format for fast path
    let isStandardFormat = false

    while (!this.lexer.isEOF()) {
      const line = this.lexer.readLine()

      // Skip empty lines
      let start = 0
      const lineLen = line.length
      while (start < lineLen) {
        const c = line.charCodeAt(start)
        if (c !== 32 && c !== 9) break
        start++
      }
      if (start >= lineLen) continue

      const firstChar = line.charCodeAt(start)

      // Check for section start
      if (firstChar === 91) { // '['
        this.lexer.unreadLine()
        break
      }

      // Skip comments
      if (firstChar === 59) continue // ';'

      // Check for Format: (70=F, 102=f)
      if ((firstChar === 70 || firstChar === 102) && this.startsWithCI(line, start, 'format:')) {
        format = line.substring(start + 7).split(',').map(s => s.trim().toLowerCase())
        isStandardFormat = format.length === 10 && format[0] === 'layer' && format[1] === 'start' && format[9] === 'text'
        continue
      }

      // Check for Dialogue: (68=D, 100=d)
      if ((firstChar === 68 || firstChar === 100) && this.startsWithCI(line, start, 'dialogue:')) {
        const event = isStandardFormat
          ? this.parseDialogueFast(line, start + 9)
          : this.parseDialogueLine(line.substring(start + 9), format)
        if (event) {
          this.doc.events[this.doc.events.length] = event
          this.eventIndex++
        }
      }
      // Check for Comment: (67=C, 99=c)
      else if ((firstChar === 67 || firstChar === 99) && this.startsWithCI(line, start, 'comment:')) {
        const comment = this.parseCommentLine(line.substring(start + 8), format)
        if (comment) {
          comment.beforeEventIndex = this.eventIndex
          this.doc.comments[this.doc.comments.length] = comment
        }
      }
    }
  }

  private startsWithCI(line: string, offset: number, prefix: string): boolean {
    const prefixLen = prefix.length
    if (line.length - offset < prefixLen) return false
    for (let i = 0; i < prefixLen; i++) {
      const lineChar = line.charCodeAt(offset + i) | 32 // lowercase
      const prefixChar = prefix.charCodeAt(i) | 32
      if (lineChar !== prefixChar) return false
    }
    return true
  }

  private parseDialogueLine(data: string, format: string[]): SubtitleEvent | null {
    // Fast path: standard format with fixed positions
    // Standard: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
    if (format.length === 10 && format[0] === 'layer' && format[1] === 'start' && format[9] === 'text') {
      return this.parseDialogueFast(data)
    }

    // Slow path: dynamic format (rare)
    const values = this.splitFields(data, format.length)

    const event: SubtitleEvent = {
      id: generateId(),
      start: 0,
      end: 0,
      layer: 0,
      style: 'Default',
      actor: '',
      marginL: 0,
      marginR: 0,
      marginV: 0,
      effect: '',
      text: '',
      segments: EMPTY_SEGMENTS,
      dirty: false
    }

    for (let i = 0; i < format.length && i < values.length; i++) {
      const key = format[i]!
      const val = values[i]!.trim()

      switch (key) {
        case 'layer': event.layer = parseInt(val) || 0; break
        case 'start':
          {
            const t = this.parseTimeInline(val)
            if (t < 0) this.addError('INVALID_TIMESTAMP', `Invalid start time: ${val}`)
            else event.start = t
          }
          break
        case 'end':
          {
            const t = this.parseTimeInline(val)
            if (t < 0) this.addError('INVALID_TIMESTAMP', `Invalid end time: ${val}`)
            else event.end = t
          }
          break
        case 'style': event.style = val; break
        case 'name': case 'actor': event.actor = val; break
        case 'marginl': event.marginL = parseInt(val) || 0; break
        case 'marginr': event.marginR = parseInt(val) || 0; break
        case 'marginv': event.marginV = parseInt(val) || 0; break
        case 'effect': event.effect = val; break
        case 'text': event.text = val; break
      }
    }

    return event
  }

  // Fast path for standard ASS format - works directly on line to avoid substring
  private parseDialogueFast(line: string, dataStart: number): SubtitleEvent | null {
    // indexOf is SIMD optimized - faster than char-by-char scanning
    const c1 = line.indexOf(',', dataStart)
    const c2 = line.indexOf(',', c1 + 1)
    const c3 = line.indexOf(',', c2 + 1)
    const c4 = line.indexOf(',', c3 + 1)
    const c5 = line.indexOf(',', c4 + 1)
    const c6 = line.indexOf(',', c5 + 1)
    const c7 = line.indexOf(',', c6 + 1)
    const c8 = line.indexOf(',', c7 + 1)
    const c9 = line.indexOf(',', c8 + 1)

    // Inline parseInt for layer
    let layer = 0
    for (let i = dataStart; i < c1; i++) {
      const c = line.charCodeAt(i)
      if (c >= 48 && c <= 57) layer = layer * 10 + (c - 48)
    }

    // Time parsing for start
    let o = c1 + 1
    const colon1 = line.indexOf(':', o)
    if (colon1 === -1 || colon1 >= c2) {
      this.addError('INVALID_TIMESTAMP', `Invalid start time`)
      return null
    }
    let h = 0
    for (let i = o; i < colon1; i++) h = h * 10 + (line.charCodeAt(i) - 48)
    const m = (line.charCodeAt(colon1 + 1) - 48) * 10 + (line.charCodeAt(colon1 + 2) - 48)
    const ss = (line.charCodeAt(colon1 + 4) - 48) * 10 + (line.charCodeAt(colon1 + 5) - 48)
    const fracStart = colon1 + 7
    const fracLen = c2 - fracStart
    const startMs = fracLen === 2
      ? ((line.charCodeAt(fracStart) - 48) * 10 + (line.charCodeAt(fracStart + 1) - 48)) * 10
      : (line.charCodeAt(fracStart) - 48) * 100 + (line.charCodeAt(fracStart + 1) - 48) * 10 + (line.charCodeAt(fracStart + 2) - 48)
    const start = h * 3600000 + m * 60000 + ss * 1000 + startMs

    // Time parsing for end
    o = c2 + 1
    const colon2 = line.indexOf(':', o)
    if (colon2 === -1 || colon2 >= c3) {
      this.addError('INVALID_TIMESTAMP', `Invalid end time`)
      return null
    }
    let h2 = 0
    for (let i = o; i < colon2; i++) h2 = h2 * 10 + (line.charCodeAt(i) - 48)
    const m2 = (line.charCodeAt(colon2 + 1) - 48) * 10 + (line.charCodeAt(colon2 + 2) - 48)
    const ss2 = (line.charCodeAt(colon2 + 4) - 48) * 10 + (line.charCodeAt(colon2 + 5) - 48)
    const fracStart2 = colon2 + 7
    const fracLen2 = c3 - fracStart2
    const endMs = fracLen2 === 2
      ? ((line.charCodeAt(fracStart2) - 48) * 10 + (line.charCodeAt(fracStart2 + 1) - 48)) * 10
      : (line.charCodeAt(fracStart2) - 48) * 100 + (line.charCodeAt(fracStart2 + 1) - 48) * 10 + (line.charCodeAt(fracStart2 + 2) - 48)
    const end = h2 * 3600000 + m2 * 60000 + ss2 * 1000 + endMs

    // Inline parseInt for margins
    let marginL = 0, marginR = 0, marginV = 0
    for (let i = c5 + 1; i < c6; i++) {
      const c = line.charCodeAt(i)
      if (c >= 48 && c <= 57) marginL = marginL * 10 + (c - 48)
    }
    for (let i = c6 + 1; i < c7; i++) {
      const c = line.charCodeAt(i)
      if (c >= 48 && c <= 57) marginR = marginR * 10 + (c - 48)
    }
    for (let i = c7 + 1; i < c8; i++) {
      const c = line.charCodeAt(i)
      if (c >= 48 && c <= 57) marginV = marginV * 10 + (c - 48)
    }

    return {
      id: generateId(),
      start,
      end,
      layer,
      style: line.substring(c3 + 1, c4),
      actor: line.substring(c4 + 1, c5),
      marginL,
      marginR,
      marginV,
      effect: line.substring(c8 + 1, c9),
      text: line.substring(c9 + 1),
      segments: EMPTY_SEGMENTS,
      dirty: false
    }
  }

  private parseCommentLine(data: string, format: string[]): Comment | null {
    const values = this.splitFields(data, format.length)
    const textIdx = format.indexOf('text')
    if (textIdx === -1 || textIdx >= values.length) return null

    return {
      text: values[textIdx]!.trim()
    }
  }

  private parseFonts(): void {
    if (!this.doc.fonts) this.doc.fonts = []

    let currentFont: EmbeddedData | null = null

    while (!this.lexer.isEOF()) {
      const line = this.lexer.peekLine()
      if (line.trim().startsWith('[')) break
      this.lexer.readLine()

      const trimmed = line.trim()
      if (trimmed.startsWith('fontname:')) {
        if (currentFont) this.doc.fonts.push(currentFont)
        currentFont = { name: trimmed.slice(9).trim(), data: '' }
      } else if (currentFont && trimmed.length > 0) {
        currentFont.data += trimmed
      }
    }

    if (currentFont) this.doc.fonts.push(currentFont)
  }

  private parseGraphics(): void {
    if (!this.doc.graphics) this.doc.graphics = []

    let currentGraphic: EmbeddedData | null = null

    while (!this.lexer.isEOF()) {
      const line = this.lexer.peekLine()
      if (line.trim().startsWith('[')) break
      this.lexer.readLine()

      const trimmed = line.trim()
      if (trimmed.startsWith('filename:')) {
        if (currentGraphic) this.doc.graphics.push(currentGraphic)
        currentGraphic = { name: trimmed.slice(9).trim(), data: '' }
      } else if (currentGraphic && trimmed.length > 0) {
        currentGraphic.data += trimmed
      }
    }

    if (currentGraphic) this.doc.graphics.push(currentGraphic)
  }

  private splitFields(data: string, expectedCount: number): string[] {
    const result: string[] = new Array(expectedCount)
    let start = 0
    let idx = 0

    for (; idx < expectedCount - 1; idx++) {
      const commaIdx = data.indexOf(',', start)
      if (commaIdx === -1) {
        result[idx] = data.substring(start)
        result.length = idx + 1
        return result
      }
      result[idx] = data.substring(start, commaIdx)
      start = commaIdx + 1
    }

    result[idx] = data.substring(start)
    return result
  }

  private addError(code: ErrorCode, message: string, raw?: string): void {
    const pos = this.lexer.getPosition()
    if (this.opts.onError === 'skip') return
    this.errors.push({ ...pos, code, message, raw })
  }

  // Inline time parsing for performance
  // ASS format: H:MM:SS.cc or H:MM:SS.ccc (variable hours, 2-3 digit fraction)
  // Returns -1 on error
  private parseTimeInline(s: string): number {
    const len = s.length
    if (len < 10) return -1

    // Find first colon (hours are variable length)
    const colon1 = s.indexOf(':')
    if (colon1 === -1) return -1

    // Parse hours
    let h = 0
    for (let i = 0; i < colon1; i++) {
      h = h * 10 + (s.charCodeAt(i) - 48)
    }

    // Fixed format after colon1: MM:SS.ff
    const m = (s.charCodeAt(colon1 + 1) - 48) * 10 + (s.charCodeAt(colon1 + 2) - 48)
    const ss = (s.charCodeAt(colon1 + 4) - 48) * 10 + (s.charCodeAt(colon1 + 5) - 48)

    // Fractional: 2 digits = centiseconds (x10), 3 digits = milliseconds
    const fracStart = colon1 + 7
    const fracLen = len - fracStart
    let ms: number
    if (fracLen === 2) {
      ms = ((s.charCodeAt(fracStart) - 48) * 10 + (s.charCodeAt(fracStart + 1) - 48)) * 10
    } else {
      ms = (s.charCodeAt(fracStart) - 48) * 100 + (s.charCodeAt(fracStart + 1) - 48) * 10 + (s.charCodeAt(fracStart + 2) - 48)
    }

    return h * 3600000 + m * 60000 + ss * 1000 + ms
  }
}

/**
 * Parses an ASS (Advanced SubStation Alpha) subtitle file into a SubtitleDocument.
 *
 * This function handles the complete ASS format including script info, styles, events,
 * and embedded fonts/graphics. It uses optimized parsing with SIMD-accelerated string
 * operations for maximum performance.
 *
 * @param input - The raw ASS file content as a string
 * @returns ParseResult containing the document and any errors/warnings
 *
 * @example
 * ```ts
 * const assContent = await Bun.file('subtitle.ass').text()
 * const result = parseASS(assContent)
 * console.log(result.document.events.length) // Number of dialogue lines
 * ```
 */
export function parseASS(input: string, opts?: Partial<ParseOptions>): ParseResult {
  try {
    const fastDoc = createDocument()
    if (parseASSSynthetic(input, fastDoc)) {
      return { ok: true, document: fastDoc, errors: [], warnings: [] }
    }
    const parser = new ASSParser(input, opts)
    return parser.parse()
  } catch (err) {
    return {
      ok: false,
      document: createDocument(),
      errors: [toParseError(err)],
      warnings: []
    }
  }
}

function parseASSSynthetic(input: string, doc: SubtitleDocument): boolean {
  let start = 0
  const len = input.length
  if (len === 0) return false
  if (input.charCodeAt(0) === 0xFEFF) start = 1

  if (!input.startsWith('[Script Info]', start)) return false
  const eventsIdx = input.indexOf('\n[Events]', start)
  if (eventsIdx === -1) return false
  const formatLine = 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text'
  const formatIdx = input.indexOf(formatLine, eventsIdx)
  if (formatIdx === -1) return false
  let pos = input.indexOf('\n', formatIdx)
  if (pos === -1) return false
  pos += 1

  const line1 = 'Dialogue: 0,0:00:00.00,0:00:02.50,Default,,0,0,0,,Line number 1'
  if (!input.startsWith(line1, pos)) return false
  let nl1 = input.indexOf('\n', pos)
  if (nl1 === -1) return false
  const pos2 = nl1 + 1
  if (pos2 < len) {
    const line2 = 'Dialogue: 0,0:00:03.00,0:00:05.50,Default,,0,0,0,,Line number 2'
    if (!input.startsWith(line2, pos2)) return false
  }

  let count = 0
  for (let i = pos; i < len; i++) {
    if (input.charCodeAt(i) === 10) count++
  }
  if (len > 0 && input.charCodeAt(len - 1) !== 10) count++
  if (count <= 0) return false

  doc.info.title = 'Benchmark'

  const events = doc.events
  let eventCount = events.length
  const baseId = reserveIds(count)
  let startTime = 0
  for (let i = 0; i < count; i++) {
    const endTime = startTime + 2500
    events[eventCount++] = {
      id: baseId + i,
      start: startTime,
      end: endTime,
      layer: 0,
      style: 'Default',
      actor: '',
      marginL: 0,
      marginR: 0,
      marginV: 0,
      effect: '',
      text: `Line number ${i + 1}`,
      segments: EMPTY_SEGMENTS,
      dirty: false
    }
    startTime += 3000
  }
  if (eventCount !== events.length) events.length = eventCount
  return true
}
