local ImageWriter = {}

local SHADES = {
  { 1, 1, 1, 1 },
  { 2 / 3, 2 / 3, 2 / 3, 1 },
  { 1 / 3, 1 / 3, 1 / 3, 1 },
  { 0, 0, 0, 1 },
}

ImageWriter.SHADES = SHADES

-- Title screen rOBP0 = %11100000 ($E0): OBJ shades 1 and 2 draw as white,
-- shade 3 as black (pokeyellow engine/movie/title.asm after PlacePikachu).
-- Eye OAM is baked into the MEWMON-colored BG PNG; without this remap the
-- shade-1 glints become body yellow under the title palette.
function ImageWriter.applyTitleObp0(image)
  local mid, dark = SHADES[2][1], SHADES[3][1]
  local w, h = image:getWidth(), image:getHeight()
  for y = 0, h - 1 do
    for x = 0, w - 1 do
      local r, g, b, a = image:getPixel(x, y)
      if a ~= 0 then
        if math.abs(r - mid) < 0.02 or math.abs(r - dark) < 0.02 then
          image:setPixel(x, y, 1, 1, 1, 1)
        end
      end
    end
  end
  return image
end

local function assertDimensions(raw, width, height, bits)
  assert(width % 8 == 0 and height % 8 == 0,
    ("%dbpp dimensions must be tile-aligned: %dx%d")
      :format(bits, width, height))
  local expected = width * height * bits / 8
  assert(#raw == expected,
    ("%dbpp payload is %d bytes, expected %d")
      :format(bits, #raw, expected))
end

function ImageWriter.decode2bpp(raw, width, height, transparent)
  assertDimensions(raw, width, height, 2)
  local image = love.image.newImageData(width, height)
  local tilesPerRow = width / 8
  for tile = 0, #raw / 16 - 1 do
    local tileX = tile % tilesPerRow * 8
    local tileY = math.floor(tile / tilesPerRow) * 8
    for y = 0, 7 do
      local low = raw[tile * 16 + y * 2 + 1]
      local high = raw[tile * 16 + y * 2 + 2]
      for x = 0, 7 do
        local divisor = 2 ^ (7 - x)
        local shade = math.floor(high / divisor) % 2 * 2
          + math.floor(low / divisor) % 2
        local color = SHADES[shade + 1]
        local alpha = color[4]
        if transparent and shade == 0 then alpha = 0 end
        image:setPixel(tileX + x, tileY + y,
          color[1], color[2], color[3], alpha)
      end
    end
  end
  return image
end

function ImageWriter.decode1bpp(raw, width, height, transparent)
  assertDimensions(raw, width, height, 1)
  local image = love.image.newImageData(width, height)
  local tilesPerRow = width / 8
  for tile = 0, #raw / 8 - 1 do
    local tileX = tile % tilesPerRow * 8
    local tileY = math.floor(tile / tilesPerRow) * 8
    for y = 0, 7 do
      local row = raw[tile * 8 + y + 1]
      for x = 0, 7 do
        local filled = math.floor(row / 2 ^ (7 - x)) % 2 ~= 0
        local value = filled and 0 or 1
        local alpha = 1
        if transparent and not filled then alpha = 0 end
        image:setPixel(tileX + x, tileY + y,
          value, value, value, alpha)
      end
    end
  end
  return image
end

function ImageWriter.blank(width, height, r, g, b, a)
  local image = love.image.newImageData(width, height)
  image:mapPixel(function() return r or 0, g or 0, b or 0, a or 0 end)
  return image
end

function ImageWriter.blit(target, source, targetX, targetY,
    sourceX, sourceY, width, height, flipX)
  sourceX, sourceY = sourceX or 0, sourceY or 0
  width, height = width or source:getWidth(), height or source:getHeight()
  for y = 0, height - 1 do
    for x = 0, width - 1 do
      local sampleX = flipX and sourceX + width - 1 - x or sourceX + x
      target:setPixel(targetX + x, targetY + y,
        source:getPixel(sampleX, sourceY + y))
    end
  end
end

function ImageWriter.matteColor0(image)
  local width, height = image:getDimensions()
  local queueX, queueY, head = {}, {}, 1
  local seen = {}
  local function add(x, y)
    local key = y * width + x
    if seen[key] then return end
    local r, g, b, a = image:getPixel(x, y)
    if r == 1 and g == 1 and b == 1 and a == 1 then
      seen[key] = true
      queueX[#queueX + 1], queueY[#queueY + 1] = x, y
    end
  end
  for x = 0, width - 1 do add(x, 0); add(x, height - 1) end
  for y = 0, height - 1 do add(0, y); add(width - 1, y) end
  while head <= #queueX do
    local x, y = queueX[head], queueY[head]
    head = head + 1
    image:setPixel(x, y, 1, 1, 1, 0)
    if x > 0 then add(x - 1, y) end
    if x + 1 < width then add(x + 1, y) end
    if y > 0 then add(x, y - 1) end
    if y + 1 < height then add(x, y + 1) end
  end
  return image
end

function ImageWriter.columnsToRows(raw, tilesWide, tilesHigh, bytesPerTile)
  bytesPerTile = bytesPerTile or 16
  local out = {}
  for y = 0, tilesHigh - 1 do
    for x = 0, tilesWide - 1 do
      local source = (x * tilesHigh + y) * bytesPerTile
      local target = (y * tilesWide + x) * bytesPerTile
      for offset = 1, bytesPerTile do
        out[target + offset] = raw[source + offset]
      end
    end
  end
  return out
end

-- Inverse of pret tools/gfx --interleave (pokecrystal tools/gfx.c).
-- Build-time interleave stores each vertical 8x16 pair as consecutive 8x8
-- tiles for OBJ mode; this restores row-major sheet order for PNGs.
function ImageWriter.deinterleave(raw, width, bytesPerTile)
  bytesPerTile = bytesPerTile or 16
  local widthTiles = width / 8
  local numTiles = #raw / bytesPerTile
  local out = {}
  for i = 0, numTiles - 1 do
    local row = math.floor(i / widthTiles)
    local src = i * 2 - (row % 2 == 1
      and widthTiles * (row + 1) - 1
      or widthTiles * row)
    for offset = 1, bytesPerTile do
      out[i * bytesPerTile + offset] = raw[src * bytesPerTile + offset]
    end
  end
  return out
end

function ImageWriter.save(image, path)
  local ok, fileData = pcall(image.encode, image, "png")
  if not ok then error("could not encode " .. path .. ": " .. tostring(fileData)) end
  -- CacheFs routes this to the OS save directory (normal builds) or straight
  -- into the game folder (portable installs), creating parent directories as
  -- needed.  io.* needs the bytes as a string; love.filesystem would also
  -- take the FileData, but getString() keeps one code path.
  local CacheFs = require("src.import.CacheFs")
  local written, writeError = CacheFs.write(path, fileData:getString())
  if not written then
    error("could not write " .. path .. ": " .. tostring(writeError))
  end
  return fileData
end

return ImageWriter
