local re = re
local sdk = sdk
local d2d = d2d
local imgui = imgui
local log = log
local json = json
local draw = draw
local require = require
local tostring = tostring
local pairs = pairs
local ipairs = ipairs
local math = math
local string = string
local table = table
local type = type

local Core = require("_CatLib")
local CONST = require("_CatLib.const")
local Imgui = require("_CatLib.imgui")

local mod = Core.NewMod("Auto Meal")
mod.EnableCJKFont(18)

if mod.Config.CurrentSet == nil then
    mod.Config.CurrentSet = -1
end

if mod.Config.FallbackAnotherWeapon == nil then
    mod.Config.FallbackAnotherWeapon = true
end

if mod.Config.WeaponTypeConfig == nil then
    mod.Config.WeaponTypeConfig = {}
    for _ = 0, 14-1, 1 do
        table.insert(mod.Config.WeaponTypeConfig, -1)
    end
end

if mod.Config.GUIDelaySeconds == nil then
    mod.Config.GUIDelaySeconds = 3.0
end

if mod.Config.EquipMySetConfig == nil then
    mod.Config.EquipMySetConfig = {}
end

if mod.Config.ConsumeItems == nil then
    mod.Config.ConsumeItems = true
end
if mod.Config.NotifyNoEnoughItems == nil then
    mod.Config.NotifyNoEnoughItems = true
end
if mod.Config.NotifyNoEnoughItemDetails == nil then
    mod.Config.NotifyNoEnoughItemDetails = true
end
if mod.Config.SystemLog == nil then
    mod.Config.SystemLog = true
    mod.Config.MealUI = true
end

local RequestUTCTime = 0
local RequestMealSetIndex = -1
local RequestMessage = ""

local NOTIFICATION_MSG = {
    [CONST.LanguageType.English] = {
        FromDefault = "Apply default meal set: <COLOR 00FFFF>%s</COLOR>",
        FromOption = "Apply meal set: <COLOR 00FFFF>%s</COLOR> [<COLOR FFFF00>%s</COLOR>]",

        ManualEat = "Manual",
        UseDefaultSetting = "Use Default Setting",
        InvalidSetting = "Auto Meal: Invalid Setting",
        NotEnough = "Food not enough.",
        NotEnoughDetail = "Food not enough, %s.",
    },
    [CONST.LanguageType.SimplifiedChinese] = {
        FromDefault = "已享用默认烹饪组合：<COLOR 00FFFF>%s</COLOR>",
        FromOption = "已享用烹饪组合：<COLOR 00FFFF>%s</COLOR>【<COLOR FFFF00>%s</COLOR>】",

        ManualEat = "手动",
        UseDefaultSetting = "使用默认设置",
        InvalidSetting = "Auto Meal: 无效设置",
        NotEnough = "食材数量不足。",
        NotEnoughDetail = "食材数量不足：%s。",
    },
    [CONST.LanguageType.TraditionalChinese] = {
        FromDefault = "已享用默认烹飪組合：<COLOR 00FFFF>%s</COLOR>",
        FromOption = "已享用烹飪組合：<COLOR 00FFFF>%s</COLOR>【<COLOR FFFF00>%s</COLOR>】",

        ManualEat = "手动",
        UseDefaultSetting = "使用默认设置",
        InvalidSetting = "Auto Meal: 無效設定",
        NotEnough = "食材數量不足。",
        NotEnoughDetail = "食材數量不足：%s。",
    },
}

-- app.FacilityUtil isEnoughItem(app.ItemDef.ID, System.Int16, app.ItemUtil.STOCK_TYPE)

local function Localized()
    local lang = Core.GetLanguage()
    if not NOTIFICATION_MSG[lang] then
        lang = CONST.LanguageType.English
    end

    return NOTIFICATION_MSG[lang]
end

-- app.savedata.cEquipIndex
---@param equipParam app.savedata.cEquipParam
---@param myset app.savedata.cEquipMyset
local function EquipSetEquals(equipParam, myset)
    local usingIndex = equipParam._EquipIndex -- TODO: 这里并没有 Weapon type，在哪区分的？
    local box = equipParam._EquipBox

    ---@type app.savedata.cMysetEquipWork[]
    local mysetWorks = myset._MysetEquipWork
    ---@type app.savedata.cEquipIndex
    local mysetIndex = myset._MysetParam._EquipIndex

    local usingIndexArray = usingIndex.Index
    local mysetIndexArray = mysetIndex.Index

    local len = usingIndexArray:get_Count()
    if len ~= mysetIndexArray:get_Count() then
        return false
    end

    for i = 0, len-1, 1 do
        if usingIndexArray:get_Item(i) ~= mysetIndexArray:get_Item(i) then
            return false
        end
    end

    if len ~= mysetWorks:get_Count() then
        return false
    end

    for i = 0, len-1, 1 do
        local idx = usingIndexArray:get_Item(i)
        if idx == -1 then
            goto continue
        end
        local usingWork = box:get_Item(idx)
        local mysetWork = mysetWorks:get_Item(i)
        if usingWork.Category ~= mysetWork.Category then
            return false
        end
        if usingWork.Category == -1 then
            goto continue
        end
        if usingWork.GenderType ~= mysetWork.GenderType then
            return false
        end
        if usingWork.FreeVal0 ~= mysetWork.FreeVal0 then
            return false
        end
        if usingWork.FreeVal1 ~= mysetWork.FreeVal1 then
            return false
        end

        -- TODO:FIXME using 有 FreeVal2/FreeVal3；Myset 有 InsectId/IsNone
        -- if usingWork.FreeVal2 ~= mysetWork.FreeVal2 then
        --     return false
        -- end
        if not Core.CSharpIntArrayEquals(usingWork.EquipmentAccessoryIdArray, mysetWork.EquipmentAccessoryIdArray) then
            return false
        end
        if not Core.CSharpIntArrayEquals(usingWork.BowgunCustomizeId, mysetWork.BowgunCustomizeId) then
            return false
        end

        ::continue::
    end

    return true
end

-- cMealMySet
local MealMySetUtil = Core.WrapTypedef("app.MealMySetUtil")
local MealUtil = Core.WrapTypedef("app.MealUtil")
local ChatLogUtil = Core.WrapTypedef("app.ChatLogUtil")
local ItemUtil = Core.WrapTypedef("app.ItemUtil")
local ItemIDFixedToID = Core.TypeField("app.ItemDef", "FixedToID"):get_data()
local ItemUtil_StockTypePouch = 0
local ItemUtil_StockTypeBox = 1
local ItemUtil_StockTypeBoth = 2
local ItemUtil_StockTypeBoth_PouchBox = 3
local ItemUtil_StockTypeBoth_BoxPouch = 4

---@return app.savedata.cMealMySet
local function GetMealMySet(index, requireValid)
    local data = MealMySetUtil:StaticCall("getMySetData(System.Int32)", index)

    if requireValid == true then
        if data == nil then
            log.error(string.format("Meal at %d is nil", index))
            Core.SendMessage(string.format("Meal at %d is nil", index))
            return nil
        end
        if not data:isValidData() then
            local msgCfg = Localized()
            Core.SendMessage(msgCfg.InvalidSetting)
            return nil
        end
    end
    return data
end

local MealStatusGUI = nil
local function InitGUI()
    if mod.Config.MealUI and MealStatusGUI == nil then
        local comps = Core.FindComponents("app.GUI090201")
        if comps:get_Count() <= 0 then
            return
        end
        MealStatusGUI = comps:get_Item(0)
    end
end

-- DO NOT CALL THIS DIRECTLY
local function ShowMealStatus(mealEffect)
    if not mod.Config.MealUI then return end

    InitGUI()
    if not MealStatusGUI then
        return
    end
    if mealEffect then
        MealStatusGUI:setupParameters(mealEffect)

        -- DO NOT CALL THIS DIRECTLY OR GAME WILL CRASH
        MealStatusGUI:requestOpen()
    end
end

local function RequestMeal(idx, msg)
    RequestMealSetIndex = idx
    RequestMessage = msg
    RequestUTCTime = Core.GetTime() + mod.Config.GUIDelaySeconds
    InitGUI()
    if MealStatusGUI ~= nil then
        MealStatusGUI:set__Timer(3.0) -- indicates it needs to be shown
    end
end


-- 我不知道为什么，但如果预先生成全局变量，那么接取并出发会报错（第一次不会），但是手动、接取但不出发不会报错
---@param idx number custom meal set index
---@return app.cMealEffect
local function MakeMealEffect(idx)
    if idx < 0 then return end

    local meal = GetMealMySet(idx)
    local mysetUse = Core.Ctor("app.cMealMySetUse")
    mysetUse:fromSaveFormat(meal)

    -- 基础食材 PORTABLE_FOOD + 主食物 + 副食物
    -- app.user_data.MealData.cData

    local mealData = Core.GetVariousDataManager()._Setting._FacilitySetting._FacilityDiningSetting._MealData
    local data = mealData:getDataByIndex(mysetUse:get_PortableFoodType())

    local name = Core.GetItemName(mysetUse:get_PortableFoodType())
    -- Core.SendMessage("主食材：" .. tostring(name))

    local mealEffect = Core.Ctor("app.cMealEffect")
    -- nobody knows the bool arg, but it was false 3tims and true at last call
    mealEffect:copyFrom(data, true)

    local ids = {7} -- 7 携带食料
    local itemIDs = mysetUse:get_AddFoods()
    local foodCount = itemIDs:get_Count()
    if foodCount > 0 then
        Core.ForEach(itemIDs, function (id)
            local food = MealUtil:StaticCall("getFoodDataFromItemId(app.ItemDef.ID)", id)
            if mod.Config.Debug then
                Core.SendMessage("SubFood " .. tostring(Core.GetItemName(id)))
            end
            mealEffect:addParam(food, true) -- 预览的话就是 false
            table.insert(ids, id)
        end)
    end

    return mealEffect, ids
end

mod.HookFunc("app.GUIManager", "update()", function ()
    if RequestUTCTime <= 0 then return end
    local cur = Core.GetTime()
    if cur < RequestUTCTime then return end
    RequestUTCTime = 0

    if Core.IsLoading() then return end

    local idx = RequestMealSetIndex
    local msg = RequestMessage
    RequestMealSetIndex = -1 -- make sure no constant call if exeption thrown
    RequestMessage = ""

    local meal, itemIDs = MakeMealEffect(idx)
    if not meal then return end

    local hunter = Core.GetPlayerCharacter()
    if not hunter then return end

    -- setMealEffect throws exception??
    local enough = true
    local s = {}
    if mod.Config.ConsumeItems then
        for _, id in pairs(itemIDs) do
            local haveNum = ItemUtil:StaticCall("getItemNum(app.ItemDef.ID, app.ItemUtil.STOCK_TYPE)", id, ItemUtil_StockTypeBoth)
            if haveNum <= 0 then
                enough = false
                
                if mod.Config.NotifyNoEnoughItems and mod.Config.NotifyNoEnoughItemDetails then
                    local name = Core.GetItemName(id)
                    if mod.Config.Debug then
                        Core.SendMessage("%s wants %d, has %d", name, 1, haveNum)
                    end

                    s[#s+1] = name
                else
                    break
                end
            end
        end
    end
    if enough then
        hunter:get_HunterStatus()._MealEffect:setMealEffect(meal, true)
        if mod.Config.ConsumeItems then
            for _, id in pairs(itemIDs) do
                ItemUtil:StaticCall("changeItemNum(app.ItemDef.ID, System.Int16, app.ItemUtil.STOCK_TYPE)", id, -1, ItemUtil_StockTypeBoth_BoxPouch)
            end
        end

        ShowMealStatus(meal)
        if mod.Config.SystemLog then
            Core.SendMessage(msg)
        end
    else
        if mod.Config.NotifyNoEnoughItems then
            if mod.Config.NotifyNoEnoughItemDetails then
                Core.SendMessage(Localized().NotEnoughDetail, table.concat(s,", "))
            else
                Core.SendMessage(Localized().NotEnough)
            end
        end
    end
end)

Core.OnQuestStartEnter(function()
    if MealStatusGUI ~= nil and MealStatusGUI:get__Timer() > 2 then
        RequestUTCTime = Core.GetTime() + mod.Config.GUIDelaySeconds
    end
end)

local function AutoApplyMeal(setIndex)
    local msg
    local msgCfg = Localized()

    local mgr = Core.GetSaveDataManager()
    local equipMysetName
    if setIndex ~= nil then
        equipMysetName = msgCfg.ManualEat
    elseif setIndex == nil and mgr ~= nil then
        local save = mgr:getCurrentUserSaveData() -- app.savedata.cUserSaveParam
        local equipParam = save:get_Equip() -- app.savedata.cEquipParam
        local equipMyset = equipParam:get_EquipMySet() -- app.savedata.cEquipMyset[]

        local equipIndex = equipParam._EquipIndex -- app.savedata.cEquipIndex

        for i, equipSetIndex in pairs(mod.Config.EquipMySetConfig) do
            local myset = equipMyset:get_Item(i-1)
            if myset._MysetParam.MysetName == "" then
                goto continue
            end

            if EquipSetEquals(equipParam, myset) then
                setIndex = equipSetIndex
                equipMysetName = myset._MysetParam.MysetName
                break
            end

            ::continue::
        end
    end

    if setIndex ~= nil then
        local data = GetMealMySet(setIndex, true)
        if not data then
            return
        end
        msg = string.format(msgCfg.FromOption, data.Name, equipMysetName)
    else
        local weaponType = Core.GetPlayerWeaponType()
        local wpSetIndex = mod.Config.WeaponTypeConfig[weaponType+1]

        if wpSetIndex == -1 and mod.Config.FallbackAnotherWeapon then
            weaponType = Core.GetPlayerSubWeaponType()
            wpSetIndex = mod.Config.WeaponTypeConfig[weaponType+1]
        end

        if wpSetIndex == -1 then
            setIndex = mod.Config.CurrentSet

            local data = GetMealMySet(setIndex, true)
            if not data then
                return
            end
            msg = string.format(msgCfg.FromDefault, data.Name)
        else
            setIndex = wpSetIndex

            local data = GetMealMySet(setIndex, true)
            if not data then
                return
            end
            msg = string.format(msgCfg.FromOption, data.Name, Core.GetWeaponTypeName(weaponType))
        end
    end

    RequestMeal(setIndex, msg)
end

Core.OnAcceptQuest(function ()
    AutoApplyMeal()
end)

local MYSET_MAX = Core.TypeField("app.MealDef", "MYSET_MAX"):get_data() or 40
local MealMySetNames = {}

local function InitMealMySetNames()
    local firstMealIndex = -1

    for i = 0, MYSET_MAX - 1, 1 do
        local myset = GetMealMySet(i) -- cMealMySet
        if myset ~= nil and myset:isValidData() then
            if firstMealIndex == -1 then
                firstMealIndex = i
            end
            MealMySetNames[i] = myset:get_field("Name")

            -- if mod.Config.Debug then
            --     -- mealsData[i] = myset
            --     local msg = "Name " .. MealMySetNames[i] .. ": ".. tostring(myset:get_field("BaseFoodType"))
    
            --     local adds = myset:get_field("AddFood")
            --     local len = adds:call("get_Count")
            --     for j = 0, len - 1, 1 do
            --         local addFood = adds:call("get_Item", j)
            --         msg = msg .. ", " .. tostring(addFood)
            --     end
            --     imgui.text(msg)
            -- end
        end
    end
    MealMySetNames[-1] = Localized().UseDefaultSetting

    return firstMealIndex
end

mod.Menu(function ()
	local configChanged = false
    local changed = false

    changed, mod.Config.ConsumeItems = imgui.checkbox("Consume Items", mod.Config.ConsumeItems)
    configChanged = configChanged or changed

    Imgui.Tree("Notification and Dispaly", function ()
        changed, mod.Config.NotifyNoEnoughItems = imgui.checkbox("Notify No Enough Items", mod.Config.NotifyNoEnoughItems)
        configChanged = configChanged or changed
        changed, mod.Config.NotifyNoEnoughItemDetails = imgui.checkbox("Notify No Enough Item Names", mod.Config.NotifyNoEnoughItemDetails)
        configChanged = configChanged or changed

        changed, mod.Config.MealUI = imgui.checkbox("Display Meal UI", mod.Config.MealUI)
        configChanged = configChanged or changed

        if mod.Config.MealUI then
            changed, mod.Config.GUIDelaySeconds = imgui.slider_float("GUI Delay Seconds", mod.Config.GUIDelaySeconds, 0.0, 6.0)
            configChanged = configChanged or changed
        end

        changed, mod.Config.SystemLog = imgui.checkbox("Display System Notification", mod.Config.SystemLog)
        configChanged = configChanged or changed
    end)

    -- local mealsData = {}
    local firstMealIndex = InitMealMySetNames()

    if firstMealIndex == -1 then
        imgui.text("No meal set")
        return
    end

    Imgui.Rect(function ()
        mod.Runtime.ManualSet = mod.Runtime.ManualSet or mod.Config.CurrentSet
        Imgui.Button("Apply this meal:", function ()
            AutoApplyMeal(mod.Runtime.ManualSet)
            RequestUTCTime = 1
        end)
        imgui.same_line()
        _, mod.Runtime.ManualSet = imgui.combo("Manual Eat This", mod.Runtime.ManualSet, MealMySetNames)
    end)

    imgui.text()

    if imgui.button("Apply Auto Meal Now") then
        AutoApplyMeal()
        RequestUTCTime = 1
    end

    if mod.Config.CurrentSet == -1 then
        mod.Config.CurrentSet = firstMealIndex
    end

    changed, mod.Config.CurrentSet = imgui.combo("Default Set", mod.Config.CurrentSet, MealMySetNames)
    configChanged = configChanged or changed

    -- imgui.text("Current Set: " .. tostring(mod.Config.CurrentSet))
    if mod.Config.Debug then
        if mod.Config.MealUI then
            InitGUI()
            imgui.text("Timer: " .. tostring(MealStatusGUI:get__Timer()))
        end
        imgui.text("UTCTime: " ..tostring(Core.GetTime()))
        imgui.text("Req UTCTime: " ..tostring(RequestUTCTime))
    end

    if imgui.tree_node("Weapon Type") then
        changed, mod.Config.FallbackAnotherWeapon =
            imgui.checkbox("Use Another Weapon If Main Weapon is Default", mod.Config.FallbackAnotherWeapon)
        configChanged = configChanged or changed

        local wpType = Core.GetPlayerWeaponType()
        local wp2Type = Core.GetPlayerSubWeaponType()
        for i = 0, 14-1, 1 do
            local weaponName = Core.GetWeaponTypeName(i)

            local opened = imgui.tree_node(weaponName)
            if mod.Config.WeaponTypeConfig[i+1] ~= -1 then
                imgui.same_line()
                imgui.text(MealMySetNames[mod.Config.WeaponTypeConfig[i+1]])
            end
            if i == wpType then
                imgui.same_line()
                imgui.text("#1 weapon type")
            end
            if i == wp2Type then
                imgui.same_line()
                imgui.text("#2 weapon type")
            end

            if opened then
                changed, mod.Config.WeaponTypeConfig[i+1] = imgui.combo(weaponName, mod.Config.WeaponTypeConfig[i+1], MealMySetNames)
                configChanged = configChanged or changed

                imgui.tree_pop()
            end
        end

        imgui.tree_pop()
    end

    if imgui.tree_node("Equipment Loadout") then
        local mgr = Core.GetSaveDataManager()
        local save = mgr:getCurrentUserSaveData() -- app.savedata.cUserSaveParam
        local equipParam = save:get_Equip() -- app.savedata.cEquipParam
        local equipMyset = equipParam:get_EquipMySet() -- app.savedata.cEquipMyset[]

        Core.ForEach(equipMyset, function (myset, i)
            local name = myset._MysetParam.MysetName
            if name == "" then
                mod.Config.EquipMySetConfig[i+1] = nil
                return
            end
            -- EquipMySetData[i] = myset
            -- EquipMySetNameMap[i] = name
            if mod.Config.EquipMySetConfig[i+1] == nil then
                mod.Config.EquipMySetConfig[i+1] = -1
            end

            local equipWorks = myset._MysetEquipWork

            -- 0 and 7: EQUIP_INDEX WEAPON, RESERVE_WEAPON
            local wpMsg = string.format("%s|%s", Core.GetWeaponTypeName(equipWorks:get_Item(0).FreeVal0), Core.GetWeaponTypeName(equipWorks:get_Item(7).FreeVal0))

            local opened = imgui.tree_node(name .. ": " .. wpMsg)

            if mod.Config.EquipMySetConfig[i+1] ~= -1 then
                imgui.same_line()
                imgui.text(MealMySetNames[mod.Config.EquipMySetConfig[i+1]])
            end

            if EquipSetEquals(equipParam, myset) then
                imgui.same_line()
                imgui.text("using")
            end

            if opened then
                changed, mod.Config.EquipMySetConfig[i+1] = imgui.combo(name, mod.Config.EquipMySetConfig[i+1], MealMySetNames)
                configChanged = configChanged or changed

                imgui.tree_pop()
            end
        end)

        imgui.tree_pop()
    end

    return configChanged
end)
