-----------------------------------
-- Instance
-----------------------------------
-- TOAU storyline missions
-- Salvage
-- WOTG storyline missions
-- Certain Campaign Ops
-- Moblin Maze Mongers
-- The Notorious Monster battles that occur when Sandworm sucks in players using Doomvoid
--
-- Instance zones:
-- - ILRUSI_ATOLL                    = 55,
-- - PERIQIA                         = 56,
-- - THE_ASHU_TALIF                  = 60,
-- - LEBROS_CAVERN                   = 63,
-- - MAMOOL_JA_TRAINING_GROUNDS      = 66,
-- - LEUJAOAM_SANCTUM                = 69,
-- - ZHAYOLM_REMNANTS                = 73,
-- - ARRAPAGO_REMNANTS               = 74,
-- - BHAFLAU_REMNANTS                = 75,
-- - SILVER_SEA_REMNANTS             = 76,
-- - NYZUL_ISLE                      = 77,
-- - EVERBLOOM_HOLLOW                = 86,
-- - RUHOTZ_SILVERMINES              = 93,
-- - GHOYUS_REVERIE                  = 129,
-- - MAQUETTE_ABDHALJS_LEGION_A      = 183,
-- - RALA_WATERWAYS_U                = 259,
-- - YORCIA_WEALD_U                  = 264,
-- - CIRDAS_CAVERNS_U                = 271,
-- - OUTER_RAKAZNAR_U1               = 275,
-- - MAQUETTE_ABDHALJS_LEGION_B      = 287, -- See: ambuscade.lua
-- - DYNAMIS_SAN_DORIA_D             = 294,
-- - DYNAMIS_BASTOK_D                = 295,
-- - DYNAMIS_WINDURST_D              = 296,
-- - DYNAMIS_JEUNO_D                 = 297,
-----------------------------------
xi = xi or {}
xi.instance = {}

--[[
    [zoneId] =
    {
        {
            instanceIdInDatabase,
            onTrigger startEvent args (to be unpacked),
            onEventFinish valid entry args for registrant (to be unpacked),
            event args for joining party members (to be unpacked)
        }
    },
--]]

xi.instance.lookup =
{
    [xi.zone.ILRUSI_ATOLL] =
    {
        -- Assault: Golden Salvage
        -- Assault: Lamia No.13
        -- Assault: Extermination
        -- Assault: Demolition Duty
        -- Assault: Searat Salvation
        -- Assault: Apkallu Seizure
        -- Assault: Lost and Found
        -- Assault: Deserter
        -- Assault: Desperately Seeking Cephalopods
        -- Assault: Bellerophon's Bliss
    },

    [xi.zone.PERIQIA] =
    {
        { 5600, { 143, 79, -6, 0, 99, 3, 0 }, { 143, 4 }, { 147, 3 } }, -- Shades of Vengeance (TOAU31)
        { 5601, { 143, 31, -4, 0, 70, 0, 1 }, { 143, 4 }, { 147, 0 } }, -- Assault: Seagull Grounded
        -- Assault: Requiem
        -- Assault: Saving Private Ryaaf
        -- Assault: Shooting Down the Baron
        -- Assault: Stop the Bloodshed
        -- Assault: Defuse the Threat
        -- Assault: Operation: Snake Eyes
        -- Assault: Wake the Puppet
        -- Assault: The Price is Right
    },

    [xi.zone.THE_ASHU_TALIF] =
    {
        { 6000, { 221, 53, -6, 0, 99, 6, 0 }, { 221, 4 }, { 222, 6 } }, -- The Black Coffin (TOAU 15)
        { 6001, { 221, 54, -9, 0, 99, 6, 0 }, { 221, 4 }, { 222, 6 } }, -- Against All Odds
        -- Testing the Waters (TOAU 34)
        -- Legacy of the Lost (TOAU 35)
        -- Assault: Royal Painter Escort
        -- Assault: Scouting the Ashu Talif
        -- Assault: Targeting the Captain
    },

    [xi.zone.LEBROS_CAVERN] =
    {
        { 6300, { 203, 21, -4, 0, 50, 0, 1 }, { 203, 4 }, { 208, 0 } }, -- Assault: Excavation Duty
        -- Assault: Lebros Supplies
        -- Assault: Troll Fugitives
        -- Assault: Evade and Escape
        -- Assault: Siegemaster Assassination
        -- Assault: Apkallu Breeding
        -- Assault: Wamoura Farm Raid
        -- Assault: Egg Conservation
        -- Assault: Operation: Black Pearl
        -- Assault: Better Than One
    },

    [xi.zone.MAMOOL_JA_TRAINING_GROUNDS] =
    {
        { 6600, { 505, 11, -4, 0, 60, 0, 1 }, { 505, 4 }, { 511, 0 } }, -- Assault: Imperial Agent Rescue
        -- Assault: Preemptive Strike
        -- Assault: Sagelord Elimination
        -- Assault: Breaking Morale
        -- Assault: The Double Agent
        -- Assault: Imperial Treasure Retrieval
        -- Assault: Blitzkrieg
        -- Assault: Marids in the Mist
        -- Assault: Azure Ailments
        -- Assault: The Susanoo Shuffle
    },

    [xi.zone.LEUJAOAM_SANCTUM] =
    {
        { 6900, { 140, 1, -4, 0, 50, 0, 1 }, { 140, 4 }, { 147, 0 } }, -- Assault: Leujaoam Cleansing
        -- Assault: Orichalcum Survey
        -- Assault: Escort Professor Chanoix
        -- Assault: Shanarha Grass Conservation
        -- Assault: Counting Sheep
        -- Assault: Supplies Recovery
        -- Assault: Azure Experiments
        -- Assault: Imperial Code
        -- Assault: Red Versus Blue
        -- Assault: Bloody Rondo
    },

    [xi.zone.ZHAYOLM_REMNANTS] =
    {
        { 7300, { 407, 0, -6, 0, 0, 7 }, { 407, 4 }, { 411, 7 } }, -- Salvage I, Zhayolm Remnants
    },

    [xi.zone.ARRAPAGO_REMNANTS] =
    {
        { 7400, { 408, 0, -6, 0, 0, 8 }, { 408, 4 }, { 411, 8 } }, -- Salvage I, Arrapago Remnants
    },

    [xi.zone.BHAFLAU_REMNANTS] =
    {
        { 7500, { 409, 0, -6, 0, 0, 9 }, { 409, 4 }, { 411, 9 } }, -- Salvage I, Bhaflau Remnants
    },

    [xi.zone.SILVER_SEA_REMNANTS] =
    {
        { 7600, { 410, 0, -6, 0, 0, 10 }, { 410, 4 }, { 411, 10 } }, -- Salvage I, Silver Sea Remnants
    },

    [xi.zone.NYZUL_ISLE] =
    {
        { 7700, { 405, 58,  -6, 0, 99, 5, 0 }, { 116, 1 }, { 411, 5 } },        -- Path of Darkness
        { 7701, { 405, 59, -10, 0, 99, 5, 0 }, { 116, 1 }, { 411, 5 } },        -- Nashmeira's Plea
        { 7702, { 405, 60, -34, 0, 99, 5, 1, 0, 14 }, { 116, 1 }, { 411, 5 } }, -- Waking the Colossus/Divine Interference
        -- Forging a New Myth
        { 7704, { 405, 51,  -4, 0, 75, 5, 1 }, { 116, 2 }, { 411, 5 } },        -- Nyzul Isle Investigation
    },

    [xi.zone.EVERBLOOM_HOLLOW] =
    {
        -- Honor Under Fire
        -- Bonds That Never Die
        -- A Nation on the Brink
        -- Dungeons and Dancers
        -- Campaign Ops:
        -- Brave Dawn I (San d'Oria)
        -- Brave Dawn II (San d'Oria)
        -- Brave Dawn III (San d'Oria)
        -- Granite Rose I (San d'Oria)
        -- Granite Rose II (San d'Oria)
        -- Granite Rose III (San d'Oria)
        -- Pit Spider I (San d'Oria)
        -- Pit Spider II (San d'Oria)
        -- Pit Spider III (San d'Oria)
        -- Doomvoid - King Arthro
        -- Doomvoid - Lambton Worm
        -- Moblin Maze Mongers
    },

    [xi.zone.RUHOTZ_SILVERMINES] =
    {
        { 9300, {   3, 0, 0, 19 }, {   3, 4 }, {   4, 1 } }, -- Light in the Darkness (WOTG Bastok Quest 3)
        { 9301, { 203, 0, 0, 36 }, { 203, 4 }, { 201, 1 } }, -- Fire in the Hole (WOTG Bastok Quest 6)
        -- { 0, { 0,  0, 34 } }, -- Seeing Blood-red (SCH AF3)
        -- { 0, { 0, 23,  0 } }, -- Distorter of Time
        -- Campaign Ops:
        -- Brave Dawn I (Bastok)
        -- Brave Dawn II (Bastok)
        -- Brave Dawn III (Bastok)
        -- By Light of Fire I (Bastok)
        -- Granite Rose I (Bastok)
        -- Granite Rose II (Bastok)
        -- Granite Rose III (Bastok)
        -- Pit Spider I (Bastok)
        -- Pit Spider II (Bastok)
        -- Doomvoid - Lambton Worm
        -- Doomvoid - Guivre
        -- Moblin Maze Mongers
    },

    [xi.zone.GHOYUS_REVERIE] =
    {
        -- A Feast for Gnats
        -- A Manifest Problem
        -- In a Haze of Glory
        -- Sins of the Mothers
        -- Campaign Ops:
        -- Brave Dawn I (Windurst)
        -- Brave Dawn II (Windurst)
        -- Brave Dawn III (Windurst)
        -- Granite Rose I (Windurst)
        -- Granite Rose II (Windurst)
        -- Pit Spider I (Windurst)
        -- Pit Spider II (Windurst)
        -- Doomvoid - Lambton Worm
        -- Doomvoid - Serket
    },

    [xi.zone.MAQUETTE_ABDHALJS_LEGION_A] =
    {
        -- Hall of An (Leader entry: { 8009, 0, 0, 0, 0, 6, 1 }, Party entry: { 8003, 0, 0, 0, 0, 6, 1 })
        -- Hall of Ki
        -- Hall of Im
        -- Hall of Muru
        -- Hall of Mul
    },

    [xi.zone.RALA_WATERWAYS_U] =
    {
        -- {  0, 0 }, -- Endeavoring to Awaken
        -- {  1, 0 }, -- Endeavoring to Awaken
        -- -- Blank
        { 25900, { 5511, 258, 8 }, { 5511, 8 }, { 258, 8 } }, -- Behind the Sluices
        -- {  4, 0 }, -- Stonewalled
        -- {  5, 0 }, -- The Gates
        -- {  6, 0 }, -- Saved by the Bell
        -- {  7, 0 }, -- Quiescence
        -- {  8, 0 }, -- The Charlatan
        -- {  9, 0 }, -- Yggdrasil Beckons
        -- { 10, 0 }, -- Yggdrasil Beckons
        -- { 11, 0 }, -- Watery Grave
        -- { 12, 0 }, -- Mistress of Ceremonies
        -- { 13, 0 }, -- A Barrel of Laughs
        -- { 14, 0 }, -- Sinister Reign
        -- { 15, 0 }, -- The Ygnas Directive 6
        -- { 16, 0 }, -- Skirmishes
        -- { 17, 0 }, -- Fractures
        -- { 18, 0 }, -- Alluvion skirmishes
        -- { 19, 0 }, -- The Silent Forest
        -- { 20, 0 }, -- Wind of Eternity
    },

    [xi.zone.YORCIA_WEALD_U] =
    {

    },

    [xi.zone.CIRDAS_CAVERNS_U] =
    {

    },

    [xi.zone.OUTER_RAKAZNAR_U1] =
    {

    },

    [xi.zone.DYNAMIS_SAN_DORIA_D] =
    {

    },

    [xi.zone.DYNAMIS_BASTOK_D] =
    {

    },

    [xi.zone.DYNAMIS_WINDURST_D] =
    {

    },

    [xi.zone.DYNAMIS_JEUNO_D] =
    {

    },
}

local getInstanceName = function(player, instanceId)
    return switch (instanceId) : caseof
    {
        [7702] = function()
            if player:getQuestStatus(xi.questLog.AHT_URHGAN, xi.quest.id.ahtUrhgan.DIVINE_INTERFERENCE) >= xi.questStatus.QUEST_ACCEPTED then
                return 1 -- Divine Interference
            else
                return 0 -- Waking the Colossus
            end
        end,
    }
end

-- Party leader registering
local checkRegistryReqs = function(player, instanceId)
    local instanceObj = GetCachedInstanceScript(instanceId)
    if type(instanceObj.registryRequirements) == 'function' then
        return instanceObj.registryRequirements(player)
    else
        print('xi.instance: checkReqs: registryRequirements function not set for instance: ' .. instanceId)
        return false
    end
end

-- Further players joining
local checkEntryReqs = function(player, instanceId)
    local instanceObj = GetCachedInstanceScript(instanceId)
    if type(instanceObj.entryRequirements) == 'function' then
        return instanceObj.entryRequirements(player)
    else
        print('xi.instance: checkReqs: entryRequirements function not set for instance: ' .. instanceId)
        return false
    end
end

xi.instance.onTrade = function(player, npc, trade)
end

xi.instance.onTrigger = function(player, npc, instanceZoneID)
    local zoneLookup = xi.instance.lookup[instanceZoneID]

    -- Clear up after possible failed loads
    player:setLocalVar('INSTANCE_REQUESTED', 0)
    local existingInstance = player:getInstance()
    if existingInstance then
        existingInstance:fail()
    end

    -- Find the first instance you're valid for
    -- TODO: Handle being valid for multiple instances from the same entrance
    local chosenEntry
    for _, entry in ipairs(zoneLookup) do
        local instanceId    = entry[1]
        local hasValidEntry = checkRegistryReqs(player, instanceId)

        if hasValidEntry then
            chosenEntry = entry
            break
        end
    end

    if chosenEntry == nil then
        return false
    end

    -- Play the cs + args for that instance
    local instanceId          = chosenEntry[1]
    local instanceTriggerArgs = chosenEntry[2]
    local hasValidEntry       = checkRegistryReqs(player, instanceId)

    if hasValidEntry then
        player:setLocalVar('INSTANCE_ID', instanceId)
        if instanceTriggerArgs[8] ~= nil then
            instanceTriggerArgs[8] = getInstanceName(player, instanceId)
        end

        player:startEvent(unpack(instanceTriggerArgs))

        return true
    else
        return false
    end
end

xi.instance.onEventUpdate = function(player, csid, option, npc)
    local instanceId = player:getLocalVar('INSTANCE_ID')
    local party      = player:getParty()
    local ID         = zones[player:getZoneID()]

    if party ~= nil then
        for _, v in pairs(party) do
            if
                v:getID() ~= player:getID() and
                v:getZoneID() == player:getZoneID()
            then
                -- Check entry requirements for party
                if not checkEntryReqs(v, instanceId) then
                    player:messageText(npc, ID.text.MEMBER_NO_REQS, false)
                    player:instanceEntry(npc, 1)

                    return false
                end

                -- Check everyone is in range
                if v:checkDistance(player) > 50 then
                    player:messageText(npc, ID.text.MEMBER_TOO_FAR, false)
                    player:instanceEntry(npc, 1)

                    return false
                end
            end
        end
    end

    if player:getLocalVar('INSTANCE_REQUESTED') == 0 then
        player:createInstance(instanceId)
        player:setLocalVar('INSTANCE_REQUESTED', 1)
    end

    if
        player:getInstance() ~= nil or
        (player:getLocalVar('INSTANCE_REQUESTED') > 0 and
        player:getLocalVar('INSTANCE_REQUESTED') < 10)
    then
        -- return true so we don't immediately call the cancel event update (since instances don't immediately load), but
        -- increment variable to eventually return false if instance fails to load and trigger onInstanceCreatedCallback
        player:setLocalVar('INSTANCE_REQUESTED', player:getLocalVar('INSTANCE_REQUESTED') + 1)
        return true
    else
        return false
    end
end

-- 'Default' behavior. It's up to each instance whether or not they want to use this logic
xi.instance.onInstanceCreatedCallback = function(player, instance)
    local zoneLookup = xi.instance.lookup[instance:getZone():getID()]
    local instanceId = instance:getID()

    -- Collect cs for party members
    local lookupEntry
    for _, entry in ipairs(zoneLookup) do
        local entryInstanceId = entry[1]
        if instanceId == entryInstanceId then
            lookupEntry = entry

            break
        end
    end

    -- If you're in the official entrance zone, try and playout the
    -- entrance animation. Otherwise: go straight to the instance
    if player:getZoneID() == instance:getEntranceZoneID() then
        -- join initiating player as commander
        player:setInstance(instance)

        -- This packet will trigger the end of the blocking
        -- cutscene and xi.instance.onEventFinish will handle
        -- the transportation
        for _, v in ipairs(player:getParty()) do
            if v:getZoneID() == player:getZoneID() then
                if v:getID() ~= player:getID() then
                    -- player will be brought into instance either way
                    -- this makes the animation trigger reliably
                    v:release()
                    v:startEvent(unpack(lookupEntry[4]))

                    v:setInstance(instance)
                    local npc = player:getEventTarget()
                    if npc ~= nil then
                        v:instanceEntry(npc, 4)
                    end
                end

                v:timer(35000, function(playerArg)
                    -- failsafe to bring all players into instance
                    -- if a player doesn't receive the event packet, the whole party will be stuck in blackscreen
                    -- timer is destroyed if onEventFinish works properly and player gets zoned
                    -- a properly-functioning event loop will take 20s to zoning into the instance
                    -- this _should_ be completely unnecessary due to the looping logic with INSTANCE_REQUESTED, but just in case
                    local instanceArg = playerArg:getInstance()
                    if instanceArg then
                        print(fmt('Player {} failed to cleanly transition into instance event, forcing entry via setPos.', playerArg:getName()))
                        playerArg:setPos(0, 0, 0, 0, instanceArg:getZone():getID())
                    end
                end)
            end
        end

        -- finally, send commander in
        player:startEvent(unpack(lookupEntry[4])) -- will fail if previous event is working as it should, otherwise catches secondary event to enter
        local npc = player:getEventTarget()
        if npc ~= nil then
            player:instanceEntry(npc, 4)
        end
    else
        for _, v in ipairs(player:getParty()) do
            v:setInstance(instance)
            v:setPos(0, 0, 0, 0, instance:getZone():getID())
        end
    end
end

xi.instance.onEventFinish = function(player, csid, option, npc)
    local instance = player:getInstance()

    if instance then
        local instanceZoneId         = instance:getZone():getID()
        local zoneLookup             = xi.instance.lookup[instanceZoneId]
        local csidEntry, optionEntry = unpack(zoneLookup[1][3])

        if csid == csidEntry and option == optionEntry then
            for _, v in ipairs(player:getParty()) do
                v:setPos(0, 0, 0, 0, instance:getZone():getID())
            end

            return true
        end
    end

    return false
end

local function setInstanceLastTimeUpdateMessage(instance, players, remainingTimeLimit, text)
    local message        = 0
    local lastTimeUpdate = instance:getLastTimeUpdate()

    if lastTimeUpdate == 0 and remainingTimeLimit < 600 then
        message = 600
    elseif lastTimeUpdate == 600 and remainingTimeLimit < 300 then
        message = 300
    elseif lastTimeUpdate == 300 and remainingTimeLimit < 60 then
        message = 60
    elseif lastTimeUpdate == 60 and remainingTimeLimit < 30 then
        message = 30
    elseif lastTimeUpdate == 30 and remainingTimeLimit < 10 then
        message = 10
    end

    if message ~= 0 then
        for i, player in pairs(players) do
            if remainingTimeLimit >= 60 then
                player:messageSpecial(text.TIME_REMAINING_MINUTES, message / 60)
            else
                player:messageSpecial(text.TIME_REMAINING_SECONDS, message)
            end
        end

        instance:setLastTimeUpdate(message)
    end
end

xi.instance.updateInstanceTime = function(instance, elapsed, text)
    local players            = instance:getChars()
    local remainingTimeLimit = instance:getTimeLimit() * 60 - (elapsed / 1000)
    local wipeTime           = instance:getWipeTime()

    if
        remainingTimeLimit < 0 or
        (wipeTime ~= 0 and (elapsed - wipeTime) / 1000 > 180
        )
    then
        instance:fail()

        return
    end

    if wipeTime == 0 then
        local wipe = true

        for i, player in pairs(players) do
            if player:getHP() ~= 0 then
                wipe = false
                break
            end
        end

        if wipe then
            for i, player in pairs(players) do
                player:messageSpecial(text.PARTY_FALLEN, 3)
            end

            instance:setWipeTime(elapsed)
        end
    else
        for i, player in pairs(players) do
            if player:getHP() ~= 0 then
                instance:setWipeTime(0)
                break
            end
        end
    end

    setInstanceLastTimeUpdateMessage(instance, players, remainingTimeLimit, text)
end
