///|
/// 投影画布:在 S1 的 `TermScreen` 之上加一层"格子归属"。
///
/// 归属规则就是 S0 定下的 z-order 策略,它属于投影器而不是 cell buffer:
/// `@moui_tui` 必须保持与 VT 终端同构的纯缓冲区,不能认识语义树。
/// - 每个绘制动作分配一个 paint sequence,并带一个优先级
/// - 高优先级可以覆盖低优先级已占的格子,反之不行
/// - 同优先级先到先得,所以两个兄弟盒重叠时不会互相打断笔画
priv struct Canvas {
  screen : @tui.TermScreen
  mut cols : Int
  mut rows : Int
  /// 本帧每格的 paint sequence,0 表示未被占。
  mut owners : Array[Array[Int]]
  /// 本帧每格首字符占用的 cell 数:1 单宽、2 宽字符左半、-1 宽字符右半。
  mut widths : Array[Array[Int]]
  /// 上一帧的同一对掩码:这一帧没再画的格子要靠它擦掉。
  mut prev_owners : Array[Array[Int]]
  mut prev_widths : Array[Array[Int]]
  /// 本帧被样式带碰过的格子。样式带只改 attrs、故意不认领格子,
  /// 归属清扫永远轮不到它们,带的擦除只能凭这份掩码。
  mut bands : Array[Array[Int]]
  /// 上一帧的带掩码:这一帧没再碰的带格子要靠它擦掉。
  mut prev_bands : Array[Array[Int]]
  /// 上一次真正写给终端的栅格:增量的唯一依据。
  mut sent : Array[Array[@tui.Cell]]
  /// 尺寸变化后必须先整屏重建,否则缩窗留下的残字擦不掉。
  mut rebuild : Bool
  mut prio : Array[Int]
}

///|
priv struct Cursor {
  mut row : Int
  mut col : Int
  c0 : Int
  r1 : Int
  c1 : Int
  attrs : @tui.CellAttrs
  seq : Int
}

///|
fn cell_mask(cols : Int, rows : Int, fill : Int) -> Array[Array[Int]] {
  // 每行要有自己的底层数组,嵌套 make 会让所有行共享同一份。
  let mask : Array[Array[Int]] = []
  for _ in 0.. Array[Array[@tui.Cell]] {
  let blank = @tui.blank_cell(@tui.CellAttrs::default())
  let grid : Array[Array[@tui.Cell]] = []
  for _ in 0.. Canvas {
  {
    screen: @tui.TermScreen::new(cols, rows),
    cols,
    rows,
    owners: cell_mask(cols, rows, 0),
    widths: cell_mask(cols, rows, 0),
    prev_owners: cell_mask(cols, rows, 0),
    prev_widths: cell_mask(cols, rows, 0),
    bands: cell_mask(cols, rows, 0),
    prev_bands: cell_mask(cols, rows, 0),
    sent: blank_grid(cols, rows),
    rebuild: false,
    prio: [],
  }
}

///|

///|
/// 换尺寸 = 换栅格,不是换终端窗口。
///
/// `TermScreen::resize` 走的是终端语义:保内容、缩窗时把顶部行推进回看缓冲。
/// 投影器两样都不能要 ——
/// - 全帧 `line_at(y)` 把 `y` 当可视行号用,回看缓冲一旦非空,整个 diff 就会
///   读到回看行,于是新帧里混进旧宽度的行(实测 60 列表头出现在 100 列帧的第 1 行);
/// - 残格在重建帧里照样被 `runs()` 编码出去,旧布局的字和新增量的字叠在同一行。
/// 所以这里显式清空而不是只标脏。放弃的方案:让 `rebuild` 分支先清屏再靠
/// `sweep_stale` 擦残格 —— 清屏由终端做了,但栅格里的残格仍会被逐行重画出去,
/// 而且 `prev_owners` 在换尺寸时无效可查,擦不到。
fn Canvas::resize(self : Canvas, cols : Int, rows : Int) -> Unit {
  if cols == self.cols && rows == self.rows {
    return
  }
  self.cols = cols
  self.rows = rows
  self.owners = cell_mask(cols, rows, 0)
  self.widths = cell_mask(cols, rows, 0)
  // 掩码按新尺寸重建:上一帧的归属在新栅格上没有可对应的格子。
  self.prev_owners = cell_mask(cols, rows, 0)
  self.prev_widths = cell_mask(cols, rows, 0)
  self.bands = cell_mask(cols, rows, 0)
  self.prev_bands = cell_mask(cols, rows, 0)
  self.sent = blank_grid(cols, rows)
  self.rebuild = true
  self.screen.resize(cols, rows)
  self.screen.clear_all()
}

///|
/// 帧起始:只换掩码,不整屏刷白。
///
/// 整屏刷白会让每个曾有过内容的行都变脏,增量退化成逐行全量重绘;
/// 保留旧掩码、在帧末只擦这一帧没再画的格子,内容不变的帧才能零字节。
fn Canvas::begin_frame(self : Canvas) -> Unit {
  self.prio = []
  self.prev_owners = self.owners
  self.prev_widths = self.widths
  self.prev_bands = self.bands
  self.owners = cell_mask(self.cols, self.rows, 0)
  self.widths = cell_mask(self.cols, self.rows, 0)
  self.bands = cell_mask(self.cols, self.rows, 0)
}

///|
/// 强制下一帧整屏重建(换调色板这类「内容没变但每一格的 attrs 都变了」)。
fn Canvas::force_rebuild(self : Canvas) -> Unit {
  self.rebuild = true
}

///|
/// 帧末清扫:上一帧画过、这一帧没认领的格子刷白。
/// 宽字符右半由左半一并清掉,否则会在行里留下吃不掉的一格。
fn Canvas::sweep_stale(self : Canvas) -> Unit {
  let blank = @tui.CellAttrs::default()
  for y in 0.. Unit {
  self.widths[y][x] = 1
  ignore(self.screen.put(y, x, " ", 1, attrs))
}

///|
fn Canvas::new_seq(self : Canvas, prio : Int) -> Int {
  let seq = self.prio.length() + 1
  self.prio.push(prio)
  seq
}

///|
/// 归属判定:空格与自己都可写,否则只有更高优先级才能抢占。
fn Canvas::can_claim(self : Canvas, y : Int, x : Int, seq : Int) -> Bool {
  if y < 0 || y >= self.rows || x < 0 || x >= self.cols {
    return false
  }
  let other = self.owners[y][x]
  if other == 0 || other == seq {
    true
  } else {
    self.prio[seq - 1] > self.prio[other - 1]
  }
}

///|
fn Canvas::take_cell(self : Canvas, y : Int, x : Int, seq : Int) -> Unit {
  self.owners[y][x] = seq
}

///|
/// 抢占别人的格子时,要把那一格的宽字符伴侣一起清掉:
/// 残留右半格 width==0 不产生文本,行会凭空少一格。
fn Canvas::drop_partner(
  self : Canvas,
  y : Int,
  x : Int,
  seq : Int,
  attrs : @tui.CellAttrs,
) -> Unit {
  let previous = self.widths[y][x]
  let partner = if previous == 2 {
    x + 1
  } else if previous == -1 {
    x - 1
  } else {
    -1
  }
  if partner >= 0 && partner < self.cols {
    self.take_cell(y, partner, seq)
    self.erase_cell(y, partner, attrs)
  }
}

///|
/// 写入一个字符:先确认整个跨度都能归属本 seq,再落盘。
fn Canvas::put(
  self : Canvas,
  y : Int,
  x : Int,
  ch : String,
  attrs : @tui.CellAttrs,
  seq : Int,
) -> Bool {
  if y < 0 || y >= self.rows || x < 0 || x >= self.cols {
    return false
  }
  let width = @tui.unicode_width(ch)
  let span = if width > 1 { 2 } else { 1 }
  if x + span > self.cols {
    return false
  }
  // 跨度必须整体可归属才落盘:半格被抢会让宽字符占掉别人的列。
  let mut i = x
  let mut claimable = true
  while i < x + span {
    if !self.can_claim(y, i, seq) {
      claimable = false
    }
    i = i + 1
  }
  if !claimable {
    return false
  }
  i = x
  while i < x + span {
    if self.owners[y][i] != seq && self.owners[y][i] != 0 {
      self.drop_partner(y, i, seq, attrs)
    }
    self.take_cell(y, i, seq)
    i = i + 1
  }
  let written = self.screen.put(y, x, ch, width, attrs)
  self.widths[y][x] = width
  if width > 1 {
    self.widths[y][x + 1] = -1
  }
  written
}

///|
fn cursor_advance(cur : Cursor) -> Bool {
  cur.row = cur.row + 1
  cur.col = cur.c0
  cur.row <= cur.r1
}

///|
/// 在量化盒内写一个词:超宽硬断(中日韩整段没有空格,必须能连续折行),
/// 词尾留一格空白。
fn Canvas::put_word(self : Canvas, cur : Cursor, word : String) -> Bool {
  let plain = @tui.CellAttrs::default()
  for ch in word {
    if cur.col > cur.c1 && !cursor_advance(cur) {
      return false
    }
    let text = ch.to_string()
    if @tui.unicode_width(text) > 1 && cur.col + 1 > cur.c1 {
      if !cursor_advance(cur) {
        return false
      }
    }
    ignore(self.put(cur.row, cur.col, text, cur.attrs, cur.seq))
    cur.col = cur.col + @tui.unicode_width(text)
  }
  // 词间空位只在能被本 seq 归属时才填空白:直接抢格会把兄弟节点
  // 的首字母擦成一个空格(实测 `Entry 0` 变成 ` ntry 0`)。
  if cur.col <= cur.c1 {
    ignore(self.put(cur.row, cur.col, " ", plain, cur.seq))
  }
  cur.col = cur.col + 1
  true
}

///|
/// 每帧绘制都裁剪到节点自己的量化盒并在盒内折行。
/// 像素渲染器靠 `clip_rect` 免费拿到这件事,cell 投影必须显式做,
/// 否则兄弟节点会互相插字。
fn Canvas::write_boxed(
  self : Canvas,
  r0 : Int,
  c0 : Int,
  r1 : Int,
  c1 : Int,
  text : String,
  attrs : @tui.CellAttrs,
  seq : Int,
) -> Unit {
  let cur : Cursor = { row: r0, col: c0, c0, r1, c1, attrs, seq, }
  let mut word = ""
  for ch in text {
    if ch == '\n' {
      if word.length() > 0 && !self.put_word(cur, word) {
        return
      }
      word = ""
      if !cursor_advance(cur) {
        return
      }
    } else if ch == ' ' {
      if word.length() > 0 && !self.put_word(cur, word) {
        return
      }
      word = ""
    } else {
      word = word + ch.to_string()
    }
  }
  if word.length() > 0 {
    ignore(self.put_word(cur, word))
  }
}

///|
/// 区域边框。盒子小到画不出框时退化为一个标记格,保证区域不会被丢掉。
/// 框上不再写标题:标题是内容,归子节点画。
fn Canvas::draw_box(
  self : Canvas,
  r0 : Int,
  c0 : Int,
  r1 : Int,
  c1 : Int,
  attrs : @tui.CellAttrs,
  chrome : Int,
) -> Unit {
  if r1 - r0 < 2 || c1 - c0 < 2 {
    ignore(self.put(r0, c0, "~", attrs, chrome))
    return
  }
  ignore(self.put(r0, c0, "╭", attrs, chrome))
  ignore(self.put(r0, c1, "╮", attrs, chrome))
  ignore(self.put(r1, c0, "╰", attrs, chrome))
  ignore(self.put(r1, c1, "╯", attrs, chrome))
  let mut c = c0 + 1
  while c < c1 {
    ignore(self.put(r0, c, "─", attrs, chrome))
    ignore(self.put(r1, c, "─", attrs, chrome))
    c = c + 1
  }
  let mut r = r0 + 1
  while r < r1 {
    ignore(self.put(r, c0, "│", attrs, chrome))
    ignore(self.put(r, c1, "│", attrs, chrome))
    r = r + 1
  }
}

///|
/// 样式覆盖层:重刷 attrs,不改字符。
///
/// 焦点与选中挂在条目容器上,而条目自己不写字(内容归子节点),
/// 所以高亮不能走 `put` 的归属抢占 —— 那会为了"显眼"吃掉兄弟的文字。
/// 归属判定管「这一格归谁」,覆盖层管「这一格长什么样」。
///
/// 唯一例外是下划线:它是跟着字形走的装饰,画在没被任何节点认领的格子上
/// 就成了一条独立横线。量化盒比标签宽、还常有整空格行(实测 counter 焦点
/// 带 3 行 × 13 格而 `[-]` 只占 1 行 3 格),整段照抄 attrs 会在标签下方
/// 画出两根悬空的黄线。底色/反白在空格上是色带,仍然全幅铺开。
fn Canvas::style_range(
  self : Canvas,
  r0 : Int,
  c0 : Int,
  r1 : Int,
  c1 : Int,
  attrs : @tui.CellAttrs,
) -> Unit {
  let mut y = r0.clamp(min=0, max=self.rows - 1)
  let y1 = r1.clamp(min=0, max=self.rows - 1)
  let x0 = c0.clamp(min=0, max=self.cols - 1)
  let x1 = c1.clamp(min=0, max=self.cols - 1)
  let bare = @tui.attrs_with(attrs, underline=Some(false))
  while y <= y1 {
    let cells = self.screen.line_at(y).cells
    let mut x = x0
    while x <= x1 {
      // 宽字符右半由左半那笔一并重写(put 会连伴侣一起给 attrs)。
      if self.widths[y][x] != -1 && x < cells.length() {
        // 带不认领格子,这里留痕:带移走后靠 sweep_stale_bands 擦属性。
        self.bands[y][x] = 1
        ignore(
          self.screen.put(
            y,
            x,
            cells[x].ch,
            self.widths[y][x].clamp(min=1, max=2),
            if self.owners[y][x] == 0 {
              bare
            } else {
              attrs
            },
          ),
        )
      }
      x = x + 1
    }
    y = y + 1
  }
}

///|
/// 最终内容与上一次写给终端的栅格是否不同。逐格比而不比行文本:
/// 属性变了但字符没变(焦点态换前景色)也必须重画。
fn Canvas::row_changed(self : Canvas, y : Int) -> Bool {
  let current = self.screen.line_at(y).cells
  let previous = self.sent[y]
  if current.length() != previous.length() {
    return true
  }
  let mut x = 0
  let mut changed = false
  while x < current.length() {
    if current[x] != previous[x] {
      changed = true
    }
    x = x + 1
  }
  changed
}

///|
/// 写入代脏 → 最终差异。
///
/// `@moui_tui` 的"写一笔就标脏一行"对 VT 缓冲区是对的,但投影器一帧内
/// 会把同一格写好几遍(先画边框、内容再抢占、帧末再清扫),瞬时中间态
/// 全都算变化的话增量通路永远非空,内容没变的帧也要重画整屏。
/// 所以这里只把 buffer 的脏行当成候选集,逐行用最终栅格对比上次输出。
fn Canvas::final_damage(self : Canvas, candidates : @tui.Damage) -> @tui.Damage {
  if candidates.full || self.rebuild {
    let rebuild : @tui.Damage = { full: true, rows: [], }
    self.rebuild = false
    self.sent = self.snapshot()
    rebuild
  } else {
    let rows : Array[Int] = []
    for y in candidates.rows {
      if y >= 0 && y < self.rows && self.row_changed(y) {
        rows.push(y)
        self.sent[y] = self.screen.line_at(y).cells.copy()
      }
    }
    let partial : @tui.Damage = { full: false, rows, }
    partial
  }
}

///|
fn Canvas::snapshot(self : Canvas) -> Array[Array[@tui.Cell]] {
  let grid : Array[Array[@tui.Cell]] = []
  for y in 0.. Unit {
  let blank = @tui.CellAttrs::default()
  for y in 0.. String {
  self.sweep_stale()
  self.sweep_stale_bands()
  // 脏 mask 必须取走才能清零,否则下一帧会背上上一帧的脏行。
  let damage = self.final_damage(self.screen.take_damage())
  if damage.is_empty() {
    ""
  } else {
    frame_update(self.screen, damage)
  }
}