-- Draws a 4-shade GB image through a GBC palette.
--
-- Every 2bpp sheet the importer writes is grayscale: shade 0 is white
-- (or transparent), shade 3 is black (ImageWriter.SHADES).  A GBC game picks
-- four real colors per tile instead, so this recovers the shade index from the
-- red channel and substitutes the palette entry -- one shader for map tiles,
-- OW sprites, battle pics and menu chrome alike.
--
-- Shade recovery is exact rather than approximate: the source values are
-- 1, 2/3, 1/3, 0, so rounding (1 - r) * 3 lands on 0..3 with a half-step of
-- headroom either side, which survives texture filtering set to nearest.
--
-- Alpha passes through untouched, so a sheet written with transparent shade 0
-- (OW sprites, the font) keeps its cutout.

-- COLOR (the port's own display option, Gold's answer to the Gen 1 COLORS
-- row).  Every colour in the Gen 2 port arrives here, because a CGB game's
-- colour IS its palettes -- so one substitution at this seam recolours the
-- whole game without a single screen knowing about it:
--
--   DMG      the original grey Game Boy.  Every palette collapses to the
--            four hardware shades, and the sheets that are drawn straight
--            (chrome, text) are already those shades, so the screen is
--            uniformly monochrome.
--   CLASSIC  the DMG pea-soup green.  Rendered as DMG and then run through
--            the green ramp as a final full-screen pass, which is what
--            GbcPalette.presentColors is for -- a shade map is exactly what
--            this shader already does, so it needs no second shader.

local GbcPalette = {}

GbcPalette.MODES = { "gbc", "dmg", "classic" }
GbcPalette.MODE_LABELS = { gbc = "GEN 2", dmg = "DMG", classic = "CLASSIC",
                            custom = "GBC" }
GbcPalette.mode = "gbc"

GbcPalette.CUSTOM_MODE = "custom"

-- rBGP's four shades as the hardware shows them.
local DMG_SHADES = {
  { 255, 255, 255 }, { 170, 170, 170 }, { 85, 85, 85 }, { 0, 0, 0 },
}
-- #9BBC0F / #8BAC0F / #306230 / #0F380F, the same ramp Gen 1's CLASSIC uses.
local CLASSIC_SHADES = {
  { 155, 188, 15 }, { 139, 172, 15 }, { 48, 98, 48 }, { 15, 56, 15 },
}

local SHADER_SOURCE = [[
extern vec3 pal0;
extern vec3 pal1;
extern vec3 pal2;
extern vec3 pal3;

vec4 effect(vec4 tint, Image tex, vec2 uv, vec2 screen) {
  vec4 px = Texel(tex, uv);
  float shade = floor((1.0 - px.r) * 3.0 + 0.5);
  vec3 rgb = pal0;
  if (shade > 2.5) {
    rgb = pal3;
  } else if (shade > 1.5) {
    rgb = pal2;
  } else if (shade > 0.5) {
    rgb = pal1;
  }
  return vec4(rgb, px.a) * tint;
}
]]

-- rBGP, the DMG background palette register, as a remap of an ALREADY DRAWN
-- texture.
--
-- A frame this port has finished drawing holds CGB colours and no shade index
-- any more, so the remap has to run backwards: match the pixel to the palette
-- entry that produced it, then substitute whatever the rBGP byte sends that
-- entry to.  With one palette that is exact, because the four entries are the
-- only colours the texture can hold; a composited frame is only as exact as
-- GbcPalette.remapTable's dedupe (see the `ambiguous` count there).
--
-- `remapTol` is a SQUARED distance and it is what keeps this pass off pixels
-- that were never a palette colour -- a letterboxed border, a linear-filtered
-- resample -- rather than snapping them to the nearest entry.  Palette colours
-- land on exact 1/255 steps through a nearest-filtered canvas, so the default
-- is a couple of steps of headroom and nothing like the gap between two
-- entries of the same palette.
local REMAP_SOURCE = [[
extern int remapCount;
extern float remapTol;
extern vec3 remapSrc[32];
extern vec3 remapDst[32];

vec4 effect(vec4 tint, Image tex, vec2 uv, vec2 screen) {
  vec4 px = Texel(tex, uv);
  vec3 mapped = px.rgb;
  float best = remapTol;
  for (int i = 0; i < 32; i++) {
    if (i >= remapCount) { break; }
    vec3 d = px.rgb - remapSrc[i];
    float dist = dot(d, d);
    if (dist < best) {
      best = dist;
      // Indexed by the LOOP variable and never by a value carried out of the
      // loop: GLSL ES 1.00 only allows a constant-index-expression into a
      // uniform array, which a loop counter is and `best`'s winner is not.
      mapped = remapDst[i];
    }
  }
  return vec4(mapped, px.a) * tint;
}
]]

-- The compiled-in array length above.  A map's eight BG palettes are 32
-- colours before dedupe, which is the worst case this has to hold.
GbcPalette.REMAP_MAX = 32
-- Squared RGB distance, in 0..1 units: three 8-bit steps.
GbcPalette.REMAP_TOLERANCE = (3 / 255) ^ 2

local shader = nil
local failed = false
local remapShader = nil
local remapFailed = false

-- nil (and a one-shot warning) if shaders are unavailable, so callers can fall
-- back to the plain grayscale draw instead of crashing a whole boot.
function GbcPalette.shader()
  if shader or failed then return shader end
  if not (love and love.graphics and love.graphics.newShader) then
    failed = true
    return nil
  end
  local ok, result = pcall(love.graphics.newShader, SHADER_SOURCE)
  if not ok then
    failed = true
    return nil
  end
  shader = result
  return shader
end

-- The same contract as GbcPalette.shader for the backwards pass: nil rather
-- than an error, so a caller can fall back to its own approximation.
function GbcPalette.remapShader()
  if remapShader or remapFailed then return remapShader end
  if not (love and love.graphics and love.graphics.newShader) then
    remapFailed = true
    return nil
  end
  local ok, result = pcall(love.graphics.newShader, REMAP_SOURCE)
  if not ok then
    remapFailed = true
    return nil
  end
  remapShader = result
  return remapShader
end

function GbcPalette.available()
  return GbcPalette.shader() ~= nil
end

local function channel(colors, index)
  local c = colors and colors[index]
  if not c then return 0, 0, 0 end
  return (c[1] or 0) / 255, (c[2] or 0) / 255, (c[3] or 0) / 255
end

GbcPalette.customRamp = nil

function GbcPalette.setCustomRamp(ramp)
  local prev = GbcPalette.customRamp
  GbcPalette.customRamp = ramp
  if (prev ~= nil) ~= (ramp ~= nil) or prev ~= ramp then
    pcall(function() require("src.render.SpriteRenderer").invalidate() end)
  end
end

-- What a palette actually draws as under the current COLOR mode.  Anything
-- that reads a colour out of a palette directly -- a canvas cleared to BG
-- colour 0, say -- has to go through this too, or the backdrop would keep its
-- cart colour while everything on top of it went grey.
function GbcPalette.resolve(colors)
  if GbcPalette.customRamp then return GbcPalette.customRamp end
  if GbcPalette.mode == "gbc" then return colors end
  return DMG_SHADES
end

--------------------------------------------------------------------------
-- rBGP as data
--------------------------------------------------------------------------

-- %11100100.  The `dc` macro emits colour 3 FIRST, so `dc 3, 2, 1, 0` packs to
-- $e4 and reads back as "colour i shows shade i": the identity.
GbcPalette.BGP_IDENTITY = 0xe4

-- The byte's four 2-bit fields, colour 0 in the low bits, returned 1-based so
-- shades[i + 1] is the shade colour i shows.
function GbcPalette.bgpShades(byte)
  byte = byte or GbcPalette.BGP_IDENTITY
  local shades = {}
  for index = 0, 3 do
    shades[index + 1] = math.floor(byte / (4 ^ index)) % 4
  end
  return shades
end

-- CopyPals (home/palettes.asm), which is the whole of what DmgToCgbBGPals does
-- on a CGB: a palette's four entries are REORDERED by the rBGP byte, so a pixel
-- drawn as colour i comes back as colour bgp(i) OF ITS OWN PALETTE.
--
-- This is why a brightness veil can never be right and this table can: $f9
-- (`dc 3, 3, 2, 1`) means "one step darker along this palette's own ramp", and
-- no two palettes have the same ramp.  Returns `colors` itself for the identity
-- so the common case allocates nothing.
function GbcPalette.remap(colors, byte)
  if not colors then return nil end
  if not byte or byte == GbcPalette.BGP_IDENTITY then return colors end
  local shades = GbcPalette.bgpShades(byte)
  local out = {}
  for index = 1, 4 do
    out[index] = colors[shades[index] + 1] or colors[4]
  end
  return out
end

-- The rBGP byte every subsequent GbcPalette.use / .with / .color folds in, or
-- nil for the identity.
--
-- This is the FORWARD half of the register and the exact one: a screen that is
-- still being drawn can take the permutation on its palettes before they ever
-- reach the shader, which is bit for bit DmgToCgbBGPals.  Only a frame that is
-- already baked (World's map canvas) needs the backwards pass above.
--
-- GbcPalette.useRaw deliberately does NOT fold it in: the CLASSIC present pass
-- goes through useRaw and is not a BG palette, so a byte left standing there
-- would permute the green ramp itself.
GbcPalette.bgp = nil

-- Set the active byte, returning the previous one so a caller can restore it.
-- The identity is stored as nil, so `setBgp($e4)` is the same as clearing it.
function GbcPalette.setBgp(byte)
  local previous = GbcPalette.bgp
  if byte == GbcPalette.BGP_IDENTITY then byte = nil end
  GbcPalette.bgp = byte
  return previous
end

-- One colour out of a palette, mode and active rBGP byte applied.  `index` is
-- 1-based.
function GbcPalette.color(colors, index)
  local resolved = GbcPalette.remap(GbcPalette.resolve(colors), GbcPalette.bgp)
  if resolved and resolved[index] then return resolved[index] end
  return GbcPalette.remap(DMG_SHADES, GbcPalette.bgp)[index]
end

-- The palette the finished frame is presented through, or nil when the frame
-- is already the right colour.  Only CLASSIC needs one: the scene under it has
-- rendered in the four DMG shades, so mapping those to the green ramp is the
-- same shade substitution every other call here makes.
function GbcPalette.presentColors()
  if GbcPalette.mode ~= "classic" then return nil end
  return CLASSIC_SHADES
end

function GbcPalette.setMode(mode)
  if mode == GbcPalette.CUSTOM_MODE then
    GbcPalette.mode = mode
    return mode
  end
  for _, name in ipairs(GbcPalette.MODES) do
    if name == mode then
      GbcPalette.mode = mode
      return mode
    end
  end
  GbcPalette.mode = "gbc"
  return GbcPalette.mode
end

function GbcPalette.modeLabel(mode)
  return GbcPalette.MODE_LABELS[mode or GbcPalette.mode] or "GEN 2"
end

-- Advance GBC -> DMG -> CLASSIC -> GBC.  `delta` may be -1 to step back, so
-- the OPTION screen's left press walks the ladder the other way.
function GbcPalette.cycle(delta)
  local at = 1
  for index, name in ipairs(GbcPalette.MODES) do
    if name == GbcPalette.mode then at = index break end
  end
  local count = #GbcPalette.MODES
  at = (at - 1 + (delta or 1)) % count + 1
  GbcPalette.mode = GbcPalette.MODES[at]
  GbcPalette.setCustomRamp(nil)
  return GbcPalette.mode
end

function GbcPalette.applyOptions(opts)
  local mode = GbcPalette.setMode(opts and opts.color or "gbc")
  local id = opts and opts.palette
  if id and id ~= "" then
    local ok, Palette = pcall(require, "src.render.Palette")
    GbcPalette.setCustomRamp(ok and Palette.ramp(id) or nil)
  else
    GbcPalette.setCustomRamp(nil)
  end
  return mode
end

-- Point the shader at one 4-color palette. Colors are 0-255 triples, matching
-- palettes.lua.  Returns false when there is no shader to configure.
--
-- Mode first, then rBGP: on the hardware the register indexes whatever the
-- palette buffer holds, and in DMG/CLASSIC mode that buffer IS the four grey
-- shades, so remapping the resolved palette is what the DMG itself does.
function GbcPalette.use(colors)
  return GbcPalette.useRaw(
    GbcPalette.remap(GbcPalette.resolve(colors), GbcPalette.bgp))
end

-- The same, ignoring the COLOR mode.  The present pass needs it: it IS the
-- mode, so running its own palette back through resolve would flatten the
-- green ramp to grey and the mode would do nothing.
function GbcPalette.useRaw(colors)
  local sh = GbcPalette.shader()
  if not sh then return false end
  for i = 0, 3 do
    local r, g, b = channel(colors, i + 1)
    sh:send("pal" .. i, { r, g, b })
  end
  love.graphics.setShader(sh)
  return true
end

function GbcPalette.clear()
  if love and love.graphics then love.graphics.setShader() end
end

--------------------------------------------------------------------------
-- The backwards pass: remapping a frame that is already drawn
--------------------------------------------------------------------------

local function colorKey(c)
  return math.floor(c[1] or 0) .. "," .. math.floor(c[2] or 0)
    .. "," .. math.floor(c[3] or 0)
end

-- Source and destination colour lists for REMAP_SOURCE, deduplicated.
--
-- `bgPalettes` is a LIST OF PALETTES the rBGP byte reaches: DmgToCgbBGPals
-- pushes one byte through all eight BG palettes at once, so they all take the
-- same permutation.  `objPalettes` is the list it does NOT reach -- OBJ colours
-- go through the separate DmgToCgbObjPals, which the flash never calls -- and
-- they are here mapping to THEMSELVES, so a sprite's colours are recognised and
-- left alone instead of being matched onto a BG entry and swept along.
--
-- Returns src, dst (0..1 triples, both padded to REMAP_MAX) and two counts:
-- `count`, the live length, and `ambiguous`.
--
-- `ambiguous` is the exact limit of this whole pass, and it is worth naming: a
-- colour that is entry 1 of one palette and entry 2 of another has two right
-- answers, and a composited frame threw away which one this pixel was.  The
-- first writer wins, which is why BG palettes are walked first and in slot
-- order -- the reading that matches "the whole picture flashes".  Nothing here
-- is approximate when `ambiguous` is 0.
--
-- A map's eight BG palettes are 32 entries, which is REMAP_MAX exactly, so a
-- map whose palettes share nothing at all fills the array and the OBJ list is
-- dropped.  BG is walked first for that reason too: losing the sprite guard
-- costs a few sprite pixels, losing a BG palette would cost the effect.
function GbcPalette.remapTable(bgPalettes, byte, objPalettes)
  local src, dst = {}, {}
  local seen = {}
  local ambiguous = 0

  local function add(colors, mapped)
    if not (colors and mapped) then return end
    for index = 1, 4 do
      local from, to = colors[index], mapped[index]
      if from and to and #src < GbcPalette.REMAP_MAX then
        local key = colorKey(from)
        local at = seen[key]
        if at then
          -- Same colour, different destination: the pixel cannot say which
          -- palette drew it, so the first answer stands and this is counted.
          if colorKey(dst[at]) ~= colorKey(to) then ambiguous = ambiguous + 1 end
        else
          src[#src + 1] = { from[1], from[2], from[3] }
          dst[#dst + 1] = { to[1], to[2], to[3] }
          seen[key] = #src
        end
      end
    end
  end

  for _, colors in ipairs(bgPalettes or {}) do
    local resolved = GbcPalette.resolve(colors)
    add(resolved, GbcPalette.remap(resolved, byte))
  end
  for _, colors in ipairs(objPalettes or {}) do
    local resolved = GbcPalette.resolve(colors)
    add(resolved, resolved)
  end

  local count = #src
  -- Shader:send fills the whole declared array, so the tail is padded with a
  -- copy of the first entry; `count` keeps the loop off it either way.
  for index = count + 1, GbcPalette.REMAP_MAX do
    src[index] = src[1] and { src[1][1], src[1][2], src[1][3] } or { 0, 0, 0 }
    dst[index] = dst[1] and { dst[1][1], dst[1][2], dst[1][3] } or { 0, 0, 0 }
  end
  return src, dst, count, ambiguous
end

-- Bind the remap shader for a draw of an already-rendered texture.  Returns
-- false when there is no shader or no palette, so a caller can fall back to
-- whatever approximation it had before; on success it also returns the
-- `ambiguous` count, which is 0 when the pass is exact.
function GbcPalette.useRemap(bgPalettes, byte, objPalettes)
  local sh = GbcPalette.remapShader()
  if not sh then return false end
  local src, dst, count, ambiguous =
    GbcPalette.remapTable(bgPalettes, byte, objPalettes)
  if count == 0 then return false end
  local sendSrc, sendDst = {}, {}
  for index = 1, GbcPalette.REMAP_MAX do
    sendSrc[index] = { src[index][1] / 255, src[index][2] / 255,
      src[index][3] / 255 }
    sendDst[index] = { dst[index][1] / 255, dst[index][2] / 255,
      dst[index][3] / 255 }
  end
  -- pcall rather than an assert: a driver that will not take a 32-entry vec3
  -- array should drop the effect, not take the battle down with it.
  local ok = pcall(function()
    sh:send("remapCount", count)
    sh:send("remapTol", GbcPalette.REMAP_TOLERANCE)
    sh:send("remapSrc", unpack(sendSrc))
    sh:send("remapDst", unpack(sendDst))
  end)
  if not ok then
    remapFailed = true
    remapShader = nil
    return false
  end
  love.graphics.setShader(sh)
  return true, ambiguous
end

-- Run `body` with `colors` active, restoring whatever shader was set before.
-- Nested use is safe: the previous shader is captured, not assumed to be nil.
function GbcPalette.with(colors, body)
  local previous = love and love.graphics and love.graphics.getShader
    and love.graphics.getShader() or nil
  local applied = GbcPalette.use(colors)
  local ok, err = pcall(body)
  if love and love.graphics then love.graphics.setShader(previous) end
  if not ok then error(err, 0) end
  return applied
end

function GbcPalette.withRaw(colors, body)
  local previous = love and love.graphics and love.graphics.getShader
    and love.graphics.getShader() or nil
  local applied = GbcPalette.useRaw(colors)
  local ok, err = pcall(body)
  if love and love.graphics then love.graphics.setShader(previous) end
  if not ok then error(err, 0) end
  return applied
end

return GbcPalette
