class 'WayManager'

local ACTIVE = true
local STATE_NAME = "STATE_WayManager"

NodeType = {
	SCENE 		= 1,
	LINK 		= 2,
	ZOOM 		= 3,
	TOOL_APPLY 	= 4,
	TOOL_FIND 	= 5,
	CLICK 		= 6,
	DRAG_N_DROP = 7,
	NOTE 		= 8,
	TASK 		= 9,
	ADDITIONAL 	= 10
}

NodeWeights = {
	[NodeType.LINK] 		= 100,
	[NodeType.SCENE] 		= 1,
	[NodeType.ZOOM] 		= 3,
	[NodeType.TOOL_APPLY] 	= -11,
	[NodeType.TOOL_FIND] 	= -11,
	[NodeType.CLICK] 		= -11,
	[NodeType.DRAG_N_DROP] 	= -11,
	[NodeType.NOTE] 		= -12,
	[NodeType.TASK] 		= -14,
	[NodeType.ADDITIONAL] 	= -10
}

function WayManager:__init()
	self.current_node = nil
	self.root_node = nil
	
	self.is_node_debug_info_shown = false
end

function WayManager:clear()
	self.current_node = nil
	self.root_node = nil
end

function WayManager:addLink(location_title, target_name, root_node)
	local scene = self:findNode(NodeType.SCENE, location_title) or 
		WayPoint(NodeType.SCENE, location_title, location_title)
	
	local link = self:addNode(NodeType.LINK, location_title, target_name, root_node)
	
	-- Link can belong do deleted node if so, just ignore it.
	if link then
		self:connectNode(scene, link)
	end
	
	return link
end

function WayManager:addZoom(zoom_title, target_name, root_node)
	local node = self:addNode(NodeType.ZOOM, zoom_title, target_name, root_node)
	
	return node
end

function WayManager:addToolApplying(tool_name, target_name, root_node)
	local node = self:getConnectedNode(NodeType.TOOL_APPLY, tool_name, target_name, root_node)

	if not node then
		local collection = getCollectionTools(tool_name)
		node = self:addNode(NodeType.TOOL_APPLY, tool_name, target_name, root_node, TOOL_FOUND, tool_name)
		if node and collection then
			for _, tool in ipairs(collection.tools) do
				node:addCondition(TOOL_FOUND, tool)
			end
			node:setState(NodeState.PENDING)
		end
	end

	return node
end

function WayManager:addToolFinding(tool_name, target_name, root_node)
	local node = self:addNode(NodeType.TOOL_FIND, tool_name, target_name, root_node)
	
	return node
end

function WayManager:addClickNode(area_name, root_node)
	local node = self:addNode(NodeType.CLICK, area_name, area_name, root_node)
	
	return node
end

function WayManager:addDragDropNode(get_area, drop_area, root_node)
	local node = self:addNode(NodeType.DRAG_N_DROP, get_area, drop_area, root_node)
	
	return node
end

function WayManager:addTaskNode(object, root_node)
	local node = self:addNode(NodeType.TASK, object, object, root_node)
	if node then
		node.isConditionsSatisfied = function() return false end
	end
	return node
end

function WayManager:addAdditionalNode(target, root_node, weight)
	local node = self:addNode(NodeType.ADDITIONAL, target, target, root_node, nil, nil, weight)
	
	return node
end

function WayManager:tryAddDependency(state, value, node)
	if node then self:addAdditionalDependency(state, value, node) end
end

function WayManager:addAdditionalDependency(state, value, node)
	if HasGameState(state, value) then return end
	
	node:addCondition(state, value)
	
	return dep_node
end

-- New node factory function. state and value - state from with Node depends. 
-- Can be nil if Node is active after creation.
function WayManager:addNode(type, info, target_name, root_node, state, value, weight)
	if (type == NodeType.CLICK and current_scene:getPlayField():isObjectClicked(info)) or
		(type == NodeType.ZOOM and HasGameState(ZOOM_DISABLED, current_scene:getPlayField():getName()..'_'..info..'_disable')) or
		(type == NodeType.TASK and IsTaskObjectFound(current_scene:getPlayField():getName(), info)) or
		(type == NodeType.TOOL_FIND and IsToolFound(info)) or
		(type == NodeType.TOOL_APPLY and (IsToolApplied(info) or IsToolTargetUsed(info, target_name, current_scene:getPlayField():getName()))) or
		(type == NodeType.DRAG_N_DROP and IsDragObjectUsed(info)) then
		return
	end

	if not root_node then 
		local parent_zoom = getZoomZoneOfObject(target_name)
		if parent_zoom then
			root_node = self:getConnectedNode(NodeType.ZOOM, parent_zoom.name)
			if not root_node then
				return
			end
		else
			root_node = root_node or self.current_node
		end
	end
	weight = weight or NodeWeights[type]
	
	local node = self:getConnectedNode(type, info, target_name, root_node)
	
	if not node then
		node = WayPoint(type, info, target_name, state, value)
		self:connectNode(node, root_node, weight)
	end

	self:showNodeDebugInfo(node)
	
	return node
end

function WayManager:connectNode(node, root_node, weight)
	weight = weight or NodeWeights[node.type]
	
	root_node.connected_nodes[node] = weight
	
	return root_node
end

-- Finds node at current location.
function WayManager:getConnectedNode(type, info, target_name, root_node)
	assert(type and info and "No type or info")

	root_node = root_node or self.current_node
	local result = nil
	local parent_node = nil

	local function proceed_node(current_node) 
		for node, _ in pairs(current_node.connected_nodes) do
					  if node.type == type and node.info == info and not (target_name and node.target_area ~= target_name)
					  then
								result = node
								parent_node = current_node
								break
					  else
								if node.type ~= NodeType.SCENE then 
									proceed_node(node)
								end
					  end
		end
	end

	proceed_node(root_node)
	return result, parent_node
end

function WayManager:findNode(type, info)
	local node_to_visit = {}
	local added_nodes = {}
	local index  = 0
	local size           = 0

	local function proceed_node(node)
		if node.type == type and node.info == info then
			return node
		else
			for node, _ in pairs(node.connected_nodes) do
				if not added_nodes[node] then
					added_nodes[node] = true
					table.insert(node_to_visit, node)
					size  = size + 1
				end
			end
		end
		if index < size then
			index = index + 1
			return proceed_node(node_to_visit[index])
		end
		return nil
	end
	
	if self.root_node then
		return proceed_node(self.root_node)
	end
end

function WayManager:findNodeNamed(node_name)
	local node = nil
	local function check_branch(entry_area, current_node) -- entry area - area in current scene for this branch
		if current_node.type == NodeType.SCENE then
			return
		end

		if current_node.target_area == node_name then
			node = current_node
		end
		
		for next_node, _ in pairs(current_node.connected_nodes) do
			check_branch(entry_area, next_node)
		end
	end

	for node, _ in pairs(self.current_node.connected_nodes) do
		check_branch(node, node)
	end

	return node
end

function WayManager:getGraphSize()
	local nodes = {}
	local size = 0
	
	local function count_connections(node)
		for node, _ in pairs(node.connected_nodes) do
			if not nodes[node] then
				size = size + 1
				nodes[node] = true
				count_connections(node)
			end
		end
	end
	
	count_connections(self.root_node)
	return size
end

function WayManager:printGraph()
	local nodes = {}
	local doc = XMLDocument('graph')
	local root = doc:GetRootElement()

	root:AddAttribute("size", tostring(self:getGraphSize()))

	local function print_connections(node, xml_element)
		local elem_node = XMLElement("node")
		xml_element:AddElement(elem_node)
		elem_node:AddAttribute("name", node:__tostring())

		-- add visible
		do
			if node.target_is_visible == TargetVisibility.INVISIBLE then
				elem_node:AddAttribute("target_is_visible", 'INVISIBLE')
			elseif node.target_is_visible == TargetVisibility.VISIBLE then
				elem_node:AddAttribute("target_is_visible", 'VISIBLE')
			end
		end

		-- add state
		do
			if node.state == NodeState.PENDING then
				elem_node:AddAttribute("state", 'PENDING')
			elseif node.state == NodeState.ENABLED then
				elem_node:AddAttribute("state", 'ENABLED')
			elseif node.state == NodeState.TRIGGERED then
				elem_node:AddAttribute("state", 'TRIGGERED')
			elseif node.state == NodeState.IGNORED then
				elem_node:AddAttribute("state", 'IGNORED')
			end
		end
		elem_node:AddAttribute("dependencies", node.state_string)

		for node_i, _ in pairs(node.connected_nodes) do
			if not nodes[node_i] then
				nodes[node_i] = true
				print_connections(node_i, elem_node)
			end
		end
	end

	print_connections(self.root_node, root)
	doc:Save('graph.xml')
end

function WayManager:setCurrentNode(scene)
	assert(scene.type == NodeType.SCENE and 'Current node should be scene node')
	
	if not self.root_node then
		self.root_node = scene
	end
	self.current_node = scene
	
	scene:trigger()
end

function WayManager:getCurrentNode()
	return self.current_node
end

function WayManager:removeNode(node)
	local target_node, parent_node = self:getConnectedNode(node.type, node.info)
	if target_node then
		parent_node.connected_nodes[target_node] = nil
	end
end

function WayManager:clearDisabledTrees()
	for node, _ in pairs(self:getCurrentNode().connected_nodes) do
		if node:isRootOfDisabledTree() then
	 self:getCurrentNode().connected_nodes[node] = nil
		end
	end
end

function WayManager:saveToGameState(Tag)
	if current_scene and current_scene:getPlayField().mode == level_modes.featured or not self.current_node then
		return
	end
	local function GetNodes()
		local nodes = {}
		local result = {}
		
		local function count_connections(node)
	 local key, value = node:getGameStateString()
	 result[key] = value
	 
	 for node, _ in pairs(node.connected_nodes) do
		 if not nodes[node] then
			 nodes[node] = true
			 count_connections(node)
		 end
	 end
		end
		count_connections(self.root_node)
		
		return result
	end
	------------------------------------------
	
	local function ToString(val, is_quote_surround)
		local function GetAddress(obj)
	 local mt = getmetatable(obj) or {}
	 local _tos = mt.__tostring
	 mt.__tostring = nil
	 
	 local key = tostring(obj)
	 
	 mt.__tostring = _tos
	 return key
		end
		
		local function KeyValToString(key, value)
	 return "[ " .. ToString(key, is_quote_surround) .. " ] = " .. ToString(value, is_quote_surround)
		end
		
		local function GetString(key1, value1, ...)
	 if key1 then
		 return KeyValToString(key1, value1) .. ", " .. GetString(...)
	 end
	 
	 return ""
		end
		
		local function UnpackTable(t)
	 local function nextKey(key, value)
		 if key then
			 return key, value, nextKey(next(t, key))
		 end
	 end
	 
	 return nextKey(next(t))
		end
		
		------------------
		local t = type(val)
		
		if t == "table" then
	 return '{' .. GetString(UnpackTable(val)) .. '}'
		elseif t == "userdata" then
	 return "GetNode('" .. ToString(GetAddress(val)) .."')"
		elseif t == "string" then
	 return is_quote_surround and ("'" .. val .. "'") or val
		else
	 return tostring(val)
		end
	end
	------------------------------------------
	
	local nodes = ToString(GetNodes())
	local current_node  = ToString(self.current_node)
	local root_node     = ToString(self.root_node)
	
	local state = 
		[[
	  function __WAY_MANAGER_RESTORE(self)
		  local way_manager_save_table = {
		['nodes'] = ]] .. nodes .. [[,
					 ['current_node'] = function(GetNode) return ]] .. current_node .. [[ end,
					 ['root_node'] = function(GetNode) return ]] .. root_node .. [[ end,
					 }
				 local nodes = way_manager_save_table.nodes

				 local function GetNode( addr )
					 return nodes[addr]
				 end

				 for i,v in pairs(nodes) do
					 nodes[i] = v(GetNode)
				 end

				for str_addr,node in pairs(nodes) do
					for i,name in pairs({'connected_nodes'}) do
				local new_table = {}
				local field = node[name]
		
				for addr,v in pairs(field) do
			  if type(addr) == 'string' and GetNode(addr) then
					 new_table[GetNode(addr)] = v
		 else
				new_table[addr] = v
		 end
			end
		
			node[name] = new_table
		 end
	 end

	 self.current_node = way_manager_save_table.current_node(GetNode)
	 self.root_node = way_manager_save_table.root_node(GetNode)
	 end]]

	 SetGameState(STATE_NAME, state, Tag) 
end

function WayManager:restoreFromGameState(Tag)
	local state = HasGameState(STATE_NAME, nil, Tag) and GetGameState(STATE_NAME, Tag):get(0)
	if not state then
		self:clear()
		return
	end
	
	runmsg, errormsg = pcall(loadstring(state))
	__WAY_MANAGER_RESTORE(self)
end

function WayManager:startLevel(level_name)
	if levels['l_'..level_name].mode == level_modes.featured then
		return
	end
	local Tag_old = nil
	local Tag_new = levels['l_'..level_name].collectors_edition and EGameStateTag.CE or EGameStateTag.SE
	
	if current_scene:getPlayField() then
		Tag_old = levels['l_' .. current_scene:getPlayField():getName()].collectors_edition and EGameStateTag.CE or EGameStateTag.SE
		
		self:clearDisabledTrees()
		
		if self.current_node then 
			self.current_node.target_is_visible = TargetVisibility.NOT_DEPENDS
			
			local function check_node(node)
				node.target_is_visible = node.target_is_visible and current_scene:getPlayField():hasEntityNamed(node.target_area) 
					and current_scene:getPlayField():getHint():IsAccessible(current_scene:getPlayField():getEntityNamed(node.target_area)) and TargetVisibility.VISIBLE 
					or (node.target_is_visible and TargetVisibility.INVISIBLE)
				for sub_node, _ in pairs(node.connected_nodes) do
					if sub_node.type ~= NodeType.SCENE then
						check_node(sub_node)
					end
				end
			end
			check_node(self:getCurrentNode())
		end
		
		if Tag_old ~= Tag_new then
			self:saveToGameState(Tag_old)
			self:restoreFromGameState(Tag_new)
		end
	else
		self:restoreFromGameState(Tag_new)
	end

	local level_node = self:findNode(NodeType.SCENE, level_name)
	
	if level_node then
		-- Console:pushText('Successfully changed node to '..level_node.info)
	else
		level_node = WayPoint(NodeType.SCENE, level_name, level_name)
		-- Console:pushText('Created new root node '..level_node.info)
	end
	self:setCurrentNode(level_node)
end

function WayManager:showNodeDebugInfo(node)
	if isDebugMode() and current_scene and current_scene:getPlayField():hasEntityNamed(node.target_area) then
		local text = node:__tostring()..' '
		text = text..'state: '

		if node.state == NodeState.ENABLED then
			text = text..'ENABLED '
		elseif node.state == NodeState.PENDING then
			text = text..'PENDING '
		elseif node.state == NodeState.TRIGGERED then
			text = text..'TRIGGERED '
		elseif node.state == NodeState.IGNORED then
			text = text..'IGNORED '
		end

		text = text .. 'state_string' .. node.state_string .. '\n'

		local tf = CreateTextEntity()
		tf:attachTo( current_scene:getPlayField():getEntityNamed(node.target_area) )
		tf:setName(node:__tostring())
		tf:setText(text)
		tf:setBounds(200, 70)
		tf:setScrScale( Vector2f(1, 1) )
		tf:setScrRotationDeg(0)
		tf:setRespondable(false)
		tf:setVisible(self.is_node_debug_info_shown)
	end
end

path_manager = WayManager()