local MogIt,mog = ...;
_G["MogIt"] = mog;
local L = mog.L;

local ItemInfo = LibStub("LibItemInfo-1.0");

LibStub("Libra"):Embed(mog);

local character = DataStore_Containers and DataStore:GetCharacter();

mog.frame = CreateFrame("Frame","MogItFrame",UIParent,"ButtonFrameTemplate");
mog.list = {};

function mog:Error(msg)
	DEFAULT_CHAT_FRAME:AddMessage("MogIt: "..msg,0.9,0.5,0.9);
end

--// Slash Commands
function mog:ToggleFrame()
	ToggleFrame(mog.frame);
end

function mog:TogglePreview()
	ToggleFrame(mog.view);
end
--//


--// Bindings
SLASH_MOGIT1 = "/mog";
SLASH_MOGIT2 = "/mogit";
SlashCmdList["MOGIT"] = mog.ToggleFrame;

BINDING_HEADER_MogIt = "MogIt";
BINDING_NAME_MogIt = L["Toggle Mogit"];
BINDING_NAME_MogItPreview = L["Toggle Preview"];
--//


--// LibDataBroker
mog.LDBI = LibStub("LibDBIcon-1.0");
mog.mmb = LibStub("LibDataBroker-1.1"):NewDataObject("MogIt",{
	type = "launcher",
	icon = "Interface\\Icons\\INV_Enchant_EssenceCosmicGreater",
	OnClick = function(self,btn)
		if btn == "RightButton" then
			mog:TogglePreview();
		else
			mog:ToggleFrame();
		end
	end,
	OnTooltipShow = function(self)
		if not self or not self.AddLine then return end
		self:AddLine("MogIt");
		self:AddLine(L["Left click to toggle MogIt"],1,1,1);
		self:AddLine(L["Right click to toggle the preview"],1,1,1);
	end,
});
--//


--// Module API
mog.moduleVersion = 3;
mog.modules = {};
mog.moduleList = {};

function mog:GetModule(name)
	return mog.modules[name];
end

function mog:GetActiveModule()
	return mog.active;
end

function mog:RegisterModule(name,version,data)
	if mog.modules[name] then
		--mog:Error(L["The \124cFFFFFFFF%s\124r module is already loaded."]:format(name));
		return mog.modules[name];
	--elseif type(version) ~= "number" or version < mog.moduleVersion then
		--mog:Error(L["The \124cFFFFFFFF%s\124r module needs to be updated to work with this version of MogIt."]:format(name));
		--return;
	--elseif version > mog.moduleVersion then
		--mog:Error(L["The \124cFFFFFFFF%s\124r module requires you to update MogIt for it to work."]:format(name));
		--return;
	end
	data = data or {};
	data.version = version;
	data.name = name;
	mog.modules[name] = data;
	table.insert(mog.moduleList,data);
	if mog.menu.active == mog.menu.modules then
		mog.menu:Rebuild(1);
	end
	return data;
end

function mog:SetModule(module,text)
	if mog.active and mog.active ~= module and mog.active.Unlist then
		mog.active:Unlist(module);
	end
	mog.active = module;
	mog:BuildList(true);
	mog:FilterUpdate();
	mog.frame.path:SetText(text or module.label or module.name or "");
end

function mog:BuildList(top,module)
	if (module and mog.active and mog.active.name ~= module) then return end;
	mog.list = mog.active and mog.active.BuildList and mog.active:BuildList() or {};
	mog:SortList(nil,true);
	mog.scroll:update(top and 1);
	mog.filt.models:SetText(#mog.list);
end
--//


--// Item Cache
local itemCacheCallbacks = {
	BuildList = mog.BuildList;
	ModelOnEnter = function()
		local owner = GameTooltip:GetOwner();
		if owner and GameTooltip[mog] then
			owner:OnEnter();
		end
	end,
	ItemMenu = function()
		mog.Item_Menu:Rebuild(1);
	end,
	SetMenu = function()
		mog.Set_Menu:Rebuild(1);
	end,
};

local pendingCallbacks = {};

for k in pairs(itemCacheCallbacks) do
	pendingCallbacks[k] = {};
end

function mog:AddItemCacheCallback(name, func)
	itemCacheCallbacks[name] = func;
	pendingCallbacks[name] = {};
end

function mog:GetItemInfo(id, callback)
	if not callback then return ItemInfo[id] end
	if ItemInfo[id] then
		-- clear pending items when they are cached
		pendingCallbacks[callback][id] = nil;
		return ItemInfo[id];
	elseif itemCacheCallbacks[callback] then
		-- add to pending items for this callback if not cached
		pendingCallbacks[callback][id] = true;
	end
end

function mog.ItemInfoReceived()
	for k, callback in pairs(pendingCallbacks) do
		-- execute the callback if any items are pending for it
		if next(callback) then
			itemCacheCallbacks[k]();
		end
	end
end

ItemInfo.RegisterCallback(mog, "OnItemInfoReceivedBatch", "ItemInfoReceived");
--//

function mog:HasItem(itemID)
	itemID = self:ToNumberItem(itemID)
	return GetItemCount(itemID, true) > 0 or (character and select(3, DataStore:GetContainerItemCount(character, itemID)) > 0)
end


--// Events
local defaults = {
	profile = {
		tooltipItemID = false,
		
		noAnim = false,
		url = "Battle.net",
		
		dressupPreview = false,
		singlePreview = false,
		previewUIPanel = false,
		previewFixedSize = false,
		previewConfirmClose = true,
		
		sortWishlist = false,
		loadModulesWishlist = false,
		
		tooltip = true,
		tooltipWidth = 300,
		tooltipHeight = 300,
		tooltipMouse = false,
		tooltipDress = false,
		tooltipRotate = true,
		tooltipMog = true,
		tooltipMod = "None",
		tooltipCustomModel = false,
		tooltipRace = 1,
		tooltipGender = 0,
		
		minimap = {},
		
		point = "CENTER",
		gridWidth = 600,
		gridHeight = 400,
		rows = 2;
		columns = 3,
		gridDress = "preview",
		sync = true,
		previewProps = {
			["*"] = {
				w = 335,
				h = 385,
				point = "CENTER",
			}
		},
	}
}

function mog.LoadSettings()
	mog:UpdateGUI();
	
	if mog.db.profile.minimap.hide then
		mog.LDBI:Hide("MogIt");
	else
		mog.LDBI:Show("MogIt");
	end
	
	mog.tooltip:SetSize(mog.db.profile.tooltipWidth, mog.db.profile.tooltipHeight);
	mog.tooltip.rotate:SetShown(mog.db.profile.tooltipRotate);
	
	mog.scroll:update();
	
	mog:SetSinglePreview(mog.db.profile.singlePreview);
end

function mog:LoadBaseModules()
	for i, module in ipairs(self.baseModules) do
		if GetAddOnEnableState(myName, module) > 0 and not IsAddOnLoaded(module) then
			LoadAddOn(module)
		end
	end
end

mog.frame:RegisterEvent("ADDON_LOADED");
mog.frame:RegisterEvent("PLAYER_LOGIN");
mog.frame:RegisterEvent("GET_ITEM_INFO_RECEIVED");
mog.frame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED");
mog.frame:SetScript("OnEvent", function(self, event, ...)
	return mog[event] and mog[event](mog, ...)
end);

function mog:ADDON_LOADED(addon)
	if addon == MogIt then
		local AceDB = LibStub("AceDB-3.0")
		mog.db = AceDB:New("MogItDB", defaults, true)
		mog.db.RegisterCallback(mog, "OnProfileChanged", "LoadSettings")
		mog.db.RegisterCallback(mog, "OnProfileCopied", "LoadSettings")
		mog.db.RegisterCallback(mog, "OnProfileReset", "LoadSettings")

		if not mog.db.global.version then
			mog:Error(L["MogIt has loaded! Type \"/mog\" to open it."]);
		end
		mog.db.global.version = GetAddOnMetadata(MogIt,"Version");
		
		mog.LDBI:Register("MogIt",mog.mmb,mog.db.profile.minimap);
		
		
		for name,module in pairs(mog.moduleList) do
			if module.MogItLoaded then
				module:MogItLoaded()
			end
		end
	elseif mog.modules[addon] then
		mog.modules[addon].loaded = true;
		if mog.menu.active == mog.menu.modules then
			mog.menu:Rebuild(1)
		end
	end
end

local function sortCharacters(a, b)
	local characterA, realmA = a:match("(.+) %- (.+)");
	local characterB, realmB = b:match("(.+) %- (.+)");
	if realmA ~= realmB then
		-- your own realm gets sorted before others
		if realmA == myRealm then
			return true;
		end
		if realmB == myRealm then
			return false;
		end
		return realmA < realmB;
	else
		return characterA < characterB;
	end
end

function mog:PLAYER_LOGIN()
	C_Timer.After(1, function()
		-- this function doesn't yield correct results immediately, so we delay it
		for slot, v in pairs(mog.mogSlots) do
			local isTransmogrified, _, _, _, _, visibleItemID = GetTransmogrifySlotInfo(slot);
			if isTransmogrified then
				mog:GetItemInfo(visibleItemID);
			end
		end
	end)
	self.realmCharacters = {};
	for characterKey in pairs(mog.wishlist.db.sv.profileKeys) do
		local character, realm = characterKey:match("(.+) %- (.+)");
		if self:IsConnectedRealm(realm, true) then
			table.insert(self.realmCharacters, characterKey);
		end
	end
	sort(self.realmCharacters, sortCharacters);
	
	mog:LoadSettings();
	self.frame:SetScript("OnSizeChanged", function(self, width, height)
		mog.db.profile.gridWidth = width;
		mog.db.profile.gridHeight = height;
		mog:UpdateGUI(true);
	end)
end

function mog:PLAYER_EQUIPMENT_CHANGED(slot, hasItem)
	local slotName = mog.mogSlots[slot];
	local itemID, itemAppearanceModID = GetInventoryItemID("player", slot);
	if slotName then
		local isTransmogrified, _, _, _, _, visibleItemID, _, visibleItemAppearanceModID = GetTransmogrifySlotInfo(slot);
		if isTransmogrified then
			mog:GetItemInfo(visibleItemID);
			itemID = visibleItemID;
			itemAppearanceModID = visibleItemAppearanceModID;
		end
	end
	-- don't do anything if the slot is not visible (necklace, ring, trinket)
	if mog.db.profile.gridDress == "equipped" then
		for i, frame in ipairs(mog.models) do
			if frame.data.item then
				if hasItem then
					if (slot ~= INVSLOT_HEAD or ShowingHelm()) and (slot ~= INVSLOT_BACK or ShowingCloak()) then
						frame:TryOn(itemID, slotName, itemAppearanceModID);
					end
				else
					frame:UndressSlot(slot);
				end
				frame:TryOn(frame.data.item);
			end
		end
	end
end
--//


--// Data API
mog.data = {};

function mog:AddData(data, id, key, value)
	if not (data and id and key) then return end;
	
	--if data == "item" then
	--	id = mog:ItemToString(id);
	--end
	
	if not mog.data[data] then
		mog.data[data] = {};
	end
	if not mog.data[data][key] then
		mog.data[data][key] = {};
	end
	mog.data[data][key][id] = value;
	return value;
end

function mog:DeleteData(data, id, key)
	if not mog.data[data] then return end;
	if id and key then
		mog.data[data][key][id] = nil;
	elseif id then
		for k,v in pairs(mog.data[data]) do
			v[id] = nil;
		end
	elseif key then
		mog.data[data][key] = nil;
	else
		mog.data[data] = nil;
	end
end

function mog:GetData(data, id, key)
	return mog.data[data] and mog.data[data][key] and mog.data[data][key][id];
end

mog.itemStringShort = "item:%d:0";
mog.itemStringLong = "item:%d:0:0:0:0:0:0:0:0:0:0:0:%d:%d";

function mog:ToStringItem(id, bonus)
	-- itemID, enchantID, instanceDifficulty, numBonusIDs, bonusID1
	if bonus then
		return format(mog.itemStringLong, id, bonus and 1, bonus);
	else
		return format(mog.itemStringShort, id);
	end
end

local bonusDiffs = {
	[0] = true,
	-- MoP
	[451] = true, -- Raid Finder
	[449] = true, -- Heroic (Raid)
	[450] = true, -- Mythic (Raid)
	-- WoD
	[518] = true, -- dungeon-level-up-1
	[519] = true, -- dungeon-level-up-2
	[520] = true, -- dungeon-level-up-3
	[521] = true, -- dungeon-level-up-4
	[522] = true, -- dungeon-normal
	[524] = true, -- dungeon-heroic
	[525] = true, -- trade-skill
	[526] = true, -- trade-skill
	[527] = true, -- trade-skill
	[558] = true, -- trade-skill
	[559] = true, -- trade-skill
	[566] = true, -- raid-heroic
	[567] = true, -- raid-mythic
	[615] = true, -- timewalker
	[642] = true, -- dungeon-mythic
};

mog.itemStringPattern = "item:(%d+):%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+:(%d+):([%d:]+)";

function mog:ToNumberItem(item)
	if type(item) == "string" then
		local id, numBonusIDs, bonus = item:match(mog.itemStringPattern);
		-- bonus ID can also be warforged, socketed, etc
		-- if there is more than one bonus ID, need to check all
		if numBonusIDs then
			numBonusIDs = tonumber(numBonusIDs);
			if numBonusIDs == 1 and not bonusDiffs[tonumber(bonus)] then
				bonus = nil;
			elseif numBonusIDs > 1 then
				for bonusID in gmatch(bonus, "%d+") do
					bonusID = tonumber(bonusID);
					if bonusDiffs[bonusID] then
						bonus = bonusID;
						break;
					end
				end
			end
		end
		id = id or item:match("item:(%d+)");
		return tonumber(id), tonumber(bonus);
	elseif type(item) == "number" then
		return item;
	end
end

function mog:NormaliseItemString(item)
	return self:ToStringItem(self:ToNumberItem(item));
end
--//


--// Slot Conversion
mog.slots = {
	"HeadSlot",
	"ShoulderSlot",
	"BackSlot",
	"ChestSlot",
	"ShirtSlot",
	"TabardSlot",
	"WristSlot",
	"HandsSlot",
	"WaistSlot",
	"LegsSlot",
	"FeetSlot",
	"MainHandSlot",
	"SecondaryHandSlot",
};

mog.slotsType = {
	INVTYPE_HEAD = "HeadSlot",
	INVTYPE_SHOULDER = "ShoulderSlot",
	INVTYPE_CLOAK = "BackSlot",
	INVTYPE_CHEST = "ChestSlot",
	INVTYPE_ROBE = "ChestSlot",
	INVTYPE_BODY = "ShirtSlot",
	INVTYPE_TABARD = "TabardSlot",
	INVTYPE_WRIST = "WristSlot",
	INVTYPE_HAND = "HandsSlot",
	INVTYPE_WAIST = "WaistSlot",
	INVTYPE_LEGS = "LegsSlot",
	INVTYPE_FEET = "FeetSlot",
	INVTYPE_2HWEAPON = "MainHandSlot",
	INVTYPE_WEAPON = "MainHandSlot",
	INVTYPE_WEAPONMAINHAND = "MainHandSlot",
	INVTYPE_WEAPONOFFHAND = "SecondaryHandSlot",
	INVTYPE_RANGED = "MainHandSlot",
	INVTYPE_RANGEDRIGHT = "MainHandSlot",
	INVTYPE_SHIELD = "SecondaryHandSlot",
	INVTYPE_HOLDABLE = "SecondaryHandSlot",
};

-- all slot IDs that can be transmogrified
mog.mogSlots = {
	[INVSLOT_HEAD] = "HeadSlot",
	[INVSLOT_SHOULDER] = "ShoulderSlot",
	[INVSLOT_BACK] = "BackSlot",
	[INVSLOT_CHEST] = "ChestSlot",
	[INVSLOT_WRIST] = "WristSlot",
	[INVSLOT_HAND] = "HandsSlot",
	[INVSLOT_WAIST] = "WaistSlot",
	[INVSLOT_LEGS] = "LegsSlot",
	[INVSLOT_FEET] = "FeetSlot",
	[INVSLOT_MAINHAND] = "MainHandSlot",
	[INVSLOT_OFFHAND] = "SecondaryHandSlot",
}

function mog:GetSlot(id)
	return mog.slots[id] or mog.slotsType[id];
end
--//