--[[
   hot_reload.lua  -  reload Lua scripts without restarting K-Meleon.

   The C side (luamacro) runs a ReadDirectoryChangesW thread on lua/ and
   posts WM_HOT_RELOAD to the frame when a .lua file changes. The frame's
   subclassed WndProc then calls _kmeleon_hot_reload(rel_path) for each
   changed file, on the UI thread, in luamacro's own Lua runtime.

   Per change we:
     * clear package.loaded[module_name]  (so the next `require` re-reads)
     * read the file ourselves (so we can strip a UTF-8 BOM that Lua 5.1's
       loadfile chokes on)
     * loadstring + run  (re-executes module body, redefining its globals)
     * log result to luamacro_errors.log

   Quirks handled:
     * Editors (and the watcher) often fire BEFORE the writer has released
       the file - retry on EACH "Permission denied" for up to ~250ms.
     * UTF-8 BOM (EF BB BF) is stripped: stock Lua 5.1 emits "unexpected
       symbol near 'i' " otherwise.

   Limitations:
     * Already-bound toolbar / menu / keyboard handlers keep the OLD
       function reference unless the new script re-registers them. Most
       CCF scripts attach via setmenu()/setcommand() at top level which
       a re-run re-runs, so they DO refresh.
     * State held in upvalues / locals of the OLD module instance is lost.
       Save state in a global if you need it to survive a reload.
     * Editing hot_reload.lua itself works - it just redefines this very
       function in place on next save.
]]

local function log(msg)
   local f = io.open((BROWSERPATH or "") .. "luamacro_errors.log", "a")
   if f then f:write("[hot_reload] " .. tostring(msg) .. "\n"); f:close() end
end

-- "autoload/foo.lua"  ->  "autoload.foo" (Lua dotted form)
local function modname_from_relpath(rel)
   return (rel:gsub("\\", "/"):gsub("%.lua$", ""):gsub("/", "."))
end

-- Read file with retries for editors that hold a brief write lock after save.
local function read_file_with_retry(path, tries)
   tries = tries or 6   -- 6 * 50ms ~= 300ms total
   for i = 1, tries do
      local f, oerr = io.open(path, "rb")
      if f then
         local data = f:read("*a")
         f:close()
         return data
      end
      -- Permission/access errors typically clear within a frame or two.
      -- Lua has no sleep; use a busy loop that yields via os.time().
      local t = os.clock()
      while os.clock() - t < 0.050 do end
   end
   return nil, "could not open after " .. tries .. " retries"
end

function _kmeleon_hot_reload(rel_path)
   if type(rel_path) ~= "string" or rel_path == "" then return end
   if not rel_path:lower():match("%.lua$") then return end

   local full = (BROWSERPATH or "") .. "lua\\" .. rel_path:gsub("/", "\\")
   local mod  = modname_from_relpath(rel_path)

   -- Drop the cached module so any later require() re-reads from disk.
   if package and package.loaded then package.loaded[mod] = nil end

   -- Read source ourselves so we can strip a UTF-8 BOM (Lua 5.1's loadfile
   -- doesn't) and retry past the editor's brief post-save write lock.
   local src, rerr = read_file_with_retry(full)
   if not src then
      log("read FAILED  " .. rel_path .. " : " .. tostring(rerr))
      return
   end
   -- Strip UTF-8 BOM (EF BB BF)
   if src:sub(1, 3) == "\239\187\191" then src = src:sub(4) end

   local chunk, lerr = loadstring(src, "@" .. full)
   if not chunk then
      log("loadstring FAILED  " .. rel_path .. " : " .. tostring(lerr))
      return
   end
   local ok, err = pcall(chunk)
   if ok then
      log("reloaded  " .. rel_path)
   else
      log("runtime ERROR in " .. rel_path .. " : " .. tostring(err))
   end
end

log("hot_reload installed")
