-- LibGPS2 & its files © sirinsidiator                          --
-- Distributed under The Artistic License 2.0 (see LICENSE)     --
------------------------------------------------------------------

local LIB_NAME = "LibGPS2"
local lib = LibStub:NewLibrary(LIB_NAME, 999) -- only for test purposes. releases will get a smaller number

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

local LMP = LibStub("LibMapPing", true)
if(not LMP) then
	error(string.format("[%s] Cannot load without LibMapPing", LIB_NAME))
end

local DUMMY_PIN_TYPE = LIB_NAME .. "DummyPin"
local LIB_IDENTIFIER_FINALIZE = LIB_NAME .. "_Finalize"
lib.LIB_EVENT_STATE_CHANGED = "OnLibGPS2MeasurementChanged"

local LOG_WARNING = "Warning"
local LOG_NOTICE = "Notice"
local LOG_DEBUG = "Debug"

local POSITION_MIN = 0.085
local POSITION_MAX = 0.915

local TAMRIEL_MAP_INDEX, COLDHARBOUR_MAP_INDEX
if(GetAPIVersion() == 100013) then -- TODO: remove
	function DoesCurrentMapMatchMapForPlayerLocation()
		return GetPlayerLocationName() == GetMapName()
	end
	TAMRIEL_MAP_INDEX = 1
	COLDHARBOUR_MAP_INDEX = 23
else
	TAMRIEL_MAP_INDEX = GetZoneIndex(2)
	COLDHARBOUR_MAP_INDEX = GetZoneIndex(131)
end

--lib.debugMode = 1 -- TODO
lib.mapMeasurements = lib.mapMeasurements or {}
local mapMeasurements = lib.mapMeasurements
lib.mapStack = lib.mapStack or {}
local mapStack = lib.mapStack

local MAP_PIN_TYPE_PLAYER_WAYPOINT = MAP_PIN_TYPE_PLAYER_WAYPOINT
local currentWaypointX, currentWaypointY, currentWaypointMapId = 0, 0, nil
local needWaypointRestore = false
local orgSetMapToMapListIndex = nil
local orgSetMapToQuestCondition = nil
local orgSetMapToPlayerLocation = nil
local orgSetMapToQuestZone = nil
local orgSetMapFloor = nil
local orgProcessMapClick = nil
local measuring = false

SLASH_COMMANDS["/libgpsdebug"] = function(value)
	lib.debugMode = (tonumber(value) == 1)
	df("[%s] debug mode %s", LIB_NAME, lib.debugMode and "enabled" or "disabled")
end

local function LogMessage(type, message, ...)
	if not lib.debugMode then return end
	df("[%s] %s: %s", LIB_NAME, type, zo_strjoin(" ", message, ...))
end

local function GetAddon()
	local addOn
	local function errornous() addOn = 'a' + 1 end
	local function errorHandler(err) addOn = string.match(err, "'GetAddon'.+user:/AddOns/(.-:.-):") end
	xpcall(errornous, errorHandler)
	return addOn
end

local function FinalizeMeasurement()
	EVENT_MANAGER:UnregisterForUpdate(LIB_IDENTIFIER_FINALIZE)
	LMP:UnsuppressPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
	if needWaypointRestore then
		LogMessage(LOG_DEBUG, "Update waypoint pin", LMP:GetMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT))
		LMP:RefreshMapPin(MAP_PIN_TYPE_PLAYER_WAYPOINT)
		needWaypointRestore = false
	end
	measuring = false
	CALLBACK_MANAGER:FireCallbacks(lib.LIB_EVENT_STATE_CHANGED, measuring)
end

local function HandlePingEvent(pingType, pingTag, x, y, isPingOwner)
	if(not isPingOwner or pingType ~= MAP_PIN_TYPE_PLAYER_WAYPOINT or not measuring) then return end
	-- we delay our handler until all events have been fired and so that other addons can react to it first in case they use IsMeasuring
	EVENT_MANAGER:UnregisterForUpdate(LIB_IDENTIFIER_FINALIZE)
	EVENT_MANAGER:RegisterForUpdate(LIB_IDENTIFIER_FINALIZE, 0, FinalizeMeasurement)
end

local function GetPlayerPosition()
	return GetMapPlayerPosition("player")
end

local function GetPlayerWaypoint()
	return LMP:GetMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
end

local function SetMeasurementWaypoint(x, y)
	-- this waypoint stays invisible for others
	LMP:SuppressPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
	LMP:SetMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT, MAP_TYPE_LOCATION_CENTERED, x, y)
end

local function SetPlayerWaypoint(x, y)
	LMP:SetMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT, MAP_TYPE_LOCATION_CENTERED, x, y)
end

local function RemovePlayerWaypoint()
	LMP:RemoveMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
end

local function GetReferencePoints()
	local x1, y1 = GetPlayerPosition()
	local x2, y2 = GetPlayerWaypoint()
	return x1, y1, x2, y2
end

local function IsMapMeasured(mapId)
	return (mapMeasurements[mapId or GetMapTileTexture()] ~= nil)
end

local function StoreTamrielMapMeasurements()
	-- no need to actually measure the world map
	if (orgSetMapToMapListIndex(TAMRIEL_MAP_INDEX) ~= SET_MAP_RESULT_FAILED) then
		mapMeasurements[GetMapTileTexture()] = {
			scaleX = 1,
			scaleY = 1,
			offsetX = 0,
			offsetY = 0,
			mapIndex = TAMRIEL_MAP_INDEX
		}
		return true
	end

	return false
end

local function CalculateMeasurements(mapId, localX, localY)
	-- select the map corner farthest from the player position
	local wpX, wpY = POSITION_MIN, POSITION_MIN
	-- on some maps we cannot set the waypoint to the map border (e.g. Aurdion)
	-- Opposite corner:
	if (localX < 0.5) then wpX = POSITION_MAX end
	if (localY < 0.5) then wpY = POSITION_MAX end

	SetMeasurementWaypoint(wpX, wpY)

	-- add local points to seen maps
	local measurementPositions = {}
	table.insert(measurementPositions, { mapId = mapId, pX = localX, pY = localY, wpX = wpX, wpY = wpY })

	-- switch to zone map in order to get the mapIndex for the current location
	local x1, y1, x2, y2
	while not(GetMapType() == MAPTYPE_ZONE and GetMapContentType() ~= MAP_CONTENT_DUNGEON) do
		if (MapZoomOut() ~= SET_MAP_RESULT_MAP_CHANGED) then break end
		-- collect measurements for all maps we come through on our way to the zone map
		x1, y1, x2, y2 = GetReferencePoints()
		table.insert(measurementPositions, { mapId = GetMapTileTexture(), pX = x1, pY = y1, wpX = x2, wpY = y2 })
	end

	-- some non-zone maps like Eyevea zoom directly to the Tamriel map
	local mapIndex = GetCurrentMapIndex() or TAMRIEL_MAP_INDEX

	-- switch to world map so we can calculate the global map scale and offset
	if orgSetMapToMapListIndex(TAMRIEL_MAP_INDEX) == SET_MAP_RESULT_FAILED then
		-- failed to switch to the world map
		LogMessage(LOG_NOTICE, "Could not switch to world map")
		return
	end

	-- get the two reference points on the world map
	x1, y1, x2, y2 = GetReferencePoints()

	-- calculate scale and offset for all maps that we saw
	local scaleX, scaleY, offsetX, offsetY
	for _, m in ipairs(measurementPositions) do
		if (mapMeasurements[m.mapId]) then break end -- we always go up in the hierarchy so we can stop once a measurement already exists
		LogMessage(LOG_DEBUG, "Store map measurement for", m.mapId:sub(10, -7))
		scaleX = (x2 - x1) / (m.wpX - m.pX)
		scaleY = (y2 - y1) / (m.wpY - m.pY)
		offsetX = x1 - m.pX * scaleX
		offsetY = y1 - m.pY * scaleY
		if (math.abs(scaleX - scaleY) > 1e-3) then
			LogMessage(LOG_WARNING, "Current map measurement might be wrong", m.mapId:sub(10, -7), mapIndex, m.pX, m.pY, m.wpX, m.wpY, x1, y1, x2, y2, offsetX, offsetY, scaleX, scaleY)
		end

		-- store measurements
		mapMeasurements[m.mapId] = {
			scaleX = scaleX,
			scaleY = scaleY,
			offsetX = offsetX,
			offsetY = offsetY,
			mapIndex = mapIndex
		}
	end
	return mapIndex
end

local function StoreCurrentWaypoint()
	currentWaypointX, currentWaypointY = GetPlayerWaypoint()
	currentWaypointMapId = GetMapTileTexture()
end

local function ClearCurrentWaypoint()
	currentWaypointX, currentWaypointY = 0, 0, nil
end

local function GetColdharbourMeasurement()
	-- switch to the Coldharbour map
	orgSetMapToMapListIndex(COLDHARBOUR_MAP_INDEX)
	local coldharbourId = GetMapTileTexture()
	if(not IsMapMeasured(coldharbourId)) then
		-- calculate the measurements of Coldharbour without worrying about the waypoint
		local mapIndex = CalculateMeasurements(coldharbourId, GetPlayerPosition())
		if (mapIndex ~= COLDHARBOUR_MAP_INDEX) then
			LogMessage(LOG_WARNING, "CalculateMeasurements returned different index while measuring Coldharbour map. expected:", COLDHARBOUR_MAP_INDEX, "actual:", mapIndex)
			if(not IsMapMeasured(coldharbourId)) then
				LogMessage(LOG_WARNING, "Failed to measure Coldharbour map.")
				return
			end
		end
	end
	return mapMeasurements[coldharbourId]
end

local function RestoreCurrentWaypoint()
	if(not currentWaypointMapId) then
		LogMessage(LOG_DEBUG, "Called RestoreCurrentWaypoint without calling StoreCurrentWaypoint.")
		return
	end

	local wasSet = false
	if (currentWaypointX ~= 0 or currentWaypointY ~= 0) then
		-- calculate waypoint position on the worldmap
		local measurements = mapMeasurements[currentWaypointMapId]
		local x = currentWaypointX * measurements.scaleX + measurements.offsetX
		local y = currentWaypointY * measurements.scaleY + measurements.offsetY

		if (x > 0 and x < 1 and y > 0 and y < 1) then
			-- if it is inside the Tamriel map we set it there
			if(orgSetMapToMapListIndex(TAMRIEL_MAP_INDEX) ~= SET_MAP_RESULT_FAILED) then
				SetPlayerWaypoint(x, y)
				wasSet = true
			else
				LogMessage(LOG_DEBUG, "Cannot reset waypoint because switching to the world map failed")
			end
		else -- when the waypoint is outside of the Tamriel map check if it is in Coldharbour
			measurements = GetColdharbourMeasurement()
			if(measurements) then
				-- calculate waypoint coodinates within coldharbour
				x = (x - measurements.offsetX) / measurements.scaleX
				y = (y - measurements.offsetY) / measurements.scaleY
				if not(x < 0 or x > 1 or y < 0 or y > 1) then
					if(orgSetMapToMapListIndex(COLDHARBOUR_MAP_INDEX) ~= SET_MAP_RESULT_FAILED) then
						SetPlayerWaypoint(x, y)
						wasSet = true
					else
						LogMessage(LOG_DEBUG, "Cannot reset waypoint because switching to the Coldharbour map failed")
					end
				else
					LogMessage(LOG_DEBUG, "Cannot reset waypoint because it was outside of our reach")
				end
			else
				LogMessage(LOG_DEBUG, "Cannot reset waypoint because Coldharbour measurements are unavailable")
			end
		end
	end

	if(wasSet) then
		LogMessage(LOG_DEBUG, "Waypoint was restored, request pin update")
		needWaypointRestore = true -- we need to update the pin on the worldmap afterwards
	else
		RemovePlayerWaypoint()
	end
	ClearCurrentWaypoint()
end

local function InterceptMapPinManager()
	if (lib.mapPinManager) then return end
	ZO_WorldMap_AddCustomPin(DUMMY_PIN_TYPE, function(pinManager)
		lib.mapPinManager = pinManager
		ZO_WorldMap_SetCustomPinEnabled(_G[DUMMY_PIN_TYPE], false)
	end , nil, { level = 0, size = 0, texture = "" })
	ZO_WorldMap_SetCustomPinEnabled(_G[DUMMY_PIN_TYPE], true)
	ZO_WorldMap_RefreshCustomPinsOfType(_G[DUMMY_PIN_TYPE])
end

local function HookSetMapToQuestCondition()
	orgSetMapToQuestCondition = SetMapToQuestCondition
	local function NewSetMapToQuestCondition(...)
		local result = orgSetMapToQuestCondition(...)
		if(result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured()) then
			LogMessage(LOG_DEBUG, "SetMapToQuestCondition")

			local success, mapResult = lib:CalculateMapMeasurements()
			if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
				result = mapResult
			end
		end
		-- All stuff is done before anyone triggers an "OnWorldMapChanged" event due to this result
		return result
	end
	SetMapToQuestCondition = NewSetMapToQuestCondition
end

local function HookSetMapToQuestZone()
	orgSetMapToQuestZone = SetMapToQuestZone
	local function NewSetMapToQuestZone(...)
		local result = orgSetMapToQuestZone(...)
		if(result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured()) then
			LogMessage(LOG_DEBUG, "SetMapToQuestZone")

			local success, mapResult = lib:CalculateMapMeasurements()
			if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
				result = mapResult
			end
		end
		-- All stuff is done before anyone triggers an "OnWorldMapChanged" event due to this result
		return result
	end
	SetMapToQuestZone = NewSetMapToQuestZone
end

local function HookSetMapToPlayerLocation()
	orgSetMapToPlayerLocation = SetMapToPlayerLocation
	local function NewSetMapToPlayerLocation(...)
		if not DoesUnitExist("player") then return SET_MAP_RESULT_MAP_FAILED end
		local result = orgSetMapToPlayerLocation(...)
		if(result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured()) then
			LogMessage(LOG_DEBUG, "SetMapToPlayerLocation")

			local success, mapResult = lib:CalculateMapMeasurements()
			if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
				result = mapResult
			end
		end
		-- All stuff is done before anyone triggers an "OnWorldMapChanged" event due to this result
		return result
	end
	SetMapToPlayerLocation = NewSetMapToPlayerLocation
end

local function HookSetMapToMapListIndex()
	orgSetMapToMapListIndex = SetMapToMapListIndex
	local function NewSetMapToMapListIndex(mapIndex)
		local result = orgSetMapToMapListIndex(mapIndex)
		if(result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured()) then
			LogMessage(LOG_DEBUG, "SetMapToMapListIndex")

			local success, mapResult = lib:CalculateMapMeasurements()
			if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
				result = mapResult
			end
		end

		-- All stuff is done before anyone triggers an "OnWorldMapChanged" event due to this result
		return result
	end
	SetMapToMapListIndex = NewSetMapToMapListIndex
end

local function HookProcessMapClick()
	orgProcessMapClick = ProcessMapClick
	local function NewProcessMapClick(...)
		local result = orgProcessMapClick(...)
		if(result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured()) then
			LogMessage(LOG_DEBUG, "ProcessMapClick")
			local success, mapResult = lib:CalculateMapMeasurements()
			if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
				result = mapResult
			end
		end
		return result
	end
	ProcessMapClick = NewProcessMapClick
end

local function HookSetMapFloor()
	orgSetMapFloor = SetMapFloor
	local function NewSetMapFloor(...)
		local result = orgSetMapFloor(...)
		if result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured() then
			LogMessage(LOG_DEBUG, "SetMapFloor")
			local success, mapResult = lib:CalculateMapMeasurements()
			if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
				result = mapResult
			end
		end
		return result
	end
	SetMapFloor = NewSetMapFloor
end

local function Initialize() -- wait until we have defined all functions
	--- Unregister handler from older libGPS ( < 3)
	EVENT_MANAGER:UnregisterForEvent("LibGPS2_SaveWaypoint", EVENT_PLAYER_DEACTIVATED)
	EVENT_MANAGER:UnregisterForEvent("LibGPS2_RestoreWaypoint", EVENT_PLAYER_ACTIVATED)

	--- Unregister handler from older libGPS ( <= 5.1)
	EVENT_MANAGER:UnregisterForEvent(LIB_NAME .. "_Init", EVENT_PLAYER_ACTIVATED)

	if (lib.Unload) then
		-- Undo action from older libGPS ( >= 5.2)
		lib:Unload()
	end

	--- Register new Unload
	function lib:Unload()
		SetMapToQuestCondition = orgSetMapToQuestCondition
		SetMapToQuestZone = orgSetMapToQuestZone
		SetMapToPlayerLocation = orgSetMapToPlayerLocation
		SetMapToMapListIndex = orgSetMapToMapListIndex
		ProcessMapClick = orgProcessMapClick
		SetMapFloor = orgSetMapFloor

		LMP:UnregisterCallback("AfterPingAdded", HandlePingEvent)
		LMP:UnregisterCallback("AfterPingRemoved", HandlePingEvent)
	end

	InterceptMapPinManager()

	--- Unregister handler from older libGPS, as it is now managed by LibMapPing ( >= 6)
	EVENT_MANAGER:UnregisterForEvent(LIB_NAME .. "_UnmuteMapPing", EVENT_MAP_PING)

	HookSetMapToQuestCondition()
	HookSetMapToQuestZone()
	HookSetMapToPlayerLocation()
	HookSetMapToMapListIndex()
	HookProcessMapClick()
	HookSetMapFloor()

	StoreTamrielMapMeasurements()
	SetMapToPlayerLocation() -- initial measurement so we can get back to where we are correctly

	LMP:RegisterCallback("AfterPingAdded", HandlePingEvent)
	LMP:RegisterCallback("AfterPingRemoved", HandlePingEvent)
end

------------------------ public functions ----------------------

--- Returns true as long as the player exists.
function lib:IsReady()
	return DoesUnitExist("player")
end

--- Returns true if the library is currently doing any measurements.
function lib:IsMeasuring()
	return measuring
end

--- Removes all cached measurement values.
function lib:ClearMapMeasurements()
	mapMeasurements = { }
end

--- Removes the cached measurement values for the map that is currently active.
function lib:ClearCurrentMapMeasurements()
	local mapId = GetMapTileTexture()
	mapMeasurements[mapId] = nil
end

--- Returns a table with the measurement values for the active map or nil if the measurements could not be calculated for some reason.
--- The table contains scaleX, scaleY, offsetX, offsetY and mapIndex.
--- scaleX and scaleY are the dimensions of the active map on the Tamriel map.
--- offsetX and offsetY are the offset of the top left corner on the Tamriel map.
--- mapIndex is the mapIndex of the parent zone of the current map.
function lib:GetCurrentMapMeasurements()
	local mapId = GetMapTileTexture()
	if (not mapMeasurements[mapId]) then
		-- try to calculate the measurements if they are not yet available
		lib:CalculateMapMeasurements()
	end
	return mapMeasurements[mapId]
end

--- Calculates the measurements for the current map and all parent maps.
--- This method does nothing if there is already a cached measurement for the active map.
--- return[1] boolean - True, if a valid measurement was calculated
--- return[2] SetMapResultCode - Specifies if the map has changed or failed during measurement (independent of the actual result of the measurement)
function lib:CalculateMapMeasurements(returnToInitialMap)
	-- cosmic map cannot be measured (GetMapPlayerWaypoint returns 0,0)
	if (GetMapType() == MAPTYPE_COSMIC) then return false, SET_MAP_RESULT_CURRENT_MAP_UNCHANGED end

	-- no need to take measurements more than once
	local mapId = GetMapTileTexture()
	if (mapMeasurements[mapId] or mapId == "") then return false end

	if (lib.debugMode) then
		LogMessage("Called from", GetAddon(), "for", mapId)
	end

	-- get the player position on the current map
	local localX, localY = GetPlayerPosition()
	if (localX == 0 and localY == 0) then
		-- cannot take measurements while player position is not initialized
		return false, SET_MAP_RESULT_CURRENT_MAP_UNCHANGED
	end

	returnToInitialMap = (returnToInitialMap ~= false)

	measuring = true
	CALLBACK_MANAGER:FireCallbacks(lib.LIB_EVENT_STATE_CHANGED, measuring)

	-- check some facts about the current map, so we can reset it later
	--	local oldMapIsZoneMap, oldMapFloor, oldMapFloorCount
	if returnToInitialMap then
		lib:PushCurrentMap()
	end

	local hasWaypoint = LMP:HasMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
	if(hasWaypoint) then StoreCurrentWaypoint() end

	local mapIndex = CalculateMeasurements(mapId, localX, localY)

	-- Until now, the waypoint was abused. Now the waypoint must be restored or removed again (not from Lua only).
	if(hasWaypoint) then
		RestoreCurrentWaypoint()
	else
		RemovePlayerWaypoint()
	end

	if (returnToInitialMap) then
		local result = lib:PopCurrentMap()
		return true, result
	end

	return true, (mapId == GetMapTileTexture()) and SET_MAP_RESULT_CURRENT_MAP_UNCHANGED or SET_MAP_RESULT_MAP_CHANGED
end

--- Converts the given map coordinates on the current map into coordinates on the Tamriel map.
--- Returns x and y on the world map and the mapIndex of the parent zone
--- or nil if the measurements of the active map are not available.
function lib:LocalToGlobal(x, y)
	local measurements = lib:GetCurrentMapMeasurements()
	if (measurements) then
		x = x * measurements.scaleX + measurements.offsetX
		y = y * measurements.scaleY + measurements.offsetY
		return x, y, measurements.mapIndex
	end
end

--- Converts the given global coordinates into a position on the active map.
--- Returns x and y on the current map or nil if the measurements of the active map are not available.
function lib:GlobalToLocal(x, y)
	local measurements = lib:GetCurrentMapMeasurements()
	if (measurements) then
		x =(x - measurements.offsetX) / measurements.scaleX
		y =(y - measurements.offsetY) / measurements.scaleY
		return x, y
	end
end

--- Converts the given map coordinates on the specified zone map into coordinates on the Tamriel map.
--- This method is useful if you want to convert global positions from the old LibGPS version into the new format.
--- Returns x and y on the world map and the mapIndex of the parent zone
--- or nil if the measurements of the zone map are not available.
function lib:ZoneToGlobal(mapIndex, x, y)
	lib:GetCurrentMapMeasurements()
	-- measurement done in here:
	SetMapToMapListIndex(mapIndex)
	x, y, mapIndex = lib:LocalToGlobal(x, y)
	return x, y, mapIndex
end

--- This function zooms and pans to the specified position on the active map.
function lib:PanToMapPosition(x, y)
	-- if we don't have access to the mapPinManager we cannot do anything
	if (not lib.mapPinManager) then return end

	local mapPinManager = lib.mapPinManager
	-- create dummy pin
	local pin = mapPinManager:CreatePin(_G[DUMMY_PIN_TYPE], "libgpsdummy", x, y)

	-- replace GetPlayerPin to return our dummy pin
	local getPlayerPin = mapPinManager.GetPlayerPin
	mapPinManager.GetPlayerPin = function() return pin end

	-- let the map pan to our dummy pin
	ZO_WorldMap_PanToPlayer()

	-- cleanup
	mapPinManager.GetPlayerPin = getPlayerPin
	mapPinManager:RemovePins(DUMMY_PIN_TYPE)
end

--- This function sets the current map as player chosen so it won't switch back to the previous map.
function lib:SetPlayerChoseCurrentMap()
	-- replace the original functions
	local oldIsChangingAllowed = ZO_WorldMap_IsMapChangingAllowed
	local oldSetMapToMapListIndex = SetMapToMapListIndex
	ZO_WorldMap_IsMapChangingAllowed = function() return true end
	SetMapToMapListIndex = function() return SET_MAP_RESULT_MAP_CHANGED end

	-- make our rigged call to set the player chosen flag
	ZO_WorldMap_SetMapByIndex()

	-- cleanup
	ZO_WorldMap_IsMapChangingAllowed = oldIsChangingAllowed
	SetMapToMapListIndex = oldSetMapToMapListIndex
end

--- Repeatedly calls ProcessMapClick on the given global position starting on the Tamriel map until nothing more would happen.
--- Returns SET_MAP_RESULT_FAILED, SET_MAP_RESULT_MAP_CHANGED or SET_MAP_RESULT_CURRENT_MAP_UNCHANGED depending on the result of the API calls.
function lib:MapZoomInMax(x, y)
	local result = SetMapToMapListIndex(TAMRIEL_MAP_INDEX)

	if (result ~= SET_MAP_RESULT_FAILED) then
		local localX, localY = x, y

		while WouldProcessMapClick(localX, localY) do
			result = orgProcessMapClick(localX, localY)
			if (result == SET_MAP_RESULT_FAILED) then break end
			localX, localY = lib:GlobalToLocal(x, y)
		end
	end

	return result
end

--- Stores information about how we can back to this map on a stack.
function lib:PushCurrentMap()
	local wasPlayerLocation, currentMapIndex, targetMapTileTexture, currentMapFloor, currentMapFloorCount
	wasPlayerLocation = DoesCurrentMapMatchMapForPlayerLocation()
	currentMapIndex = GetCurrentMapIndex()
	targetMapTileTexture = GetMapTileTexture()
	currentMapFloor, currentMapFloorCount = GetMapFloorInfo()

	mapStack[#mapStack + 1] = {wasPlayerLocation, currentMapIndex, targetMapTileTexture, currentMapFloor, currentMapFloorCount}
end

--- Switches to the map that was put on the stack last.
--- Returns SET_MAP_RESULT_FAILED, SET_MAP_RESULT_MAP_CHANGED or SET_MAP_RESULT_CURRENT_MAP_UNCHANGED depending on the result of the API calls.
function lib:PopCurrentMap()
	local result = SET_MAP_RESULT_FAILED
	local data = table.remove(mapStack, #mapStack)
	if(not data) then
		LogMessage(LOG_DEBUG, "PopCurrentMap failed. No data on map stack.")
		return result
	end

	local wasPlayerLocation, currentMapIndex, targetMapTileTexture, currentMapFloor, currentMapFloorCount = unpack(data)

	local currentTileTexture = GetMapTileTexture()
	if(currentTileTexture ~= targetMapTileTexture) then
		if(wasPlayerLocation) then -- switch back to the player location
			result = orgSetMapToPlayerLocation()

		elseif(currentMapIndex ~= nil and currentMapIndex > 0) then -- set to a zone map
			result = orgSetMapToMapListIndex(currentMapIndex)

		else -- here is where it gets tricky
			local target = mapMeasurements[targetMapTileTexture]
			assert(target, string.format("No measurement for \"%s\".", targetMapTileTexture))

			-- switch to the parent zone
			if(target.mapIndex == TAMRIEL_MAP_INDEX) then -- zone map has no mapIndex (e.g. Eyevea or Hew's Bane on first PTS patch for update 9)
				-- switch to the tamriel map just in case
				result = orgSetMapToMapListIndex(TAMRIEL_MAP_INDEX)
				if(result == SET_MAP_RESULT_FAILED) then return result end
				-- get global coordinates of target map center
				local x = target.offsetX + (target.scaleX / 2)
				local y = target.offsetY + (target.scaleY / 2)
				assert(WouldProcessMapClick(x, y), string.format("Cannot process click at %s/%s on map \"%s\" in order to get to \"%s\".", tostring(x), tostring(y), GetMapTileTexture(), targetMapTileTexture))
				result = orgProcessMapClick(x, y)
				if(result == SET_MAP_RESULT_FAILED) then return result end
			else
				result = orgSetMapToMapListIndex(target.mapIndex)
				if(result == SET_MAP_RESULT_FAILED) then return result end
			end

			-- switch to the sub zone
			currentTileTexture = GetMapTileTexture()
			if(currentTileTexture ~= targetMapTileTexture) then
				-- determine where on the zone map we have to click to get to the sub zone map
				-- get global coordinates of target map center
				local x = target.offsetX + (target.scaleX / 2)
				local y = target.offsetY + (target.scaleY / 2)
				-- transform to local coordinates
				local current = mapMeasurements[currentTileTexture]
				assert(current, string.format("No measurement for \"%s\".", currentTileTexture))
				x = (x - current.offsetX) / current.scaleX
				y = (y - current.offsetY) / current.scaleY

				assert(WouldProcessMapClick(x, y), string.format("Cannot process click at %s/%s on map \"%s\" in order to get to \"%s\".", tostring(x), tostring(y), GetMapTileTexture(), targetMapTileTexture))
				result = orgProcessMapClick(x, y)
				if(result == SET_MAP_RESULT_FAILED) then return result end
			end

			-- switch to the correct floor (e.g. Elden Root)
			if (currentMapFloorCount > 0) then
				result = orgSetMapFloor(currentMapFloor)
			end
		end
	else
		result = SET_MAP_RESULT_CURRENT_MAP_UNCHANGED
	end

	return result
end

Initialize()