local rootContainer = DatabindingAddDataContainerFromPath("", "player_menu_data")
local itemList = DatabindingAddUiItemList(DatabindingAddDataContainerFromPath("", "AnimalSightingMissions"), "listItems")
AddTextEntryByHash(-179955176, "") -- ANIMAL_SIGHTINGS_SUBH

local currentMenu = nil

Menu = {}
Menu.__index = Menu

setmetatable(Menu, {
    __call = function(cls, ...)
        return cls._construct(...)
    end
})

--- Create a new menu
---@return Menu
function Menu._construct(header)
    local self = setmetatable({}, Menu)

    self.header = header
    self.subheader = ""
    self.footer = ""
    self.footerColor = `COLOR_WHITE`
    self.isOpen = false
    self.onOpen = nil
    self.onClose = nil
    
    self.items = {}

    return self
end

--- Set the menu opened callback
---@param callback function
---@return Menu
function Menu:OnOpen(callback)
    if (callback ~= nil) then self.onOpen = callback end

    if (not callback and self.onOpen) then
        self.isOpen = true
        self.onOpen()
    end

    return self
end

--- Set the menu closed callback
---@param callback function
---@return Menu
function Menu:OnClose(callback)
    if (callback ~= nil) then self.onClose = callback end

    if (not callback and self.onClose) then
        self.isOpen = false
        self.onClose()
    end

    return self
end

--- Set the header text of the menu
---@param text string
---@return Menu
function Menu:SetHeader(text)
    if (text ~= nil) then self.header = text end

    if (self.isOpen) then
        print("Setting header text", self.header)
        DatabindingAddDataString(rootContainer, "header_text", self.header)
    end

    return self
end

--- Set the subheader text of the menu
---@param text string
---@return Menu
function Menu:SetSubheader(text)
    if (text ~= nil) then self.subheader = text end

    if (self.isOpen) then
        AddTextEntryByHash(-179955176, self.subheader)
    end

    return self
end

--- Set the footer text of the menu
---@param text string
---@return Menu
function Menu:SetFooter(text)
    if (text ~= nil) then self.footer = text end

    if (self.isOpen) then
        DatabindingAddDataString(rootContainer, "footer_tooltip_text", self.footer)
    end

    return self
end

--- Set the color of the footer text
---@param color hash
---@return Menu
function Menu:SetFooterColor(color)
    if (color ~= nil) then self.footerColor = color end

    if (self.isOpen) then
        DatabindingAddDataHash(rootContainer, "footer_tooltip_color", self.footerColor)
    end

    return self
end

--- Add an item to the menu
---@param text string
---@return Item
function Menu:AddItem(text)
    local item = Item(#self.items+1)
        :SetText(text)

    table.insert(self.items, item)

    return item
end

--- Add an image item to the menu
---@param text string
---@param imgDict hash
---@param imgTexture hash
---@return Item
function Menu:AddImageItem(text, imgDict, imgTexture)
    local item = Item(#self.items+1, "imageItem")
        :SetText(text)
        :SetImgTextureDict(imgDict)
        :SetImgTexture(imgTexture)

    table.insert(self.items, item)

    return item
end

--- Add an image to the menu
---@param imgDict hash
---@param imgTexture hash
---@return Item
function Menu:AddImage(imgDict, imgTexture)
    local item = Item(#self.items+1, "image")
        :SetImgTextureDict(imgDict)
        :SetImgTexture(imgTexture)

    table.insert(self.items, item)

    return item
end

--- Add a small image item to the menu
---@param text string
---@param imgDict hash
---@param imgTexture hash
function Menu:AddSmallImageItem(text, imgDict, imgTexture)
    local item = Item(#self.items+1, "smallImageItem")
        :SetText(text)
        :SetImgTextureDict(imgDict)
        :SetImgTexture(imgTexture)

    table.insert(self.items, item)

    return item
end

function Menu:AddSlider(text)
    local item = Item(#self.items+1, "slider")
        :SetText(text)

    table.insert(self.items, item)

    return item
end

--- Remove an item from the menu
---@param item Item
---@return Menu
function Menu:RemoveItem(item)
    for k, v in pairs(self.items) do
        if (v.data == item.data) then
            table.remove(self.items, k)
            DatabindingRemoveBindingArrayItemByDataContextId(itemList, v.data)
            break
        end
    end

    return self
end

--- Get an item by index
---@param index integer
---@return [Item]
function Menu:GetItem(index)
    return self.items[index]
end

--- Get an item entry id by index
---@param index integer
---@return entryId
function Menu:GetItemEntryId(index)
    return DatabindingGetItemContextByIndex(itemList, index)
end

--- Get all items
---@return [Item]
function Menu:GetItems()
    return self.items
end

-- Get all items entry id
---@return [entryId]
function Menu:GetItemsEntryId()
    local entriesId = {}
    for i=1, #self.items do
        table.insert(entriesId, self.items[i].data)
    end

    return entriesId
end

--- Open the menu
---@return Menu
function Menu:Open()
    currentMenu = self

    self.isOpen = true
    DatabindingClearBindingArray(itemList)

    -- Init menu
    self:SetHeader()
    self:SetSubheader()
    self:SetFooter()
    self:SetFooterColor()

    for _, item in ipairs(self.items) do
        item:init(itemList)
    end

    LaunchUiappByHashWithEntry(`PLAYER_MENU`, -464479041)

    Citizen.CreateThread(function()
        while IsUiappRunningByHash(`PLAYER_MENU`) == 1 and (not currentMenu or currentMenu == self) do
            while EventsUiIsPending(`PLAYER_MENU`) do
                local msg = DataView.ArrayBuffer(8 * 4)
                msg:SetInt32(0, 0) -- Event type
                msg:SetInt32(8, 0)
                msg:SetInt32(16, 0)
                msg:SetInt32(24, 0) -- Item data container
    
                if (Citizen.InvokeNative(0x90237103F27F7937, `PLAYER_MENU`, msg:Buffer()) ~= 0) then -- EVENTS_UI_PEEK_MESSAGE
                    -- Item unfocused
                    if (msg:GetInt32(0) == `ITEM_UNFOCUSED`) then
                        for k, v in ipairs(currentMenu.items) do
                            if (msg:GetInt32(24) == v.data) then
                                v:OnUnfocused()
                                break
                            end
                        end
                    end

                    -- Item focused
                    if msg:GetInt32(0) == `ITEM_FOCUSED` then
                        for k, v in ipairs(currentMenu.items) do
                            if (msg:GetInt32(24) == v.data) then
                                v:OnFocused()
                                break
                            end
                        end
                    end

                    -- Item selected
                    if msg:GetInt32(0) == `ITEM_SELECTED` then
                        for k, v in ipairs(currentMenu.items) do
                            if (msg:GetInt32(24) == v.data) then
                                if (DatabindingReadDataBool(v.data, "dynamic_list_item_enabled")) then
                                    v:OnSelected()
                                end
                                break
                            end
                        end
                    end
                end
    
                EventsUiPopMessage(`PLAYER_MENU`)
            end
            Citizen.Wait(0)
        end
        self:OnClose()
    end)

    self:OnOpen()

    return self
end

--- Close the menu
---@return Menu
function Menu:Close()
    CloseUiapp("player_menu")

    return self
end



-------------------------------
--           DATAVIEW        --
-------------------------------
DataView = setmetatable({
    EndBig = ">",
    EndLittle = "<",
    Types = {
        Int8 = { code = "i1" },
        Uint8 = { code = "I1" },
        Int16 = { code = "i2" },
        Uint16 = { code = "I2" },
        Int32 = { code = "i4" },
        Uint32 = { code = "I4" },
        Int64 = { code = "i8" },
        Uint64 = { code = "I8" },
        Float32 = { code = "f", size = 4 }, -- a float (native size)
        Float64 = { code = "d", size = 8 }, -- a double (native size)

        LuaInt = { code = "j" }, -- a lua_Integer
        UluaInt = { code = "J" }, -- a lua_Unsigned
        LuaNum = { code = "n" }, -- a lua_Number
        String = { code = "z", size = -1, }, -- zero terminated string
    },

    FixedTypes = {
        String = { code = "c" }, -- a fixed-sized string with n bytes
        Int = { code = "i" }, -- a signed int with n bytes
        Uint = { code = "I" }, -- an unsigned int with n bytes
    },
}, {
    __call = function(_, length)
        return DataView.ArrayBuffer(length)
    end
})
DataView.__index = DataView

--[[ Create an ArrayBuffer with a size in bytes --]]
function DataView.ArrayBuffer(length)
    return setmetatable({
        blob = string.blob(length),
        length = length,
        offset = 1,
        cangrow = true,
    }, DataView)
end

--[[ Wrap a non-internalized string --]]
function DataView.Wrap(blob)
    return setmetatable({
        blob = blob,
        length = blob:len(),
        offset = 1,
        cangrow = true,
    }, DataView)
end

--[[ Return the underlying bytebuffer --]]
function DataView:Buffer() return self.blob end
function DataView:ByteLength() return self.length end
function DataView:ByteOffset() return self.offset end
function DataView:SubView(offset, length)
    return setmetatable({
        blob = self.blob,
        length = length or self.length,
        offset = 1 + offset,
        cangrow = false,
    }, DataView)
end

--[[ Return the Endianness format character --]]
local function ef(big) return (big and DataView.EndBig) or DataView.EndLittle end

--[[ Helper function for setting fixed datatypes within a buffer --]]
local function packblob(self, offset, value, code)
    -- If cangrow is false the dataview represents a subview, i.e., a subset
    -- of some other string view. Ensure the references are the same before
    -- updating the subview
    local packed = self.blob:blob_pack(offset, code, value)
    if self.cangrow or packed == self.blob then
        self.blob = packed
        self.length = packed:len()
        return true
    else
        return false
    end
end

--[[
    Create the API by using DataView.Types
--]]
for label,datatype in pairs(DataView.Types) do
    if not datatype.size then  -- cache fixed encoding size
        datatype.size = string.packsize(datatype.code)
    elseif datatype.size >= 0 and string.packsize(datatype.code) ~= datatype.size then
        local msg = "Pack size of %s (%d) does not match cached length: (%d)"
        error(msg:format(label, string.packsize(datatype.code), datatype.size))
        return nil
    end

    DataView["Get" .. label] = function(self, offset, endian)
        offset = offset or 0
        if offset >= 0 then
            local o = self.offset + offset
            local v,_ = self.blob:blob_unpack(o, ef(endian) .. datatype.code)
            return v
        end
        return nil
    end

    DataView["Set" .. label] = function(self, offset, value, endian)
        if offset >= 0 and value then
            local o = self.offset + offset
            local v_size = (datatype.size < 0 and value:len()) or datatype.size
            if self.cangrow or ((o + (v_size - 1)) <= self.length) then
                if not packblob(self, o, value, ef(endian) .. datatype.code) then
                    error("cannot grow subview")
                end
            else
                error("cannot grow dataview")
            end
        end
        return self
    end
end

for label,datatype in pairs(DataView.FixedTypes) do
    datatype.size = -1 -- Ensure cached encoding size is invalidated

    DataView["GetFixed" .. label] = function(self, offset, typelen, endian)
        if offset >= 0 then
            local o = self.offset + offset
            if (o + (typelen - 1)) <= self.length then
                local code = ef(endian) .. "c" .. tostring(typelen)
                local v,_ = self.blob:blob_unpack(o, code)
                return v
            end
        end
        return nil -- Out of bounds
    end

    DataView["SetFixed" .. label] = function(self, offset, typelen, value, endian)
        if offset >= 0 and value then
            local o = self.offset + offset
            if self.cangrow or ((o + (typelen - 1)) <= self.length) then
                local code = ef(endian) .. "c" .. tostring(typelen)
                if not packblob(self, o, value, code) then
                    error("cannot grow subview")
                end
            else
                error("cannot grow dataview")
            end
        end
        return self
    end
end