-- GuildMap by Bru on Blackhand(DE) - wow@docbru.de

-- reference(s) to Ace Libraries
local Astrolabe = AceLibrary:GetInstance("Astrolabe-GM-0.2");
local L = AceLibrary("AceLocale-2.2"):new("GuildMap");
local BZ = AceLibrary("Babble-Zone-2.2");

-- construct addon and add mixins
GuildMap = AceLibrary("AceAddon-2.0"):new("AceDebug-2.0", "AceEvent-2.0");

GuildMap.CONSTANTS = {

	-- seconds after which the UI shall be repainted
	UPDATE_INTERVAL = 0.25;

	-- seconds to wait between announces when a player is moving
	EARLIEST_ANNOUNCE = 3.0;
	-- seconds after which an announce must take place when a player is standing still
	LATEST_ANNOUNCE = 30.0;

	-- seconds after which a player without updated status info will be removed
	PLAYER_TIMEOUT = 45.0;

	-- constants (do not modify)
	MAX_MINIMAP_ICONS = 75;
	MAX_WORLDMAP_ICONS = 250;
}	

-- main temporary data table
GuildMap.data = {
	position = {continent = 0, zone = 0, x = 0, y = 0},
	lastAnnounce = 0,
	announceVersion = true,
	pendingAnnounce = false,
	players = {},
	worldMapOpen = false,
	versionWarning = true,
};


-- Called when the addon is loaded
function GuildMap:OnInitialize()
	self:InitRegionData();

	--self:SetDebugging(true);
	--self:SetupDummies();
	
	-- register events
	self:RegisterEvent("CHAT_MSG_ADDON");
	self:RegisterEvent("WORLD_MAP_UPDATE");
	self:RegisterEvent("RAID_ROSTER_UPDATE");
	self:RegisterEvent("PARTY_MEMBERS_CHANGED");
	
	-- register job
    self:ScheduleRepeatingEvent(self.Update, self.CONSTANTS.UPDATE_INTERVAL, self);

	local loadedMessage = string.format(L["GuildMap (rev. %s) by Bru loaded"], self.version);
	self:Print(loadedMessage);
	UIErrorsFrame:AddMessage(loadedMessage, 0.0, 1.0, 0.0, 1.0, UIERRORS_HOLD_TIME);
end

-- Called when the addon is enabled
function GuildMap:OnEnable()
	self:Debug("ENABLED");
end

-- Called when the addon is disabled
function GuildMap:OnDisable()
    self:Debug("DISABLED");
end





function GuildMap:RAID_ROSTER_UPDATE()
	self:UpdatePartyOrRaid();
end

function GuildMap:PARTY_MEMBERS_CHANGED()
	self:UpdatePartyOrRaid();
end


function GuildMap:CHAT_MSG_ADDON(prefix, message, distribution, sender)
	-- other addon? return
	if (prefix ~= "GUILDMAP") then
		return
	end

	-- do not care about messages from own player
	local playerName = UnitName("player");
	if (playerName and playerName == sender) then
		return;
	end

	if ( string.find(message, "#") ) then
		local arr = self:Split(message, "#");
		for k, v in pairs(arr) do
			self:HandleMessage(sender, v);
		end
	else
		self:HandleMessage(sender, message);
	end
end


function GuildMap:WORLD_MAP_UPDATE()
	self.data.worldMapOpen = self:IsWorldMapOpen();
	if (self.data.worldMapOpen) then
		self:UpdateWorldMap();
	end
end



function GuildMap:Print(msg, ...)
	if (DEFAULT_CHAT_FRAME) then
	
		for i = 1, select("#", ...), 1 do
			if ( arg[i] ) then
				msg = msg .. "#" .. i .. "=" .. select(i, ...);
			end
		end	
	
		DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00" .. msg);
	end
end


function GuildMap:IsWorldMapOpen()
	if (WorldMapFrame:IsVisible()) then
		return true;
	else
		return false;
	end
end


function GuildMap:UpdateWorldMap() 


	local currFrame = 1;	

	local currentContinent = GetCurrentMapContinent();
	local currentZone = GetCurrentMapZone();
	
	-- there was a crash for battlegrounds (which return -1 for continent I think)
	if (currentContinent < 0 or currentContinent > 3) then
		for i = 1, self.CONSTANTS.MAX_WORLDMAP_ICONS do
			local poi = getglobal("GuildMapMain" .. i);
			poi:Hide();		
		end
		return;
	end
	

	for player, playerData in pairs(self.data.players) do
	
		if (playerData.show) then
		
			local playerX = playerData.x / 10000;
			local playerY = playerData.y / 10000;

			local mapx, mapy = Astrolabe:TranslateWorldMapPosition(playerData.continent, playerData.zone, playerX, playerY, currentContinent, currentZone);
			if (mapx and mapy and mapx >= 0 and mapx <= 1 and mapy >= 0 and mapy <= 1) then 

				local mnX = mapx * WorldMapDetailFrame:GetWidth();
				local mnY = -mapy * WorldMapDetailFrame:GetHeight();

				local poi = getglobal("GuildMapMain" .. currFrame);
				poi:SetPoint("CENTER", "WorldMapDetailFrame", "TOPLEFT", mnX, mnY);
				poi:Show();

				poi.unit = player;

				currFrame = currFrame + 1;		

			end

		
		end
		
		-- no more than 250 players on world map
		if (currFrame == self.CONSTANTS.MAX_WORLDMAP_ICONS) then
			break;
		end

	end

	for i = currFrame, self.CONSTANTS.MAX_WORLDMAP_ICONS do
		local poi = getglobal("GuildMapMain" .. i);
		poi:Hide();		
	end


end


function GuildMap:UpdateMiniMap()

	local currFrame = 1;	

	local continent = self.data.position.continent;
	local zone = self.data.position.zone;
	
	if (continent < 1 or continent > 3) then
		for i = 1, self.CONSTANTS.MAX_MINIMAP_ICONS do
			local poi = getglobal("GuildMapMini" .. i);
			poi:Hide();		
		end
		return;
	end


	for player, playerData in pairs(self.data.players) do

		if (playerData.show and continent == playerData.continent) then

			local x = self.data.position.x / 10000;
			local y = self.data.position.y / 10000;

			local playerX = playerData.x / 10000;
			local playerY = playerData.y / 10000;

			local distFromCenter, deltax, deltay = Astrolabe:ComputeMiniMapDistance(continent, zone, x, y, playerData.continent, playerData.zone, playerX, playerY);
			
			if (distFromCenter) then
				local poi = getglobal("GuildMapMini" .. currFrame);
				local poiTexture = getglobal("GuildMapMini" .. currFrame .. "Texture");

				local inSight = false;
				if (Squeenix) then
					-- rectangular map
					inSight = abs(deltax) < 64 and abs(deltay) < 64;
				else
					inSight = distFromCenter <= 56.5;
				end


				if (inSight) then
					poiTexture:SetTexture("Interface\\AddOns\\GuildMap\\Images\\MiniMapIcon");

					poi:SetPoint("CENTER", "Minimap", "CENTER", deltax, deltay);
					poi:Show();
					poi.unit = player;
					currFrame = currFrame + 1;
				elseif (distFromCenter < 400) then
					local roundDeltax = deltax * 56.5 / distFromCenter;
					local roundDeltay = deltay * 56.5 / distFromCenter;

					if (Squeenix) then
						if (deltax > 64) then 
							deltax = 64;
						elseif (deltax < -64) then
							deltax = -64;
						end

						if (deltay > 64) then 
							deltay = 64;
						elseif (deltay < -64) then
							deltay = -64;
						end
					else
						deltax = roundDeltax;
						deltay = roundDeltay;
					end					

					poiTexture:SetTexture(self:Angle(roundDeltax, roundDeltay));

					poi:SetPoint("CENTER", "Minimap", "CENTER", deltax, deltay);
					poi:Show();
					poi.unit = player;
					currFrame = currFrame + 1;

				end
			end
			
			
		end

		-- no more than 75 players on mini map
		if (currFrame == self.CONSTANTS.MAX_MINIMAP_ICONS) then
			break;
		end
		
	end
	
	
	for i = currFrame, self.CONSTANTS.MAX_MINIMAP_ICONS do
		local poi = getglobal("GuildMapMini" .. i);
		poi:Hide();		
	end
	
end


function GuildMap:Update() 	


	self:UpdatePartyOrRaid();

	local announceDelta = GetTime() - self.data.lastAnnounce;


	local positionChanged = self:UpdatePlayerPos();
	-- an announce is scheduled if the player position has changed or 
	-- the maximum time without an announce was exceeded
	self.data.pendingAnnounce =
		self.data.pendingAnnounce or
		positionChanged or
		(announceDelta > self.CONSTANTS.LATEST_ANNOUNCE);

	if (self.data.pendingAnnounce and announceDelta > self.CONSTANTS.EARLIEST_ANNOUNCE) then
		self:AnnouncePosition();
	end

	-- garbage collect timed out players
	self:RemoveOldPlayers();

	self.data.worldMapOpen = self:IsWorldMapOpen();
	if (self.data.worldMapOpen) then
		self:UpdateWorldMap();
	else
		self:EnsureZoneMap();
		self:UpdateMiniMap();
	end;
	
end




function GuildMap:UpdatePlayerPos() 

	local continent, zone, x, y = self:GetCurrentPlayerPosition();

	
	x = math.floor(x * 10000);
	y = math.floor(y * 10000);
	
	--[[
	if (continent < 1 or continent > 3 or zone == 0) then
		return false;
	end
	--]]
	
	
	if (self.data.position.x ~= x or
		self.data.position.y ~= y) then

		self.data.position.continent = continent;
		self.data.position.zone = zone;
		self.data.position.x = x;
		self.data.position.y = y;

		return true;
	end
	
	return false;

end

function GuildMap:AnnouncePosition()
	if (self:IsValidPosition(self.data.position.continent, self.data.position.zone, self.data.position.x, self.data.position.y)) then 
		self.data.pendingAnnounce = false;
		self.data.lastAnnounce = GetTime();
		local outZone = self:ConvertZoneOut(self.data.position.continent, self.data.position.zone);
		local message = "POS " .. self.data.position.continent .. " " .. outZone .. " " .. self.data.position.x .. " " .. self.data.position.y;
		if (self.data.announceVersion) then
			message = message .. "#V " .. self.version;
			self.data.announceVersion = false;
		end
		self:Broadcast(message);
	end
end


function GuildMap:Broadcast(message)
	local clearAFK = GetCVar("autoClearAFK");
	SetCVar("autoClearAFK", 0);
	if (IsInGuild()) then
		SendAddonMessage("GUILDMAP", message, "GUILD");
	end
	SetCVar("autoClearAFK", clearAFK);
end


function GuildMap:HandleMessage(sender, message) 
	if (strsub(message, 1, 4) == "POS ") then
		self:HandlePosition(sender, message);
	elseif (strsub(message, 1, 2) == "V ") then
		self:HandleVersion(sender, message);
	end
end

function GuildMap:HandlePosition(sender, message)
	local useless, continent, zone, x, y;

	useless, useless, val1, val2, val3, val4 = string.find(message, "POS (%d+) (%d+) (%d+) (%d+)");
	if (val1 and val2 and val3 and val4) then

		-- perform some validations to prevent crashes later ;-)
		local tmpcont = tonumber(val1);
		local tmpzone = tonumber(val2);
		local tmpx = tonumber(val3);
		local tmpy = tonumber(val4);

		if (self:IsValidPosition(tmpcont, tmpzone, tmpx, tmpy)) then 
			tmpzone = self:ConvertZoneIn(tmpcont, tmpzone);
			if (self.data.players[sender] == nil) then
				-- new player: announce 
				self.data.announceVersion = true;
				self.data.pendingAnnounce = true;
				self.data.players[sender] = { continent = tmpcont, zone = tmpzone, x = tmpx, y = tmpy, lastUpdate = GetTime(), show = true};
			else
				-- if we have the player already, do update him only to not reset the show flag
				self.data.players[sender].continent = tmpcont;
				self.data.players[sender].zone = tmpzone;
				self.data.players[sender].x = tmpx;
				self.data.players[sender].y = tmpy;
				self.data.players[sender].lastUpdate = GetTime();
			end
		end

	end
end

function GuildMap:HandleVersion(sender, message)
	local version = strsub(message, 3, strlen(message));
	if (self.data.players[sender] ~= nil) then
		self.data.players[sender].version = version;
		local revision = tonumber(version);
		if (revision and self.data.versionWarning and revision > tonumber(self.version)) then
			local updateMessage = string.format(L["%s is using a newer version (rev. %s) of GuildMap. Please update."], sender, revision);
			self:Print(updateMessage);
			self.data.versionWarning = false;
		end
	end
end

function GuildMap:MiniMap_OnClick(arg1)

	local x, y = GetCursorPosition();
	x = x / Minimap:GetEffectiveScale();
	y = y / Minimap:GetEffectiveScale();

	local cx, cy = Minimap:GetCenter();
	x = x + CURSOR_OFFSET_X - cx;
	y = y + CURSOR_OFFSET_Y - cy;
	Minimap:PingLocation(x, y);
	
end


function GuildMap:RemoveOldPlayers()
	for player, playerData in pairs(self.data.players) do
		if (GetTime() - playerData.lastUpdate > self.CONSTANTS.PLAYER_TIMEOUT) then
			self.data.players[player] = nil;
		end
	end	
end

function GuildMap:IsValidPosition(continent, zone, x, y)

	if (continent < 1 or continent > 3) then
		return false;
	end
	if (continent == 1 and (zone < 1 or zone > 24)) then
		return false;
	end
	if (continent == 2 and (zone < 1 or zone > 28)) then
		return false;
	end
	if (continent == 3 and (zone < 1 or zone > 8)) then
		return false;
	end
	
	if (x < 1 or x > 9999) then
		return false;
	end
	if (y < 1 or y > 9999) then
		return false;
	end	

	return true;

end

function GuildMap:ShowMiniMapToolTip()

	GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT");

	local unitButton;
	local newLineString = "";
	local tooltipText = "";
	
	for i=1, 25 do
		unitButton = getglobal("GuildMapMini"..i);
		if ( unitButton:IsVisible() and MouseIsOver(unitButton) ) then
			tooltipText = tooltipText .. newLineString .. unitButton.unit;
			newLineString = "\n";
		elseif (not unitButton:IsVisible()) then
			break;
		end
	end

	GameTooltip:SetText(tooltipText, 1.0, 0.82, 0.0, 1,1);
	GameTooltip:Show();
	
end

function GuildMap:ShowWorldMapToolTip()
	
	-- determine tooltip anchor
	local x, y = this:GetCenter();
	local parentX, parentY = this:GetParent():GetCenter();
	if ( x > parentX ) then
		WorldMapTooltip:SetOwner(this, "ANCHOR_LEFT");
	else
		WorldMapTooltip:SetOwner(this, "ANCHOR_RIGHT");
	end	
	
	local unitButton;
	local newLineString = "";
	local tooltipText = "";
	
	for i=1, 250 do
		unitButton = getglobal("GuildMapMain"..i);
		if ( unitButton:IsVisible() and MouseIsOver(unitButton) ) then
			tooltipText = tooltipText .. newLineString .. unitButton.unit;
			newLineString = "\n";
		elseif (not unitButton:IsVisible()) then
			break;
		end
	end

	WorldMapTooltip:SetText(tooltipText, 1.0, 0.82, 0.0, 1,1);
	WorldMapTooltip:Show();
	
end





function GuildMap:UpdatePartyOrRaid() 

	for player, playerData in pairs(self.data.players) do
		playerData.show = true;
	end	

	local numRaidMembers = GetNumRaidMembers();
	for i = 1, numRaidMembers do
		local name = GetRaidRosterInfo(i);
		if (name and self.data.players[name]) then
			self.data.players[name].show = false;	
		end
	end

	local numPartyMembers = GetNumPartyMembers();
	for i = 1, numPartyMembers do
		local name = UnitName("party" .. i);
		if (name and self.data.players[name]) then
			self.data.players[name].show = false;	
		end
	end

end



function GuildMap:Angle(x, y)
	local angle = asin(x / 56.5);
	if (x <= 0 and y <= 0) then
		angle = 180 - angle;
	elseif (x <= 0 and y > 0) then
		angle = 360 + angle;
	elseif (x > 0 and y >= 0) then
		angle = angle;
	else
		angle = 180 - angle;
	end
	local fileNumber = self:Round(angle / 10) * 10;
	if (fileNumber == 360) then
		fileNumber = 0;
	end
	
	return "Interface\\Addons\\GuildMap\\images\\MiniMapArrow" .. fileNumber;
end

-- Round function
function GuildMap:Round(x)
	return floor(x + 0.5);
end







function GuildMap:CloneTable(t)
  local new = {};
  local i, v = next(t, nil);
  while i do
    new[i] = v
    i, v = next(t, i);
  end
  return new;
end


function GuildMap:Split(msg, char)
	local arr = { };
	while (string.find(msg, char) ) do
		local iStart, iEnd = string.find(msg, char);
		tinsert(arr, strsub(msg, 1, iStart-1));
		msg = strsub(msg, iEnd+1, strlen(msg));
	end
	if ( strlen(msg) > 0 ) then
		tinsert(arr, msg);
	end
	return arr;
end


function GuildMap:GetCurrentPlayerPosition()
	local continent = GetCurrentMapContinent();
	local zone = GetCurrentMapZone();
	local x, y = GetPlayerMapPosition("player");
	
	continent = continent or -1;
	zone = zone or -1;
	x = x or -1;
	y = y or -1;
	
	return continent, zone, x, y;
end



local function CurrentZoneFix_SetupFix()
	-- CurrentZoneFix v2.0 by Legorol
	-- email: legorol@cosmosui.org
	
	local versionID = 20;
	local id = CurrentZoneFix_FixInPlace;
	
	if ( id ) then
		id = tonumber(id);
		if ( id and id >= versionID ) then
			return;
		elseif ( not SetMapToCurrentZone(true) ) then
			DEFAULT_CHAT_FRAME:AddMessage("Warning! Obsolete version "..
				"of CurrentZoneFix function detected. The old "..
				"version is being loaded either as a standalone "..
				"AddOn, or as code embedded inside another AddOn. "..
				"You must update it to avoid bugs with the map!",1,0,0)
			return;
		end
	end
	
	local continent,zone,name
	local zoneID={};
	local orig_SetMapToCurrentZone;
	
	for continent in ipairs{GetMapContinents()} do
		for zone,name in ipairs{GetMapZones(continent)} do
			zoneID[name] = zone;
		end
	end
	
	orig_SetMapToCurrentZone = SetMapToCurrentZone;
	SetMapToCurrentZone = function(deactivate)
		if (deactivate) then
			SetMapToCurrentZone = orig_SetMapToCurrentZone;
			CurrentZoneFix_FixInPlace = nil;
			return true;
		end
		
		orig_SetMapToCurrentZone();
		if ( GetCurrentMapZone()==0 and GetCurrentMapContinent()>0 ) then
			SetMapZoom(GetCurrentMapContinent(),zoneID[GetRealZoneText()]);
		else
			SetMapToCurrentZone = orig_SetMapToCurrentZone;
			CurrentZoneFix_FixInPlace = "Deactivated "..versionID;
		end
	end
	
	CurrentZoneFix_FixInPlace = versionID;
end


function GuildMap:InitRegionData()
	local continents = { GetMapContinents() };
	for c in ipairs({GetMapContinents()}) do
	
		ZoneIndexesOut[c] = {};
		ZoneIndexesIn[c] = {};
	
		local zones = { GetMapZones(c) };
		for z, zone in ipairs(zones) do
			local translatedEN = BZ:GetReverseTranslation(zone);
			local indexEN = ZoneIndexesEN[translatedEN];
			ZoneIndexesOut[c][z] = indexEN;
			ZoneIndexesIn[c][indexEN] = z;
		end
	end
	
	CurrentZoneFix_SetupFix();
end

function GuildMap:ConvertZoneOut(continent, zone)
	if (continent >= 1 and continent <= 3 and zone ~= 0) then
		return ZoneIndexesOut[continent][zone];
	else
		return zone;
	end	
end

function GuildMap:ConvertZoneIn(continent, zone)
	if (continent >= 1 and continent <= 3 and zone ~= 0) then
		return ZoneIndexesIn[continent][zone];
	else
		return zone;
	end	
end


function GuildMap:SetupDummies()
	local continents = { GetMapContinents() };
	for c in ipairs({GetMapContinents()}) do
		local zones = { GetMapZones(c) };
		for z, zone in ipairs(zones) do
			self.data.players[zone] = { continent = c, zone = z, x = 5000, y = 5000, lastUpdate = 10000000, show = true };
		end
	end
end


function GuildMap:EnsureZoneMap()
	local mapContinent = GetCurrentMapContinent();
	local mapZone = GetCurrentMapZone();
	
	local playerContinent, playerZone = Astrolabe:GetCurrentPlayerPosition();
	
	if (playerContinent ~= 0 and playerZone ~= 0 and (mapContinent ~= playerContinent or mapZone ~= playerZone)) then
		SetMapToCurrentZone();
	end
end

