--[[
-- Roomba
 - (Thanks to BalkiTheWise for the name)
 -
 ]]

Roomba = {}

LibStub("AceTimer-3.0"):Embed(Roomba)
local LAM = LibStub:GetLibrary("LibAddonMenu-1.0")
local BACKPACK = ZO_PlayerInventoryBackpack.data
local GUILDBANK = ZO_GuildBankBackpack.data
local DELAY = 150

local containerHooks = { INVENTORY_BACKPACK, INVENTORY_BANK, INVENTORY_GUILD_BANK }

Roomba.currentInventory = {}
Roomba.guildInfo = {}

local function GetItemID(link)
	return tonumber(string.match(string.match(link, "%d+:"), "%d+"))
end

function GetInstanceId(target, slotId)
    for i,v in ipairs(target) do
        if v.data.slotIndex == slotId then return v.data.itemInstanceId end
    end
	return nil
end

function FindSlot(target, slotId)
	for i,v in ipairs(target) do
		if v.data.slotIndex == slotId then return i,v.data end
	end
	return nil
end

local function ClearGuildDetails(guildId)
	return {	name = GetGuildName(guildId),
				lookUp = {},
				duplicates = {},
				index = {}
			}
end

-- Who is your Guild and what do they do
local function GetGuildDetails()
	for guildIndex = 1, GetNumGuilds()	do
		local guildId = GetGuildId(guildIndex)
		if DoesGuildHavePrivilege(guildId, GUILD_PRIVILEGE_BANK_DEPOSIT) then
			Roomba.guildInfo[guildId] = true
		end
	end
end

-- Okay, let's see.
-- First we'll take current bag inventory.
local function CheckWeHaveEnoughRoom()
	return CheckInventorySpaceAndWarn(2)
end

-- Now take stock of everything so we don't accidentally move stuff out
-- of our inventory into the bank
local function AuditCurrentInventory()
	local _, totalSlots = GetBagInfo(INVENTORY_BACKPACK)
	Roomba.currentInventory = {}
	for slot=1, totalSlots do
		local id = GetItemInstanceId(INVENTORY_BACKPACK, slot)
		local count = GetItemTotalCount(INVENTORY_BACKPACK, slot)
		Roomba.currentInventory[slot] = id
	end
end

local checkingBank = false
local currentBank = 1

-- Bank is ready! Find those duplicates!
function Roomba.RoombaReady()
	if not checkingBank then checkingBank = true else return end
	local bank = Roomba.guildInfo[GetSelectedGuildBankId()]
	if not bank then return end
	Roomba.guildInfo[GetSelectedGuildBankId()] = ClearGuildDetails(GetSelectedGuildBankId())
	bank = Roomba.guildInfo[GetSelectedGuildBankId()]
	d("Bank is ready: " ..#GUILDBANK)
    -- We only need to store A) slots with items
	for index, slot in ipairs(GUILDBANK) do
        if slot.data.equipType == 0 then -- Equipped gear cannot be stacked
            local id = slot.data.itemInstanceId
            local count, stack = GetSlotStackSize(slot.data.bagId, slot.data.slotIndex)
            -- and B) slots with items that aren't full stacks or single unstackables
            if count ~= stack then
                if bank.lookUp[id] then -- If we've run across this item before
                    if not bank.duplicates[id] then -- store the table reference as a duplicate
                        bank.duplicates[id] = bank.lookUp[id]
                    end
                else
                    bank.lookUp[id] = {} -- It's a new item!
                end
                -- Now group all items by id
                table.insert(bank.lookUp[id], {slot = slot.data.slotIndex, count = count})
            end
        end
    end
    d("Number of duplicates: " .. #bank.duplicates)
end

--[[
Right, so this got complicated. Simply calling TransferFromGuildBank did not seem reliable, it would perhaps
transfer one item in the loop, and disregard the rest.

So instead we're going event based responses. This has the problem of breaking up the flow in a way that might
be a little complicated to follow, but I'll try to make it simple, if for the basic reason that I have to read
this crap afterwards.

Let's hope this works.
]]--
local cInstanceId = nil
local cDuplicateList = nil
local cSlotIdx = nil
local cSlot = nil
local baseSlot = nil

local inBagCollection = {}

local function ResetAll()
	cSlotIdx = nil
	cSlot = nil
	inBagCollection = {}
	baseSlot = nil
end

function Roomba.SelectGuildBank(...)
	local eventId, guildBankId = ...
	currentBank = guildBankId
	checkingBank = nil
end

transitBag = {}
local currentReturnIndex

function Roomba.ReturnItemsToBank(...)
    local error = ...
    local slot
	-- Now that we've stacked it all lets move it back
    d("Getting next to move back to bank")
    currentReturnIndex, slot = next(inBagCollection)
    if slot then
        if error then
            d("trying again")
            return zo_callLater(Roomba.ReturnItemsToBank, 2000)
        end
        if FindSlot(BACKPACK, slot.bagSlot) then
            d("Moving stuff back from ".. slot.bagSlot)
            return TransferToGuildBank(INVENTORY_BACKPACK, slot.bagSlot)
        else
            -- we have a space, move to next
            table.remove(inBagCollection, currentReturnIndex)
            return zo_callLater(Roomba.ReturnItemsToBank, DELAY)
        end
    else
        -- No more to move, we're complete
        d("unregistering")
        EVENT_MANAGER:UnregisterForEvent("RoombaGuildBankError", EVENT_GUILD_BANK_TRANSFER_ERROR)
        EVENT_MANAGER:UnregisterForEvent("RoombaGuildBankSuccess", EVENT_GUILD_BANK_ITEM_ADDED)
        -- Kick off the next transaction
        d("Finished processing item")
        ResetAll()
        return zo_callLater(Roomba.BeginProcess, DELAY)
    end
end

function Roomba.BankItemsReceived(...)
	local _, gslot = ...
    local id = GetInstanceId(GUILDBANK, gslot)
    if id ~= cInstanceId then return end
    d("Moved successfully to slot " ..gslot)
    -- We've moved one back! Remove it from contention
    local slot = table.remove(inBagCollection, currentReturnIndex)
    currentReturnIndex = nil
    d("Clearing slot " .. slot.bagSlot)
    return zo_callLater(Roomba.ReturnItemsToBank, DELAY)
end


function Roomba.StartStacking()
	d("Found an item to stack")
	baseSlot = nil
    EVENT_MANAGER:UnregisterForEvent("RoombaInventoryAdded", EVENT_INVENTORY_SINGLE_SLOT_UPDATE)
	for _,slot in pairs(inBagCollection) do
		if not baseSlot then
			-- We want to stack everything on this
			d("Setting ".. slot.bagSlot .." as base slot")
			baseSlot = slot
		else
            baseSlot.count, baseSlot.stack = GetSlotStackSize(INVENTORY_BACKPACK, slot.bagSlot)
			-- Find out how much we can fit
			-- If it's too much, move all we can and target this slot as the next to fill
			if (baseSlot.stack - baseSlot.count) < slot.count then
				result = CallSecureProtected("PickupInventoryItem", INVENTORY_BACKPACK, slot.bagSlot, baseSlot.stack - baseSlot.count)
				if (result) then
					result = CallSecureProtected("PlaceInInventory", INVENTORY_BACKPACK, baseSlot.bagSlot)
				end
				baseSlot = slot
			else -- Just move it all over
				result = CallSecureProtected("PickupInventoryItem", INVENTORY_BACKPACK, slot.bagSlot, slot.count)
				if (result) then result = CallSecureProtected("PlaceInInventory", INVENTORY_BACKPACK, baseSlot.bagSlot) end
			end
			ClearCursor()
            d("Stacked item in backpack")
		end
	end
	baseSlot = nil
    -- These events will loop the move back to the guild bank
	EVENT_MANAGER:RegisterForEvent("RoombaGuildBankError", EVENT_GUILD_BANK_TRANSFER_ERROR, Roomba.ReturnItemsToBank)
	EVENT_MANAGER:RegisterForEvent("RoombaGuildBankSuccess", EVENT_GUILD_BANK_ITEM_ADDED, Roomba.BankItemsReceived)
    return zo_callLater(Roomba.ReturnItemsToBank, 1000)
end


-- Event called that item has arrived
function Roomba.ReceiveItems(...)
	if baseSlot then return end -- We're still stacking
	local _, bagId, slotId = ...
    if bagId ~= INVENTORY_BACKPACK then return end
	if not cInstanceId then return end
    if not GetInstanceId(BACKPACK, slotId) then return end -- are we empty
	if GetInstanceId(BACKPACK, slotId) ~= cInstanceId then return end
    d("Checking " .. bagId .. " inventory slot " .. slotId .. " to match ".. cInstanceId .. " with " .. (GetInstanceId(BACKPACK, slotId) or "nothing"))
    SetItemIsJunk(INVENTORY_BACKPACK, slotId, false)
    cSlot.bagSlot = slotId
	table.insert(inBagCollection, cSlot)
	-- If we have another slot to move
	if next(cDuplicateList, cSlotIdx) then
		cSlotIdx, cSlot = next(cDuplicateList, cSlotIdx)
		TransferFromGuildBank(cSlot.slot)
	else -- No more slots to move, let's stack them
		return zo_callLater(Roomba.StartStacking, DELAY)
	end
end

function Roomba.BeginProcess()
    local bank = Roomba.guildInfo[currentBank]
    if not bank then return end

    cSlotIdx = nil

    -- Pull the next job off the stack
    cInstanceId, cDuplicateList = next(bank.duplicates, cInstanceId)

    if cDuplicateList then -- First off the rank
        cSlotIdx, cSlot = next(cDuplicateList, cSlotIdx)

        -- Clear this batch
        inBagCollection = {}
        -- And start off the job
        d("Stacking up ".. zo_strformat(SI_TOOLTIP_ITEM_NAME, GetItemLink(3, cSlot.slot, LINK_STYLE_DEFAULT)))
        EVENT_MANAGER:RegisterForEvent("RoombaInventoryAdded", EVENT_INVENTORY_SINGLE_SLOT_UPDATE, Roomba.ReceiveItems)
        SetItemIsJunk(3, cSlot.slot, false) -- unjunk everything
        TransferFromGuildBank(cSlot.slot)
    else
        d("Nothing to restack")
        return false
    end
    return true
end

local function RoombaLoaded(eventCode, addOnName)

	if(addOnName ~= "Roomba") then
        return
    end

	local defaults = {
	}

	GetGuildDetails()
	SLASH_COMMANDS["/roomba"] = Roomba.BeginProcess

	settings = ZO_SavedVars:New("Roomba_Settings", 3, nil, defaults)

    -- Guild bank is evented to be ready, but wait a short while before processing.
	EVENT_MANAGER:RegisterForEvent("RoombaReady", EVENT_GUILD_BANK_ITEMS_READY, function() zo_callLater(Roomba.RoombaReady, 1000) end)
	-- Clear the flag when swapping banks
	EVENT_MANAGER:RegisterForEvent("RoombaSelected", EVENT_GUILD_BANK_SELECTED, Roomba.SelectGuildBank)
end

EVENT_MANAGER:RegisterForEvent("RoombaLoaded", EVENT_ADD_ON_LOADED, RoombaLoaded)