--
-- lurker
--
-- Copyright (c) 2018 rxi
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.
--

-- Assumes lume is in the same directory as this file if it does not exist
-- as a global
local lume = rawget(_G, "lume") or require((...):gsub("[^/.\\]+$", "lume"))

local lurker = { _version = "1.0.1" }


local dir = love.filesystem.enumerate or love.filesystem.getDirectoryItems
local time = love.timer.getTime or os.time

local function isdir(path)
    local info = love.filesystem.getInfo(path)
    return info.type == "directory"
end

local function lastmodified(path)
    local info = love.filesystem.getInfo(path, "file")
    return info.modtime
end

local lovecallbacknames = {
  "update",
  "load",
  "draw",
  "mousepressed",
  "mousereleased",
  "keypressed",
  "keyreleased",
  "focus",
  "quit",
}


function lurker.init()
  lurker.print("Initing lurker")
  lurker.path = "."
  lurker.preswap = function() end
  lurker.postswap = function() end
  lurker.interval = .5
  lurker.protected = true
  lurker.quiet = false
  lurker.lastscan = 0
  lurker.lasterrorfile = nil
  lurker.files = {}
  lurker.funcwrappers = {}
  lurker.lovefuncs = {}
  lurker.state = "init"
  lume.each(lurker.getchanged(), lurker.resetfile)
  return lurker
end


function lurker.print(...)
  print("[lurker] " .. lume.format(...))
end


function lurker.listdir(path, recursive, skipdotfiles)
  path = (path == ".") and "" or path
  local function fullpath(x) return path .. "/" .. x end
  local t = {}
  for _, f in pairs(lume.map(dir(path), fullpath)) do
    if not skipdotfiles or not f:match("/%.[^/]*$") then
      if recursive and isdir(f) then
        t = lume.concat(t, lurker.listdir(f, true, true))
      else
        table.insert(t, lume.trim(f, "/"))
      end
    end
  end
  return t
end


function lurker.initwrappers()
  for _, v in pairs(lovecallbacknames) do
    lurker.funcwrappers[v] = function(...)
      local args = {...}
      xpcall(function()
        return lurker.lovefuncs[v] and lurker.lovefuncs[v](unpack(args))
      end, lurker.onerror)
    end
    lurker.lovefuncs[v] = love[v]
  end
  lurker.updatewrappers()
end


function lurker.updatewrappers()
  for _, v in pairs(lovecallbacknames) do
    if love[v] ~= lurker.funcwrappers[v] then
      lurker.lovefuncs[v] = love[v]
      love[v] = lurker.funcwrappers[v]
    end
  end
end


function lurker.onerror(e, nostacktrace)
  lurker.print("An error occurred; switching to error state")
  lurker.state = "error"

  -- Release mouse
  local setgrab = love.mouse.setGrab or love.mouse.setGrabbed
  setgrab(false)

  -- Set up callbacks
  for _, v in pairs(lovecallbacknames) do
    love[v] = function() end
  end

  love.update = lurker.update

  love.keypressed = function(k)
    if k == "escape" then
      lurker.print("Exiting...")
      love.event.quit()
    end
  end

  local stacktrace = nostacktrace and "" or
                     lume.trim((debug.traceback("", 2):gsub("\t", "")))
  local msg = lume.format("{1}\n\n{2}", {e, stacktrace})
  local colors = {
    { lume.color("#1e1e2c", 256) },
    { lume.color("#f0a3a3", 256) },
    { lume.color("#92b5b0", 256) },
    { lume.color("#66666a", 256) },
    { lume.color("#cdcdcd", 256) },
  }
  love.graphics.reset()
  love.graphics.setFont(love.graphics.newFont(12))

  love.draw = function()
    local pad = 25
    local width = love.graphics.getWidth()

    local function drawhr(pos, color1, color2)
      local animpos = lume.smooth(pad, width - pad - 8, lume.pingpong(time()))
      if color1 then love.graphics.setColor(color1) end
      love.graphics.rectangle("fill", pad, pos, width - pad*2, 1)
      if color2 then love.graphics.setColor(color2) end
      love.graphics.rectangle("fill", animpos, pos, 8, 1)
    end

    local function drawtext(str, x, y, color, limit)
      love.graphics.setColor(color)
      love.graphics[limit and "printf" or "print"](str, x, y, limit)
    end

    love.graphics.setBackgroundColor(colors[1])
    love.graphics.clear()

    drawtext("An error has occurred", pad, pad, colors[2])
    drawtext("lurker", width - love.graphics.getFont():getWidth("lurker") -
             pad, pad, colors[4])
    drawhr(pad + 32, colors[4], colors[5])
    drawtext("If you fix the problem and update the file the program will " ..
             "resume", pad, pad + 46, colors[3])
    drawhr(pad + 72, colors[4], colors[5])
    drawtext(msg, pad, pad + 90, colors[5], width - pad * 2)

    love.graphics.reset()
  end
end


function lurker.exitinitstate()
  lurker.state = "normal"
  if lurker.protected then
    lurker.initwrappers()
  end
end


function lurker.exiterrorstate()
  lurker.state = "normal"
  for _, v in pairs(lovecallbacknames) do
    love[v] = lurker.funcwrappers[v]
  end
end


function lurker.update()
  if lurker.state == "init" then
    lurker.exitinitstate()
  end
  local diff = time() - lurker.lastscan
  if diff > lurker.interval then
    lurker.lastscan = lurker.lastscan + diff
    local changed = lurker.scan()
    if #changed > 0 and lurker.lasterrorfile then
      local f = lurker.lasterrorfile
      lurker.lasterrorfile = nil
      lurker.hotswapfile(f)
    end
  end
end


function lurker.getchanged()
  local function fn(f)
    return f:match("%.lua$") and lurker.files[f] ~= lastmodified(f)
  end
  return lume.filter(lurker.listdir(lurker.path, true, true), fn)
end


function lurker.modname(f)
  return (f:gsub("%.lua$", ""):gsub("[/\\]", "."))
end


function lurker.resetfile(f)
  lurker.files[f] = lastmodified(f)
end


function lurker.hotswapfile(f)
  lurker.print("Hotswapping '{1}'...", {f})
  if lurker.state == "error" then
    lurker.exiterrorstate()
  end
  if lurker.preswap(f) then
    lurker.print("Hotswap of '{1}' aborted by preswap", {f})
    lurker.resetfile(f)
    return
  end
  local modname = lurker.modname(f)
  local t, ok, err = lume.time(lume.hotswap, modname)
  if ok then
    lurker.print("Swapped '{1}' in {2} secs", {f, t})
  else
    lurker.print("Failed to swap '{1}' : {2}", {f, err})
    if not lurker.quiet and lurker.protected then
      lurker.lasterrorfile = f
      lurker.onerror(err, true)
      lurker.resetfile(f)
      return
    end
  end
  lurker.resetfile(f)
  lurker.postswap(f)
  if lurker.protected then
    lurker.updatewrappers()
  end
end


function lurker.scan()
  if lurker.state == "init" then
    lurker.exitinitstate()
  end
  local changed = lurker.getchanged()
  lume.each(changed, lurker.hotswapfile)
  return changed
end


return lurker.init()