local mon = peripheral.find("monitor")
assert(mon, "No monitor found")
local monName = peripheral.getName(mon)

mon.setTextScale(0.5)

local SCREEN_WIDTH, SCREEN_HEIGHT = mon.getSize()

local oldTerm = term.redirect(mon)

local function ensureDependency(path, url)
  if fs.exists(path) then
    return
  end

  assert(shell and type(shell.run) == "function", "Shell API is not available")
  assert(shell.run("wget", url, path) and fs.exists(path), "Could not download dependency: " .. path)
end

ensureDependency("Pine3D.lua", "https://raw.githubusercontent.com/Xella37/Pine3D/main/Pine3D.lua")
ensureDependency("betterblittle.lua", "https://raw.githubusercontent.com/Xella37/Pine3D/main/betterblittle.lua")
ensureDependency("morefonts-pe.lua", "https://raw.githubusercontent.com/MichielP1807/more-fonts/main/pine3d/morefonts-pe.lua")

local Pine3D = require("Pine3D")
local mf = require("morefonts-pe")

local frame = Pine3D.newFrame()
frame:setBackgroundColor(colors.black)
local meSystem = peripheral.find("meBridge") or peripheral.find("rsBridge")

local chainItemsByBaseId
local baseItemIds
local itemById
local defaultBaseId
local fallbackIcon = false
local backButton = false
local scrollUpButton = false
local scrollDownButton = false
local overviewScrollCurrentRow = 0
local overviewScrollTargetRow = 0
local drawOverview

local function ensureChainsLoaded()
  if not chainItemsByBaseId then
    local file = assert(fs.open("atc_chains.json", "r"))
    local data = textutils.unserializeJSON(file.readAll())
    file.close()

    assert(type(data) == "table" and type(data.chains) == "table", "Invalid JSON data")

    chainItemsByBaseId = {}
    baseItemIds = {}
    itemById = {}

    for _, chain in ipairs(data.chains) do
      if not defaultBaseId then
        defaultBaseId = chain.base_id
      end

      baseItemIds[#baseItemIds + 1] = chain.base_id

      local items = {}
      local itemCount = 0

      for _, item in ipairs(chain.items or {}) do
        if item.stage == "item" then
          itemById[item.item_id] = {
            id = item.item_id,
            icon_nfp = item.icon_nfp_16x16,
          }
        else
          itemCount = itemCount + 1
          items[itemCount] = {
            id = item.item_id,
            icon_nfp = item.icon_nfp_16x16,
          }
          itemById[item.item_id] = items[itemCount]
        end
      end

      chainItemsByBaseId[chain.base_id] = items
    end
  end
end

local function getPageItems(base_id)
  ensureChainsLoaded()

  return chainItemsByBaseId[base_id or defaultBaseId] or {}
end

local function getBaseItemIds()
  ensureChainsLoaded()

  return baseItemIds
end

local function getItemById(item_id)
  ensureChainsLoaded()

  return itemById[item_id]
end

local function parseNfpImage(nfp)
  if type(nfp) ~= "string" or nfp == "" then
    return nil
  end

  local image = {}
  local y = 1

  for line in (nfp .. "\n"):gmatch("(.-)\n") do
    local row = {}

    for x = 1, #line do
      local c = line:sub(x, x)
      if c ~= " " then
        local value = tonumber(c, 16)
        if value then
          row[x] = 2 ^ value
        end
      end
    end

    image[y] = row
    y = y + 1
  end

  return image
end

local function getItemIcon(item)
  if not item then
    return nil
  end

  if item.icon == nil then
    item.icon = parseNfpImage(item.icon_nfp) or false
    item.icon_nfp = nil
  end

  return item.icon or nil
end

local function getMeItemCount(itemId)
  if not meSystem or not itemId then
    return 0
  end

  if type(meSystem.getItem) == "function" then
    local ok, item = pcall(meSystem.getItem, { name = itemId })
    if ok and item then
      return tonumber(item.amount or item.count or item.qty) or 0
    end
  end

  return 0
end

local function getMeItemCounts(itemIds)
  local counts = {}

  for i = 1, #itemIds do
    counts[itemIds[i]] = 0
  end

  if not meSystem then
    return counts
  end

  local unresolved = {}
  local unresolvedCount = 0

  for i = 1, #itemIds do
    local itemId = itemIds[i]
    local count = getMeItemCount(itemId)
    counts[itemId] = count

    if count == 0 then
      unresolved[itemId] = true
      unresolvedCount = unresolvedCount + 1
    end
  end

  if unresolvedCount == 0 then
    return counts
  end

  local listItems = meSystem.listItems or meSystem.getItems or meSystem.listAvailableItems
  if not listItems then
    return counts
  end

  local ok, items = pcall(listItems)
  if not ok or type(items) ~= "table" then
    return counts
  end

  for i = 1, #items do
    local item = items[i]
    local itemId = item and (item.name or item.id or item.item_id)

    if itemId and unresolved[itemId] then
      counts[itemId] = tonumber(item.amount or item.count or item.qty) or 0
      unresolved[itemId] = nil
      unresolvedCount = unresolvedCount - 1

      if unresolvedCount == 0 then
        break
      end
    end
  end

  return counts
end

local function getFallbackIcon()
  if fallbackIcon == false then
    fallbackIcon = parseNfpImage(table.concat({
      "0000000000000000",
      "000000eeee000000",
      "00000eeeeee00000",
      "0000eee00eee0000",
      "00000e00000ee000",
      "0000000000eee000",
      "000000000eee0000",
      "00000000eee00000",
      "00000000ee000000",
      "0000000000000000",
      "00000000ee000000",
      "00000000ee000000",
      "0000000000000000",
      "0000000000000000",
      "0000000000000000",
      "0000000000000000",
    }, "\n"))
  end

  return fallbackIcon
end

local function getBackButton()
  if backButton == false then
    backButton = parseNfpImage(table.concat({
      "e e",
      " e ",
      "e e",
    }, "\n"))
  end

  return backButton
end

local function getScrollUpButton()
  if scrollUpButton == false then
    scrollUpButton = parseNfpImage(table.concat({
      " 00 ",
      "0000",
      "0  0",
    }, "\n"))
  end

  return scrollUpButton
end

local function getScrollDownButton()
  if scrollDownButton == false then
    scrollDownButton = parseNfpImage(table.concat({
      "0  0",
      "0000",
      " 00 ",
    }, "\n"))
  end

  return scrollDownButton
end

local function imageSize(img)
  local w, h = 0, 0

  for y, row in pairs(img) do
    h = math.max(h, y)

    for x, value in pairs(row) do
      if type(value) == "number" and value > 0 then
        w = math.max(w, x)
      end
    end
  end

  return w, h
end

local function formatCount(n)
  n = math.floor(tonumber(n) or 0)

  if n < 1000 then
    return tostring(n)
  elseif n < 100000 then
    local v = n / 1000
    if v < 10 then
      return string.format("%.1fK", v):gsub("%.0K", "K")
    end
    return tostring(math.floor(v)) .. "K"
  elseif n < 100000000 then
    local v = n / 1000000
    if v < 10 then
      return string.format("%.1fM", v):gsub("%.0M", "M")
    end
    return tostring(math.floor(v)) .. "M"
  elseif n < 100000000000 then
    local v = n / 1000000000
    if v < 10 then
      return string.format("%.1fB", v):gsub("%.0B", "B")
    end
    return tostring(math.floor(v)) .. "B"
  else
    local v = n / 1000000000000
    if v < 10 then
      return string.format("%.1fT", v):gsub("%.0T", "T")
    end
    return tostring(math.floor(v)) .. "T"
  end
end

local function drawNfpScaled(buffer, img, x, y, scaleX, scaleY)
  scaleX = scaleX or 2
  scaleY = scaleY or 3

  for imgY, row in pairs(img) do
    for imgX, col in pairs(row) do
      -- IMPORTANT:
      -- Only write real ComputerCraft colors into the Pine3D buffer.
      -- No strings, no nil values, no 0.
      if type(col) == "number" and col > 0 then
        local px = x + (imgX - 1) * scaleX
        local py = y + (imgY - 1) * scaleY

        for dy = 0, scaleY - 1 do
          for dx = 0, scaleX - 1 do
            local drawX = px + dx
            local drawY = py + dy

            if drawX >= 1 and drawX <= buffer.width and drawY >= 1 and drawY <= buffer.height then
              buffer:setPixel(drawX, drawY, col)
            end
          end
        end
      end
    end
  end
end

local function fillRect(buffer, x, y, width, height, color)
  local startX = math.max(1, x)
  local startY = math.max(1, y)
  local endX = math.min(buffer.width, x + width - 1)
  local endY = math.min(buffer.height, y + height - 1)

  if startX > endX or startY > endY then
    return
  end

  for py = startY, endY do
    for px = startX, endX do
      buffer:setPixel(px, py, color)
    end
  end
end

local function cellToPixelX(cellX)
  return (cellX - 1) * 2 + 1
end

local function cellToPixelY(cellY)
  return (cellY - 1) * 3 + 1
end

local function assertBufferValid(frame)
  local valid = {
    [colors.white] = true,
    [colors.orange] = true,
    [colors.magenta] = true,
    [colors.lightBlue] = true,
    [colors.yellow] = true,
    [colors.lime] = true,
    [colors.pink] = true,
    [colors.gray] = true,
    [colors.lightGray] = true,
    [colors.cyan] = true,
    [colors.purple] = true,
    [colors.blue] = true,
    [colors.brown] = true,
    [colors.green] = true,
    [colors.red] = true,
    [colors.black] = true,
  }

  for y = 1, frame.buffer.height do
    for x = 1, frame.buffer.width do
      local c = frame.buffer.colorValues[y][x]

      if not valid[c] then
        error("Invalid pixel in buffer at x=" .. x .. ", y=" .. y .. ": " .. tostring(c))
      end
    end
  end
end

local function drawItem(img, cellX, cellY, count, offsetY)
  -- Convert normal monitor cells into Pine3D teletext pixels.
  local x = cellToPixelX(cellX)
  local y = cellToPixelY(cellY) + (offsetY or 0)

  local imgW, imgH = 0, 0

  local scaleX = 1
  local scaleY = 1

  if img then
    imgW, imgH = imageSize(img)
    if y <= frame.buffer.height and y + imgH * scaleY - 1 >= 1 then
      drawNfpScaled(frame.buffer, img, x, y, scaleX, scaleY)
    end
  end

  local text = formatCount(count)

  local rightX = x + 16 -- x + math.max(imgW * scaleX - 1, 7)
  local bottomY = y + 16 -- y + math.max(imgH * scaleY - 1, 4)

  local options = {
    font = "fonts/QuinqueFive",
    textAlign = "right",
    anchorHor = "right",
    anchorVer = "bottom",
    condense = true,
    scale = 1,
    dx = 1,
    dy = 1,
  }

  if bottomY >= 1 and y <= frame.buffer.height then
    -- Shadow
    mf.writeOn(frame, text, colors.black, rightX + 1, bottomY + 1, options)

    -- White count
    mf.writeOn(frame, text, colors.white, rightX, bottomY, options)
  end
end

local function runScrollableGrid(options)
    local entries = {}
    local counts = {}
    local visibleItemCount = 0
    local totalRows = 0
    local maxScrollRow = 0
    local currentScrollRow = 0
    local targetScrollRow = 0
    local refreshTimer
    local animationTimer

    local columns = 3
    local rowsPerView = 3
    local baseCellX = 4
    local baseCellY = 2
    local colStepCells = 12
    local rowStepCells = 6
    local rowStepPixels = rowStepCells * 3
    local scrollbarX = SCREEN_WIDTH - 1
    local scrollbarWidth = 2
    local scrollbarButtonHeight = 3
    local scrollbarTrackY = scrollbarButtonHeight + 1
    local scrollbarTrackHeight = SCREEN_HEIGHT - scrollbarButtonHeight * 2
    local scrollbarPixelX = cellToPixelX(scrollbarX)
    local scrollbarPixelWidth = scrollbarWidth * 2
    local scrollbarTrackPixelY = cellToPixelY(scrollbarTrackY)
    local scrollbarTrackPixelHeight = math.max(0, scrollbarTrackHeight * 3)

    if options.getInitialScrollState then
        local initialCurrentRow, initialTargetRow = options.getInitialScrollState()
        currentScrollRow = tonumber(initialCurrentRow) or 0
        targetScrollRow = tonumber(initialTargetRow) or currentScrollRow
    end

    local function snapScrollRow(row)
        return math.floor((tonumber(row) or 0) + 0.5)
    end

    local function persistScrollState()
        if options.setScrollState then
            options.setScrollState(snapScrollRow(targetScrollRow), snapScrollRow(targetScrollRow))
        end
    end

    local function clampScrollRow(row)
        return math.max(0, math.min(maxScrollRow, row))
    end

    local function getThumbMetrics()
        if scrollbarTrackPixelHeight <= 0 then
            return scrollbarTrackPixelY, 0
        end

        if totalRows <= rowsPerView or maxScrollRow == 0 then
            return scrollbarTrackPixelY, scrollbarTrackPixelHeight
        end

        local thumbHeight = math.max(4, math.floor(scrollbarTrackPixelHeight * rowsPerView / totalRows + 0.5))
        local thumbTravel = scrollbarTrackPixelHeight - thumbHeight
        local thumbY = scrollbarTrackPixelY + math.floor((currentScrollRow / maxScrollRow) * thumbTravel + 0.5)

        return thumbY, thumbHeight
    end

    local function scheduleAnimation()
        if not animationTimer and math.abs(targetScrollRow - currentScrollRow) > 0.001 then
            animationTimer = os.startTimer(0.05)
        end
    end

    local function scrollTo(row)
        local clampedRow = clampScrollRow(snapScrollRow(row))

        if clampedRow ~= targetScrollRow then
            targetScrollRow = clampedRow
            persistScrollState()
            scheduleAnimation()
        end
    end

    local function refreshGridData()
        entries, counts = options.getEntriesAndCounts()
    end

    local function renderGrid()
        totalRows = math.ceil(#entries / columns)
        maxScrollRow = math.max(0, totalRows - rowsPerView)
        currentScrollRow = clampScrollRow(currentScrollRow)
        targetScrollRow = clampScrollRow(snapScrollRow(targetScrollRow))
        persistScrollState()

        local firstVisibleRow = math.floor(currentScrollRow)
        local rowOffsetPixels = -math.floor((currentScrollRow - firstVisibleRow) * rowStepPixels + 0.5)
        local firstVisibleIndex = firstVisibleRow * columns + 1
        local lastVisibleIndex = math.min(#entries, (firstVisibleRow + rowsPerView + 1) * columns)

        frame.buffer:clear()

        if options.drawChrome then
            options.drawChrome()
        end

        for i = firstVisibleIndex, lastVisibleIndex do
            local entry = entries[i]
            local relativeIndex = i - firstVisibleIndex
            local col = relativeIndex % columns
            local row = math.floor(relativeIndex / columns)
            drawItem(
                options.getIcon(entry),
                baseCellX + colStepCells * col,
                baseCellY + rowStepCells * row,
                counts[options.getId(entry)] or 0,
                rowOffsetPixels
            )
        end

        visibleItemCount = math.max(0, math.min(#entries - firstVisibleRow * columns, rowsPerView * columns))

        local upButtonY = cellToPixelY(1)
        local downButtonY = cellToPixelY(SCREEN_HEIGHT - scrollbarButtonHeight + 1)
        local upColor = targetScrollRow > 0 and colors.gray or colors.lightGray
        local downColor = targetScrollRow < maxScrollRow and colors.gray or colors.lightGray
        local thumbY, thumbHeight = getThumbMetrics()

        fillRect(frame.buffer, scrollbarPixelX, upButtonY, scrollbarPixelWidth, scrollbarButtonHeight * 3, upColor)
        fillRect(frame.buffer, scrollbarPixelX, downButtonY, scrollbarPixelWidth, scrollbarButtonHeight * 3, downColor)

        if scrollbarTrackPixelHeight > 0 then
            fillRect(frame.buffer, scrollbarPixelX, scrollbarTrackPixelY, scrollbarPixelWidth, scrollbarTrackPixelHeight, colors.gray)
            fillRect(frame.buffer, scrollbarPixelX, thumbY, scrollbarPixelWidth, thumbHeight, colors.white)
        end

        drawNfpScaled(frame.buffer, getScrollUpButton(), scrollbarPixelX, upButtonY + 2, 1, 1)
        drawNfpScaled(frame.buffer, getScrollDownButton(), scrollbarPixelX, downButtonY + 3, 1, 1)

        assertBufferValid(frame)
        frame:drawBuffer()
    end

    refreshGridData()
    renderGrid()
    refreshTimer = os.startTimer(options.refreshSeconds or 30)

    while true do
        local event, p1, x, y = os.pullEvent()

        if event == "timer" and p1 == refreshTimer then
            refreshGridData()
            renderGrid()
            refreshTimer = os.startTimer(options.refreshSeconds or 30)
            scheduleAnimation()
        elseif event == "timer" and p1 == animationTimer then
            local delta = targetScrollRow - currentScrollRow

            if math.abs(delta) <= 0.001 then
                currentScrollRow = targetScrollRow
                animationTimer = nil
            else
                local step = delta * 0.35

                if step > 0 then
                    step = math.max(0.08, math.min(step, 0.45))
                else
                    step = math.min(-0.08, math.max(step, -0.45))
                end

                if math.abs(step) >= math.abs(delta) then
                    currentScrollRow = targetScrollRow
                    animationTimer = nil
                else
                    currentScrollRow = currentScrollRow + step
                    animationTimer = os.startTimer(0.05)
                end
            end

            persistScrollState()
            renderGrid()
        elseif event == "monitor_touch" and p1 == monName then
            if options.handleChromeTouch and options.handleChromeTouch(x, y) then
                return
            elseif x >= scrollbarX and x < scrollbarX + scrollbarWidth then
                if y <= scrollbarButtonHeight then
                    scrollTo(targetScrollRow - 1)
                elseif y > SCREEN_HEIGHT - scrollbarButtonHeight then
                    scrollTo(targetScrollRow + 1)
                elseif scrollbarTrackHeight > 0 then
                    local touchPixelY = cellToPixelY(y) + 1
                    local thumbY, thumbHeight = getThumbMetrics()

                    if (touchPixelY < thumbY or touchPixelY > thumbY + thumbHeight - 1) and maxScrollRow > 0 then
                        local thumbTravel = scrollbarTrackPixelHeight - thumbHeight
                        local targetPixelY = math.max(
                            scrollbarTrackPixelY,
                            math.min(scrollbarTrackPixelY + thumbTravel, touchPixelY - math.floor(thumbHeight / 2))
                        )
                        local progress = (targetPixelY - scrollbarTrackPixelY) / math.max(1, thumbTravel)
                        scrollTo(progress * maxScrollRow)
                    end
                end
            else
                local col = math.floor((x - baseCellX) / colStepCells)
                local firstVisibleRow = math.floor(currentScrollRow)
                local rowOffsetPixels = (currentScrollRow - firstVisibleRow) * rowStepPixels
                local touchPixelY = cellToPixelY(y) + 1
                local row = math.floor((touchPixelY - cellToPixelY(baseCellY) + rowOffsetPixels) / rowStepPixels)

                if col >= 0 and col < columns and row >= 0 and row < rowsPerView + 1 then
                    local index = (firstVisibleRow + row) * columns + col + 1

                    if index >= 1 and index <= #entries and index <= firstVisibleRow * columns + visibleItemCount + columns then
                        if options.onSelect(entries[index], index) then
                            return
                        end
                    end
                end
            end
        end
    end
end

local function drawPage(base_id)
    local pageItems = getPageItems(base_id)
    local defaultIcon = getFallbackIcon()
    local backIcon = getBackButton()
    local backWidth, backHeight = imageSize(backIcon)
    local backX = 1
    local pageItemIds = {}

    for i = 1, #pageItems do
        pageItemIds[i] = pageItems[i].id
    end

    runScrollableGrid({
        refreshSeconds = 5,
        getEntriesAndCounts = function()
            return pageItems, getMeItemCounts(pageItemIds)
        end,
        getId = function(entry)
            return entry.id
        end,
        getIcon = function(entry)
            return getItemIcon(entry) or defaultIcon
        end,
        drawChrome = function()
            drawNfpScaled(frame.buffer, backIcon, cellToPixelX(backX), 1)
        end,
        handleChromeTouch = function(x, y)
            if x >= backX and x < backX + backWidth and y >= 1 and y <= backHeight then
                drawOverview()
                return true
            end

            return false
        end,
        onSelect = function()
            return false
        end,
    })
end

drawOverview = function()
    local items = getBaseItemIds()
    local defaultIcon = getFallbackIcon()

    runScrollableGrid({
        refreshSeconds = 30,
        getInitialScrollState = function()
            return overviewScrollCurrentRow, overviewScrollTargetRow
        end,
        setScrollState = function(currentRow, targetRow)
            overviewScrollCurrentRow = currentRow
            overviewScrollTargetRow = targetRow
        end,
        getEntriesAndCounts = function()
            local itemCounts = getMeItemCounts(items)
            local sortedItemIds = {}

            for i = 1, #items do
                sortedItemIds[i] = items[i]
            end

            table.sort(sortedItemIds, function(a, b)
                local countA = itemCounts[a] or 0
                local countB = itemCounts[b] or 0

                if countA ~= countB then
                    return countA > countB
                end

                return a < b
            end)

            return sortedItemIds, itemCounts
        end,
        getId = function(entry)
            return entry
        end,
        getIcon = function(entry)
            return getItemIcon(getItemById(entry)) or defaultIcon
        end,
        onSelect = function(entry)
            drawPage(entry)
            return true
        end,
    })
end

drawOverview()

term.redirect(oldTerm)
