--[[
Automatically generated boot menu of the installed Linux kernels

Example:

m = require "automenu"
m.run { dir = "/",
        default = 1,
        timeout = 5,
        append = "root=/dev/hda2 ro",
}

TODO:
- add hooks
- demo adding break options from user config
- kernel flavor preference (pae/rt)
]]

local lfs = require "lfs"
local sl = require "syslinux"

local single = false
local verbosity = 2

local function modifiers ()
   return (single and " single" or "") .. ({" quiet",""," debug"})[verbosity]
end

local function boot (kernel_path, initrd_path, cmdline)
   print ("Loading " .. kernel_path .. " ...")
   local kernel = sl.loadfile (kernel_path)
   local initrd
   if (initrd_path) then
      print ("Loading " .. initrd_path .. " ...")
      initrd = sl.initramfs():load (initrd_path)
   end
   sl.boot_it (kernel, initrd, cmdline)
end

local function scan (params)
   local sep = string.sub (params.dir, -1) == "/" and "" or "/"
   if not params.items then params.items = {} end
   for name in lfs.dir (params.dir) do
      local path = params.dir .. sep .. name
      if lfs.attributes (path, "mode") == "file" then
         local from,to,version = string.find (name, "^vmlinuz%-(.*)")
         if from then
            local initrd = params.dir .. sep .. "initrd.img-" .. version
            if not lfs.attributes (initrd, "size") then
               initrd = nil
            end
            table.insert (params.items, {
                             show = name .. (initrd and " +initrd" or ""),
                             version = version,
                             execute = function ()
                                          boot (path, initrd,
                                                params.append .. modifiers ())
                                       end
                          })
         end
      end
   end
end

local function version_gt (v1, v2)
   local negatives = {"rc", "pre"}
   local m1, r1 = string.match (v1, "^(%D*)(.*)")
   local m2, r2 = string.match (v2, "^(%D*)(.*)")
   if m1 ~= m2 then
      for _, suffix in ipairs (negatives) do
         suffix = "-" .. suffix
         if m1 == suffix and m2 ~= suffix then
            return false
         elseif m1 ~= suffix and m2 == suffix then
            return true
         end
      end
      return m1 > m2
   end
   m1, r1 = string.match (r1, "^(%d*)(.*)")
   m2, r2 = string.match (r2, "^(%d*)(.*)")
   m1 = tonumber (m1) or 0
   m2 = tonumber (m2) or 0
   if m1 ~= m2 then
      return m1 > m2
   end
   if r1 == "" and r2 == "" then
      return false
   end
   return version_gt (r1, r2)
end

local function kernel_gt (k1, k2)
   return version_gt (k1.version, k2.version)
end

local function get (x)
   if type (x) == "function" then
      return x ()
   else
      return x
   end
end

local function draw (params)
   print (get (params.title) or "\n=== Boot menu ===")
   for i, item in ipairs (params.items) do
      print ((i == params.default and " > " or "   ") .. i .. "  " .. get (item.show))
   end
   print ("\nKernel arguments:\n  " .. params.append .. modifiers ())
   print ("\nHit a number to select from the menu,\n    ENTER to accept default,\n    ESC to exit\n or any other key to print menu again")
end

local function choose (params)
   draw (params)
   print ("\nBooting in " .. params.timeout .. " s...")
   while true do
      local i = sl.get_key (params.timeout * 1000)
      if i == sl.KEY.ESC then
         break
      else
         if i == sl.KEY.NONE or i == sl.KEY.ENTER then
            i = params.default
         elseif i == sl.KEY.DOWN then
            params.default = params.default < #params.items and params.default + 1 or #params.items
         elseif i == sl.KEY.UP then
            params.default = params.default > 1 and params.default - 1 or 1
         else
            i = i - string.byte "0"
         end
         if params.items[i] then
            params.items[i].execute ()
         end
         params.timeout = 0
         draw (params)
      end
   end
end

local function run (params)
   scan (params)
   if not next (params.items) then
      print ("No kernels found in directory " .. params.dir)
      os.exit (false)
   end
   table.sort (params.items, kernel_gt)
   table.insert (params.items, {
                    show = function () return "Single user: " .. (single and "true" or "false") end,
                    execute = function () single = not single end
                 })
   table.insert (params.items, {
                    show = function () return "Verbosity: " .. ({"quiet","normal","debug"})[verbosity] end,
                    execute = function () verbosity = verbosity < 3 and verbosity + 1 or 1 end
                 })
   choose (params)
end

return {
   scan = scan,
   choose = choose,
   run = run
}
