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

Roomba = ZO_Object:Subclass()

function Roomba:New(...)
    local roomba = ZO_Object.New(self)
    roomba:Initialise(...)
    return roomba
end

local LAM = LibStub:GetLibrary("LibAddonMenu-1.0")
local BACKPACK = ZO_PlayerInventoryBackpack.data
local GUILDBANK = ZO_GuildBankBackpack.data
local WM = WINDOW_MANAGER
local DELAY = 100
local settings = {}
local format = "                     %3d"
local containerHooks = { INVENTORY_BACKPACK, INVENTORY_BANK, INVENTORY_GUILD_BANK }
local currentRun = {}

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

local 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

local 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

local function TableEntryCount(t)
	local i = next(t)
	local c = 0
	while i do
		c = c + 1
		i = next(t, i)
	end
	return c
end

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

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

function Roomba:HaveStuffToStack()
	local bank = self.guildInfo[GetSelectedGuildBankId()]
	if type(bank) == "table" and next(bank.duplicates) then return true end
	return false
end

local checkingBank = false

-- Bank is ready! Find those duplicates!
function Roomba:RoombaReady()
	if ZO_GuildBankBackpack:IsHidden() then return end
	-- Are we in the process of checking the bank?
	if not checkingBank then checkingBank = true else return end

	local selectedBankId = GetSelectedGuildBankId()

	if not self.guildInfo[selectedBankId] then return end

	self.guildInfo[selectedBankId] = ClearGuildDetails(selectedBankId)
	local bank = self.guildInfo[selectedBankId]

	-- 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, texture = slot.data.iconFile, name = slot.data.name})
            end
        end
    end
    zo_callLater(function() checkingBank = nil end, 3000)
    if KEYBIND_STRIP:HasKeybindButtonGroup(self.runDescriptor) then
    	KEYBIND_STRIP:UpdateKeybindButtonGroup(self.runDescriptor)
    end
    currentRun = {}
    self.control:SetHidden(true)
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 = nil
local transitBag = nil
local currentReturnIndex = nil

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

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

function Roomba:ReturnItemsToBank(...)
    local error = ...
    local slot
	-- Now that we've stacked it all lets move it back
    currentReturnIndex, slot = next(inBagCollection)
    if slot then
        if error then
            return zo_callLater(function(...) self:ReturnItemsToBank() end, 2000)
        end
        if FindSlot(BACKPACK, slot.bagSlot) then
            self.text:SetText("Returning restacked " .. cSlot.name .. " to the Guild Bank")
            return TransferToGuildBank(INVENTORY_BACKPACK, slot.bagSlot)
        else
            -- we have a space, move to next
            table.remove(inBagCollection, currentReturnIndex)
            return zo_callLater(function(...) self:ReturnItemsToBank(...) end, DELAY)
        end
    else
        -- No more to move, we're complete
        EVENT_MANAGER:UnregisterForEvent("RoombaGuildBankError", EVENT_GUILD_BANK_TRANSFER_ERROR)
        EVENT_MANAGER:UnregisterForEvent("RoombaGuildBankSuccess", EVENT_GUILD_BANK_ITEM_ADDED)
        -- Kick off the next transaction
        ResetAll()
        return zo_callLater(function() self:BeginProcess() end, DELAY)
    end
end

function Roomba:BankItemsReceived(...)
	local eventId, gslot = ...
    local id = GetInstanceId(GUILDBANK, gslot)
    if id ~= cInstanceId then return end
    -- We've moved one back! Remove it from contention
    local slot = table.remove(inBagCollection, currentReturnIndex)
    currentReturnIndex = nil
    return zo_callLater(function(...) self:ReturnItemsToBank() end, DELAY)
end


function Roomba:StartStacking()
	baseSlot = nil
    EVENT_MANAGER:UnregisterForEvent("RoombaInventoryAdded", EVENT_INVENTORY_SINGLE_SLOT_UPDATE)
    self.text:SetText("Stacking " .. cSlot.name .. " in inventory")
	for _,slot in pairs(inBagCollection) do
		if not baseSlot then
			-- We want to stack everything on this
			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)
				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()
		end
	end
	baseSlot = nil
    -- These events will loop the move back to the guild bank
	EVENT_MANAGER:RegisterForEvent("RoombaGuildBankError", EVENT_GUILD_BANK_TRANSFER_ERROR, function(...) self:ReturnItemsToBank() end)
	EVENT_MANAGER:RegisterForEvent("RoombaGuildBankSuccess", EVENT_GUILD_BANK_ITEM_ADDED, function(...) self:BankItemsReceived(...) end)
    return zo_callLater(function(...) self:ReturnItemsToBank() end, DELAY)
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 IsItemJunk(INVENTORY_BACKPACK, slotId) then
		SetItemIsJunk(INVENTORY_BACKPACK, slotId, false)
		return zo_callLater(function(...) self:ReceiveItems(...) end, 1000)
	end
    if not GetInstanceId(BACKPACK, slotId) then return end -- are we empty
	if GetInstanceId(BACKPACK, slotId) ~= cInstanceId then return end
    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(function() self:StartStacking() end, DELAY)
	end
end

function Roomba:BeginProcess()
	if ZO_GuildBankBackpack:IsHidden() then return end
    local bank = self.guildInfo[self.currentBank]
    if not bank then return end
    if not self:CheckWeHaveEnoughRoom() then return end

	ZO_MenuBar_SelectLastVisibleButton(ZO_PlayerInventoryTabs)

    cSlotIdx = nil

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

    if cInstanceId then currentRun[cInstanceId] = true end

    self.control:SetHidden(false)
    local index = TableEntryCount(currentRun)
    local total = TableEntryCount(bank.duplicates)
    ZO_StatusBar_SmoothTransition(self.speedRow.bar, index , total, FORCE_VALUE)
    self.speedRow.name:SetText(string.format(format, (index/total)*100) .. "%")

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

        self.icon:SetTexture(cSlot.texture)

        -- Clear this batch
        inBagCollection = {}
        -- And start off the job

        -- If it suddenly doesn't exist, try the next in the list
        if not FindSlot(GUILDBANK, cSlot.slot) then return zo_callLater(function() self:BeginProcess() end, DELAY) end

        EVENT_MANAGER:RegisterForEvent("RoombaInventoryAdded", EVENT_INVENTORY_SINGLE_SLOT_UPDATE, function(...) self:ReceiveItems(...) end)
        self.text:SetText("Retrieving " .. cSlot.name .. " from Guild Bank")
        TransferFromGuildBank(cSlot.slot)
    else
        -- Now rescan and show/hide roomba button
        self.control:SetHidden(true)
        self.text:SetText("Complete")
        self:RoombaReady()
        return false
    end
    return true
end

local function RoombaLoaded(eventCode, addOnName)

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

	ZO_CreateStringId("SI_BINDING_NAME_RUN_ROOMBA", "Run Roomba")
	ZO_CreateStringId("SI_BINDING_NAME_RESCAN_ROOMBA", "Rescan Bank")
end

function Roomba:InitialiseSettings()

	self.guildInfo = {}
	self.currentBank = 1
	self.CurrentState = CurrentState
	GetGuildDetails(self)

	self.runDescriptor = {
		alignment = KEYBIND_STRIP_ALIGN_LEFT,
		{
		    name = "Run Roomba", -- or function that returns a name
		    keybind = "RUN_ROOMBA",
		    control = self,
		    callback = function(descriptor) self:BeginProcess() end,
		    visible = function(descriptor) return self:HaveStuffToStack() end,
		    icon = [[Roomba\media\Roomba.dds]],
		},
		{
		    name = "Scan Bank",
		    keybind = "RESCAN_ROOMBA",
		    control = self,
		    callback = function(descriptor) self:RoombaReady() end,
		    visible = function(descriptor) return ZO_GuildBankBackpackLoading:IsHidden() end,
		    icon = [[Roomba\media\RoombaSearch.dds]],
		},
	}

end

function Roomba:Initialise(control)
	self.control = control
	self:InitialiseFrame()
	self:InitialiseSettings()

	local bGroup = self.runDescriptor

	EVENT_MANAGER:RegisterForEvent("RoombaGuildBankOpen", EVENT_OPEN_GUILD_BANK, function()
																	self:InitialiseEvents()
																	if not KEYBIND_STRIP:HasKeybindButtonGroup(bGroup) then
																		KEYBIND_STRIP:AddKeybindButtonGroup(bGroup)
																	end	end)
	EVENT_MANAGER:RegisterForEvent("RoombaGuildBankClose", EVENT_CLOSE_GUILD_BANK, function()
																	if KEYBIND_STRIP:HasKeybindButtonGroup(bGroup) then
																		KEYBIND_STRIP:RemoveKeybindButtonGroup(bGroup)
																	end
																	self.control:SetHidden(true)
																	ResetAll()
																	self:UninitialiseEvents()
																	end)
end

function Roomba:InitialiseEvents()
    -- 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(function() self:RoombaReady() end, 1000) end)
	-- Clear the flag when swapping banks
	EVENT_MANAGER:RegisterForEvent("RoombaSelected", EVENT_GUILD_BANK_SELECTED, function(...) self:SelectGuildBank(...) end)
end

function Roomba:UninitialiseEvents()
	EVENT_MANAGER:UnregisterForEvent("RoombaInventoryAdded", EVENT_INVENTORY_SINGLE_SLOT_UPDATE)
	EVENT_MANAGER:UnregisterForEvent("RoombaSelected", EVENT_GUILD_BANK_SELECTED)
	EVENT_MANAGER:UnregisterForEvent("RoombaReady", EVENT_GUILD_BANK_ITEMS_READY)
	EVENT_MANAGER:UnregisterForEvent("RoombaGuildBankError", EVENT_GUILD_BANK_TRANSFER_ERROR)
	EVENT_MANAGER:UnregisterForEvent("RoombaGuildBankSuccess", EVENT_GUILD_BANK_ITEM_ADDED)
end

function Roomba:InitialiseFrame()
	self.control:SetDrawLayer(DL_OVERLAY)
	self.speedRow = self.control:GetNamedChild("SpeedRow")
    self.speedRow.name:SetText("                     0%")
    self.icon = self.control:GetNamedChild("Icon")
    self.text = self.control:GetNamedChild("Description")
    self.text:SetText("...")
    ZO_StatusBar_SetGradientColor(self.speedRow.bar, ZO_XP_BAR_GRADIENT_COLORS)
    ZO_StatusBar_SmoothTransition(self.speedRow.bar, 0, 20, FORCE_VALUE)
end

function Roomba_Initialise(control)
    ROOMBA = Roomba:New(control)
end


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