// VobSub .sub binary file parser (MPEG-PS packets)

/**
 * Parsed subtitle packet from VobSub .sub file
 */
export interface SubtitlePacket {
  /** Presentation timestamp in milliseconds */
  pts: number
  /** Display duration in milliseconds */
  duration: number
  /** Horizontal position in pixels */
  x: number
  /** Vertical position in pixels */
  y: number
  /** Image width in pixels */
  width: number
  /** Image height in pixels */
  height: number
  /** RLE-compressed image data */
  rleData: Uint8Array
  /** True if this is a forced subtitle */
  forced: boolean
}

/**
 * Parse subtitle packet from .sub file at given file position
 * @param data - Binary .sub file data
 * @param offset - Byte offset where packet starts
 * @returns Parsed subtitle packet or null if invalid
 * @example
 * const subData = Bun.file('movie.sub').arrayBuffer()
 * const packet = parseSubPacket(new Uint8Array(subData), 0x12345)
 */
export function parseSubPacket(data: Uint8Array, offset: number): SubtitlePacket | null {
  const len = data.length
  let pos = offset

  // Find MPEG-PS packet start code (00 00 01 BA)
  if (
    pos + 3 < len &&
    data[pos] === 0x00 &&
    data[pos + 1] === 0x00 &&
    data[pos + 2] === 0x01 &&
    data[pos + 3] === 0xBA
  ) {
    // Already aligned
  } else {
    while (pos < len - 4) {
      if (data[pos] === 0x00 && data[pos + 1] === 0x00 && data[pos + 2] === 0x01 && data[pos + 3] === 0xBA) {
        break
      }
      pos++
    }
  }

  if (pos >= len - 14) {
    return null
  }

  // Skip MPEG-PS pack header (14 bytes)
  pos += 14

  // Look for PES packet start code (00 00 01 BD - private stream 1)
  if (pos + 4 > len || data[pos] !== 0x00 || data[pos + 1] !== 0x00 || data[pos + 2] !== 0x01 || data[pos + 3] !== 0xBD) {
    return null
  }

  pos += 4

  // PES packet length (2 bytes)
  if (pos + 2 > len) return null
  const pesLength = (data[pos] << 8) | data[pos + 1]
  pos += 2

  const pesStart = pos
  const pesEnd = pesStart + pesLength

  if (pesEnd > len) {
    return null
  }

  // Skip PES header flags (2 bytes)
  pos += 2

  // PES header data length
  if (pos >= len) return null
  const pesHeaderDataLength = data[pos++]

  // Extract PTS if present
  let pts = 0
  if (pesHeaderDataLength >= 5 && pos + 5 <= len) {
    const ptsBits = data[pos]
    if ((ptsBits & 0xF0) === 0x20 || (ptsBits & 0xF0) === 0x30) {
      // PTS present
      const pts32_30 = (data[pos] & 0x0E) >> 1
      const pts29_15 = ((data[pos + 1] << 8) | data[pos + 2]) >> 1
      const pts14_0 = ((data[pos + 3] << 8) | data[pos + 4]) >> 1

      const ptsValue = (pts32_30 << 30) | (pts29_15 << 15) | pts14_0
      pts = Math.floor(ptsValue / 90)  // Convert 90kHz to milliseconds
    }
  }

  pos += pesHeaderDataLength

  // Subtitle stream ID (should be 0x20 for first subtitle stream)
  if (pos >= len) return null
  const streamId = data[pos++]

  // Subtitle packet size
  if (pos + 2 > len) return null
  const subPacketSize = (data[pos] << 8) | data[pos + 1]
  pos += 2

  const subPacketStart = pos
  const subPacketEnd = Math.min(subPacketStart + subPacketSize, pesEnd)

  if (subPacketEnd > len) {
    return null
  }

  // Parse control sequence to get dimensions and timing
  // The control sequence typically starts after the RLE data
  // For now, we'll search for it
  let controlSeqOffset = subPacketStart
  let controlInfo: ControlInfo | null = null

  const packetLen = subPacketEnd - subPacketStart
  if (packetLen >= 4) {
    const sizeField = (data[subPacketStart] << 8) | data[subPacketStart + 1]
    const offsetField = (data[subPacketStart + 2] << 8) | data[subPacketStart + 3]
    if (offsetField >= 4 && offsetField < packetLen && sizeField >= offsetField) {
      const candidate = subPacketStart + offsetField
      const maybeInfo = parseControlSequenceFast(data, candidate, subPacketEnd)
        ?? parseControlSequence(data, candidate, subPacketEnd)
      if (maybeInfo && (maybeInfo.width > 0 || maybeInfo.height > 0)) {
        controlSeqOffset = candidate
        controlInfo = maybeInfo
      }
    }
  }

  if (!controlInfo) {
    const guessedOffset = findControlSequence(data, subPacketStart, subPacketEnd)
    if (guessedOffset !== null) {
      const maybeInfo = parseControlSequenceFast(data, guessedOffset, subPacketEnd)
        ?? parseControlSequence(data, guessedOffset, subPacketEnd)
      if (maybeInfo && (maybeInfo.width > 0 || maybeInfo.height > 0)) {
        controlSeqOffset = guessedOffset
        controlInfo = maybeInfo
      }
    }
  }

  if (!controlInfo) {
    // Fallback scan for control sequence by looking for common control commands
    for (let searchPos = subPacketStart; searchPos < subPacketEnd - 10; searchPos++) {
      const maybeInfo = parseControlSequence(data, searchPos, subPacketEnd)
      if (maybeInfo && (maybeInfo.width > 0 || maybeInfo.height > 0)) {
        controlSeqOffset = searchPos
        controlInfo = maybeInfo
        break
      }
    }
  }

  if (!controlInfo) {
    // Couldn't find valid control sequence, return null
    return null
  }

  // Extract RLE data (between data start and control sequence)
  const rleStart = (controlInfo && packetLen >= 4 && controlSeqOffset > subPacketStart + 4)
    ? subPacketStart + 4
    : subPacketStart
  const rleEnd = controlSeqOffset
  const rleData = data.subarray(rleStart, rleEnd)

  return {
    pts,
    duration: controlInfo.duration,
    x: controlInfo.x,
    y: controlInfo.y,
    width: controlInfo.width,
    height: controlInfo.height,
    rleData,
    forced: controlInfo.forced,
  }
}

interface ControlInfo {
  duration: number
  x: number
  y: number
  width: number
  height: number
  forced: boolean
}

function parseControlSequenceFast(data: Uint8Array, offset: number, end: number): ControlInfo | null {
  const remaining = end - offset
  if (remaining < 11) return null
  if (data[offset] !== 0x05) return null

  const pos = offset + 1
  if (pos + 9 >= end) return null

  const x1 = (data[pos] << 4) | (data[pos + 1] >> 4)
  const x2 = ((data[pos + 1] & 0x0F) << 8) | data[pos + 2]
  const y1 = (data[pos + 3] << 4) | (data[pos + 4] >> 4)
  const y2 = ((data[pos + 4] & 0x0F) << 8) | data[pos + 5]

  if (data[pos + 6] !== 0x02) return null
  const stopTime = (data[pos + 7] << 8) | data[pos + 8]
  const duration = Math.floor(stopTime / 90 * 1024)

  const tail = data[pos + 9]
  let forced = false
  if (tail === 0xFF) {
    forced = false
  } else if (tail === 0x00) {
    if (pos + 10 >= end || data[pos + 10] !== 0xFF) return null
    forced = true
  } else {
    return null
  }

  return {
    duration,
    x: x1,
    y: y1,
    width: x2 - x1 + 1,
    height: y2 - y1 + 1,
    forced,
  }
}

/**
 * Find control sequence in subtitle packet
 */
function findControlSequence(data: Uint8Array, start: number, end: number): number | null {
  // Control sequence typically starts after RLE data and near the end.
  // Search backwards for an end-of-line marker close to the tail.
  for (let pos = end - 2; pos >= start; pos--) {
    if (data[pos] === 0x00 && data[pos + 1] === 0x00) {
      return pos
    }
  }

  // If not found with markers, use heuristic: control sequence is typically in last ~20 bytes.
  if (end - start > 20) {
    return end - 20
  }

  return start
}

/**
 * Parse control sequence
 */
function parseControlSequence(data: Uint8Array, offset: number, end: number): ControlInfo | null {
  let pos = offset
  let duration = 0
  let x = 0
  let y = 0
  let width = 0
  let height = 0
  let forced = false

  while (pos < end) {
    const cmd = data[pos++]

    if (cmd === 0x00) {
      forced = true
      continue
    }
    if (cmd === 0x01) {
      if (pos + 2 > end) break
      pos += 2
      continue
    }
    if (cmd === 0x02) {
      if (pos + 2 > end) break
      const stopTime = (data[pos] << 8) | data[pos + 1]
      duration = Math.floor(stopTime / 90 * 1024)
      pos += 2
      continue
    }
    if (cmd === 0x03 || cmd === 0x04) {
      if (pos + 2 > end) break
      pos += 2
      continue
    }
    if (cmd === 0x05) {
      if (pos + 6 > end) break
      const x1 = (data[pos] << 4) | (data[pos + 1] >> 4)
      const x2 = ((data[pos + 1] & 0x0F) << 8) | data[pos + 2]
      const y1 = (data[pos + 3] << 4) | (data[pos + 4] >> 4)
      const y2 = ((data[pos + 4] & 0x0F) << 8) | data[pos + 5]

      x = x1
      y = y1
      width = x2 - x1 + 1
      height = y2 - y1 + 1
      pos += 6
      continue
    }
    if (cmd === 0x06) {
      if (pos + 4 > end) break
      pos += 4
      continue
    }
    if (cmd === 0xFF) {
      break
    }
  }

  return {
    duration,
    x,
    y,
    width,
    height,
    forced,
  }
}

/**
 * Create .sub binary data from subtitle packets
 * @param packets - Array of subtitle packets to encode
 * @returns Binary .sub file data
 * @example
 * const packets = [...]
 * const subData = createSubBinary(packets)
 * Bun.write('output.sub', subData)
 */
export function createSubBinary(packets: SubtitlePacket[]): Uint8Array {
  const chunks: Uint8Array[] = []

  for (const packet of packets) {
    const chunk = createSubPacketBinary(packet)
    chunks.push(chunk)
  }

  // Calculate total size
  const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
  const result = new Uint8Array(totalSize)

  let offset = 0
  for (const chunk of chunks) {
    result.set(chunk, offset)
    offset += chunk.length
  }

  return result
}

/**
 * Create single subtitle packet binary
 */
function createSubPacketBinary(packet: SubtitlePacket): Uint8Array {
  const controlSeq = createControlSequence(packet)
  const controlOffset = 4 + packet.rleData.length
  const subPacketSize = 4 + packet.rleData.length + controlSeq.length

  // Estimate sizes
  const pesHeaderSize = 3 + 5  // Flags + PTS
  const pesPayloadSize = 1 + 2 + subPacketSize  // Stream ID + size + data
  const pesLength = pesHeaderSize + pesPayloadSize

  const totalSize = 14 + 6 + pesLength  // Pack header + PES header + payload

  const data = new Uint8Array(totalSize)
  let pos = 0

  // MPEG-PS pack header (simplified)
  data[pos++] = 0x00
  data[pos++] = 0x00
  data[pos++] = 0x01
  data[pos++] = 0xBA

  // SCR (6 bytes in MPEG-2 format)
  const scr = packet.pts * 90
  data[pos++] = 0x44 | ((scr >> 30) & 0x03)
  data[pos++] = (scr >> 22) & 0xFF
  data[pos++] = 0x04 | ((scr >> 14) & 0xFC)
  data[pos++] = (scr >> 7) & 0xFF
  data[pos++] = 0x04 | ((scr << 1) & 0xFC)
  data[pos++] = 0x01  // SCR extension + marker

  // Mux rate and stuffing (4 bytes total for MPEG-2)
  data[pos++] = 0x01
  data[pos++] = 0x89
  data[pos++] = 0xC3
  data[pos++] = 0xF8  // Stuffing length = 0

  // PES packet header
  data[pos++] = 0x00
  data[pos++] = 0x00
  data[pos++] = 0x01
  data[pos++] = 0xBD  // Private stream 1

  // PES length
  data[pos++] = (pesLength >> 8) & 0xFF
  data[pos++] = pesLength & 0xFF

  // PES flags
  data[pos++] = 0x80  // Original/copy
  data[pos++] = 0x80  // PTS present

  // PES header data length
  data[pos++] = 5  // PTS length

  // PTS
  const ptsValue = packet.pts * 90
  data[pos++] = 0x21 | ((ptsValue >> 29) & 0x0E)
  data[pos++] = (ptsValue >> 22) & 0xFF
  data[pos++] = 0x01 | ((ptsValue >> 14) & 0xFE)
  data[pos++] = (ptsValue >> 7) & 0xFF
  data[pos++] = 0x01 | ((ptsValue << 1) & 0xFE)

  // Subtitle stream ID
  data[pos++] = 0x20

  // Subtitle packet size
  data[pos++] = (subPacketSize >> 8) & 0xFF
  data[pos++] = subPacketSize & 0xFF

  // Subpicture header (size + control sequence offset)
  data[pos++] = (subPacketSize >> 8) & 0xFF
  data[pos++] = subPacketSize & 0xFF
  data[pos++] = (controlOffset >> 8) & 0xFF
  data[pos++] = controlOffset & 0xFF

  // RLE data
  data.set(packet.rleData, pos)
  pos += packet.rleData.length

  // Control sequence
  data.set(controlSeq, pos)

  return data
}

/**
 * Create control sequence for subtitle packet
 */
function createControlSequence(packet: SubtitlePacket): Uint8Array {
  const seq: number[] = []

  // Coordinates (cmd 0x05)
  const x1 = packet.x
  const x2 = packet.x + packet.width - 1
  const y1 = packet.y
  const y2 = packet.y + packet.height - 1

  seq.push(0x05)
  seq.push((x1 >> 4) & 0xFF)
  seq.push(((x1 & 0x0F) << 4) | ((x2 >> 8) & 0x0F))
  seq.push(x2 & 0xFF)
  seq.push((y1 >> 4) & 0xFF)
  seq.push(((y1 & 0x0F) << 4) | ((y2 >> 8) & 0x0F))
  seq.push(y2 & 0xFF)

  // Stop display (cmd 0x02)
  const stopTime = Math.floor(packet.duration * 90 / 1024)
  seq.push(0x02)
  seq.push((stopTime >> 8) & 0xFF)
  seq.push(stopTime & 0xFF)

  // Force flag (cmd 0x00)
  if (packet.forced) {
    seq.push(0x00)
  }

  // End of sequence
  seq.push(0xFF)

  return new Uint8Array(seq)
}
