-- factoryos.ui.canvas — buffered blit renderer over any term-like target
-- (term, monitor peripheral, or a recording stub in tests).
--
-- Cells are {ch,fg,bg}; flush() emits one term.blit per dirty row. fg/bg are
-- colors.* values; converted to blit digits at write time.

local M = {}

local function blitOf(c)
  if colors and colors.toBlit then return colors.toBlit(c) end
  -- fallback table (colours 2^n → hex digit order)
  local map = { [1]="0",[2]="1",[4]="2",[8]="3",[16]="4",[32]="5",[64]="6",
    [128]="7",[256]="8",[512]="9",[1024]="a",[2048]="b",[4096]="c",
    [8192]="d",[16384]="e",[32768]="f" }
  return map[c] or "f"
end
M.blitOf = blitOf

local Canvas = {}
Canvas.__index = Canvas

function Canvas.new(target)
  local self = setmetatable({}, Canvas)
  self.t = target
  self.w, self.h = target.getSize()
  self.fg = colors and colors.white or 1
  self.bg = colors and colors.black or 32768
  self:reset()
  return self
end

function Canvas:reset()
  self.cells = {}
  self.dirty = {}
  for y = 1, self.h do
    local row = {}
    for x = 1, self.w do row[x] = { " ", "0", "f" } end
    self.cells[y] = row
    self.dirty[y] = true
  end
end

function Canvas:resize()
  self.w, self.h = self.t.getSize()
  self:reset()
end

--- Write string at (x,y). chars clipped to bounds; nil cells skipped.
function Canvas:write(x, y, str, fg, bg)
  if y < 1 or y > self.h or not str then return end
  local row = self.cells[y]
  local fgc, bgc = fg and blitOf(fg) or nil, bg and blitOf(bg) or nil
  for i = 1, #str do
    local cx = x + i - 1
    if cx >= 1 and cx <= self.w then
      local cell = row[cx]
      cell[1] = str:sub(i, i)
      if fgc then cell[2] = fgc end
      if bgc then cell[3] = bgc end
      self.dirty[y] = true
    end
  end
end

function Canvas:fill(x, y, w, h, ch, fg, bg)
  ch = ch or " "
  local s = string.rep(ch, w)
  for yy = y, y + h - 1 do self:write(x, yy, s, fg, bg) end
end

function Canvas:set(x, y, ch, fg, bg) self:write(x, y, ch, fg, bg) end

--- Push dirty rows to the target. Returns rows written.
function Canvas:flush()
  local n = 0
  for y = 1, self.h do
    if self.dirty[y] then
      local row = self.cells[y]
      local t = table.concat(self:_col(row, 1))
      local f = table.concat(self:_col(row, 2))
      local b = table.concat(self:_col(row, 3))
      self.t.setCursorPos(1, y)
      self.t.blit(t, f, b)
      self.dirty[y] = nil
      n = n + 1
    end
  end
  return n
end

function Canvas:_col(row, i)
  local out = {}
  for x = 1, self.w do out[x] = row[x][i] end
  return out
end

--- Snapshot a row as {txt,fg,bg} strings — used by the compositor.
function Canvas:row(y)
  local row = self.cells[y]
  return {
    txt = table.concat(self:_col(row, 1)),
    fg  = table.concat(self:_col(row, 2)),
    bg  = table.concat(self:_col(row, 3)),
  }
end

function Canvas:dirtyRows()
  local out = {}
  for y = 1, self.h do if self.dirty[y] then out[#out + 1] = y end end
  self.dirty = {}
  return out
end

--- A null term-like target for wall mode: the canvas accumulates cells,
--- the compositor ships rows to display tiles.
function M.virtual(w, h)
  local sz = { w, h }
  return {
    getSize = function() return sz[1], sz[2] end,
    setCursorPos = function() end,
    blit = function() end,
    setTextScale = function() end,
  }
end

M.Canvas = Canvas
return M
