--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| 	Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]

-- Copyright © 2022-2072, Nak, https://steamcommunity.com/id/Nak2/
-- All Rights Reserved. Not allowed to be reuploaded.
local NikNaks = NikNaks
local util_TraceLine, Vector, table_insert, table_Count, table_SortByKey = util.TraceLine, Vector, table.insert, table.Count, table.SortByKey
local floor, band = math.floor, bit.band
local FindEntityHull = util.FindEntityHull

NikNaks.NodeGraph = {}

--- @class NodeGraph
local n_meta = {}
n_meta.__index = n_meta
n_meta.__tostring = function( self ) return
	"NodeGraph: " .. ( self._file or "New" )
end
NikNaks.__metatables["NodeGraph"] = n_meta

--- @class ain_node
local ain_node = {}
ain_node.__tostring = function( self )
	return "Ain Node: " .. self:GetID()
end
ain_node.__index = ain_node

--- @class ain_link
local ain_link = {}
ain_link.__index = ain_link

-- Load AIN file.
local error_detected = {}
do
	-- Reads and returns the node from AIN file
	--- @param b BitBuffer
	local function readNode( b )
		--- @class ain_node
		local t = {}
			t.pos = b:ReadVector()
			t.yaw = b:ReadFloat()
			t.flOffsets = {}
			for i = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do
				t.flOffsets[i] = b:ReadFloat()	-- Float
			end
			t.nodeType = b:ReadByte() 			-- Byte
			t.nodeInfo = b:ReadUShort()			-- UShort
			t.zone = b:ReadShort()

		-- Clamp Invalid
		if t.nodeType < NikNaks.NODE_TYPE_INVALID or t.nodeType > ( NikNaks.NODE_TYPE_WATER or 5 ) then
			t.nodeType = NikNaks.NODE_TYPE_INVALID
		end

		-- Trade down ( Cause Source-nodes fly in the air )
		if t.nodeType == NikNaks.NODE_TYPE_GROUND then
			local trace = util_TraceLine( {
				start = t.pos + Vector( 0, 0, 50 ),
				endpos = t.pos - Vector( 0, 0, 128 ),
				mask = MASK_SOLID_BRUSHONLY
			} )
			-- Sometimes trace-line fails for some reason.
			if trace then
				t.pos = trace.Hit and trace.HitPos or t.pos
			end
		end

		return setmetatable( t, ain_node )
	end

	--- Read and returns the link from AIN file.
	--- @param b BitBuffer
	local function readLink( b )
		--- @class ain_link
		--- @field moves number[]
		local l = {}
		l.srcId = b:ReadShort() + 1 		-- Short
		l.destId = b:ReadShort() + 1		-- Short
		l.moves = {}

		for i = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do
			l.moves[i] = b:ReadByte() 	-- Byte
		end

		return setmetatable( l, ain_link )
	end

	--- Parses the link data.
	--- @param self NodeGraph
	--- @param link ain_link
	local function parseLink( self, link )
		local from 	= self._nodes[link.srcId]
		local to 	= self._nodes[link.destId]

		from._connect[#from._connect + 1] = { to, link }
		to._connect[#to._connect + 1] = { from, link }
		if to.zone ~= from.zone then
			error_detected[to] = true
			error_detected[from] = true
		end

		for i = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do
			local _type = link.moves[i]
			if _type ~= 0 then
				if not from._connect_hull[i] then from._connect_hull[i] = {} end
				if not to._connect_hull[i] then to._connect_hull[i] = {} end

				table_insert( from._connect_hull[i], { to, _type } )
				table_insert( to._connect_hull[i], 	{ from, _type } )
			end
		end
	end

	-- Easy Add function
	local function add( tab, x, y, node )
		if not tab[x] then tab[x] = {} end

		local c = tab[x][y]
		if not c then
			c = {}
			tab[x][y] = c
		end

		c[#c + 1] = node
	end

	--- Function to add the node to a nodegraph.
	--- @param self NodeGraph
	--- @param node ain_node
	local function addNodeToGraph( self, node )
		local ng = self._nodegraph
		local p = node.pos
		local xf, yf = p.x / 1000, p.y / 1000
		local x, y = floor( xf ), floor( yf )
		add( ng, x, y, node )

		local L = xf % 1 < 0.5
		local B = yf % 1 < 0.5

		if L then
			add( ng, x - 1, y, node )
			if B then
				add( ng, x, y - 1, node )
				add( ng, x - 1, y - 1, node )
			else
				add( ng, x, y + 1, node )
				add( ng, x - 1, y + 1, node )
			end
		else
			add( ng, x + 1, y, node )
			if B then
				add( ng, x, y - 1, node )
				add( ng, x + 1, y - 1, node )
			else
				add( ng, x, y + 1, node )
				add( ng, x + 1, y + 1, node )
			end
		end

	end

	-- Tries to patch the table error_detected
	local function scan( node, tab, zone )
		for _, t in pairs( node:GetConnections() ) do
			local n = t[1]
			if not tab[n:GetID()] then
				tab[n:GetID()] = n

				local z = n:GetZone()
				zone[z] = ( zone[z] or 0 ) + 1
				scan( t[1], tab, zone )
			end
		end
	end

	--- @return number
	local function patchZones()
		local fixes = 0

		for _ = 1, table_Count( error_detected ) do
			local node = next( error_detected )
			if not node then break end
			local tab, zone = {}, {}
			scan( node, tab, zone )

			-- Take the most commen zone
			zone = table_SortByKey( zone )[1]

			for _, n in pairs( tab ) do
				if n.zone ~= zone then
					fixes = fixes + 1
					n.zone = zone
				end
				error_detected[n] = nil
			end
		end

		return fixes
	end

	-- Load map data and add it to the nodegraph.
	local l = {
	--	["info_hint"] = true,
		["info_node_hint"] = NikNaks.NODE_TYPE_GROUND,
		["info_node_air_hint"] = NikNaks.NODE_TYPE_AIR,
		["info_node_climb"] = NikNaks.NODE_TYPE_CLIMB
	}

	local function parseEntity( self, ent )
		if not ent.classname then return end

		local _type = l[ent.classname]
		if not _type then return end

		-- local node = self:GetNode(v.nodeid)
		-- For some reason, node ID won't match the entities. I guess there are some internal magic to correct this, 
		-- but our only way is to use the position-data instead. Note that Z position might change cause of trace.
		local node = self:FindNode( ent.origin, _type )

		if not node then
			ErrorNoHaltWithStack( "Unable to locate " .. _type .. "'s node!" )
		else
			node.hint = ent
		end
	end

	local function parseMap( self )
		for _, v in pairs( NikNaks.CurrentMap:GetEntities() ) do
			parseEntity( self, v )
		end
	end

	local thisMap = "maps/graphs/" .. game.GetMap() .. ".ain"

	--- Loads the NodeGraph from a file. Note: Can output "AIN_ERROR_ZONEPATCH", if the file had invalid zones that got patched.
	--- @param fileName string
	--- @return NodeGraph|nil
	--- @return number AIN_ERROR_*
	local function loadAin( fileName )
		-- Make sure you can't create a nodegraph if entities hasn't been initialised 
		assert( NikNaks.PostInit, "Can't use AIN before InitPostEntity!" )

		if not fileName and _nodeG then return _nodeG end
		if not fileName then fileName = "maps/graphs/" .. game.GetMap() .. ".ain" end
		if not string.match( fileName, "%.ain$" ) and not string.match( fileName, "%.dat$" ) then
			-- Add file type
			fileName = fileName .. ".ain"
		end

		--if thisMapObject and fileName == thisMap then return thisMapObject end

		if not file.Exists( fileName, "GAME" ) then return end

		--- @type BitBuffer
		local b = NikNaks.BitBuffer.OpenFile( fileName, "GAME" )

		-- Create new NG object
		--- @class NodeGraph
		local n = {}
		n._version = b:ReadLong()
		n._map_version = b:ReadLong()
		if n._version ~= 37 then -- This is an old / newer AIN file.
			local s = n._version > 37 and "newer" or "older"
			print( "[NodeGraph]: This .AIN version is " .. s .. ", not the supported 37!" )
			return nil, NikNaks.AIN_ERROR_VERSIONNUM
		end

		n._file = fileName

		--- @type ain_node[]
		n._nodes = {}

		--- @type ain_link[]
		n._links = {}

		n._lookup = {}
		n._nodegraph = {} -- This is a custom table to speed up finding nearby notes. Locating notes is one of the costly things.
		setmetatable( n, n_meta )

		-- Load nodes
		local num_nodes = b:ReadLong()
		error_detected = {}
		for i = 1, num_nodes do			-- No limitsh ere
			--- @class ain_node
			local a = readNode( b )
			a._id = i
			a._connect = {} -- A list of nodes connect to this.
			a._connect_hull = {} -- A list of nodes connect to this, by hull.
			n._nodes[i] = a
			addNodeToGraph( n, a )
		end

		-- Read Links
		local num_links = b:ReadLong()
		for i = 1, num_links do			-- No limits here
			--- @class ain_link
			local link = readLink( b )
			link._id = i
			n._links[i] = link
			parseLink( n, link )
		end

		-- Read lookup
		for i = 1, num_nodes do
			n._lookup[i] = b:ReadLong()
		end

		local err = table.Count( error_detected )

		if err > 0 then
			print( "NodeGraph: Detected zone errors in " .. fileName .. "!" )
			print( "NodeGraph: Patching zones .." )
			local s = SysTime()
			local fixed = patchZones() or 0
			print( string.format( "NodeGraph: Took %fms to fix " .. fixed .. " nodes.", SysTime() - s ) )
		end

		if fileName == thisMap then
			parseMap( n )
			-- thisMapObject = n
		end

		--local leftOver = b:Read()
		--print("LEFTOVER: ", string.byte(leftOver), #leftOver)
		return n, err > 0 and NikNaks.AIN_ERROR_PATCHEDDATA
	end
	NikNaks.NodeGraph.LoadAin = loadAin

	local varNG
	--- Returns the nodegraph for the current map and caches it
	--- @return NodeGraph|boolean
	--- @return number? AIN_ERROR_*
	function NikNaks.NodeGraph.GetMap()
		assert( NikNaks.PostInit, "Can't use AIN before InitPostEntity!" )
		if varNG ~= nil then return varNG end

		local a, err = loadAin()
		varNG = a or false
		return varNG, err
	end
end

-- Node Meta
do
	function ain_node:__tostring()
		return "ain_node [" .. self:GetID() .. "]"
	end

	--- Returns the node ID.
	function ain_node:GetID()
		return self._id or -1
	end

	--- Returns true if the node is valid.
	function ain_node:IsValid()
		return self._id >= 0 and self:GetType() > NikNaks.NODE_TYPE_DELETED
	end

	--- Returns the node position.
	--- @return Vector
	function ain_node:GetPos( hull )
		if not hull then hull = 0 end
		return self.pos + Vector( 0, 0, self.flOffsets[hull] )
	end

	--- Returns the node YAW.
	function ain_node:GetYaw()
		return self.yaw
	end

	--- Returns the node type.
	--- @return number NODE_TYPE
	function ain_node:GetType()
		return self.nodeType
	end

	--- Returns the node info.
	function ain_node:GetInfo()
		return self.nodeInfo
	end

	--- Returns the node zone. It should match any notes connected to this one.
	function ain_node:GetZone()
		return self.zone
	end
end

-- Link Meta
do
	function ain_link:__tostring()
		return "ain_link [" .. self:GetID() .. "]"
	end

	--- Returns the link ID.
	function ain_link:GetID()
		return self._id or -1
	end

	--- Returns the move bitflags.
	--- @param HULL number
	function ain_link:GetMove( HULL )
		return self.moves[HULL or 0]
	end

	--- Checks to see if it has any of said move flag
	--- @param HULL number
	--- @param flag number
	function ain_link:HasMoveFlag( HULL, flag )
		return band( self.moves[HULL or 0], flag ) ~= 0
	end

	--- Returns the node ID of the source node.
	function ain_link:GetSrcID()
		return self.srcId
	end

	--- Returns the node IF of the distination node.
	function ain_link:GetDestID()
		return self.destId
	end
end

-- Special Node Meta
do
	--- Returns all the connections from this node.
	--- @return table
	function ain_node:GetConnections()
		return self._connect
	end

	local e_t = {}
	--- Returns all the connections from this node, with said hull that aren't invalid.
	--- @param hull number
	--- @return table
	function ain_node:GetConnectionsByHull( hull )
		return self._connect_hull[hull] or e_t
	end

	--- Returns true if the node_type match.
	--- @param NODE_TYPE number
	--- @return boolean
	function ain_node:IsNodeType( NODE_TYPE )
		if NODE_TYPE == NikNaks.NODE_TYPE_ANY then return true end
		if self.nodeType == NODE_TYPE then return true end
		return false
	end
end

-- Get Grid Nodes
do
	local function scan( node, tab )
		for _, t in pairs( node:GetConnections() ) do
			if not tab[t[1]:GetID()] then
				tab[t[1]:GetID()] = t[1]
				scan( t[1], tab )
			end
		end
	end

	--- A function returning all nodes connected to this one. A bit costly.
	--- @return table
	function ain_node:GetAllGridNodes()
		local p = {}
		local tab = {}
		scan( self, tab )

		for _, node in pairs( tab ) do
			p[#p + 1] = node
		end

		return p
	end
end

-- NodeGraph
do
	-- Adds a dis-cost if node isn't visible. 1 = clear, 10 = blocked
	local function traceCheck( from, to )
		local trace = util_TraceLine( {
			start = from,
			endpos = to,
			mask = MASK_SOLID_BRUSHONLY
		} )

		if not trace then return false end

		return not trace.Hit and trace.Fraction < 1
	end

	--- Returns the AIN version. Should be 37.
	function n_meta:GetVersion()
		return self._version
	end

	--- Returns the AIN map-version.
	function n_meta:GetMapVersion()
		return self._map_version
	end

	--- Returns the given ain_node at said ID. 
	--- @param id number
	function n_meta:GetNode( id )
		return self._nodes[id]
	end

	--- Returns a list of all nodes. With the ID as keys.
	function n_meta:GetAllNodes()
		return self._nodes
	end

	--- Returns the nearest node
	--- @param position Vector
	--- @param NODE_TYPE? number
	--- @param Zone? number
	--- @return ain_node
	function n_meta:FindNode( position, NODE_TYPE, Zone, HULL )
		NODE_TYPE = NODE_TYPE or NikNaks.NODE_TYPE_GROUND
		local x, y = floor( position.x / 1000 ), floor( position.y / 1000 )
		local c, v

		-- Use the nodegraph to search, if none at position, scan all nodes ( slow )
		local ng = self._nodegraph[x] and self._nodegraph[x][y]
		if ng then
			for _, node in pairs( ng ) do
				if Zone and node.zone ~= Zone then continue end
				if not node:IsNodeType( NODE_TYPE ) then continue end
				local vis = traceCheck( position, node:GetPos( HULL ) + Vector( 0, 0, 60 ) )
				if not vis then continue end
				local d = position:DistToSqr( node:GetPos( HULL ) + Vector( 0, 0, 60 )  )
				if not c or d < c then
					c = d
					v = node
				end
			end

			if v then return v end
		end

		-- We didn't find a node within the chunk, or a chunk with no matching note type. We need to scan everything.
		for _, node in pairs( self._nodes ) do
			if Zone and node.zone ~= Zone then continue end
			if not node:IsNodeType( NODE_TYPE ) then continue end
			local vis = traceCheck( position, node:GetPos( HULL ) + Vector( 0, 0, 60 )  )
			local d = position:DistToSqr( node:GetPos() )

			if not vis then
				d = d * 100
			end

			if not c or d < c then
				c = d
				v = node
			end
		end

		return v
	end

	--- Returns the nearest node with a connection matching the hull.
	--- @param position Vector
	--- @param NODE_TYPE? number
	--- @param HULL number
	--- @param Zone? number
	--- @return ain_node
	function n_meta:FindNodeWithHull( position, NODE_TYPE, Zone, HULL )
		NODE_TYPE = NODE_TYPE or NikNaks.NODE_TYPE_GROUND
		local x, y = floor( position.x / 1000 ), floor( position.y / 1000 )
		local c, v

		-- Use the nodegraph to search, if none at position, scan all nodes ( slow )
		local ng = self._nodegraph[x] and self._nodegraph[x][y]
		if ng then
			for _, node in pairs( ng ) do
				if Zone and node.zone ~= Zone then continue end
				if not node:IsNodeType( NODE_TYPE ) then continue end
				if #node._connect_hull[HULL] < 1 then continue end

				local d = position:DistToSqr( node:GetPos( HULL ) + Vector( 0, 0, 60 )  )
				if not c or d < c then
					c = d
					v = node
				end
			end

			if v then return v end
		end

		-- We didn't find a node within the chunk, or a chunk with no matching note type. We need to scan everything.
		for _, node in pairs( self._nodes ) do
			if Zone and node.zone ~= Zone then continue end
			if not node:IsNodeType( NODE_TYPE ) then continue end
			if #node._connect_hull[HULL] < 1 then continue end

			local d = position:DistToSqr( node:GetPos( HULL ) + Vector( 0, 0, 60 )  )
			if not c or d < c then
				c = d
				v = node
			end
		end
		return v
	end

	--- Returns the nearest node with said HintType.
	--- @param position Vector
	--- @param NODE_TYPE? number
	--- @param HintType number
	--- @param HintGroup? number
	--- @param Zone? number
	--- @return ain_node
	function n_meta:FindHintNode( position, NODE_TYPE, HintType, HintGroup, Zone, HULL )
		NODE_TYPE = NODE_TYPE or NikNaks.NODE_TYPE_GROUND
		local x, y = floor( position.x / 1000 ), floor( position.y / 1000 )
		local c, v

		-- Use the nodegraph to search, if none at position, scan all nodes ( slow )
		local ng = self._nodegraph[x] and self._nodegraph[x][y]
		if ng then
			for _, node in pairs( ng ) do
				if not node.hint or node.hint.hinttype ~= HintType then continue end
				if HintGroup and node.hint.group ~= HintGroup then continue end
				if Zone and node.zone ~= Zone then continue end
				if not node:IsNodeType( NODE_TYPE ) then continue end

				local d = position:DistToSqr( node:GetPos( HULL ) )
				if not c or d < c then
					c = d
					v = node
				end
			end

			if v then return v end
		end

		-- We didn't find a node within the chunk, or a chunk with no matching note type. We need to scan everything.
		for _, node in pairs( self._nodes ) do
			if not node.hint or node.hint.hinttype ~= HintType then continue end
			if HintGroup and node.hint.group ~= HintGroup then continue end
			if Zone and node.zone ~= Zone then continue end
			if not node:IsNodeType( NODE_TYPE ) then continue end

			local d = position:DistToSqr( node:GetPos( HULL ) )
			if not c or d < c then
				c = d
				v = node
			end
		end

		return v
	end

	local MAX_NODES = MAX_NODES or 4096 	-- This is the limit for Source, not NikNaks.
	--- Returns the nodegraph as a BitBuffer.
	--- @return BitBuffer
	function n_meta:SaveToBuf()
		local b = NikNaks.BitBuffer()
			b:WriteLong( self:GetVersion() ) -- Should be 37
			b:WriteLong( self._map_version )

		-- Write node num
		local note_num = #self._nodes
		if note_num > MAX_NODES then
			print( "[NodeGrpah]: Warning! Reached over the default MAX_NODES limits. " .. fileName .. " will only work with NikNak's pathfinding." )
		end

		b:WriteLong( note_num )

		for i = 1, note_num do
			local node = self._nodes[i]
			b:WriteVector( node.pos )
			b:WriteFloat( node.yaw )
			for j = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do
				b:WriteFloat( node.flOffsets[j] )
			end
			b:WriteByte( node.nodeType )
			b:WriteUShort( node.nodeInfo )
			b:WriteShort( node.zone )
		end

		-- Write links. No limits I know of.
		local num = #self._links
		b:WriteLong( num )
		for i = 1, num do
			local l = self._links[i]
			b:WriteShort( l.srcId - 1 )
			b:WriteShort( l.destId - 1 )
			for ii = 0, ( NikNaks.NUM_HULLS or 10 ) - 1 do
				b:WriteByte( l.moves[ii] )
			end
		end

		-- Write lookup
		for i = 1, note_num do
			b:WriteLong( self._lookup[i] )
		end

		return b
	end

	--- Saves the nodegraph to a file.
	--- @param filePath string
	function n_meta:SaveAin( filePath )
		filePath = filePath or self._file or "nodegraph"
		if not string.match( filePath, ".dat$" ) then filePath = filePath .. ".dat" end -- Add file type. We use .dat to stop users from opening it in notepad.
		self:SaveToBuf():SaveToFile( filePath )
		print( "[NodeGraph]: Saved to " .. tostring( "data/" .. filePath ) )
	end

	--- Overrides and generates all the zones in the nodegraph. This can fix zone-errors.
	function n_meta:GenerateZones()
		-- Load all nodes to list.
		local s = SysTime()
		local all_nodes = {}
		local nodes = self._nodes
		for i = 1, #nodes do
			local node = nodes[i]
			all_nodes[node:GetID()] = node
		end

		-- For each node ..
		local zone = 0
		for _ = 1, #nodes do
			local id, node = next( all_nodes )
			if not id then break end -- No more nodes left.
			all_nodes[id] = nil -- Remove this node
			node.zone = zone

			-- For each node connected to this one, set the zone to match.
			for _, con in pairs( node:GetAllConnections() ) do
				con.zone = zone
				all_nodes[con:GetID()] = nil -- Remove said node from the lookup table.
			end

			zone = zone + 1
		end

		print( string.format( "[NodeGraph] Generated zones within: %f", SysTime() - s ) )
	end
end

-- A* PathFinder Node functions
do
	local cost_l, t_cost, open_list, closed_list, move_list = {}, {}, {}, {}, {}
	function ain_node:ClearSearchLists()
		cost_l, t_cost, open_list, closed_list, move_list = {}, {}, {}, {}, {}
	end

	--- Sets the cost for pathfinding.
	--- @param cost number
	function ain_node:SetCostSoFar( cost )
		cost_l[self:GetID()] = cost
	end

	--- Returns the cost so far.
	--- @return number
	function ain_node:GetCostSoFar()
		return cost_l[self:GetID()] or -1
	end

	--- Sets the total cost.
	--- @param cost number
	function ain_node:SetTotalCost( cost )
		t_cost[self:GetID()] = cost
	end

	--- Returns the total cost.
	--- @return number
	function ain_node:GetTotalCost()
		return t_cost[self:GetID()] or -1
	end

	---Adds the node to the open list.
	function ain_node:AddToOpenList()
		open_list[#open_list + 1] = self
	end

	--- Returns true if the node is on the open list.
	--- @return boolean
	function ain_node:IsOpen()
		for i = 1, #open_list do
			if open_list[i]:GetID() == self:GetID() then return true end
		end

		return false
	end

	--- Returns true if the open list is empty.
	--- @return boolean
	function ain_node:IsOpenListEmpty()
		if next( open_list ) then return false end
		return true
	end

	local function sorter( a, b )
		return a:GetTotalCost() < b:GetTotalCost()
	end

	--- Updates the open list.
	function ain_node:UpdateOnOpenList()
		table.sort( open_list, sorter )
	end

	--- Pops the open list and returns the kiwest total cost node.
	function ain_node:PopOpenList()
		return table.remove( open_list, 1 )
	end

	--- Adds the node to the closed list.
	function ain_node:AddToClosedList()
		closed_list[self:GetID()] = true
	end

	--- Returns true if the node is within the closed list.
	--- @return boolean
	function ain_node:IsClosed()
		return closed_list[self:GetID()] or false
	end

	--- Removes the node from the closed list.
	function ain_node:RemoveFromClosedList()
		closed_list[self:GetID()] = nil
	end

	--- Sets the move-type to this node.
	--- @param CAP_MOVE number
	function ain_node:SetMoveType( CAP_MOVE )
		move_list[self:GetID()] = CAP_MOVE
	end

	--- Returns the move-type to this node.
	--- @return number
	function ain_node:GetMoveType()
		return move_list[self:GetID()]
	end
end

-- A* PathFinder
local LPFMeta = NikNaks.__metatables["LPathFollower"]
do
	local function heuristic_cost_estimate( start, goal, HULL )
		-- Perhaps play with some calculations on which corner is closest/farthest or whatever
		return start:GetPos( HULL ):Distance( goal:GetPos( HULL ) )
	end

	local function reconstruct_path( cameFrom, current )
		local total_path = { current }
		while ( cameFrom[current] ) do
			current = cameFrom[current]
			table.insert( total_path, current )
		end

		return total_path
	end

	-- Finds the best move option
	local function getMultiplier( moveoptions, canWalk, canJump, canClimb, canFly, JumpMultiplier, ClimbMultiplier )
		if canFly and band( moveoptions, NikNaks.CAP_MOVE_FLY ) ~= 0 then -- Flying seems to always be the best option
			return NikNaks.CAP_MOVE_FLY, 1
		end

		-- We like to jump more than walking
		if canJump and JumpMultiplier < 1 and band( moveoptions, NikNaks.CAP_MOVE_JUMP ) ~= 0 then
			return NikNaks.CAP_MOVE_JUMP, JumpMultiplier
		end

		if canWalk and band( moveoptions, NikNaks.CAP_MOVE_GROUND ) ~= 0 then
			return NikNaks.CAP_MOVE_GROUND, 1
		elseif canClimb and band( moveoptions, NikNaks.CAP_MOVE_CLIMB ) ~= 0 then -- %20 climb cost
			return NikNaks.CAP_MOVE_CLIMB, ClimbMultiplier
		elseif canJump and band( moveoptions, NikNaks.CAP_MOVE_JUMP ) ~= 0 then
			return NikNaks.CAP_MOVE_JUMP, JumpMultiplier
		end
	end

	--[[
		Tries to A* pathfind to the location.
		true = Same Nodes
		false = Unable to pathfind at all
		table = List of nodes from goal towards the start
	]]
	local function AStart( node_start, node_goal, HULL, BitCapability, JumpMultiplier, ClimbMultiplier, generator, MaxDistance )
		if not node_start or not node_goal then return false end
		if node_start == node_goal then return true end

		local band = band
		node_start:ClearSearchLists()
		node_start:AddToOpenList()

		local cameFrom = {}
		node_start:SetCostSoFar( 0 )
		node_start:SetTotalCost( heuristic_cost_estimate( node_start, node_goal, HULL ) )
		node_start:UpdateOnOpenList()

		local canWalk 	= band( BitCapability, NikNaks.CAP_MOVE_GROUND )	~= 0
		local canFly 	= band( BitCapability, NikNaks.CAP_MOVE_FLY )		~= 0
		local canClimb 	= band( BitCapability, NikNaks.CAP_MOVE_CLIMB )		~= 0
		local canJump 	= band( BitCapability, NikNaks.CAP_MOVE_JUMP )		~= 0

		while not node_start:IsOpenListEmpty()  do
			local current = node_start:PopOpenList()
			if ( current == node_goal ) then
				return reconstruct_path( cameFrom, current )
			end

			current:AddToClosedList()

			for _, tab in pairs( current:GetConnectionsByHull( HULL ) ) do
				local neighbor = tab[1]
				if not neighbor then continue end

				local moveoptions =  band( BitCapability, tab[2] or 0 )
				if moveoptions == 0 then continue end -- Unable to use this link. No options

				local CAP_MOVE, Multi = getMultiplier( moveoptions, canWalk, canJump, canClimb, canFly, JumpMultiplier, ClimbMultiplier  )
				if not CAP_MOVE then continue end

				-- Cost calculator
				local newCostSoFar
				if not generator then -- Custom generator
					newCostSoFar = current:GetCostSoFar() + heuristic_cost_estimate( current, neighbor, HULL ) * Multi
				else -- Default generator
					-- TODO: Elevator? Check L4D elevator maps and what they are.
					newCostSoFar = current:GetCostSoFar() + generator( current, neighbor, CAP_MOVE, BitCapability, heuristic_cost_estimate( current, neighbor, HULL ) * Multi )
				end

				if newCostSoFar < 0 or MaxDistance and newCostSoFar > MaxDistance then -- Check if we went over max-distance
					continue
				end

				if ( neighbor:IsOpen() or neighbor:IsClosed() ) and neighbor:GetCostSoFar() <= newCostSoFar then
					-- This node is already open/close and the cost is shorter
					continue
				else
					neighbor:SetCostSoFar( newCostSoFar );
					neighbor:SetTotalCost( newCostSoFar + heuristic_cost_estimate( neighbor, node_goal, HULL ) )
					neighbor:SetMoveType( CAP_MOVE )

					if ( neighbor:IsClosed() ) then
						neighbor:RemoveFromClosedList()
					end

					if ( neighbor:IsOpen() ) then
						-- This area is already on the open list, update its position in the list to keep costs sorted
						neighbor:UpdateOnOpenList()
					else
						neighbor:AddToOpenList()
					end

					cameFrom[neighbor] = current
				end
			end
		end

		return false
	end


	--- A* pathfinding using the NodeGraph.
	--- @param start_pos Vector|Entity
	--- @param end_pos Vector|Entity
	--- @param NODE_TYPE? number
	--- @param options? table
	--- @param HULL_SIZE? number
	--- @param generator? function		-- A funtion that allows you to calculate your own cost: func( node, fromNode, CAP_MOVE, elevator, length )
	--- @return LPathFollower|boolean
	function n_meta:PathFind( start_pos, end_pos, NODE_TYPE, options, HULL_SIZE, generator )
		if UseZone == nil then UseZone = true end
		if not options then options = {} end

		local MaxDistance 		= options.MaxDistance or 100000
		local BitCapability 	= options.BitCapability or ( NODE_TYPE == NikNaks.NODE_TYPE_AIR and NikNaks.CAP_MOVE_FLY or NikNaks.CAP_MOVE_GROUND ) -- Make default walk, unless NODE_TYPE is fly )
		local JumpMultiplier 	= options.JumpMultiplier or 1.4
		local ClimbMultiplier 	= options.ClimbMultiplier or 1.2

		if not JumpMultiplier then JumpMultiplier = 1.4 			-- Default, make it kinda hate jumping around
		elseif JumpMultiplier < 0.3 then JumpMultiplier = 0.3 end	-- Make sure it can't go below 0.3. Can inf loop if so.

		if not NODE_TYPE then NODE_TYPE = NikNaks.NODE_TYPE_GROUND end			-- Default node: Ground.

		-- Entity checks
		local ent	= start_pos.OBBMins and start_pos
		local ent_e	= end_pos.OBBMins and end_pos

		if ent then
			start_pos = ent:GetPos()
			if not HULL_SIZE then
				if ent.GetHullType then
					HULL_SIZE = ent:GetHullType() -- Get the hulltype from the hulltype function, if that is the starting entity.
				else
					HULL_SIZE = FindEntityHull( ent ) or 0 -- Get the hull size from the OBB, use Hull Human as fallback
				end
			end
		elseif not HULL_SIZE then
			HULL_SIZE = 0 -- Hull Human
		end

		if ent_e then
			end_pos = ent_e:GetPos() + ent_e:OBBCenter()
		end

		-- Find the start and end node.
		local start_node = self:FindNode( start_pos, NODE_TYPE )
		if not start_node then return false end

		local offset = start_pos - start_node:GetPos( HULL_SIZE ) -- Sway the position a bit for the node to be located
		local end_node 	= self:FindNode( end_pos + offset,	NODE_TYPE, start_node:GetZone() ) -- Find an end-node. matching the starting node's zone.
		if not end_node then return false end

		-- Path find to location
		local t = AStart( start_node, end_node, HULL_SIZE, BitCapability, JumpMultiplier, ClimbMultiplier, generator, MaxDistance )
		local def_cap = NODE_TYPE == NikNaks.NODE_TYPE_AIR and NikNaks.CAP_MOVE_FLY or NikNaks.CAP_MOVE_GROUND

		if t == false then -- Unable to pathfind to location
			return false
		elseif t == true then -- Same location, return an "empty" path object.
			local p = LPFMeta.CreatePathFollower( start_pos )
			local s = p:AddSegment( start_pos, end_pos, 0, def_cap )
			s.target_ent = ent_e -- The last position of the pathfollower, is an entity.
			p._generator = generator
			p._MaxDistance = MaxDistance
			return p
		else -- A table of locations
			local p = LPFMeta.CreatePathFollower( start_pos )
			local lP = start_pos

			-- Add end pos
			for i = #t, 1, -1  do
				local node = t[i]
				local s = p:AddSegment( lP, node:GetPos(), 0, node:GetMoveType() or def_cap )
				s.node = node
				lP = t[i]:GetPos()
			end

			local s = p:AddSegment( lP, end_pos, 0, def_cap )
			s.target_ent = ent_e
			p._generator = generator
			p._MaxDistance = MaxDistance

			return p
		end
	end

	--- A cheap lookup function. Checks to see if we can reach the position using nearby nodes.
	--- Note that this use zones and might have false positives on maps with a broken NodeGraph.
	--- @param start_pos Vector
	--- @param end_pos Vector
	--- @param NODE_TYPE? number
	--- @param HULL_SIZE? number
	--- @param max_dis? number -- Distance to nearest node
	--- @return boolean
	function n_meta:CanMaybeReach( start_pos, end_pos, NODE_TYPE, HULL_SIZE, max_dis )
		if not HULL_SIZE then HULL_SIZE = 0 end
		if not NODE_TYPE then NODE_TYPE = NikNaks.NODE_TYPE_GROUND end

		local a = self:FindNode( start_pos,	NODE_TYPE )
		if not a then
			return false
		elseif max_dis and a:GetPos():Distance( start_pos ) > max_dis then
			return false
		end

		local b = self:FindNode( end_pos, NODE_TYPE, a:GetZone() )
		if not b then
			return false
		elseif max_dis and b:GetPos():Distance( start_pos ) > max_dis then
			return false
		end

		return true
	end

end

-- ASync. Calculates 10 paths pr tick.
do
	local async, run = {}, false
	local function remove_hook()
		if not run then return end
		run = false
		hook.Remove( "Think","ain_apath" )
	end

	local function add_hook()
		if run then return end
		run = true

		hook.Add( "Think", "ain_apath", function()
			local n = #async
			if n < 1 then
				return remove_hook() -- None left to calculate
			end

			for i = n, 1, -1 do
				local ok, message = coroutine.resume( async[i] )
				if ( ok == false ) then
					ErrorNoHalt( " Error: ", message, "\n" )
					table.remove( async, 1 )
				elseif message then
					table.remove( async, 1 )
				end
			end
		end )
	end

	--- A* pathfinding using the NodeGraph. Returns the result in the callback. Calculates 20 paths pr tick.
	--- @param start_pos Vector|Entity
	--- @param end_pos Vector|Entity
	--- @param callback function 		-- Returns the result. LPathFollower or false
	--- @param NODE_TYPE? number
	--- @param options? table
	--- @param HULL_SIZE? number
	--- @param generator? function		-- A funtion that allows you to calculate your own cost: func( node, fromNode, CAP_MOVE, elevator, length )
	function n_meta:PathFindASync( start_pos, end_pos, callback, NODE_TYPE, options, HULL_SIZE, generator )
		add_hook() -- Make sure the async runs
		local count = 0

		local function awaitGen( current, neighbor, CAP_MOVE, elevator, h_cost_estimate )
			count = count + 1
			if count > 20 then
				count = 0
				coroutine.yield()
			end
			if generator then return generator( current, neighbor, CAP_MOVE, elevator, h_cost_estimate ) end
			return current:GetCostSoFar() + h_cost_estimate
		end

		table.insert( async, coroutine.create( function()
			callback( self:PathFind( start_pos, end_pos, NODE_TYPE, HULL_SIZE, BitCapability, JumpMultiplier, awaitGen, MaxDistance, UseZone ) )
			return true
		end ) )
	end
end

if SERVER then return end
do
	local c = {
		[NikNaks.NODE_TYPE_AIR 		] = Color( 155, 155, 255 ),
		[NikNaks.NODE_TYPE_GROUND 	] = Color( 155, 255, 155 ),
		--[NODE_TYPE_WATER 	]= Color(0,0,255) ,
		[NikNaks.NODE_TYPE_CLIMB 	] = Color( 155, 155, 155 )
	}
	local l = {}
	for i = 0, 9 do
		l[i] = Material( "sprites/key_" .. i )
	end

	local hType = {
		[0]    = "None",
		[2]    = "Window",
		[12]   = "Act Busy",
		[13]   = "Visually Interesting",
		[14]   = "Visually Interesting(Dont aim)",
		[15]   = "Inhibit Combine Mines",
		[16]   = "Visually Interesting (Stealth mode)",
		[100]  = "Crouch Cover Medium",	-- Angles + FOV is important
		[101]  = "Crouch Cover Low",		-- Angles + FOV is important
		[102]  = "Waste Scanner Spawn",
		[103]  = "Entrance / Exit Pinch (Cut content from Antlion guard)",
		[104]  = "Guard Point",
		[105]  = "Enemy Disadvantage Point",
		[106]  = "Health Kit (Cut content from npc_assassin)",
		[400]  = "Antlion: Burrow Point",
		[401]  = "Antlion: Thumper Flee Point",
		[450]  = "Headcrab: Burrow Point",
		[451]  = "Headcrab: Exit Pod Point",
		[500]  = "Roller: Patrol Point",
		[501]  = "Roller: Cleanup Spot",
		[700]  = "Crow: Fly to point",
		[701]  = "Crow: Perch point",
		[900]  = "Follower: Wait point",
		[901]  = "Override jump permission",
		[902]  = "Player squad transition point",
		[903]  = "NPC exit point",
		[904]  = "Strider mnode",
		[950]  = "Player Ally: Push away destination",
		[951]  = "Player Ally: Fear withdrawal destination",
		[1000] = "HL1 World: Machinery",
		[1001] = "HL1 World: Blinking Light",
		[1002] = "HL1 World: Human Blood",
		[1003] = "HL1 World: Alien Blood",
	}

	local function getH( num )
		if not num then return "?" end
		if hType[num] then
			return hType[num] .. "[" .. num .. "]"
		end
		return num
	end

	function ain_node:DebugRender( size )
		--if self.zone ~= 4 then return end
		size = size or 32

		if self.hint then
			render.SetMaterial( l[self.zone % 10] )
			render.DrawSprite( self:GetPos(), size, size, HSVToColor( ( CurTime() * 420 ) % 360, 0.5, 0.5 ) )

			if LocalPlayer():GetPos():DistToSqr( self:GetPos() ) < 40000 then
				local angle = EyeAngles()
				angle:RotateAroundAxis( angle:Up(), -90 )
				angle:RotateAroundAxis( angle:Forward(), 90 )
				cam.Start3D2D( self:GetPos() + Vector( 0, 0, 30 ), angle, 0.1 )
					draw.DrawText( "HintType: " .. getH( self.hint.hinttype ), "DermaLarge", 0, 0, color_white, TEXT_ALIGN_CENTER )

					if self.hint.targetnode and self.hint.targetnode > -1 then
						draw.DrawText( "Target node: " .. self.hint.targetnode, "DermaLarge", 0, 30, color_white, TEXT_ALIGN_CENTER )
					elseif ( self.hint.hinttype or 0 ) > 0 or not self.hint.group then
						draw.DrawText( "ID node: " .. self.hint.nodeid, "DermaLarge", 0, 30, color_white, TEXT_ALIGN_CENTER )
					else
						draw.DrawText( "Group node: " .. self.hint.group, "DermaLarge", 0, 30, color_white, TEXT_ALIGN_CENTER )
					end
				cam.End3D2D()
			end
		elseif self.zone <= 9 then
			render.SetMaterial( l[self.zone % 10] )
			render.DrawSprite( self:GetPos() + Vector( 0, 0, 15 ), size, size, c[self:GetType()] )
		else
			local angle = EyeAngles()
				angle:RotateAroundAxis( angle:Up(), -90 )
				angle:RotateAroundAxis( angle:Forward(), 90 )
			cam.Start3D2D( self:GetPos() + Vector( 0, 0, 30 ), angle, 0.5 )
				draw.DrawText( "" .. self.zone, "DermaLarge", 0, 0, color_white, TEXT_ALIGN_CENTER )
			cam.End3D2D()
		end

		local h = 0
		for _, v in pairs( self._connect ) do
			if v[2]:HasMoveFlag( h, NikNaks.CAP_MOVE_GROUND ) then -- or v[2]:HasMoveFlag( h, CAP_MOVE_FLY ) then
				render.DrawLine( self:GetPos(), v[1]:GetPos(), c[self:GetType()] )
			end
		end
	end
end

function n_meta:DebugRender()
	local lp = LocalPlayer()
	if not lp then return end
	local x, y = math.floor( lp:GetPos().x / 1000 ), math.floor( lp:GetPos().y / 1000 )

	-- Use the nodegraph to search, if none at position, scan all nodes ( slow )
	local ng = self._nodegraph[x] and self._nodegraph[x][y]
	if not ng then return end

	for _, v in pairs( ng ) do
		v:DebugRender()
	end
end
