-- factoryos.ui.wm — window manager: z-order, draw, focus, nav routing.
--
-- Windows: {id,title,x,y,w,h,widgets,modal,onBack,app,focusable list derived}
-- The desktop is a borderless bottom window so focus/nav work uniformly.

local W = require("factoryos.ui.widgets")
local C = W.colors

local WM = {}
WM.__index = WM

function WM.new(canvas)
  return setmetatable({ cv = canvas, wins = {}, seq = 0 }, WM)
end

local function focusables(win)
  local out = {}
  for _, wd in ipairs(win.widgets or {}) do
    if wd.focusable then out[#out + 1] = wd end
  end
  return out
end

function WM:open(spec)
  self.seq = self.seq + 1
  local win = {
    id = spec.id or ("win" .. self.seq),
    title = spec.title or "",
    x = spec.x or 4, y = spec.y or 3,
    w = spec.w or 40, h = spec.h or 14,
    widgets = spec.widgets or {},
    modal = spec.modal, desktop = spec.desktop,
    onBack = spec.onBack, app = spec.app,
    border = spec.border ~= false,
    focus = 0,
  }
  local f = focusables(win)
  if spec.focus then
    for i, wd in ipairs(f) do if wd == spec.focus then win.focus = i end end
  end
  if win.focus == 0 and #f > 0 then win.focus = 1 end
  self.wins[#self.wins + 1] = win
  self:draw()
  return win
end

function WM:close(win)
  for i = #self.wins, 1, -1 do
    local w = self.wins[i]
    if w == win or (not win and not w.desktop and i == #self.wins) then
      if not w.desktop then
        table.remove(self.wins, i)
        self:draw()
        return true
      end
    end
  end
  return false
end

--- Top non-desktop window, or the desktop itself.
function WM:active()
  return self.wins[#self.wins]
end

function WM:focusWindow(win)
  for i, w in ipairs(self.wins) do
    if w == win then
      table.remove(self.wins, i)
      self.wins[#self.wins + 1] = w
      self:draw()
      return true
    end
  end
end

function WM:windows() return self.wins end

--- Move focus to a specific widget in a window (default: active).
function WM:focusWidget(wd, win)
  win = win or self:active()
  local f = focusables(win)
  for i, w in ipairs(f) do
    if w == wd then win.focus = i; self:draw(); return true end
  end
  return false
end

-- ── focus navigation ──────────────────────────────────────────────────

local function center(wd) return wd.x + wd.w / 2, wd.y + wd.h / 2 end

--- Spatial scoring: a candidate must be axis-dominant (its center lies in a
--- 90° cone in the move direction). Score = 4×axis distance + perpendicular
--- offset; lowest wins.
local function score(dir, cx, cy, tx, ty)
  local dx, dy = tx - cx, ty - cy
  local ax, ay = math.abs(dx), math.abs(dy)
  if dir == "up"    and dy < 0 and ay >= ax then return 4 * ay + ax end
  if dir == "down"  and dy > 0 and ay >= ax then return 4 * ay + ax end
  if dir == "left"  and dx < 0 and ax >= ay then return 4 * ax + ay end
  if dir == "right" and dx > 0 and ax >= ay then return 4 * ax + ay end
  return nil
end

function WM:_moveFocus(win, dir)
  local f = focusables(win)
  if #f == 0 then return end
  local cur = f[win.focus] or f[1]
  -- widget may consume (listbox scroll)
  if cur and W.handleNav(cur, dir) then self:draw(); return end
  local cx, cy = 0, 0
  if cur then cx, cy = center(cur) end
  local best, bs
  for i, wd in ipairs(f) do
    if wd ~= cur then
      local tx, ty = center(wd)
      local s = score(dir, cx, cy, tx, ty)
      if s and (not bs or s < bs) then best, bs = i, s end
    end
  end
  if not best then
    -- wrap: pick the extreme widget on the opposite edge
    local bx
    for i, wd in ipairs(f) do
      if wd ~= cur then
        local tx, ty = center(wd)
        local s = dir == "up" and ty or dir == "down" and -ty
          or dir == "left" and tx or -tx
        if not bx or s > bx then best, bx = i, s end
      end
    end
  end
  if best then
    if cur and cur.type == "listbox" then cur.scroll = math.min(cur.scroll, math.max(0, cur.sel - cur.h)) end
    win.focus = best
    self:draw()
  end
end

--- Route a NAV_* key.
function WM:nav(key)
  local win = self:active()
  if not win then return false end
  local dir = key == "NAV_UP" and "up" or key == "NAV_DOWN" and "down"
    or key == "NAV_LEFT" and "left" or key == "NAV_RIGHT" and "right"
  if dir then
    self:_moveFocus(win, dir)
  elseif key == "NAV_ENTER" then
    local wd = focusables(win)[win.focus]
    if wd then W.activate(wd); self:draw() end
  elseif key == "NAV_BACK" then
    if win.onBack and win.onBack(win) ~= false then
      -- handled by window
    elseif not win.desktop then
      self:close(win)
    end
  end
  return true
end

--- Modal confirm dialog. cb(true|false).
function WM:confirm(title, message, cb)
  local w = math.max(#message + 6, #title + 8, 30)
  local win
  win = self:open({
    title = title, modal = true,
    w = w, h = 7,
    x = math.max(1, math.floor((self.cv.w - w) / 2)),
    y = math.max(1, math.floor((self.cv.h - 7) / 2)),
    widgets = {
      W.Label({ x = 3, y = 3, w = w - 4, text = message }),
      W.Button({ x = math.floor(w / 2) - 12, y = 5, text = "CANCEL",
        onActivate = function() self:close(win); cb(false) end }),
      W.Button({ x = math.floor(w / 2) + 4, y = 5, text = "CONFIRM",
        onActivate = function() self:close(win); cb(true) end }),
    },
    onBack = function() self:close(win); cb(false); return true end,
  })
  return win
end

--- Forward raw input (char/key) to the focused widget. Returns true when
--- consumed — callers should only fall back to nav mapping if false.
function WM:inputChar(ch)
  local win = self:active()
  local wd = win and focusables(win)[win.focus]
  if wd and W.handleChar(wd, ch) then self:draw(); return true end
  return false
end

function WM:inputKey(k)
  local win = self:active()
  local wd = win and focusables(win)[win.focus]
  if wd and W.handleKey(wd, k) then self:draw(); return true end
  return false
end

--- Hit-test a screen-space point (monitor_touch / mouse_click coords).
--- Raises the touched window, focuses the widget under the point, and
--- lets it handle the tap. Returns true if any window was hit.
function WM:touch(x, y)
  local top = self:active()
  if not top then return false end
  if top.modal then
    return self:_touchWin(top, x, y) -- modal traps taps outside its frame
  end
  for i = #self.wins, 1, -1 do
    local win = self.wins[i]
    if x >= win.x and x < win.x + win.w
      and y >= win.y and y < win.y + win.h then
      if i ~= #self.wins then self:focusWindow(win) end
      return self:_touchWin(win, x, y)
    end
  end
  return false
end

function WM:_touchWin(win, x, y)
  local ox = win.x + (win.border and 1 or 0)
  local oy = win.y + (win.border and 1 or 0)
  local lx, ly = x - ox + 1, y - oy + 1
  local f = focusables(win)
  for idx, wd in ipairs(f) do
    if lx >= wd.x and lx < wd.x + wd.w
      and ly >= wd.y and ly < wd.y + wd.h then
      win.focus = idx
      W.touch(wd, lx - wd.x + 1, ly - wd.y + 1)
      self:draw()
      return true
    end
  end
  self:draw()
  return true
end

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

function WM:_drawWindow(win)
  local cv = self.cv
  local ox, oy = win.x, win.y
  if win.border then
    W.draw(cv, W.Panel({ x = 1, y = 1, w = win.w, h = win.h, title = win.title,
      color = win == self:active() and not win.desktop and C.accent or C.panel }),
      ox - 1, oy - 1, false)
    ox, oy = ox + 1, oy + 1
  end
  local f = focusables(win)
  for _, wd in ipairs(win.widgets) do
    W.draw(cv, wd, ox, oy, wd == f[win.focus])
  end
end

function WM:draw()
  local cv = self.cv
  cv:fill(1, 1, cv.w, cv.h, " ", nil, C.bg)
  for _, win in ipairs(self.wins) do self:_drawWindow(win) end
  return cv:flush()
end

return WM
