package.path = "./?.lua;./?/init.lua;" .. package.path

local T = require("tests.harness")
local check, eq = T.check, T.eq
love = love or require("tests.love_stub")

local TouchSkin = require("src.core.TouchSkin")
local Playfield = require("src.render.Playfield")
local Renderer = require("src.render.Renderer")
local Chrome = require("src.ui.gen2.Chrome")
local Zoom = require("src.render.Zoom")

local EPS = 1e-6

local function setWindow(w, h)
  love.graphics.getDimensions = function() return w, h end
  love.graphics.getPixelDimensions = function() return w, h end
end

local function cfg(viewport, extra)
  return ([[
overlays = 1
overlay0_name = "bezel"
overlay0_full_screen = true
overlay0_normalized = true
overlay0_viewport = "%s"
%s
overlay0_descs = 1
overlay0_desc0 = "nul,0.5,0.5,rect,0.02,0.02"
]]):format(viewport, extra or "")
end

local function useSkin(viewport, extra)
  local skin = assert(TouchSkin.parse(cfg(viewport, extra)))
  TouchSkin.setActive(skin)
  TouchSkin.setOverlayLive(false)
  return skin
end

local function inside(x, y, w, h, bx, by, bw, bh)
  return x >= bx - EPS and y >= by - EPS
    and x + w <= bx + bw + EPS and y + h <= by + bh + EPS
end

setWindow(640, 576)
TouchSkin.setActive(nil)
Renderer:init()
local plain = Renderer:frameRects()
eq(plain.cut, false, "no skin: no cutout")
eq(plain.vux, 0, "no skin: the picture starts at the window origin")
eq(plain.vuw, 640, "no skin: the picture is the whole window")
eq(plain.Sp, 4, "no skin: 640x576 fits four whole GB pixels")
eq(plain.uox, 0, "no skin: the UI letterbox fills the window")
eq(select(3, Playfield.rect(640, 576)), 640, "no skin: the playfield is the window")
eq(Chrome.fitScale(640, 576), 4, "no skin: Gold fits the window the same way")

local WINDOWS = {
  { 640, 576 }, { 1280, 720 }, { 1920, 1080 },
  { 800, 480 }, { 480, 800 }, { 360, 640 },
}
local VIEWPORTS = {
  "0.2335,0.0855,0.5335,0.830",
  "0.2,0.15,0.6,0.5",
  "0.05,0.05,0.9,0.35",
  "0.3,0.1,0.4,0.8",
}
local UI_SIZES = { { 160, 144 }, { 304, 144 } }

local escapes, uncut, cases = 0, 0, 0
for _, win in ipairs(WINDOWS) do
  setWindow(win[1], win[2])
  for _, vp in ipairs(VIEWPORTS) do
    useSkin(vp)
    Renderer:init()
    for _, size in ipairs(UI_SIZES) do
      Renderer:setUISize(size[1], size[2])
      for off = -8, 8 do
        Zoom.offset = off
        for _, fill in ipairs({ false, true }) do
          for _, centered in ipairs({ true, false }) do
            Renderer.uiFill = fill
            Renderer.uiCentered = centered
            Renderer.worldActive = true
            cases = cases + 1
            local r = Renderer:frameRects()
            local ux, uy, uw, uh = Renderer.clipToView(r, r.uox, r.uoy,
              r.uvpw, r.uvph)
            if not inside(ux, uy, uw, uh, r.vux, r.vuy, r.vuw, r.vuh) then
              escapes = escapes + 1
            end
            if uw < r.uvpw - EPS or uh < r.uvph - EPS then uncut = uncut + 1 end
            local vw, vh = Renderer:worldViewSize()
            local sp = Zoom.scale(r.Sp)
            if vw * sp > r.vuw + 2 * sp + EPS
              or vh * sp > r.vuh + 2 * sp + EPS then
              escapes = escapes + 1
            end
          end
        end
      end
    end
  end
end
check(cases > 1000, "the sweep covers every window x cutout x zoom x layout")
eq(escapes, 0, "no zoom, battle surface or UI layout puts a rect past the cutout")
eq(uncut, 0, "and the UI was sized to fit, so the clip never has to cut it")

setWindow(1280, 720)
useSkin("0.25,0.1,0.5,0.6")
Renderer:init()
Renderer:setUISize(160, 144)
Renderer.uiFill, Renderer.uiCentered = false, true
Zoom.offset = 0
local r = Renderer:frameRects()
eq(r.cut, true, "the skin's cutout is folded into the frame")
eq(r.vux, 320, "cutout x")
eq(r.vuy, 72, "cutout y")
eq(r.vuw, 640, "cutout width")
eq(r.vuh, 432, "cutout height")
eq(r.Sp, 3, "the fit is measured against the cutout, not the window")
check(inside(r.uox, r.uoy, r.uvpw, r.uvph, r.vux, r.vuy, r.vuw, r.vuh),
  "the UI letterbox sits inside the cutout")
check(inside(r.ox, r.oy, r.vpw, r.vph, r.vux, r.vuy, r.vuw, r.vuh),
  "so does the world letterbox")

local lo, hi = Zoom.offsetRange(r.Sp)
for off = lo, hi do
  Zoom.offset = off
  local z = Renderer:frameRects()
  check(inside(z.uox, z.uoy, z.uvpw, z.uvph, z.vux, z.vuy, z.vuw, z.vuh),
    "zoom " .. Zoom.offsetLabel(off) .. " keeps the UI in the cutout")
  local vw, vh = Renderer:worldViewSize()
  local sp = Zoom.scale(z.Sp)
  check(vw * sp <= z.vuw + 2 * sp and vh * sp <= z.vuh + 2 * sp,
    "zoom " .. Zoom.offsetLabel(off) .. " keeps the world pass capped")
end
Zoom.offset = 0

useSkin("0.25,0.1,0.3,0.8")
local tallR = Renderer:frameRects()
local tallVw, tallVh = Renderer:worldViewSize()
local tallSp = Zoom.scale(tallR.Sp)
check(tallVw * tallSp >= tallR.vuw and tallVh * tallSp >= tallR.vuh,
  "the world pass covers the whole cutout, not just the GB box inside it")
check(tallVh * tallSp > tallR.uvph,
  "so a cutout taller than 160x144 shows map where the UI letterbox ends")
check(tallR.uvph < tallR.vuh and tallR.uvpw <= tallR.vuw,
  "while the UI keeps its whole-pixel letterbox inside that cutout")
useSkin("0.25,0.1,0.5,0.6")

setWindow(480, 800)
useSkin("0.3,0.1,0.4,0.3")
Renderer:init()
Renderer:setUISize(304, 144)
Renderer.uiFill, Renderer.uiCentered = false, true
local tight = Renderer:frameRects()
check(tight.vuw < 304, "the cutout cannot hold the WIDE battle at 1x")
check(inside(tight.uox, tight.uoy, tight.uvpw, tight.uvph,
  tight.vux, tight.vuy, tight.vuw, tight.vuh),
  "so the surface is scaled down to the cutout rather than over the bezel")
Renderer:setUISize(160, 144)

setWindow(1280, 720)
useSkin("0.25,0.1,0.5,0.6")
local px, py, pw, ph, active = Playfield.rect(1280, 720)
eq(active, true, "Gold sees the cutout too")
eq(px, 320, "the playfield is the cutout, at its origin")
eq(py, 72, "on both axes")
eq(pw, 640, "with its full width")
eq(ph, 432, "and its full height")
eq(Chrome.fitScale(1280, 720), 3, "Chrome fits whole GB pixels in it")
local cox, coy = Chrome.fitOrigin(1280, 720)
eq(cox, 320 + 80, "and centres the panel on the cutout")
eq(coy, 72, "on both axes")

local ew2, eh2, ex2, ey2, act2 = Playfield.push(1280, 720)
eq(act2, true, "push reports the frame is contained")
eq(ex2, px, "push translates to the playfield origin")
eq(ey2, py, "on both axes")
eq(ew2, pw, "and hands the scene the playfield size")
eq(Playfield.cutout(ew2, eh2), nil, "inside the frame there is no cutout left")
eq(select(3, Playfield.rect(ew2, eh2)), pw, "so the playfield is the surface")
eq(Chrome.fitScale(ew2, eh2), 3, "and Chrome fits it without re-applying")
eq(select(1, Chrome.fitOrigin(ew2, eh2)), 80, "centred on that surface alone")
eq(select(1, Playfield.dimensions()), pw, "screens read the playfield as the display")
Playfield.pop()
eq(Playfield.entered, false, "pop leaves the frame")
eq(select(1, Playfield.cutout(1280, 720)), 320, "and the cutout is visible again")

setWindow(960, 1901)
useSkin("0,0,1,0.5", "overlay0_aspect_ratio = 0.5625")
local deck = TouchSkin.page()
local _, dy, _, dh = TouchSkin.pageBox(deck, 960, 1901)
check(dy > 0 and math.abs(dy + dh - 1901) < 1,
  "a portrait deck taller than the display pins to the lower edge")
local hx, hy, hw, hh = Playfield.cutout(960, 1901)
eq(hy, 0, "and the screen rect it left flush takes the room above it")
eq(hx, 0, "without moving sideways")
eq(hw, 960, "or changing width")
eq(hh, math.floor(dy + dh * 0.5), "growing by exactly the headroom")

useSkin("0,0.1,1,0.5", "overlay0_aspect_ratio = 0.5625")
check(select(2, Playfield.cutout(960, 1901)) > dy,
  "a screen rect the author inset from the top keeps that bezel margin")

setWindow(1280, 720)
useSkin("0.4,0.4,0.1,0.1")
local sx, sy, sw, sh = Playfield.rect(1280, 720)
check(inside(sx, sy, sw, sh, 512, 288, 128, 72),
  "a cutout smaller than 160x144 still bounds the playfield")
check(sw <= 128 and sh <= 72, "the playfield never exceeds the cutout")

TouchSkin.setActive(nil)
eq(Playfield.cutout(1280, 720), nil, "no skin, no cutout")
eq(select(3, Playfield.rect(1280, 720)), 1280, "and the playfield is the window")
local saved = TouchSkin.viewport
TouchSkin.viewport = function() error("boom") end
eq(Playfield.cutout(1280, 720), nil, "a throwing viewport is no cutout")
TouchSkin.viewport = function() return 10, 10, 0, 0 end
eq(Playfield.cutout(1280, 720), nil, "a zero-sized cutout is no cutout")
TouchSkin.viewport = function() return -50, -50, 200, 200 end
eq(select(1, Playfield.cutout(1280, 720)), 0, "a cutout off the surface is clamped")
eq(select(3, Playfield.cutout(1280, 720)), 150, "to what is left of it")
TouchSkin.viewport = saved
TouchSkin.setActive(nil)
setWindow(640, 576)

T.finish("skin_viewport_containment")
