--- love_file_handler.lua — LÖVE2D file handler for logger.lua
--
-- Uses love.filesystem to write logs into LÖVE's save directory, which works
-- correctly on every platform LÖVE supports (Windows, macOS, Linux, Android).
--
-- Requires:
--   logger.lua     in the same directory (or on package.path)
--   LÖVE2D 11.x+   (love.filesystem must be available)
--
-- Usage inside main.lua / conf.lua:
--
--   local Logger      = require("logger")
--   local LoveHandler = require("love_file_handler")
--
--   local log = Logger.new("game", {
--     handlers = {
--       Logger.handlers.ConsoleHandler(),          -- print() fallback
--       LoveHandler.new("logs/game.log"),          -- love.filesystem
--     },
--   })
--
--   function love.load()
--     log:info("Game started — LÖVE %s", love.getVersion())
--   end

-- ── Sanity check ──────────────────────────────────────────────────────────

assert(love and love.filesystem,
  "love_file_handler requires the LÖVE2D runtime (love.filesystem)")

-- ── Module table ──────────────────────────────────────────────────────────

local LoveHandler = {}

-- ── Helpers ───────────────────────────────────────────────────────────────

--- Ensure every directory component in `path` exists.
-- love.filesystem.createDirectory is recursive on LÖVE 11+.
local function ensure_dir(path)
  local dir = path:match("^(.*)/[^/]+$")
  if dir and dir ~= "" then
    love.filesystem.createDirectory(dir)
  end
end

--- Return current file size in bytes, or 0 on error / missing file.
local function file_size(path)
  local info = love.filesystem.getInfo(path)
  return info and info.size or 0
end

--- Shift rotated files: .4→.5, .3→.4, …, base→.1
local function rotate(path, max_files)
  for i = max_files - 1, 1, -1 do
    local src  = path .. "." .. i
    local dest = path .. "." .. (i + 1)
    if love.filesystem.getInfo(src) then
      -- love.filesystem has no rename; copy + delete
      local data = love.filesystem.read(src)
      if data then
        love.filesystem.write(dest, data)
        love.filesystem.remove(src)
      end
    end
  end
  -- move base → .1
  if love.filesystem.getInfo(path) then
    local data = love.filesystem.read(path)
    if data then
      love.filesystem.write(path .. ".1", data)
      love.filesystem.remove(path)
    end
  end
end

-- ── Constructor ───────────────────────────────────────────────────────────

--- Create a new LÖVE2D file handler.
--
-- @param path      string   Path inside the LÖVE save directory.
--                           Subdirectories are created automatically.
--                           Default: "logs/game.log"
--
-- @param opts      table    Optional settings:
--   max_size   (number)  Rotate when the log file reaches this many bytes.
--                        Default: 1 MB (1 048 576). Set to 0 to disable.
--   max_files  (number)  How many rotated files to keep. Default: 5.
--   buffered   (bool)    Accumulate lines in memory and flush in batches.
--                        Reduces I/O overhead on mobile / web exports.
--                        Default: false.
--   flush_size (number)  Lines to buffer before auto-flushing (buffered mode).
--                        Default: 20.
--   on_error   (func)    Called with (err_string) on any I/O error.
--                        Default: prints a warning to love.graphics console.
--
-- @return  handler, control_table
--   handler        — function accepted by Logger:add_handler()
--   control        — table with :flush(), :close(), :path(), :size() methods
function LoveHandler.new(path, opts)
  path = path or "logs/game.log"
  opts = opts or {}

  local max_size   = (opts.max_size  == nil) and (1024 * 1024) or opts.max_size
  local max_files  = opts.max_files  or 5
  local buffered   = opts.buffered   or false
  local flush_size = opts.flush_size or 20
  local on_error   = opts.on_error   or function(err)
    if love.graphics and love.graphics.print then
      print("[love_file_handler] I/O error: " .. tostring(err))
    end
  end

  -- State
  local buffer   = {}   -- pending lines (buffered mode)
  local closed   = false

  ensure_dir(path)

  -- ── Internal write ────────────────────────────────────────────────────

  local function raw_write(lines)
    if #lines == 0 then return end
    local text = table.concat(lines, "\n") .. "\n"

    -- Rotate if needed
    if max_size and max_size > 0 and file_size(path) + #text >= max_size then
      rotate(path, max_files)
    end

    -- Append
    local ok, err = love.filesystem.append(path, text)
    if not ok then
      on_error(tostring(err))
    end
  end

  -- ── Public control table ──────────────────────────────────────────────

  local control = {}

  --- Flush buffered lines to disk immediately.
  function control:flush()
    if #buffer > 0 then
      raw_write(buffer)
      buffer = {}
    end
  end

  --- Close the handler (flushes first). After close() the handler is a no-op.
  function control:close()
    self:flush()
    closed = true
  end

  --- Full path as seen by love.filesystem.
  function control:path()
    return path
  end

  --- Real filesystem path to the save directory file (for external tools).
  function control:real_path()
    local save_dir = love.filesystem.getSaveDirectory()
    return save_dir .. "/" .. path
  end

  --- Current log file size in bytes.
  function control:size()
    return file_size(path)
  end

  --- List rotated files that exist (e.g. {"logs/game.log.1", ...})
  function control:rotated_files()
    local result = {}
    for i = 1, max_files do
      local p = path .. "." .. i
      if love.filesystem.getInfo(p) then
        table.insert(result, p)
      end
    end
    return result
  end

  --- Read the entire current log file as a string (nil on error).
  function control:read()
    self:flush()
    return love.filesystem.read(path)
  end

  -- ── Handler function ──────────────────────────────────────────────────

  local function handler(record)
    if closed then return end

    if buffered then
      table.insert(buffer, record.formatted)
      if #buffer >= flush_size then
        raw_write(buffer)
        buffer = {}
      end
    else
      raw_write({ record.formatted })
    end
  end

  return handler, control
end

-- ── Convenience: hook love.quit to flush ──────────────────────────────────

--- Wrap love.quit so a control table's flush() is called automatically.
-- Call once per handler after Logger.new(); safe to call multiple times.
--
-- @param control   The control table returned by LoveHandler.new()
function LoveHandler.auto_flush_on_quit(control)
  local original_quit = love.quit or function() end
  love.quit = function()
    control:flush()
    return original_quit()
  end
end

return LoveHandler
