local Chest = require("storage.chest")
local ItemGroup = require("storage.itemgroup")
local Storage = require("storage")
local Text = require("gfx.text")
local List = require("gfx.list")
local Event = require("gfx.event")
local Padding = require("gfx.padding")
local Container = require("gfx.container")
local Element = require("gfx.element")
local Progress = require("gfx.progress")
local Children = require("gfx.prop.children")
local Orientation = require("gfx.prop.orientation")


local CACHE_FILE = "/.storage.cache"
LOCK_FILE = "/.storage.lock"

local function lock()
  if fs.exists(LOCK_FILE) then
    return false
  end

  -- Create lock file
  return fs.open(LOCK_FILE, "w+").close()
end

local function unlock()
  if not fs.exists(LOCK_FILE) then
    return false
  end

  -- Delete lock file
  local result, reason = pcall(fs.delete, LOCK_FILE)
  if not result then
    print("ERROR: Lock file could not be deleted: " .. reason)
  end

  return result
end

local function isLocked()
  return fs.exists(LOCK_FILE)
end

local function saveState(fname, ctrl)
  local ser = ctrl:toSerializable()
  local file = fs.open(fname, "w+")
  file.write(textutils.serialize(ser))
  file.close()
end

local function loadState(fname, node)
  if not isLocked() then
    print("Not locked! Loading cache...")
    local file = fs.open(fname, "r")
    if file ~= nil then
        local ser = textutils.unserialize(file.readAll())
        file.close()
        return Storage:fromSerializable(ser)
    end
  end

  print("Controller must scan chests...")
  local nodeName = peripheral.getName(node)
  local storageChests = {peripheral.find("inventory")}
  for i,v in ipairs(storageChests) do
    if peripheral.getName(v) == nodeName then
      table.remove(storageChests, i)
      break
    end
  end
  local controller = Storage:fromPeripherals(Storage.assumeHomogeneous(storageChests))
  saveState(fname, controller)
  unlock()
  return controller
end

local function itemList(groups, wBudget, hBudget, savedState, onClick, setPage, runImport)
  local state = savedState or {}
  state.tab = state.tab or 1

  local pages = math.ceil(#groups / (hBudget - 1))

  -- Tab controls
  local btnPrev = Text:new{
    text = " < ",
    bgColor = state.tab == 1 and colors.black or colors.gray,
    fgColor = state.tab == 1 and colors.red or colors.white,
    onClick = function(e, x, y, s)
      if state.tab > 1 then
        state.tab = state.tab - 1
        setPage(nil)
      end
      return true
    end
  }

  local btnImport = Text:new{
    text = "IMPORT",
    bgColor = colors.gray,
    fgColor = colors.white,
    onClick = function(e, x, y, s)
      runImport()
      return true
    end
  }

  local btnNext = Text:new{
    text = " > ",
    bgColor = state.tab == pages and colors.black or colors.gray,
    fgColor = state.tab == pages and colors.red or colors.white,
    onClick = function(e, x, y, s)
      if state.tab < pages then
        state.tab = state.tab + 1
        setPage(nil)
      end
      return true
    end
  }

  local padImport = Padding:new{
    left = math.floor((wBudget - btnPrev:getWidth() - btnNext:getWidth() - btnImport:getWidth())/2),
    element = btnImport
  }

  local padNext = Padding:new{
    left = wBudget - btnPrev:getWidth() - padImport:getWidth() - btnNext:getWidth(),
    element = btnNext
  }

  local tabLine = List:new{
    [Children:getId()] = {
      btnPrev,
      padImport,
      padNext
    },
    [Orientation:getId()] = Orientation.HORIZONTAL
  }

  local bgColors = {
    colors.gray,
    colors.black
  }
  local entryBudget = hBudget - tabLine:getHeight()
  local entryStart = 1 + (state.tab - 1) * (entryBudget)
  local entryEnd = math.min(state.tab * entryBudget, #groups)
  local entries = {}

  for i=entryStart,entryEnd do
    local group = groups[i]
    local text = group:getSimpleName()
    local count = tostring(group:getItemCount())

    -- Fit text inside of width budget
    local countLen = #count
    if countLen + 2 > wBudget then
      error("Width budget is too small")
    end

    local textBudget = wBudget - 2 - countLen
    if #text > textBudget then
      -- Truncate to available budget
      text = text:sub(1,textBudget)
    end

    local nbtColor = group:getNBT() and colors.cyan or nil

    local textLabel = Text:new{ text = text, bgColor = bgColors[i % 2], fgColor = nbtColor }
    local countLabel = Text:new{ text = count, bgColor = bgColors[i % 2] }
    local paddedText = Padding:new{
      left = 0,
      right = wBudget - #count - #text,
      top = 0,
      bottom = 0,
      bgColor = bgColors[i % 2],
      element = textLabel
    }

    local list = List:new{
      [Children:getId()] = {
        paddedText,
        countLabel
      },
      [Orientation:getId()] = Orientation.HORIZONTAL,
      onClick = onClick and function(element, x, y, source)
        return onClick(element, x, y, source, group)
      end or nil
    }
    table.insert(entries, list)
  end

  local tabLinePad = Padding:new{
    top = hBudget - (entryEnd - entryStart) - 1 - tabLine:getHeight(),
    element = tabLine
  }

  table.insert(entries, tabLinePad)

  return List:new{
    [Children:getId()] = entries,
    [Orientation:getId()] = Orientation.VERTICAL
  }, state
end

local function updatePageRender(state, pages)
  if state.nextPage ~= nil then
    state.changingPage = state.nextPage ~= state.currentPage
    state.currentPage = state.nextPage
    state.nextPage = nil

    state.monitor.clear()
    state._pageRender = pages[state.currentPage](state)
    os.queueEvent("dummy_event")
  else
    state.changingPage = false
  end
end

local function renderDefault(state, rootElement)
  rootElement:setParent(state.monitor)
  return function()
    local event = {os.pullEvent()}
    local handled = rootElement:handleEvent(event)
    if not handled and event[1] == "monitor_resize" then
      local width, height = state.monitor.getSize()
      if state.width ~= width or state.height ~= height then
        state.width = width
        state.height = height
  
        -- Trigger re-render
        rootElement:setDirty(true)
      end
    end
  
    if not handled and not Event.isClickEvent(event) then
      os.queueEvent(table.unpack(event))
    end
  
    rootElement:draw()
  end
end

local function NOOP() end

local function getCenterPad(contentWidth, widthBudget)
  if widthBudget < contentWidth then
    return 0, 0
  end

  local pad = (widthBudget - contentWidth) / 2
  return math.floor(pad), math.ceil(pad)
end

local function fitText(text, widthBudget)
  if #text <= widthBudget then
    return text
  end
  return text:sub(1, #widthBudget)
end

local PAGES = {
  MAIN = function(state)
    local pageState = state:currentPageState({})

    if pageState.stacks == nil then
      pageState.stacks = ItemGroup.collectStacks(state.controller:find(function(stack)
        return not stack:isEmpty()
      end))

      table.sort(pageState.stacks, function(a, b) return a:getItemCount() > b:getItemCount() end)
    end

    local listResult, listState = itemList(
      pageState.stacks,
      state.width,
      state.height,
      pageState.listState,
      function(element, x, y, source, group)
        state:setPage("GROUP_DETAIL", group)
        return true
      end,
      function(page)
        if page == nil then
          state:reloadPage()
        else
          state:setPage(page)
        end
      end,
      function()
        print("Importing...")
        state.node:rescan()
        -- Safely handle transfers, priming computer for a full reset/rescan in case server stops mid-transaction
        state:itemTransaction(function()
          for _,nodeStack in ipairs(state.node:find(function(s) return not s:isEmpty() end)) do
            if not state.controller:insertStack(nodeStack) then
              print("Couldn't find a free slot for: "..nodeStack:getSimpleName())
            end
          end
        end)
        -- Force reload stacks
        state:currentPageState({}).stacks = nil
        state:reloadPage()
      end
    )
    pageState.listState = listState

    return renderDefault(state, listResult)
  end,

  GROUP_DETAIL = function(state, newPage)
    local paddingSide = 1
    local paddingTop = 1

    local group = state:getExtra()
    if group == nil then
      print("No group passed to GROUP_DETAIL")
      state:setPage("MAIN")
      return NOOP
    end

    local itemName = fitText(group:getSimpleName(), state.width - (paddingSide * 2))
    local itemLeftPad, itemRightPad = getCenterPad(#itemName, state.width - (paddingSide * 2))

    local paddedTitle = Padding:new{
      top = 1,
      left = itemLeftPad,
      right = itemRightPad,
      bottom = 1,
      element = Text:new{
        text = itemName,
        bgColor = colors.gray
      },
      bgColor = colors.gray
    }

    local simpleDetailsList = List:new{
      [Orientation:getId()] = Orientation.HORIZONTAL,
      [Children:getId()] = {
        Padding:new{
          top = 0,
          left = 1,
          right = 1,
          bottom = 0,
          bgColor = colors.red,
          element = List:new{
            bgColor = colors.red,
            [Orientation:getId()] = Orientation.VERTICAL,
            [Children:getId()] = {
              Text:new{ text = "Count", bgColor = colors.red },
              Text:new{ text = tostring(group:getItemCount()), bgColor = colors.red }
            }
          }
        },
        Padding:new{
          bgColor = colors.blue,
          top = 0,
          left = 1,
          right = 1,
          bottom = 0,
          element = List:new{
            bgColor = colors.blue,
            [Orientation:getId()] = Orientation.VERTICAL,
            [Children:getId()] = {
              Text:new{ text = "Slots", bgColor = colors.blue },
              Text:new{ text = tostring(group:getStackCount()), bgColor = colors.blue }
            }
          }
        }
      }
    }

    local damage = group:getDamage()
    local maxDamage = group:getMaxDamage()
    local damageBar = nil
    if damage ~= nil and maxDamage ~= nil then
      damageBar = Progress:new{
        progress = (damage == 0 and maxDamage or damage / maxDamage),
        width = state.width - 2,
        height = 1,
        fgColor = colors.green
      }
    end

    local infoElements = List:new{
      [Children:getId()] = {
        paddedTitle,
        Padding:new{
          top = 0,
          bottom = 1,
          left = 0,
          right = 0,
          element = simpleDetailsList
        },
        damageBar
      },
      [Orientation:getId()] = Orientation.VERTICAL,
      width = state.width
    }

    local stuffContainer = Container:new{
      [Children:getId()] = {
        Padding:new{
          top = paddingTop,
          left = paddingSide,
          right = paddingSide,
          bottom = 0,
          element = infoElements,
        },
        Padding:new{
          x = 1,
          y = state.height - 2,
          top = 1,
          left = 1,
          right = 1,
          bottom = 1,
          bgColor = colors.gray,
          element = Text:new{
            text = "Request",
            bgColor = colors.gray
          },
          onClick = function(e, x, y, s)
            state:setPage("REQUEST", group)
            return true
          end
        }
      },
      width = state.width,
      height = state.height,
      parent = state.monitor,
      onClick = function(e, x, y, s)
        state:setPage("MAIN")
        return true
      end
    }

    return renderDefault(state, stuffContainer)
  end,

  REQUEST = function(state, newPage)
    local group = state:getExtra()
    if group == nil then
      print("No group passed to REQUEST")
      state:setPage("MAIN")
      return NOOP
    end

    local pageState = state:currentPageState({
      request = 0
    })

    local itemName = fitText(group:getSimpleName(), state.width)
    local itemLeftPad, itemRightPad = getCenterPad(#itemName, state.width)

    local paddedTitle = Padding:new{
      top = 1,
      left = itemLeftPad,
      right = itemRightPad,
      bottom = 1,
      element = Text:new{
        text = itemName,
        bgColor = colors.gray
      },
      bgColor = colors.gray
    }

    local function makeRequestButton(increment)
      local text = increment > 0 and ("+"..tostring(increment)) or tostring(increment)
      return Text:new{ id = text, text = text }
    end

    local PADDING_RQC_H = 1
    local paddedRequestCount = Padding:new{
      top = 3,
      left = PADDING_RQC_H,
      right = PADDING_RQC_H,
      bottom = 0,
      element = List:new{
        [Orientation:getId()] = Orientation.VERTICAL,
        [Children:getId()] = {
          List:new{
            [Orientation:getId()] = Orientation.HORIZONTAL,
            [Children:getId()] = {
              Text:new{ text = "Request" },
              Padding:new{
                top = 0,
                bottom = 0,
                left = math.floor((state.width - (PADDING_RQC_H * 2) - #("Request") - #("Available")) / 2),
                right = math.ceil((state.width - (PADDING_RQC_H * 2) - #("Request") - #("Available")) / 2),
                element = Text:new{ text = "/" }
              },
              Text:new{ text = "Available" },
            }
          },
          List:new{
            [Orientation:getId()] = Orientation.HORIZONTAL,
            [Children:getId()] = {
              Text:new{ id = "data_request", text = tostring(pageState.request) },
              Padding:new{
                id = "data_divider",
                top = 0,
                bottom = 0,
                left = math.floor((state.width - (PADDING_RQC_H * 2) - #tostring(pageState.request) - #tostring(group:getItemCount())) / 2),
                right = math.ceil((state.width - (PADDING_RQC_H * 2) - #tostring(pageState.request) - #tostring(group:getItemCount())) / 2),
                element = Text:new{ text = "/" }
              },
              Text:new{ id = "data_available", text = tostring(group:getItemCount()) },
            }
          },
          Progress:new{
            id = "request_capacity",
            [Orientation:getId()] = Orientation.HORIZONTAL,
            width = state.width - (PADDING_RQC_H * 2),
            height = 1,
            progress = pageState.request / group:getItemCount()
          },
          Element:new{
            height = 1,
            width = 0
          },
          Padding:new{
            top = 0,
            bottom = 0,
            left = math.floor((state.width - 12)/2),
            right = math.ceil((state.width - 12)/2),
            element = List:new{
              [Orientation:getId()] = Orientation.HORIZONTAL,
              [Children:getId()] = {
                makeRequestButton(-5),
                Element:new{ width = 1 },
                makeRequestButton(-1),
                Element:new{ width = 2 },
                makeRequestButton(1),
                Element:new{ width = 1 },
                makeRequestButton(5),
              }
            }
          }
        }
      }
    }

    local stuffContainer = List:new{
      [Orientation:getId()] = Orientation.VERTICAL,
      [Children:getId()] = {
        paddedTitle,
        paddedRequestCount
      },
      onClick = function(e, x, y, s)
        state:setPage("GROUP_DETAIL", group)
        return true
      end
    }

    local function updateDisplayState()
      local dataRequestText = paddedRequestCount:findById("data_request")
      local dataDividerPad = paddedRequestCount:findById("data_divider")
      local dataAvailableText = paddedRequestCount:findById("data_available")
      local requestCapProgress = paddedRequestCount:findById("request_capacity")

      dataRequestText:setText(tostring(pageState.request))
      dataDividerPad:setPadding{
        left = math.floor((state.width - (PADDING_RQC_H * 2) - #tostring(pageState.request) - #tostring(group:getItemCount())) / 2),
        right = math.ceil((state.width - (PADDING_RQC_H * 2) - #tostring(pageState.request) - #tostring(group:getItemCount())) / 2)
      }
      dataAvailableText:setText(tostring(group:getItemCount()))
      requestCapProgress:setProgress(pageState.request / group:getItemCount())

      stuffContainer:setDirty(true)
    end

    local function bindRequestButton(increment)
      local id = increment > 0 and ("+"..tostring(increment)) or tostring(increment)
      paddedRequestCount:findById(id):setOnClick(function(e, x, y, s)
        local newValue = pageState.request + increment
        if newValue >= 0 and newValue <= group:getItemCount() then
          pageState.request = newValue
          updateDisplayState()
        end
        return true
      end)
    end

    bindRequestButton(-5)
    bindRequestButton(-1)
    bindRequestButton(1)
    bindRequestButton(5)

    return renderDefault(state, stuffContainer)
  end
}


local accessNode = Chest:fromPeripheral("minecraft:trapped_chest", true)
---@diagnostic disable-next-line: need-check-nil
local controller = loadState(CACHE_FILE, accessNode:getInventory())
local monitor = peripheral.find("monitor")
local width, height = monitor.getSize()

local CONTROLLER_STATE = {
  controller = controller,
  node = accessNode,
  width = width,
  height = height,
  monitor = monitor,
  nextPage = "MAIN",
  exit = false,
  pageState = {}
}

function CONTROLLER_STATE:itemTransaction(transact)
  lock()
  transact()
  saveState(CACHE_FILE, self.controller)
  unlock()
end

function CONTROLLER_STATE:currentPageState(default)
  self.pageState[self.currentPage] = self.pageState[self.currentPage] or default
  return self.pageState[self.currentPage]
end

function CONTROLLER_STATE:setPage(page, extra)
  self.nextPage = page
  self.extra = extra
end

function CONTROLLER_STATE:getExtra()
  return self.extra
end

function CONTROLLER_STATE:reloadPage()
  self.nextPage = self.currentPage
end

while not CONTROLLER_STATE.exit do
  updatePageRender(CONTROLLER_STATE, PAGES)
  CONTROLLER_STATE._pageRender()
end