/**
 * Frame buffer: a 2D grid of terminal cells with differential rendering.
 *
 * Two buffers (current + previous) are maintained. After rendering nodes
 * into the current buffer, diffFrames() produces the minimal set of
 * changed runs. The ANSI serializer converts those runs into escape
 * sequences, tracking SGR state to minimize output bytes.
 */

// --- Cell representation ---

// Attribute bitfield
export const Attr = {
  None: 0,
  Bold: 1,
  Dim: 2,
  Italic: 4,
  Underline: 8,
  Inverse: 16,
  Strikethrough: 32
};
// Default/no color sentinel — means "use terminal default"
export const NO_COLOR = -1;
// Canonical blank cell. Every cleared slot in every buffer points at this one
// frozen object, so two blank slots — even across the prev/next buffers — are
// pointer-equal, which `cellsEqual` short-circuits on. It is frozen because it
// must never be mutated, and nothing ever mutates it: in-place writers either
// materialize a blank into its own scratch first (getMutable) or skip blanks
// outright (applyColorField, via `char === " "`), and every slot the buffer
// means to write gets a scratch cell, never this one.
const SHARED_BLANK = Object.freeze({
  char: " ",
  fg: NO_COLOR,
  bg: NO_COLOR,
  attrs: Attr.None,
  link: "",
  width: 1
});

// Per-glyph width memo. Bun.stringWidth is a native call; a terminal frame is
// dominated by a tiny set of repeated glyphs (ASCII, box-drawing borders, block
// elements, braille spinners), so caching width by code point collapses
// hundreds of native calls per frame into a handful of Map lookups. Bounded by
// the number of distinct glyphs an app ever paints (small). ASCII is handled
// inline without touching the map.
const charWidthCache = new Map();

/** Display width of a single code-point string, memoized. */
export function charWidth(char) {
  const code = char.codePointAt(0) ?? 0;
  if (code >= 0x20 && code < 0x7f) return 1; // printable ASCII — always 1
  let w = charWidthCache.get(code);
  if (w === undefined) {
    w = Bun.stringWidth(char);
    charWidthCache.set(code, w);
  }
  return w;
}
export function createCell(char = " ", fg = NO_COLOR, bg = NO_COLOR, attrs = Attr.None, width = 1, link = "") {
  return {
    char,
    fg,
    bg,
    attrs,
    link,
    width
  };
}
function cellsEqual(a, b) {
  // Identity short-circuit: cleared cells in both buffers share the single
  // SHARED_BLANK reference, so blank↔blank regions (the common case in a sparse
  // diff) compare in one pointer check instead of six field comparisons.
  if (a === b) return true;
  return a.char === b.char && a.fg === b.fg && a.bg === b.bg && a.attrs === b.attrs && a.link === b.link && a.width === b.width;
}

// --- Clip region ---

/** Rectangular clip region (exclusive right/bottom). */

// --- Frame buffer ---

export class FrameBuffer {
  /** The visible grid: each slot points at either SHARED_BLANK or its scratch. */

  /**
   * One persistent, mutable Cell per slot, allocated once. Writing a slot
   * mutates its scratch cell in place and points `cells[i]` at it — so a frame
   * allocates zero new cells no matter how much it paints. Blank slots point at
   * the shared frozen blank instead, keeping the diff's identity fast path.
   */

  /** Rows touched by set()/writeString()/fillRect() since last clear(). */
  dirtyRows = new Set();
  constructor(cols, rows) {
    this.cols = cols;
    this.rows = rows;
    const n = cols * rows;
    this.cells = new Array(n);
    this.scratch = new Array(n);
    for (let i = 0; i < n; i++) {
      this.cells[i] = SHARED_BLANK;
      this.scratch[i] = createCell();
    }
    this.dirtyRows.clear();
  }

  /** Mutate the scratch cell for a slot in place and make it the visible cell. */
  writeCell(idx, char, fg, bg, attrs, width, link) {
    const cell = this.scratch[idx];
    cell.char = char;
    cell.fg = fg;
    cell.bg = bg;
    cell.attrs = attrs;
    cell.width = width;
    cell.link = link;
    this.cells[idx] = cell;
  }
  clear() {
    for (let i = 0; i < this.cells.length; i++) {
      this.cells[i] = SHARED_BLANK;
    }
    this.dirtyRows.clear();
  }

  /** Reset only rows that were previously painted, then clear the dirty set. */
  clearDirtyRows() {
    for (const row of this.dirtyRows) {
      const start = row * this.cols;
      const end = start + this.cols;
      for (let i = start; i < end; i++) {
        this.cells[i] = SHARED_BLANK;
      }
    }
    this.dirtyRows.clear();
  }

  /** Find the last row that has non-empty content. Returns 0 if all empty. */
  contentHeight() {
    for (let row = this.rows - 1; row >= 0; row--) {
      for (let col = 0; col < this.cols; col++) {
        const cell = this.cells[row * this.cols + col];
        if (cell.char !== " " || cell.fg !== NO_COLOR || cell.bg !== NO_COLOR || cell.attrs !== Attr.None) {
          return row + 1;
        }
      }
    }
    return 0;
  }
  get(col, row) {
    return this.cells[row * this.cols + col];
  }

  /**
   * Return a writable cell for a slot, for callers that mutate it in place
   * (e.g. the selection-inverse overlay). A blank slot is first materialized
   * into its own scratch cell — never hand out the frozen shared blank — and
   * the row is marked dirty. Callers must pass in-bounds coordinates.
   */
  getMutable(col, row) {
    const idx = row * this.cols + col;
    if (this.cells[idx] === SHARED_BLANK) {
      this.writeCell(idx, " ", NO_COLOR, NO_COLOR, Attr.None, 1, "");
      this.dirtyRows.add(row);
    }
    return this.cells[idx];
  }
  set(col, row, cell) {
    if (col < 0 || col >= this.cols || row < 0 || row >= this.rows) return;
    // Copy the source cell's fields into this slot's scratch rather than
    // aliasing the reference — callers (e.g. cross-buffer scroll copies) may
    // hand us a cell owned by another buffer, and the two pools must stay
    // independent. A blank source is forwarded as the shared blank.
    const idx = row * this.cols + col;
    if (cell === SHARED_BLANK) {
      this.cells[idx] = SHARED_BLANK;
    } else {
      this.writeCell(idx, cell.char, cell.fg, cell.bg, cell.attrs, cell.width, cell.link);
    }
    this.dirtyRows.add(row);
  }

  /** Write a string at (col, row) with given attributes. Returns columns consumed. */
  writeString(col, row, str, fg = NO_COLOR, bg = NO_COLOR, attrs = Attr.None, clip, link = "") {
    const clipRight = clip ? clip.right : this.cols;
    const clipBottom = clip ? clip.bottom : this.rows;
    const clipLeft = clip ? clip.left : 0;
    const clipTop = clip ? clip.top : 0;
    if (row < clipTop || row >= clipBottom) return 0;

    // writeString only ever touches this one row, so compute its base offset
    // once and mark it dirty a single time after the loop (if anything landed).
    const rowStart = row * this.cols;
    let wrote = false;
    let c = col;
    for (const char of str) {
      if (c >= clipRight) break;
      // Memoized per-glyph width — Bun.stringWidth is a native call and the
      // single hottest cost in paint. charWidth() fast-paths ASCII and
      // caches everything else (borders, blocks, CJK, emoji) by code point.
      const w = charWidth(char);
      if (w === 0) continue;

      // Skip cells left of clip, but still advance column
      if (c + w <= clipLeft) {
        c += w;
        continue;
      }
      // Skip wide chars that straddle the right clip edge
      if (w === 2 && c + 1 >= clipRight) {
        c += w;
        continue;
      }

      // c < clipRight ≤ cols holds from the loop guard; c may still be < 0
      // when a wide glyph straddles the left edge, so guard the low bound.
      if (c >= 0) {
        this.writeCell(rowStart + c, char, fg, bg, attrs, w, link);
        wrote = true;
      }
      if (w === 2 && c + 1 >= 0 && c + 1 < this.cols) {
        this.writeCell(rowStart + c + 1, "", fg, bg, attrs, 0, link);
        wrote = true;
      }
      c += w;
    }
    if (wrote) this.dirtyRows.add(row);
    return c - col;
  }

  /** Fill a rectangular region with a cell value. */
  fillRect(col, row, width, height, char = " ", fg = NO_COLOR, bg = NO_COLOR, attrs = Attr.None, clip) {
    const rMin = Math.max(row, clip ? clip.top : 0);
    const rMax = Math.min(row + height, clip ? clip.bottom : this.rows);
    const cMin = Math.max(col, clip ? clip.left : 0);
    const cMax = Math.min(col + width, clip ? clip.right : this.cols);

    // rMin/rMax/cMin/cMax are already clamped to valid bounds above. The column
    // span is the same for every row, so test it once rather than per row.
    if (cMax <= cMin) return;
    for (let r = rMin; r < rMax; r++) {
      const rowStart = r * this.cols;
      for (let c = cMin; c < cMax; c++) {
        this.writeCell(rowStart + c, char, fg, bg, attrs, 1, "");
      }
      this.dirtyRows.add(r);
    }
  }
}

// --- Diff engine ---

/**
 * Compare two frame buffers and produce the minimal set of changed runs.
 * Optionally restrict to a set of dirty rows for faster diffing.
 */
export function diffFrames(prev, curr, dirtyRows) {
  const changes = [];
  const {
    cols,
    rows,
    cells
  } = curr;
  for (let row = 0; row < rows; row++) {
    if (dirtyRows && !dirtyRows.has(row)) continue;
    const rowStart = row * cols;
    let runStart = -1;
    for (let col = 0; col < cols; col++) {
      const idx = rowStart + col;
      const prevCell = prev.cells[idx];
      const currCell = cells[idx];
      if (cellsEqual(prevCell, currCell)) {
        if (runStart >= 0) {
          changes.push({
            row,
            startCol: runStart,
            cells,
            start: rowStart + runStart,
            end: idx
          });
          runStart = -1;
        }
      } else {
        if (runStart < 0) runStart = col;
      }
    }
    if (runStart >= 0) {
      changes.push({
        row,
        startCol: runStart,
        cells,
        start: rowStart + runStart,
        end: rowStart + cols
      });
    }
  }
  return changes;
}

// --- SGR state tracker + ANSI serializer ---

function sgrReset() {
  return {
    fg: NO_COLOR,
    bg: NO_COLOR,
    attrs: Attr.None
  };
}

/**
 * Build the minimal SGR escape sequence to transition `from` → the target
 * (`toFg`, `toBg`, `toAttrs`). Returns empty string if no change is needed.
 * Takes the target as primitives so the caller can carry SGR state in a single
 * reused object instead of allocating one per cell.
 */
function buildSGRDelta(from, toFg, toBg, toAttrs) {
  if (from.fg === toFg && from.bg === toBg && from.attrs === toAttrs) {
    return "";
  }
  const parts = [];

  // Selectively disable removed attributes instead of full reset.
  // SGR 22 disables both Bold and Dim, so if one is removed but the other
  // kept, we disable with 22 then re-enable the surviving one.
  const attrsRemoved = from.attrs & ~toAttrs;
  if (attrsRemoved) {
    if (attrsRemoved & (Attr.Bold | Attr.Dim)) {
      parts.push("22"); // disables both bold and dim
      // Re-enable the survivor if only one was removed
      if (toAttrs & Attr.Bold) parts.push("1");
      if (toAttrs & Attr.Dim) parts.push("2");
    }
    if (attrsRemoved & Attr.Italic) parts.push("23");
    if (attrsRemoved & Attr.Underline) parts.push("24");
    if (attrsRemoved & Attr.Inverse) parts.push("27");
    if (attrsRemoved & Attr.Strikethrough) parts.push("29");
  }

  // Attributes to add (that weren't already re-enabled above)
  const attrsAdded = toAttrs & ~from.attrs;
  // Skip bold/dim if they were already handled by the removal path above
  const boldDimHandled = attrsRemoved & (Attr.Bold | Attr.Dim);
  if (attrsAdded & Attr.Bold && !boldDimHandled) parts.push("1");
  if (attrsAdded & Attr.Dim && !boldDimHandled) parts.push("2");
  if (attrsAdded & Attr.Italic) parts.push("3");
  if (attrsAdded & Attr.Underline) parts.push("4");
  if (attrsAdded & Attr.Inverse) parts.push("7");
  if (attrsAdded & Attr.Strikethrough) parts.push("9");

  // Foreground — no longer reset by attribute changes, only emitted when changed
  if (from.fg !== toFg) {
    if (toFg === NO_COLOR) {
      parts.push("39"); // default fg
    } else {
      const r = toFg >> 16 & 0xff;
      const g = toFg >> 8 & 0xff;
      const b = toFg & 0xff;
      parts.push(`38;2;${r};${g};${b}`);
    }
  }

  // Background
  if (from.bg !== toBg) {
    if (toBg === NO_COLOR) {
      parts.push("49"); // default bg
    } else {
      const r = toBg >> 16 & 0xff;
      const g = toBg >> 8 & 0xff;
      const b = toBg & 0xff;
      parts.push(`48;2;${r};${g};${b}`);
    }
  }
  if (parts.length === 0) return "";
  return `\x1b[${parts.join(";")}m`;
}

/**
 * Serialize changed runs into a minimal ANSI byte sequence.
 * Tracks SGR state across runs to avoid redundant attribute codes.
 */
export function serializeChanges(changes, initialState, enableLinks = true) {
  if (changes.length === 0) return "";

  // Local, mutable SGR state carried across all runs/cells (copied so a
  // caller-provided initialState is never mutated).
  const state = initialState ? {
    fg: initialState.fg,
    bg: initialState.bg,
    attrs: initialState.attrs
  } : sgrReset();
  let currentLink = "";
  const parts = [];
  for (const run of changes) {
    // CUP: move cursor to position (1-indexed)
    parts.push(`\x1b[${run.row + 1};${run.startCol + 1}H`);
    for (let i = run.start; i < run.end; i++) {
      const cell = run.cells[i];
      // Skip continuation cells (part of a wide character)
      if (cell.width === 0) continue;
      const sgr = buildSGRDelta(state, cell.fg, cell.bg, cell.attrs);
      if (sgr) parts.push(sgr);
      // Carry SGR state in one reused object — no per-cell allocation.
      state.fg = cell.fg;
      state.bg = cell.bg;
      state.attrs = cell.attrs;

      // OSC 8 hyperlink transitions (only when terminal supports it)
      if (enableLinks && cell.link !== currentLink) {
        if (currentLink) {
          parts.push("\x1b]8;;\x1b\\"); // close previous link
        }
        if (cell.link) {
          parts.push(`\x1b]8;;${cell.link}\x1b\\`); // open new link
        }
        currentLink = cell.link;
      }
      parts.push(cell.char);
    }
  }

  // Close any open link
  if (enableLinks && currentLink) {
    parts.push("\x1b]8;;\x1b\\");
  }

  // Reset SGR state so terminal attributes don't bleed
  if (state.attrs !== Attr.None || state.fg !== NO_COLOR || state.bg !== NO_COLOR) {
    parts.push("\x1b[0m");
  }
  return parts.join("");
}

/**
 * Wrap frame data in synchronized output markers (DEC private mode 2026).
 * The terminal holds all rendering until ESU, then paints atomically.
 */
export function wrapSynchronized(data) {
  return `\x1b[?2026h${data}\x1b[?2026l`;
}

// --- Color helpers ---

/**
 * Resolve any ColorInput to a packed 24-bit RGB number for Cell fg/bg.
 * Accepts hex (#rgb, #rrggbb), named colors, rgb(), hsl(), numbers, etc.
 * Returns NO_COLOR for invalid input.
 */
export function color(input) {
  return Bun.color(input, "number") ?? NO_COLOR;
}
//# sourceMappingURL=frame-buffer.jsx.map
