-- Path of Building
--
-- Class: Passive Spec
-- Passive tree spec class.
-- Manages node allocation and pathing for a given passive spec
--
local launch, main = ...

local pairs = pairs
local ipairs = ipairs
local t_insert = table.insert
local m_min = math.min
local m_max = math.max
local m_floor = math.floor

local PassiveSpecClass = common.NewClass("PassiveSpec", "UndoHandler", function(self, build)
	self.UndoHandler()

	self.build = build
	self.tree = build.tree

	-- Make a local copy of the passive tree that we can modify
	self.nodes = { }
	for _, treeNode in ipairs(self.tree.nodes) do
		self.nodes[treeNode.id] = setmetatable({ 
			linked = { }
		}, treeNode.meta)
	end
	for id, node in pairs(self.nodes) do
		for _, otherId in ipairs(node.linkedId) do
			t_insert(node.linked, self.nodes[otherId])
		end
	end

	-- List of currently allocated nodes
	-- Keys are node IDs, values are nodes
	self.allocNodes = { }

	self:SelectClass(0)
end)

function PassiveSpecClass:Load(xml, dbFileName)
	self.title = xml.attrib.title
	for _, node in pairs(xml) do
		if type(node) == "table" then
			if node.elem == "URL" then
				if type(node[1]) ~= "string" then
					launch:ShowErrMsg("^1Error parsing '%s': 'URL' element missing content", fileName)
					return true
				end
				self:DecodeURL(node[1])
			end
		end
	end
	self:ResetUndo()
end

function PassiveSpecClass:Save(xml)
	xml.attrib = { 
		title = self.title,
	}
	t_insert(xml, {
		elem = "URL", 
		[1] = self:EncodeURL("https://www.pathofexile.com/passive-skill-tree/")
	})
	self.modFlag = false
end

-- Import passive spec from the provided class IDs and node hash list
function PassiveSpecClass:ImportFromNodeList(classId, ascendClassId, hashList)
	self:ResetNodes()
	self:SelectClass(classId)
	for _, id in pairs(hashList) do
		local node = self.nodes[id]
		if node then
			node.alloc = true
			self.allocNodes[id] = node
		end
	end
	self:SelectAscendClass(ascendClassId)
end

-- Decode the given passive tree URL
-- Supports both the official skill tree links as well as PoE Planner links
function PassiveSpecClass:DecodeURL(url)
	local b = common.base64.decode(url:gsub("^.+/",""):gsub("-","+"):gsub("_","/"))
	if not b or #b < 6 then
		return "Invalid tree link (unrecognised format)"
	end
	local classId, ascendClassId, bandits, nodes
	if b:byte(1) == 0 and b:byte(2) == 2 then
		-- Hold on to your headgear, it looks like a PoE Planner link
		-- Let's grab a scalpel and start peeling back the 50 layers of base 64 encoding
		local treeLinkLen = b:byte(4) * 256 + b:byte(5)
		local treeLink = b:sub(6, 6 + treeLinkLen - 1)
		b = common.base64.decode(treeLink:gsub("^.+/",""):gsub("-","+"):gsub("_","/"))
		classId = b:byte(3)
		ascendClassId = b:byte(4)
		bandits = b:byte(5)
		nodes = b:sub(8, -1)
	else
		local ver = b:byte(1) * 16777216 + b:byte(2) * 65536 + b:byte(3) * 256 + b:byte(4)
		if ver > 4 then
			return "Invalid tree link (unknown version number '"..ver.."')"
		end
		classId = b:byte(5)	
		ascendClassId = 0--(ver >= 4) and b:byte(6) or 0   -- This value would be reliable if the developer of a certain online skill tree planner *cough* PoE Planner *cough* hadn't bollocked up
														   -- the generation of the official tree URL. The user would most likely import the PoE Planner URL instead but that can't be relied upon.
		nodes = b:sub(ver >= 4 and 8 or 7, -1)
	end	
	if not self.tree.classes[classId] then
		return "Invalid tree link (bad class ID '"..classId.."')"
	end
	self:ResetNodes()
	self:SelectClass(classId)
	for i = 1, #nodes - 1, 2 do
		local id = nodes:byte(i) * 256 + nodes:byte(i + 1)
		local node = self.nodes[id]
		if node then
			node.alloc = true
			self.allocNodes[id] = node
			if ascendClassId == 0 and node.ascendancyName then
				-- Just guess the ascendancy class based on the allocated nodes
				ascendClassId = self.tree.ascendNameMap[node.ascendancyName].ascendClassId
			end
		end
	end
	self:SelectAscendClass(ascendClassId)
	if bandits then
		-- Decode bandits from PoEPlanner
		local lookup = { [0] = "None", "Alira", "Kraityn", "Oak" }
		self.build.banditNormal = lookup[bandits % 4]
		self.build.banditCruel = lookup[m_floor(bandits / 4) % 4]
		self.build.banditMerciless = lookup[m_floor(bandits / 16) % 4]
	end
end

-- Encodes the current spec into a URL, using the official skill tree's format
-- Prepends the URL with an optional prefix
function PassiveSpecClass:EncodeURL(prefix)
	local a = { 0, 0, 0, 4, self.curClassId, self.curAscendClassId, 0 }
	for id, node in pairs(self.allocNodes) do
		if node.type ~= "classStart" and node.type ~= "ascendClassStart" then
			t_insert(a, m_floor(id / 256))
			t_insert(a, id % 256)
		end
	end
	return (prefix or "")..common.base64.encode(string.char(unpack(a))):gsub("+","-"):gsub("/","_")
end

-- Change the current class, preserving currently allocated nodes if they connect to the new class's starting node
function PassiveSpecClass:SelectClass(classId)
	if self.curClassId then
		-- Deallocate the current class's starting node
		local oldStartNodeId = self.curClass.startNodeId
		self.nodes[oldStartNodeId].alloc = false
		self.allocNodes[oldStartNodeId] = nil
	end

	self.curClassId = classId
	local class = self.tree.classes[classId]
	self.curClass = class 
	self.curClassName = class.name

	-- Allocate the new class's starting node
	local startNode = self.nodes[class.startNodeId]
	startNode.alloc = true
	self.allocNodes[startNode.id] = startNode

	-- Reset the ascendancy class
	-- This will also rebuild the node paths and dependancies
	self:SelectAscendClass(0)
end

function PassiveSpecClass:SelectAscendClass(ascendClassId)
	self.curAscendClassId = ascendClassId
	local ascendClass = self.curClass.classes[ascendClassId] or self.curClass.classes[0]
	self.curAscendClass = ascendClass
	self.curAscendClassName = ascendClass.name

	-- Deallocate any allocated ascendancy nodes that don't belong to the new ascendancy class
	for id, node in pairs(self.allocNodes) do
		if node.ascendancyName and node.ascendancyName ~= ascendClass.name then
			node.alloc = false
			self.allocNodes[id] = nil
		end
	end

	if ascendClass.startNodeId then
		-- Allocate the new ascendancy class's start node
		local startNode = self.nodes[ascendClass.startNodeId]
		startNode.alloc = true
		self.allocNodes[startNode.id] = startNode
	end

	-- Rebuild all the node paths and dependancies
	self:BuildAllDepends()
	self:BuildAllPaths()
end

-- Determines if the given class's start node is connected to the current class's start node
-- Attempts to find a path between the nodes which doesn't pass through any ascendancy nodes (i.e Ascendant)
function PassiveSpecClass:IsClassConnected(classId)
	for _, other in ipairs(self.nodes[self.tree.classes[classId].startNodeId].linked) do
		-- For each of the nodes to which the given class's start node connects...
		if other.alloc then
			-- If the node is allocated, try to find a path back to the current class's starting node
			other.visited = true
			local visited = { }
			local found = self:FindStartFromNode(other, visited, true)
			for i, n in ipairs(visited) do
				n.visited = false
			end
			other.visited = false
			if found then
				-- Found a path, so the given class's start node is definately connected to the current class's start node
				-- There might still be nodes which are connected to the current tree by an entirely different path though
				-- E.g via Ascendant or by connecting to another "first passive node"
				return true
			end
		end
	end
	return false
end

-- Clear the allocated status of all non-class-start nodes
function PassiveSpecClass:ResetNodes()
	for id, node in pairs(self.nodes) do
		if node.type ~= "classStart" and node.type ~= "ascendClassStart" then
			node.alloc = false
			self.allocNodes[id] = nil
		end
	end
end

-- Allocate the given node, if possible, and all nodes along the path to the node
-- An alternate path to the node may be provided, otherwise the default path will be used
-- The path must always contain the given node, as will be the case for the default path
function PassiveSpecClass:AllocNode(node, altPath)
	if not node.path then
		-- Node cannot be connected to the tree as there is no possible path
		return
	end

	-- Allocate all nodes along the path
	for _, pathNode in ipairs(altPath or node.path) do
		pathNode.alloc = true
		self.allocNodes[pathNode.id] = pathNode

		-- Build paths from this node in the likely case that some nearby nodes will have a shorter path through this node
		self:BuildPathFromNode(pathNode)
	end

	if node.isMultipleChoiceOption then
		-- For multiple choice passives, make sure no other choices are allocated
		local parent = node.linked[1]
		for _, optNode in ipairs(parent.linked) do
			if optNode.isMultipleChoiceOption and optNode.alloc and optNode ~= node then
				optNode.alloc = false
				self.allocNodes[optNode.id] = nil
				self:BuildAllPaths()
			end
		end
	end

	-- Rebuild dependancies for all allocated nodes
	self:BuildAllDepends()
end

-- Deallocate the given node, and all nodes which depend on it (i.e which are only connected to the tree through this node)
function PassiveSpecClass:DeallocNode(node)
	for _, depNode in ipairs(node.depends) do
		depNode.alloc = false
		self.allocNodes[depNode.id] = nil
	end

	-- Rebuild all paths and dependancies for all allocated nodes
	self:BuildAllDepends()
	self:BuildAllPaths()
end

-- Count the number of allocated nodes and allocated ascendancy nodes
function PassiveSpecClass:CountAllocNodes()
	local used, ascUsed = 0, 0
	for _, node in pairs(self.allocNodes) do
		if node.type ~= "classStart" and node.type ~= "ascendClassStart" then
			if node.ascendancyName then
				if not node.isMultipleChoiceOption then
					ascUsed = ascUsed + 1
				end
			else
				used = used + 1
			end
		end
	end
	return used, ascUsed
end

-- Perform a breadth-first search of the tree, starting from this node, and determine if it is the closest node to any other nodes
function PassiveSpecClass:BuildPathFromNode(root)
	root.pathDist = 0
	root.path = { }
	local queue = { root }
	local o, i = 1, 2 -- Out, in
	while o < i do
		-- Nodes are processed in a queue, until there are no nodes left
		-- All nodes that are 1 node away from the root will be processed first, then all nodes that are 2 nodes away, etc
		local node = queue[o]
		o = o + 1
		local curDist = node.pathDist + 1
		-- Iterate through all nodes that are connected to this one
		for _, other in ipairs(node.linked) do
			-- Paths must obey two rules:
			-- 1. They must not pass through class or ascendancy class start nodes (but they can start from such nodes)
			-- 2. They cannot pass between different ascendancy classes or between an ascendancy class and the main tree
			--    The one exception to that rule is that a path may start from an ascendancy node and pass into the main tree
			--    This permits pathing from the Ascendant 'Path of the X' nodes into the respective class start areas
			if other.type ~= "classStart" and other.type ~= "ascendClassStart" and other.pathDist > curDist and (node.ascendancyName == other.ascendancyName or (curDist == 1 and not other.ascendancyName)) then
				-- The shortest path to the other node is through the current node
				other.pathDist = curDist
				other.path = wipeTable(other.path)
				other.path[1] = other
				for i, n in ipairs(node.path) do
					other.path[i+1] = n
				end
				-- Add the other node to the end of the queue
				queue[i] = other
				i = i + 1
			end
		end
	end
end

-- Reset and rebuild all node paths
-- This must be done if any nodes are deallocated
function PassiveSpecClass:BuildAllPaths()
	for id, node in pairs(self.nodes) do
		node.pathDist = node.alloc and 0 or 1000
		node.path = nil
	end
	for id, node in pairs(self.allocNodes) do
		self:BuildPathFromNode(node)
	end
end

-- Attempt to find a class start node starting from the given node
-- Unless noAscent == true it will also look for an ascendancy class start node 
function PassiveSpecClass:FindStartFromNode(node, visited, noAscend)
	-- Mark the current node as visited so we don't go around in circles
	node.visited = true
	t_insert(visited, node)

	-- For each node which is connected to this one, check if...
	for _, other in ipairs(node.linked) do
		-- Either:
		--  - the other node is a start node, or
		--  - there is a path to a start node through the other node which didn't pass through any nodes which have already been visited
		if other.alloc and (other.type == "classStart" or other.type == "ascendClassStart" or (not other.visited and self:FindStartFromNode(other, visited, noAscend))) then
			if not noAscend or other.type ~= "ascendClassStart" then
				return true
			end
		end
	end
end

-- Check all nodes for other nodes which depend on them (i.e are only connected to the tree through that node)
function PassiveSpecClass:BuildAllDepends()
	-- This table will keep track of which nodes have been visited during each path-finding attempt
	local visited = { }

	for id, node in pairs(self.nodes) do
		node.depends = wipeTable(node.depends)
		if node.alloc then
			node.depends[1] = node -- All nodes depend on themselves
			node.visited = true

			local anyStartFound = (node.type == "classStart" or node.type == "ascendClassStart")
			for _, other in ipairs(node.linked) do
				if other.alloc and not isValueInArray(node.depends, other) then
					-- The other node is allocated and isn't already dependant on this node, so try and find a path to a start node through it
					if other.type == "classStart" or other.type == "ascendClassStart" then
						-- Well that was easy!
						anyStartFound = true
					elseif self:FindStartFromNode(other, visited) then
						-- We found a path through the other node, therefore the other node cannot be dependant on this node
						anyStartFound = true
						for i, n in ipairs(visited) do
							n.visited = false
							visited[i] = nil
						end
					else
						-- No path was found, so all the nodes visited while trying to find the path must be dependant on this node
						for i, n in ipairs(visited) do
							t_insert(node.depends, n)
							n.visited = false
							visited[i] = nil
						end
					end
				end
			end
			node.visited = false
			if not anyStartFound then
				-- No start nodes were found through ANY nodes
				-- Therefore this node and all nodes depending on it are orphans and should be pruned
				-- Of course Intuitive Leap will break this if support for it is ever added
				for _, depNode in ipairs(node.depends) do
					depNode.alloc = false
					self.allocNodes[depNode.id] = nil
				end
			end
		end
	end
end

function PassiveSpecClass:CreateUndoState()
	return self:EncodeURL()
end

function PassiveSpecClass:RestoreUndoState(state)
	self:DecodeURL(state)
end
