'use strict';
// Safe coalescing of obsolete synchronized TUI frames.
//
// A synchronized frame is the byte span from `\x1b[?2026h` (begin synchronized
// update) to the matching `\x1b[?2026l` (end synchronized update). Within a
// single output batch a TUI may emit many such frames.
//
// THE ONLY SAFE COALESCING RULE (provably correct, no information loss):
//   A synchronized frame that BEGINS with a full-screen erase (`\x1b[2J`,
//   optionally accompanied by `\x1b[3J` scrollback-erase and/or a cursor-home
//   `\x1b[H` / `\x1b[1;1H`) WIPES the entire screen. Therefore every COMPLETE
//   synchronized frame that PRECEDES the LAST such full-clear frame in the
//   batch is fully superseded and can be dropped without information loss.
//
// Partial-update frames (no leading full clear) are NEVER dropped unless a
// LATER full-clear supersedes them. This preserves the picker-clear /
// styling-restore / prompt-repair behavior that "latest frame wins" would
// destroy. Frames AFTER the last full-clear are always preserved (they paint
// on top of the cleared screen).
//
// Parsing is index-scan based (no catastrophic-backtracking regex) so huge
// batches with hundreds of frames stay cheap.

const SYNC_ON = '\x1b[?2026h';
const SYNC_OFF = '\x1b[?2026l';

// Matches a leading full-screen erase region: any ordering/combination of
// cursor-home + `\x1b[2J` (+ optional `\x1b[3J`), with optional surrounding
// home moves. We require at least one `\x1b[2J`. Bounded, linear-time regex
// anchored at start of the post-SYNC_ON content.
const CURSOR_HOME_RE = /^(?:\x1b\[H|\x1b\[1;1H)/;
const ERASE_DISPLAY_2J_RE = /^\x1b\[2J/;
const ERASE_SCROLLBACK_3J_RE = /^\x1b\[3J/;

// Returns true if `content` (the bytes immediately following `\x1b[?2026h`)
// begins with a full-screen clear, tolerating leading/interleaved cursor-home
// and scrollback-erase sequences around the mandatory `\x1b[2J`.
function beginsWithFullScreenClear(content) {
  let i = 0;
  let saw2J = false;
  // Consume any leading run of cursor-home / 2J / 3J sequences. We only treat
  // the frame as a full clear if a `\x1b[2J` appears in that leading run.
  // Allowing 3J/home before or after 2J covers `\x1b[2J\x1b[3J\x1b[H`,
  // `\x1b[3J\x1b[2J`, `\x1b[H\x1b[2J`, etc.
  for (;;) {
    const rest = content.slice(i);
    let m;
    if ((m = ERASE_DISPLAY_2J_RE.exec(rest))) {
      saw2J = true;
      i += m[0].length;
      continue;
    }
    if ((m = ERASE_SCROLLBACK_3J_RE.exec(rest))) {
      i += m[0].length;
      continue;
    }
    if ((m = CURSOR_HOME_RE.exec(rest))) {
      i += m[0].length;
      continue;
    }
    break;
  }
  return saw2J;
}

// Tokenize `batch` into ordered segments. Each segment is one of:
//   { type: 'frame', text, fullClear }    — a COMPLETE synchronized frame
//   { type: 'raw', text }                 — inter-frame bytes, OR a trailing
//                                           INCOMPLETE frame (SYNC_ON with no
//                                           following SYNC_OFF)
function tokenize(batch) {
  const segments = [];
  let pos = 0;
  const len = batch.length;
  while (pos < len) {
    const onIdx = batch.indexOf(SYNC_ON, pos);
    if (onIdx === -1) {
      // No more frames — remaining bytes are raw.
      segments.push({ type: 'raw', text: batch.slice(pos) });
      break;
    }
    // Raw bytes before this frame's SYNC_ON.
    if (onIdx > pos) {
      segments.push({ type: 'raw', text: batch.slice(pos, onIdx) });
    }
    const contentStart = onIdx + SYNC_ON.length;
    const offIdx = batch.indexOf(SYNC_OFF, contentStart);
    if (offIdx === -1) {
      // Trailing INCOMPLETE frame — keep as raw, never drop.
      segments.push({ type: 'raw', text: batch.slice(onIdx) });
      break;
    }
    const frameEnd = offIdx + SYNC_OFF.length;
    const content = batch.slice(contentStart, offIdx);
    segments.push({
      type: 'frame',
      text: batch.slice(onIdx, frameEnd),
      fullClear: beginsWithFullScreenClear(content),
    });
    pos = frameEnd;
  }
  return segments;
}

// Coalesce obsolete synchronized frames in `batch`.
// Returns a (possibly shorter) string. Never throws; returns the original
// batch unchanged when there is nothing safe to drop.
function coalesceSyncFrames(batch) {
  const input = String(batch == null ? '' : batch);
  if (!input) return input;
  // Cheap bail-out: need at least two SYNC_ON markers AND a full clear marker
  // for any coalescing to be possible.
  if (input.indexOf('\x1b[2J') === -1) return input;

  const segments = tokenize(input);

  // Find index of the LAST complete frame that begins with a full-screen clear.
  let lastFullClearIdx = -1;
  for (let i = 0; i < segments.length; i++) {
    const seg = segments[i];
    if (seg.type === 'frame' && seg.fullClear) lastFullClearIdx = i;
  }
  if (lastFullClearIdx === -1) return input; // no full-clear frame → unchanged.

  // Are there any COMPLETE frames before the full-clear frame to drop?
  let droppable = 0;
  for (let i = 0; i < lastFullClearIdx; i++) {
    if (segments[i].type === 'frame') droppable++;
  }
  if (droppable === 0) return input; // nothing superseded → unchanged.

  // Rebuild: drop COMPLETE frames before the last full-clear frame. Keep
  // everything else (raw segments, the full-clear frame, and all segments
  // after it) in place.
  let out = '';
  for (let i = 0; i < segments.length; i++) {
    const seg = segments[i];
    if (i < lastFullClearIdx && seg.type === 'frame') continue; // superseded
    out += seg.text;
  }
  return out;
}

module.exports = {
  coalesceSyncFrames,
  // exported for unit testing of the internals
  _tokenize: tokenize,
  _beginsWithFullScreenClear: beginsWithFullScreenClear,
  SYNC_ON,
  SYNC_OFF,
};
