-- factoryos.ui.widgets — minimal widget toolkit.
-- A widget: {type, x,y,w,h, focusable?, onActivate?, handleNav?, draw(cv,ox,oy,focused)}
-- (ox,oy) = window content origin on the canvas. Bounds are canvas-absolute
-- via ox/oy offset.

local G = require("factoryos.ui.glyphs")
local colors_ = colors -- may be stubbed
local C = {
  fg      = colors_ and colors_.white      or 1,
  dim     = colors_ and colors_.lightGray  or 256,
  bg      = colors_ and colors_.black      or 32768,
  panel   = colors_ and colors_.gray       or 128,
  accent  = colors_ and colors_.cyan       or 512,
  warn    = colors_ and colors_.yellow     or 16,
  danger  = colors_ and colors_.red        or 16384,
  ok      = colors_ and colors_.lime       or 32,
}

local W = { colors = C }

-- ── constructors ──────────────────────────────────────────────────────

function W.Label(t)
  return { type = "label", x = t.x, y = t.y, w = t.w or #(t.text or ""),
    h = 1, text = t.text or "", color = t.color or C.fg }
end

function W.Button(t)
  return { type = "button", x = t.x, y = t.y, w = t.w or (#(t.text or "") + 4),
    h = t.h or 1, text = t.text or "", focusable = true,
    onActivate = t.onActivate, color = t.color or C.fg }
end

function W.ListBox(t)
  return { type = "listbox", x = t.x, y = t.y, w = t.w, h = t.h,
    items = t.items or {}, sel = 1, scroll = 0, focusable = true,
    onSelect = t.onSelect, onActivate = t.onActivate }
end

function W.ProgressBar(t)
  return { type = "pbar", x = t.x, y = t.y, w = t.w, h = 1,
    value = t.value or 0, color = t.color or C.ok, label = t.label }
end

function W.StatusDot(t)
  return { type = "dot", x = t.x, y = t.y, w = t.w or 10, h = 1,
    state = t.state or "unknown", text = t.text or "" }
end

function W.Table(t)
  return { type = "table", x = t.x, y = t.y, w = t.w, h = t.h,
    cols = t.cols or {}, rows = t.rows or {}, color = t.color or C.fg }
end

function W.Sep(t)
  return { type = "sep", x = t.x, y = t.y, w = t.w, h = 1 }
end

--- Single-line text input. Only usable where a keyboard exists (mainframe
--- terminal); nav pads use pickers instead. Enter→onSubmit, Esc→onCancel.
function W.TextInput(t)
  return { type = "textinput", x = t.x, y = t.y, w = t.w, h = 1,
    text = t.text or "", label = t.label, focusable = true,
    onSubmit = t.onSubmit, onCancel = t.onCancel }
end

--- Scrollable read-only text pane. Focusable; consumes up/down to scroll.
function W.ScrollText(t)
  return { type = "scrolltext", x = t.x, y = t.y, w = t.w, h = t.h,
    lines = t.lines or {}, scroll = 0, focusable = true }
end

function W.Panel(t)
  return { type = "panel", x = t.x, y = t.y, w = t.w, h = t.h,
    title = t.title, color = t.color or C.panel }
end

--- Text input needs char/key events, not nav dirs. Handled via wm:input.
function W.handleChar(wd, ch)
  if wd.type ~= "textinput" then return false end
  if wd.cursor == nil then wd.cursor = #wd.text + 1 end
  wd.text = wd.text:sub(1, wd.cursor - 1) .. ch .. wd.text:sub(wd.cursor)
  wd.cursor = wd.cursor + 1
  return true
end

function W.handleKey(wd, key)
  if wd.type ~= "textinput" then return false end
  if wd.cursor == nil then wd.cursor = #wd.text + 1 end
  local K = keys or {}
  if key == K.backspace then
    if wd.cursor > 1 then
      wd.text = wd.text:sub(1, wd.cursor - 2) .. wd.text:sub(wd.cursor)
      wd.cursor = wd.cursor - 1
    end
    return true
  elseif key == K.left and wd.cursor > 1 then
    wd.cursor = wd.cursor - 1; return true
  elseif key == K.right and wd.cursor <= #wd.text then
    wd.cursor = wd.cursor + 1; return true
  elseif key == K.enter then
    if wd.onSubmit then wd.onSubmit(wd.text, wd) end
    return true
  elseif key == K.tab or key == K.escape then
    if wd.onCancel then wd.onCancel(wd) end
    return true
  end
  return false
end

-- ── behavior ──────────────────────────────────────────────────────────

local DOT = { online = C.ok, stale = C.warn, offline = C.danger,
  unknown = C.dim, fault = C.danger }

--- Returns true if the widget consumed the nav direction.
function W.handleNav(wd, dir)
  if wd.type == "listbox" then
    local n = #wd.items
    if n == 0 then return false end
    if dir == "up" and wd.sel > 1 then
      wd.sel = wd.sel - 1
      if wd.sel <= wd.scroll then wd.scroll = wd.sel - 1 end
      return true
    elseif dir == "down" and wd.sel < n then
      wd.sel = wd.sel + 1
      if wd.sel > wd.scroll + wd.h then wd.scroll = wd.sel - wd.h end
      return true
    end
  elseif wd.type == "scrolltext" then
    local maxScroll = math.max(0, #wd.lines - wd.h)
    if dir == "up" and wd.scroll > 0 then
      wd.scroll = wd.scroll - 1
      return true
    elseif dir == "down" and wd.scroll < maxScroll then
      wd.scroll = wd.scroll + 1
      return true
    end
  end
  return false
end

function W.activate(wd)
  if wd.type == "listbox" and wd.onActivate and wd.items[wd.sel] then
    return wd.onActivate(wd.items[wd.sel], wd.sel)
  end
  if wd.onActivate then return wd.onActivate(wd) end
end

--- Touch/click at widget-relative (rx, ry). Buttons activate immediately;
--- listbox rows select+activate on tap; scrolltext pans toward the tap.
function W.touch(wd, rx, ry)
  local t = wd.type
  if t == "button" then
    W.activate(wd)
  elseif t == "listbox" then
    local row = wd.scroll + ry
    if wd.items[row] then
      wd.sel = row
      W.activate(wd)
    end
  elseif t == "scrolltext" then
    W.handleNav(wd, ry <= math.floor(wd.h / 2) and "up" or "down")
  elseif t == "textinput" then
    wd.cursor = math.max(1, math.min(#wd.text + 1, rx))
  end
end

-- ── drawing ───────────────────────────────────────────────────────────

local function fit(s, w)
  s = tostring(s)
  if #s > w then return s:sub(1, w) end
  return s .. string.rep(" ", w - #s)
end
W.fit = fit

function W.draw(cv, wd, ox, oy, focused)
  local x, y = ox + wd.x - 1, oy + wd.y - 1
  local t = wd.type

  if t == "label" then
    cv:write(x, y, fit(wd.text, wd.w), wd.color, C.bg)

  elseif t == "sep" then
    cv:write(x, y, string.rep(G.h, wd.w), C.panel, C.bg)

  elseif t == "panel" then
    local w, h = wd.w, wd.h
    cv:write(x, y, G.tl .. string.rep(G.h, w - 2) .. G.tr, wd.color, C.bg)
    for yy = 1, h - 2 do
      cv:write(x, y + yy, G.v, wd.color, C.bg)
      cv:write(x + w - 1, y + yy, G.v, wd.color, C.bg)
    end
    cv:write(x, y + h - 1, G.bl .. string.rep(G.h, w - 2) .. G.br, wd.color, C.bg)
    if wd.title then
      cv:write(x + 2, y, " " .. wd.title .. " ", C.fg, C.bg)
    end

  elseif t == "button" then
    local s = "[ " .. wd.text .. " ]"
    if focused then
      cv:write(x, y, fit(G.selL .. s .. G.selR, wd.w), C.bg, C.accent)
    else
      cv:write(x, y, fit(" " .. s .. " ", wd.w), wd.color, C.bg)
    end

  elseif t == "listbox" then
    for i = 1, wd.h do
      local idx = wd.scroll + i
      local it = wd.items[idx]
      local line = it and (it.text or tostring(it)) or ""
      local sel = idx == wd.sel
      if focused and sel then
        cv:write(x, y + i - 1, fit(G.selL .. line, wd.w), C.bg, C.accent)
      elseif sel then
        cv:write(x, y + i - 1, fit(" " .. line, wd.w), C.accent, C.bg)
      else
        cv:write(x, y + i - 1, fit(" " .. line, wd.w), C.fg, C.bg)
      end
    end

  elseif t == "pbar" then
    local v = math.max(0, math.min(1, wd.value or 0))
    local inner = wd.w - 2
    local fillw = math.floor(inner * v + 0.5)
    cv:write(x, y, "[" .. string.rep(G.fill, fillw)
      .. string.rep(G.empty, inner - fillw) .. "]", wd.color, C.bg)
    if wd.label then
      cv:write(x + math.floor((wd.w - #wd.label) / 2), y, wd.label,
        focused and C.accent or C.fg, nil)
    end

  elseif t == "dot" then
    local c = DOT[wd.state] or DOT.unknown
    cv:write(x, y, G.dot, c, C.bg)
    cv:write(x + 2, y, fit(wd.text, wd.w - 2), C.fg, C.bg)

  elseif t == "textinput" then
    local shown = wd.text
    if wd.cursor and wd.cursor <= #shown then
      shown = shown:sub(1, wd.cursor - 1) .. "_" .. shown:sub(wd.cursor + 1)
    elseif focused then
      shown = shown .. "_"
    end
    if focused then
      cv:write(x, y, fit("[" .. shown .. "]", wd.w), C.bg, C.accent)
    else
      cv:write(x, y, fit(" " .. shown .. " ", wd.w), C.fg, C.panel)
    end

  elseif t == "scrolltext" then
    for i = 1, wd.h do
      local line = wd.lines[wd.scroll + i] or ""
      cv:write(x, y + i - 1, fit(line, wd.w), C.fg, C.bg)
    end
    -- scroll indicators
    if wd.scroll > 0 then
      cv:write(x + wd.w - 1, y, "^", C.accent, C.bg)
    end
    if wd.scroll + wd.h < #wd.lines then
      cv:write(x + wd.w - 1, y + wd.h - 1, "v", C.accent, C.bg)
    end

  elseif t == "table" then
    for r = 1, math.min(wd.h, #wd.rows) do
      local cells = wd.rows[r]
      local line = ""
      for ci, cw in ipairs(wd.cols) do
        line = line .. fit(tostring(cells[ci] or ""), cw)
      end
      cv:write(x, y + r - 1, fit(line, wd.w), r == 1 and C.dim or wd.color, C.bg)
    end
  end
end

return W
