if not host:isHost() then return end

local part = models:newPart("figlens", "GUI")
local size = vec(0, 0)

local function updateSize(new)
    size:set(math.max(new.x, size.x), math.max(new.y, size.y))
end

local function capFirst(str)
    return str:sub(1,1):upper() .. str:sub(2)
end

local function titleCase(str)
    local out = {}
    for word in str:gmatch("%w+") do
        out[#out + 1] = capFirst(word)
    end
    return table.concat(out, " ")
end

local function round(num, places)
    local mult = 10 ^ (places or 0)
    return math.floor(num * mult + 0.5) / mult
end

local directions = {
    "north", "north-northeast", "northeast", "east-northeast",
    "east", "east-southeast", "southeast", "south-southeast",
    "south", "south-southwest", "southwest", "west-southwest",
    "west", "west-northwest", "northwest", "north-northwest"
}
local function rotationToDirection(rotation)
    return directions[rotation + 1]
end

local replacements = {
    ["true"] = "§atrue",
    ["false"] = "§cfalse"
}
local function formatProperty(name, property)
    property = replacements[property] or property
    if name == "rotation" then
        property = property .. " (" .. rotationToDirection(property) .. ")"
    end
    return "§7" .. titleCase(name) .. ": §f" .. property
end

local PADDING = vec(2,2)
local OUTLINE = vec(1,1)
local function renderBackground()
    part:newSprite("background"):color(0,0,0,0.7):texture("none", 1, 1):size(size + PADDING * 2):pos(PADDING.xy_)
    part:newSprite("background2"):color(0,0,0,0.7):light(15,15):texture("none", 1, 1):overlay(8,15):size(size + PADDING * 2 + OUTLINE * 2):pos((PADDING + OUTLINE).xy_ + vec(0, 0, 10))
end

local function renderText(text, x, y)
    local task = part:newText("text" .. math.random()):text(text):pos(x, y):shadow(true)
    updateSize(client.getTextDimensions(text):add(math.abs(x or 0), math.abs(y or 0)))
    return task
end

local HORSE_BPS = 42.16
local horses = {
    ["minecraft:horse"] = true,
    ["minecraft:donkey"] = true,
    ["minecraft:mule"] = true
}
local interesting_nbt = {
    PlayerCreated = "any",
    Tame = "any",
    EatingHaystack = 1,
    InLove = 1,
    Bred = 1,
}
local function entityStrategy(target)
    local lines = 0
    local item_offset = 0
    local function renderItem(target)
        local success, egg = pcall(world.newItem, target:getType():gsub(".*:", "") .. "_spawn_egg")
        part:newItem("item"):item(success and egg or "air"):displayMode("GUI"):pos(-9, -9,-20)
        item_offset = success and -20 or 0
    end

    local function renderName(target)
        local text = target:getName()
        renderText(text, item_offset, 0)
        lines = lines + 1
    end

    local function renderMod(target)
        local id = target:getType():gsub(":.*", "")
        renderText("§9" .. capFirst(id), item_offset, -10)
        lines = lines + 1
    end

    local function renderHealth(target)
        if not target.getHealth then return end
        local health = math.ceil(target:getHealth() - 0.5)
        local max_health = math.max(math.ceil(target:getMaxHealth()), health)

        local FULL = "🖤"
        local HALF = "💜"
        local EMPTY = "🤍"

        local full_count = math.floor(health / 2)
        local half_count = health % 2
        local total_hearts = math.ceil(max_health / 2)
        local empty_count = total_hearts - full_count - half_count

        local str = FULL:rep(full_count) .. HALF:rep(half_count) .. EMPTY:rep(empty_count)

        if max_health > 20 then
            renderText(formatProperty("hp", "§c" .. health .. "§7/§c" .. max_health .. "§7"), 0, -lines * 10)
        else
            renderText("§c" .. str, 0, -lines * 10)
        end
        lines = lines + 1
    end

    local function renderHorseStats(target)
        if not horses[target:getType()] then return end
        local function calculateJumpHeight(internal)
            return 3.4603 * (internal) ^ 2 + 2.1593 * internal - 0.314
        end
        local attributes = target:getNbt().Attributes
        if attributes then
            for i = 1, #attributes do
                local attribute = attributes[i]
                if attribute.Name == "minecraft:generic.movement_speed" then
                    local val = attribute.Base * HORSE_BPS
                    renderText(formatProperty("speed", round(val, 2) .. " m/s"), 0, -lines * 10)
                    lines = lines + 1
                elseif attribute.Name == "minecraft:horse.jump_strength" then
                    local val = round(calculateJumpHeight(attribute.Base), 2)
                    renderText(formatProperty("jump", val .. " block" .. (val == 1 and "" or "s")), 0, -lines * 10)
                    lines = lines + 1
                end
            end
        end
    end

    local function renderItems(target)
        local rendered = false
        for i = 1, 6 do
            local item = target:getItem(i)
            if item and item.id ~= "minecraft:air" then
                local x = -9 * 0.5 - (i - 1) * 12
                local y = -9 * 4
                part:newItem("slot" .. i):item(item):displayMode("GUI"):pos(x, y, -10):scale(0.75)
                rendered = true
            end
        end
        if rendered then
            lines = lines + 1
            updateSize(vec(9 * 6, 9 * 4.75))
        end
    end

    local function renderNbt(target)
        for name, property in next, target:getNbt() do
            if interesting_nbt[name] and (interesting_nbt[name] == "any" or interesting_nbt[name] == property) then
                renderText(formatProperty(name, property), 0,-lines * 10)
                lines = lines + 1
            end
        end
    end

    renderItem(target)
    renderName(target)
    renderMod(target)
    renderHealth(target)
    renderHorseStats(target)
    renderItems(target)
    renderNbt(target)
end

local containers = {}
local directions = {
    south = vec(1,0,0),
    north = vec(-1,0,0),
    east = vec(0,0,-1),
    west = vec(0,0,1)
}
local function updateNeighbouringChest(pos, data)
    local block = world.getBlockState(pos)
    if not block.id:find("chest$") then return end
    local index = pos:toString()
    containers[index] = data
end

local function isContainer(target)
    return not ((not target:getEntityData() or not target:getEntityData().Items) and not target.id:find("shulker") and not target.id:find("chest$"))
end

local container_screens = {
    ["net.minecraft.class_476"] = true, -- Chest, Ender Chest, Barrel
    ["net.minecraft.class_480"] = true, -- Dispenser, Dropper
    ["net.minecraft.class_488"] = true, -- Hopper
    ["net.minecraft.class_495"] = true  -- Shulker Box
}
local function updateContainers()
    local slots = host:getScreenSlotCount("")
    if not slots then return end

    local screen = host:getScreen()
    if not screen or not container_screens[screen] then return end

    local target = player:getTargetedBlock(true)
    if not isContainer(target) then return end

    local pos = target:getPos()
    local index = target.id:find("ender") and "GLOBAL" or pos:toString()
    containers[index] = containers[index] or {}
    for i = 0, slots - 37 do
        local slot = host:getScreenSlot(i)
        if slot then
            if slot.id == "minecraft:air" then
                containers[index][i] = nil
            else
                containers[index][i] = slot
            end
        end
    end

    containers[index].n = slots - 37
    local type = target.properties.type
    local facing = target.properties.facing
    if (type == "left" or type == "right") and directions[facing] then
        local dir = (type == "right" and 1 or -1) * directions[facing]
        updateNeighbouringChest(pos + dir, containers[index])
    end
end

local function blockStrategy(target)
    local lines = 0
    local function renderBlock(target)
        part:newItem("block"):item(target:asItem()):displayMode("GUI"):pos(-9,-9,-20):overlay(0,15)
    end

    local function renderName(target)
        renderText(target:asItem():getName(), -20, 0)
        lines = lines + 1
    end

    local function renderMod(target)
        local id = target.id:gsub(":.*", "")
        renderText("§9" .. capFirst(id), -20, -10)
        lines = lines + 1
    end

    local function renderContents(target)
        local index = target.id:find("ender") and "GLOBAL" or target:getPos():toString()
        local contents = containers[index]
        if not contents then
            if isContainer(target) then
                renderText("§7§oContents not loaded", 0, -lines * 10)
                lines = lines + 1
            end
            return
        end

        local rendered = false
        local held = player:getHeldItem().id
        for i = 0, contents.n do
            local item = contents[i]
            if item then
                local x = -9 * 0.75 - i % 9 * 12
                local y = -9 * 3 - math.floor(i / 9) * 12
                local task = part:newItem("item" .. i):item(item):displayMode("GUI"):pos(x, y, -100):scale(0.65)
                if item.id == held then
                    task:overlay(math.floor(math.sin(world.getTime() * 0.33 - i * 0.1) * 7.5 + 7.5),15)
                end
                local count = item:getCount()
                if count > 1 then
                    part:newText("count" .. i):text(count):pos(x + 9 * 0.5 - 10, y + 9 * 0.5 - 6, -150):scale(0.5):alignment("RIGHT"):shadow(true)
                end
                rendered = true
            end
        end
        if rendered then
            lines = lines + math.ceil(contents.n / 9) + 1.5
            updateSize(vec(9 * 12, 9 * 6.75))
            for i = 0, contents.n do
                local x = -9 * 0.75 - i % 9 * 12
                local y = -9 * 3 - math.floor(i / 9) * 12
                local slot_scale = 11
                part:newSprite("slot" .. i):color(0,0,0,1):texture("none", 1, 1):size(slot_scale - 1,slot_scale - 1):pos(x + slot_scale * 0.5 - 1, y + slot_scale * 0.5 - 1, -16):overlay(4,15)
                part:newSprite("slot" .. i .. "background"):color(0,0,0,1):texture("none", 1, 1):size(slot_scale,slot_scale):pos(x + slot_scale * 0.5, y + slot_scale * 0.5, -15):overlay(5,15)    
            end
        end
    end

    local function renderProperties(target)
        for name, property in next, target.properties do
            renderText(formatProperty(name, property), 0,-lines * 10)
            lines = lines + 1
        end
    end

    local function skullName(target)
        local data = target:getEntityData()
        data = data and data.SkullOwner
        if data and data.Name then
            renderText(formatProperty("owner", data.Name), 0, -lines * 10)
            lines = lines + 1
        end
    end

    renderBlock(target)
    renderName(target)
    renderMod(target)
    renderContents(target)
    skullName(target)
    renderProperties(target)
end

local function configOperation(method, ...)
    local old_name = config:getName()
    config:setName("figlens")
    local result = config[method](config, ...)
    config:setName(old_name)
    return result
end

local settings = configOperation("load", "settings") --[[@as { distance: number|"auto" }]]
if not settings then
    settings = {
        reach = host:getReachDistance(),
        auto_reach = true
    }
    configOperation("save", "settings", settings)
end

local function isHovering(pos)
    local gui_scale = client.getGuiScale()
    local mouse_pos = client.getMousePos() / gui_scale
    local offset = (pos.xy + OUTLINE) + mouse_pos
    local bottom_right = size + PADDING
    return offset.x > 0 and offset.y > 0 and offset.x < bottom_right.x and offset.y < bottom_right.y
end

local hovering = false
local function hover()
    hovering = true
    part:overlay(1,15)
    part:removeTask()
    size:set(100,20)
    renderBackground()
    renderText("§lConfig")
    renderText("§7Reach (scroll): §f" .. (settings.auto_reach and "auto" or settings.reach), 0, -10)

    local window_size = client.getScaledWindowSize()
    local pos = vec(-window_size.x / 2 + size.x / 2, -window_size.y / 32, 0)
    part:pos(pos)
    if not isHovering(pos) then
        hovering = false
    end
end

function events.MOUSE_SCROLL(dir)
    if not hovering then return end
    dir = math.sign(dir)
    local reach = host:getReachDistance()
    settings.reach = math.clamp(settings.reach + dir, reach - 1, 20)
    settings.auto_reach = settings.reach < reach
    configOperation("save", "settings", settings)
    return true
end

local function inMenu()
    return host:getScreen() or host:isChatOpen() or action_wheel:isEnabled()
end

local tab = keybinds:fromVanilla("key.playerlist")
function events.TICK()
    size:set(0,0)
    part:removeTask()
    part:overlay()

    if tab:isPressed() then return end

    if hovering then
        hover()
        return
    end

    local entity = player:getTargetedEntity(settings.auto_reach and host:getReachDistance() or settings.reach)
    local block = player:getTargetedBlock(true, settings.auto_reach and host:getReachDistance() or settings.reach)

    updateContainers()

    part:visible(true)
    if entity then
        entityStrategy(entity)
    elseif block and block:asItem().id ~= "minecraft:air" then
        blockStrategy(block)
    else
        part:visible(false)
        return
    end
    renderBackground()

    local window_size = client.getScaledWindowSize()
    local pos = vec(-window_size.x / 2 + size.x / 2, -12, 0)
    part:pos(pos)

    if inMenu() and isHovering(pos) then
        hover()
    end
end