local LIB_IDENTIFIER = "LibMapPing"
local lib = LibStub:NewLibrary("LibMapPing", 1)

if not lib then
	return	-- already loaded and no upgrade necessary
end
local g_mapPinManager

local function Log(message, ...)
	df("[%s] %s", LIB_IDENTIFIER, message:format(...))
end

local MAP_PIN_TYPE_PLAYER_WAYPOINT = MAP_PIN_TYPE_PLAYER_WAYPOINT
local MAP_PIN_TYPE_PING = MAP_PIN_TYPE_PING
local MAP_PIN_TYPE_RALLY_POINT = MAP_PIN_TYPE_RALLY_POINT

local MAP_PIN_TAG_PLAYER_WAYPOINT = "waypoint"
local MAP_PIN_TAG_RALLY_POINT = "rally"
local PING_CATEGORY = "pings"

local MAP_PIN_TAG = {
	[MAP_PIN_TYPE_PLAYER_WAYPOINT] = MAP_PIN_TAG_PLAYER_WAYPOINT,
	--[MAP_PIN_TYPE_PING] = group pings have individual tags for each member
	[MAP_PIN_TYPE_RALLY_POINT] = MAP_PIN_TAG_RALLY_POINT,
}

local GET_MAP_PING_FUNCTION = {
	[MAP_PIN_TYPE_PLAYER_WAYPOINT] = GetMapPlayerWaypoint,
	[MAP_PIN_TYPE_PING] = GetMapPing,
	[MAP_PIN_TYPE_RALLY_POINT] = GetMapRallyPoint,
}

local REMOVE_MAP_PING_FUNCTION = {
	[MAP_PIN_TYPE_PLAYER_WAYPOINT] = RemovePlayerWaypoint,
	[MAP_PIN_TYPE_PING] = function()
		-- there is no such function for group pings, but we can set it to 0, 0 which effectively hides it
		PingMap(MAP_PIN_TYPE_PING, MAP_TYPE_LOCATION_CENTERED, 0, 0)
	end,
	[MAP_PIN_TYPE_RALLY_POINT] = RemoveRallyPoint,
}

lib.mutePing = {}
lib.suppressPing = {}
lib.isPingSet = {}

local function GetKey(pingType, pingTag)
	if(pingType == MAP_PIN_TYPE_PLAYER_WAYPOINT) then
		pingTag = MAP_PIN_TAG_PLAYER_WAYPOINT
	elseif(pingType == MAP_PIN_TYPE_RALLY_POINT) then
		pingTag = MAP_PIN_TAG_RALLY_POINT
	end
	return string.format("%d_%s", pingType, pingTag)
end

function GetMapPlayerWaypoint()
	if(lib:IsPingSuppressed(MAP_PIN_TYPE_PLAYER_WAYPOINT, MAP_PIN_TAG_PLAYER_WAYPOINT)) then
		return 0, 0
	end
	return GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_PLAYER_WAYPOINT]()
end

function GetMapPing(pingTag)
	if(lib:IsPingSuppressed(MAP_PIN_TYPE_PING, pingTag)) then
		return 0, 0
	end
	return GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_PING](pingTag)
end

function GetMapRallyPoint()
	if(lib:IsPingSuppressed(MAP_PIN_TYPE_RALLY_POINT, MAP_PIN_TAG_RALLY_POINT)) then
		return 0, 0
	end
	return GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_RALLY_POINT]()
end

function lib:SetMapPing(pingType, mapType, x, y)
	PingMap(pingType, mapType, x, y)
end

function lib:RemoveMapPing(pingType)
	if(REMOVE_MAP_PING_FUNCTION[pingType]) then
		REMOVE_MAP_PING_FUNCTION[pingType]()
	end
end

function lib:GetMapPing(pingType, pingTag)
	local x, y = 0, 0
	if(GET_MAP_PING_FUNCTION[pingType]) then
		x, y = GET_MAP_PING_FUNCTION[pingType](pingTag)
	end
	return x, y
end

function lib:HasMapPing(pingType, pingTag) -- TODO: this should return true immediately after calling set, but false inside the map ping event when a ping was set
	pingTag = pingTag or MAP_PIN_TAG[pingType]
	if(not pingTag) then
		Log("No pingTag specified for HasMapPing")
		return false
	end
	local key = GetKey(pingType, pingTag)
	return (lib.isPingSet[key] == true)
end

function lib:RefreshMapPin(pingType, pingTag)
	if(not g_mapPinManager) then
		Log("PinManager not available. Using ZO_WorldMap_UpdateMap instead.")
		ZO_WorldMap_UpdateMap()
		return true
	end

	pingTag = pingTag or MAP_PIN_TAG[pingType]
	if(not pingTag) then
		Log("No pingTag specified for RefreshMapPing")
		return false
	end

	g_mapPinManager:RemovePins(PING_CATEGORY, pingType, pingTag)

	local x, y = lib:GetMapPing(pingType, pingTag)
	if(lib:IsPositionOnMap(x, y)) then
		g_mapPinManager:CreatePin(pingType, pingTag, x, y)
		return true
	end
	return false
end

function lib:IsPositionOnMap(x, y)
	return not (x < 0 or y < 0 or x > 1 or y > 1 or (x == 0 and y == 0))
end

function lib:MutePing(pingType, pingTag)
	local key = GetKey(pingType, pingTag)
	local mute = lib.mutePing[key] or 0
	lib.mutePing[key] = mute + 1
end

function lib:UnmutePing(pingType, pingTag)
	local key = GetKey(pingType, pingTag)
	local mute = (lib.mutePing[key] or 0) - 1
	if(mute < 0) then mute = 0 end
	lib.mutePing[key] = mute
end

function lib:IsPingMuted(pingType, pingTag)
	local key = GetKey(pingType, pingTag)
	return lib.mutePing[key] and lib.mutePing[key] > 0
end

function lib:SuppressPing(pingType, pingTag)
	local key = GetKey(pingType, pingTag)
	local suppress = lib.suppressPing[key] or 0
	lib.suppressPing[key] = suppress + 1
end

function lib:UnsuppressPing(pingType, pingTag)
	local key = GetKey(pingType, pingTag)
	local suppress = (lib.suppressPing[key] or 0) - 1
	if(suppress < 0) then suppress = 0 end
	lib.suppressPing[key] = suppress
end

function lib:IsPingSuppressed(pingType, pingTag)
	local key = GetKey(pingType, pingTag)
	return lib.suppressPing[key] and lib.suppressPing[key] > 0
end

local function InterceptMapPinManager()
	if (g_mapPinManager) then return end
	local orgRefreshCustomPins = ZO_WorldMapPins.RefreshCustomPins
	function ZO_WorldMapPins:RefreshCustomPins()
		g_mapPinManager = self
	end
	ZO_WorldMap_RefreshCustomPinsOfType()
	ZO_WorldMapPins.RefreshCustomPins = orgRefreshCustomPins
end
InterceptMapPinManager()

-- TODO keep an eye on worldmap.lua for changes
local function HandleMapPing(eventCode, pingEventType, pingType, pingTag, x, y, isPingOwner)
	if(pingEventType == PING_EVENT_ADDED) then
		lib.cm:FireCallbacks("BeforePingAdded", pingType, pingTag, x, y, isPingOwner)
		lib.isPingSet[GetKey(pingType, pingTag)] = true
		g_mapPinManager:RemovePins(PING_CATEGORY, pingType, pingTag)
		if(not lib:IsPingSuppressed(pingType, pingTag)) then
			g_mapPinManager:CreatePin(pingType, pingTag, x, y)
			if(isPingOwner and not lib:IsPingMuted(pingType, pingTag)) then
				PlaySound(SOUNDS.MAP_PING)
			end
		end
		lib.cm:FireCallbacks("AfterPingAdded", pingType, pingTag, x, y, isPingOwner)
	elseif(pingEventType == PING_EVENT_REMOVED) then
		lib.cm:FireCallbacks("BeforePingRemoved", pingType, pingTag, x, y, isPingOwner)
		lib.isPingSet[GetKey(pingType, pingTag)] = false
		g_mapPinManager:RemovePins(PING_CATEGORY, pingType, pingTag)
		if (isPingOwner and not lib:IsPingSuppressed(pingType, pingTag) and not lib:IsPingMuted(pingType, pingTag)) then
			PlaySound(SOUNDS.MAP_PING_REMOVE)
		end
		lib.cm:FireCallbacks("AfterPingRemoved", pingType, pingTag, x, y, isPingOwner)
	end
end

EVENT_MANAGER:UnregisterForEvent(LIB_IDENTIFIER, EVENT_ADD_ON_LOADED)
EVENT_MANAGER:RegisterForEvent(LIB_IDENTIFIER, EVENT_ADD_ON_LOADED, function(_, addonName)
	if(addonName == "ZO_Ingame") then
		EVENT_MANAGER:UnregisterForEvent(LIB_IDENTIFIER, EVENT_ADD_ON_LOADED)
		-- don't let worldmap do anything as we manage it instead
		EVENT_MANAGER:UnregisterForEvent("ZO_WorldMap", EVENT_MAP_PING)
		EVENT_MANAGER:RegisterForEvent(LIB_IDENTIFIER, EVENT_MAP_PING, HandleMapPing)
	end
end)

lib.cm = ZO_CallbackObject:New()
function lib:RegisterCallback(eventName, callback)
	lib.cm:RegisterCallback(eventName, callback)
end

function lib:UnregisterCallback(eventName, callback)
	lib.cm:UnregisterCallback(eventName, callback)
end