local _, GF = ...

local FavoriteInstances = {}
GF.FavoriteInstances = FavoriteInstances

local ROOT_KEY = "favorite_instances"
local ACTIVITY_IDENTITY_PREFIX = "activity:"

local function currentTabID()
	local mainFrame = GF.MainFrame
	return mainFrame and mainFrame.GetCurrentTabID
		and mainFrame:GetCurrentTabID() or GF.TAB_BROWSE
end

local function records()
	local db = GF.GetDB and GF.GetDB() or nil
	if not db then
		return {}
	end
	if type(db.favoriteInstances) ~= "table" then
		db.favoriteInstances = {}
	end
	return db.favoriteInstances
end

local function findRecord(identity)
	for index, entry in ipairs(records()) do
		if entry.identity == identity then
			return entry, index
		end
	end
	return nil, nil
end

local function recordKind(record)
	if record and record.kind == "activity" then
		return "activity"
	end
	local identity = record and record.identity
	if type(identity) == "string"
		and identity:match("^" .. ACTIVITY_IDENTITY_PREFIX .. "%d+:%d+$")
	then
		return "activity"
	end
	return "instance"
end

local function activityIdentity(categoryID, activityID)
	categoryID = tonumber(categoryID)
	activityID = tonumber(activityID)
	if not (categoryID and categoryID > 0 and activityID and activityID > 0) then
		return nil
	end
	return string.format("%s%d:%d", ACTIVITY_IDENTITY_PREFIX, categoryID, activityID)
end

local function activityIDsForRecord(record)
	local categoryID = tonumber(record and record.categoryID)
	local activityID = tonumber(record and record.activityID)
	if not (categoryID and activityID) and record and type(record.identity) == "string" then
		local categoryText, activityText = record.identity:match(
			"^" .. ACTIVITY_IDENTITY_PREFIX .. "(%d+):(%d+)$")
		categoryID = categoryID or tonumber(categoryText)
		activityID = activityID or tonumber(activityText)
	end
	return categoryID, activityID
end

local function nodeLabel(node)
	local label = node and node.label
	if type(label) == "string" and label ~= "" then
		return label
	end
	local info = node and node.activityInfo
	local fullName = info and info.fullName
	if type(fullName) == "string" and fullName ~= "" then
		return fullName
	end
	local shortName = info and info.shortName
	if type(shortName) == "string" and shortName ~= "" then
		return shortName
	end
	return nil
end

local function nodeBreadcrumb(node)
	local navTree = GF.NavTree
	local finder = navTree and navTree.FindNodePathByKey
	if type(finder) ~= "function" or not (node and node.key) then
		return nil
	end
	local _, path = navTree:FindNodePathByKey(node.key)
	local labels = {}
	for index = 1, math.max(0, #(path or {}) - 1) do
		local label = nodeLabel(path[index])
		if label then
			labels[#labels + 1] = label
		end
	end
	return #labels > 0 and table.concat(labels, " · ") or nil
end

local function activityCandidateForNode(node)
	local info = node and node.activityInfo
	local categoryID = tonumber(node and node.categoryID)
		or tonumber(info and info.categoryID)
	local activityID = tonumber(node and node.activityID)
	if not (categoryID and activityID) or node.categoryBrowse == true then
		return nil
	end
	-- Dungeon and raid difficulties must continue to collapse to the stable
	-- instance identity supplied by QuickSearch instead of becoming duplicates.
	if categoryID == tonumber(GF.CAT_DUNGEON)
		or categoryID == tonumber(GF.CAT_RAID)
	then
		return nil
	end
	local identity = activityIdentity(categoryID, activityID)
	local label = nodeLabel(node)
	if not (identity and label) then
		return nil
	end
	return {
		identity = identity,
		kind = "activity",
		activityID = activityID,
		categoryID = categoryID,
		label = label,
		breadcrumb = nodeBreadcrumb(node),
	}
end

local function nodeAllowedForTab(node, tabID)
	if not node or node.disabled then
		return false
	end
	local workspace = GF.LFGWorkspaceView
	if workspace and workspace.IsNodeAllowed
		and not workspace:IsNodeAllowed(node)
	then
		return false
	end
	if tabID == GF.TAB_CREATE then
		if node.browseOnly then
			return false
		end
		if workspace and workspace.CanCreateSelection then
			return workspace:CanCreateSelection(node) == true
		end
		local canCreate = GF.NavData and GF.NavData.CanCreateFromNode
		return type(canCreate) == "function" and canCreate(node) == true
	end
	local accepts = GF.NavData and GF.NavData.AcceptsBrowseSelection
	return type(accepts) == "function" and accepts(node) == true
end

local function resolveActivityRecord(record)
	local categoryID, activityID = activityIDsForRecord(record)
	local finder = GF.NavData and GF.NavData.FindNodeByActivityID
	local node = type(finder) == "function" and finder(activityID) or nil
	if not node then
		return nil
	end
	local info = node.activityInfo
	local currentCategoryID = tonumber(node.categoryID)
		or tonumber(info and info.categoryID)
	if currentCategoryID ~= categoryID then
		return nil
	end
	return node
end

local function favoriteRoot()
	for _, root in ipairs(GF.navTree or {}) do
		if root and root.key == ROOT_KEY then
			return root
		end
	end
	return nil
end

local function refreshOpenFavoritePanel(root)
	local flyout = GF.NavFlyout
	if root and flyout and flyout.IsNodeOpen and flyout:IsNodeOpen(root)
		and flyout.ReanchorOpenPanels
	then
		flyout:ReanchorOpenPanels()
	end
end

local function notifyChanged()
	local root = FavoriteInstances:RefreshRoot(currentTabID())
	if GF.NavTree and GF.NavTree.Refresh then
		GF.NavTree:Refresh()
	end
	refreshOpenFavoritePanel(root)
end

function FavoriteInstances:GetRecords()
	return records()
end

function FavoriteInstances:GetCount()
	return #records()
end

function FavoriteInstances:IsFavorite(identity)
	return findRecord(identity) ~= nil
end

function FavoriteInstances:GetCandidateForNode(node)
	local search = GF.QuickSearch
	local entry = search and search.GetEntryForNode
		and search:GetEntryForNode(node) or nil
	if entry and type(entry.identity) == "string" then
		return {
			identity = entry.identity,
			kind = "instance",
			label = entry.label,
			breadcrumb = entry.breadcrumb,
		}
	end
	return activityCandidateForNode(node)
end

function FavoriteInstances:AddCandidate(candidate)
	if type(candidate) ~= "table"
		or type(candidate.identity) ~= "string"
		or candidate.identity == ""
	then
		return false, "invalid"
	end
	local kind = candidate.kind == "activity" and "activity" or "instance"
	local categoryID = tonumber(candidate.categoryID)
	local activityID = tonumber(candidate.activityID)
	if kind == "activity"
		and candidate.identity ~= activityIdentity(categoryID, activityID)
	then
		return false, "invalid"
	end
	if self:IsFavorite(candidate.identity) then
		return false, "duplicate"
	end
	local list = records()
	list[#list + 1] = {
		identity = candidate.identity,
		kind = kind,
		categoryID = kind == "activity" and categoryID or nil,
		activityID = kind == "activity" and activityID or nil,
		label = type(candidate.label) == "string" and candidate.label or nil,
		breadcrumb = type(candidate.breadcrumb) == "string"
			and candidate.breadcrumb or nil,
	}
	notifyChanged()
	return true
end

function FavoriteInstances:Remove(identity)
	local _, index = findRecord(identity)
	if not index then
		return false
	end
	table.remove(records(), index)
	notifyChanged()
	return true
end

function FavoriteInstances:GetIdentityOrder()
	local order = {}
	for index, entry in ipairs(records()) do
		order[index] = entry.identity
	end
	return order
end

function FavoriteInstances:CommitOrder(identityOrder, suppressRefresh)
	if type(identityOrder) ~= "table" then
		return false
	end
	local list = records()
	if #identityOrder ~= #list then
		return false
	end
	local byIdentity = {}
	for _, entry in ipairs(list) do
		if type(entry.identity) ~= "string" or byIdentity[entry.identity] then
			return false
		end
		byIdentity[entry.identity] = entry
	end
	local ordered, seen = {}, {}
	for index, identity in ipairs(identityOrder) do
		local entry = type(identity) == "string" and byIdentity[identity] or nil
		if not entry or seen[identity] then
			return false
		end
		seen[identity] = true
		ordered[index] = entry
	end
	for index, entry in ipairs(ordered) do
		list[index] = entry
	end
	if not suppressRefresh then
		notifyChanged()
	end
	return true
end

local function orderedRecords(identityOrder)
	local list = records()
	if type(identityOrder) ~= "table" or #identityOrder ~= #list then
		return list
	end
	local byIdentity, ordered, seen = {}, {}, {}
	for _, entry in ipairs(list) do
		byIdentity[entry.identity] = entry
	end
	for index, identity in ipairs(identityOrder) do
		local entry = byIdentity[identity]
		if not entry or seen[identity] then
			return list
		end
		seen[identity] = true
		ordered[index] = entry
	end
	return ordered
end

function FavoriteInstances:BuildNodes(tabID, identityOrder)
	local list = orderedRecords(identityOrder)
	local L = GF.L or {}
	if #list == 0 then
		return {
			{
				key = ROOT_KEY .. "::empty",
				level = 1,
				label = L.FAVORITE_INSTANCE_EMPTY
					or "Right-click an activity in the list to add it to Favorites",
				isLeaf = false,
				disabled = true,
				navKind = ROOT_KEY,
				favoriteEmpty = true,
			},
		}
	end

	local search = GF.QuickSearch
	local resolved, labelCounts = {}, {}
	for index, record in ipairs(list) do
		local kind = recordKind(record)
		local entry = kind == "instance" and search
			and search.GetEntryByIdentity
			and search:GetEntryByIdentity(record.identity) or nil
		local activityNode = kind == "activity"
			and resolveActivityRecord(record) or nil
		if entry then
			record.label = entry.label or record.label
			record.breadcrumb = entry.breadcrumb or record.breadcrumb
		elseif activityNode then
			record.label = nodeLabel(activityNode) or record.label
			record.breadcrumb = nodeBreadcrumb(activityNode) or record.breadcrumb
		end
		local baseLabel = entry and entry.label
			or activityNode and nodeLabel(activityNode)
			or record.label or record.identity
		resolved[index] = {
			record = record,
			kind = kind,
			entry = entry,
			activityNode = activityNode,
			baseLabel = baseLabel,
		}
		labelCounts[baseLabel] = (labelCounts[baseLabel] or 0) + 1
	end

	local nodes = {}
	for index, item in ipairs(resolved) do
		local record, entry = item.record, item.entry
		local isActivity = item.kind == "activity"
		local activeTabID = tabID or currentTabID()
		local children = not isActivity and search and search.BuildDifficultyNodes
			and search:BuildDifficultyNodes(entry, activeTabID) or {}
		local label = item.baseLabel
		if labelCounts[item.baseLabel] > 1 and record.breadcrumb then
			label = string.format("%s |cff888888· %s|r", label, record.breadcrumb)
		end
		local disabled = isActivity
			and not nodeAllowedForTab(item.activityNode, activeTabID)
			or not isActivity and (entry == nil or #children == 0)
		if disabled then
			label = string.format(
				L.FAVORITE_INSTANCE_UNAVAILABLE_FMT or "%s · Unavailable",
				label)
		end
		nodes[#nodes + 1] = {
			key = ROOT_KEY .. "::result::" .. record.identity,
			level = 1,
			label = label,
			isLeaf = false,
			disabled = disabled,
			disabledTooltip = disabled,
			disabledReason = disabled and (L.NAV_ACTIVITY_UNAVAILABLE
				or "This activity is not currently available.") or nil,
			navKind = ROOT_KEY,
			children = children,
			childrenLoaded = true,
			favoriteResult = true,
			favoriteIdentity = record.identity,
			favoriteOrder = index,
			favoriteActivity = isActivity,
			favoriteActivityID = isActivity and record.activityID or nil,
			favoriteCategoryID = isActivity and record.categoryID or nil,
			quickSearchIdentity = not isActivity and record.identity or nil,
			quickSearchResult = not isActivity,
			_gfQuickSearchProxy = not isActivity,
			_gfQuickSearchGeneration = not isActivity and search and search.GetGeneration
				and search:GetGeneration() or 0,
		}
	end
	return nodes
end

function FavoriteInstances:RefreshRoot(tabID, identityOrder)
	local root = favoriteRoot()
	if not root then
		return nil
	end
	root.children = self:BuildNodes(tabID or currentTabID(), identityOrder)
	root.childrenLoaded = true
	return root
end

function FavoriteInstances:Invalidate()
	local root = favoriteRoot()
	if root then
		root.children = nil
		root.childrenLoaded = false
	end
end

function FavoriteInstances:ResolveTarget(proxy, tabID)
	if not (proxy and proxy.favoriteIdentity) then
		return nil
	end
	local record = findRecord(proxy.favoriteIdentity)
	if recordKind(record) == "activity" then
		local node = resolveActivityRecord(record)
		return nodeAllowedForTab(node, tabID or currentTabID()) and node or nil
	end
	local search = GF.QuickSearch
	local entry = search and search.GetEntryByIdentity
		and search:GetEntryByIdentity(proxy.favoriteIdentity) or nil
	return entry and search.ResolveEntrySource
		and search:ResolveEntrySource(entry, tabID or currentTabID()) or nil
end

function FavoriteInstances:ResolveBrowseSource(proxy)
	return self:ResolveTarget(proxy, GF.TAB_BROWSE)
end
