--[[
| 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.
-- License: https://github.com/Nak2/NikNaks/blob/main/LICENSE

local NikNaks = NikNaks
NikNaks.Map = {}

local obj_tostring = "BSP %s [ %s ]"
local format = string.format

--- @class BSPObject
local meta = NikNaks.__metatables["BSP"]

--- @return File
local function openFile( self )
	assert( self._mapfile, "BSP object has nil mapfile path!" )
	return file.Open( self._mapfile, "rb", "GAME" )
end

--- Reads the lump header.
--- @param self BSPObject
--- @param f BitBuffer
local function read_lump_h( self, f )
	-- "How do we stop people loading L4D2 maps in other games?"
	-- "I got it, we scrample the header."

	--- @class BSPLumpHeader
	local t = {}
	if self._version ~= 21 or self._isL4D2 == false then
		t.fileofs = f:ReadLong()
		t.filelen = f:ReadLong()
		t.version = f:ReadLong()
		t.fourCC  = f:ReadLong()
	elseif self._isL4D2 == true then
		t.version = f:ReadLong()
		t.fileofs = f:ReadLong()
		t.filelen = f:ReadLong()
		t.fourCC = f:ReadLong()
	elseif NikNaks._Source:find( "niknak" ) then -- Try and figure it out
		local fileofs = f:ReadLong() -- Version
		local filelen = f:ReadLong() -- fileofs
		local version = f:ReadLong() -- filelen
		t.fourCC  = f:ReadLong()
		if fileofs <= 8 then
			self._isL4D2 = true
			t.version = fileofs
			t.fileofs = filelen
			t.filelen = version
		else
			self._isL4D2 = false
			t.fileofs = fileofs
			t.filelen = filelen
			t.version = version
		end
	end

	return t
end

--- Parse LZMA. These are for gamelumps, entities, PAK files and staticprops .. ect from TF2
--- @param str string
--- @return string
local function LZMADecompress( str )
	if str:sub( 0, 4 ) ~= "LZMA" then return str end

	local actualSize = str:sub( 5, 8 )
	local lzmaSize 	= NikNaks.BitBuffer.StringToInt( str:sub( 9, 12 ) )
	if lzmaSize <= 0 then return "" end -- Invalid length

	local t = str:sub( 13, 17 )
	local data = str:sub( 18, 18 + lzmaSize ) -- Why not just read all of it? What data is after this? Tell me your secrets Valve.
	return util.Decompress( t .. actualSize .. "\0\0\0\0" .. data ) or str
end

local thisMap = "maps/" .. game.GetMap() .. ".bsp"
local thisMapObject

--- Reads the BSP file and returns it as an object.
--- @param fileName string
--- @return BSPObject
--- @return BSP_ERROR_CODE?
function NikNaks.Map( fileName )
	-- Handle filename
	if not fileName then
		if thisMapObject then return thisMapObject end -- Return this map
		fileName = thisMap
	else
		if not string.match( fileName, ".bsp$" ) then fileName = fileName .. ".bsp" end -- Add file header
		if not string.match( fileName, "^maps/" ) and not file.Exists( fileName, "GAME" ) then -- Map doesn't exists and no folder indecated.
			fileName = "maps/" .. fileName	-- Add "maps" folder
		end
	end

	if not file.Exists( fileName, "GAME" ) then
		-- File not found
		return nil, NikNaks.BSP_ERROR_FILENOTFOUND
	end

	local f = file.Open( fileName, "rb", "GAME" )
	if not f then
		-- Unable to open file
		return nil, NikNaks.BSP_ERROR_FILECANTOPEN
	end

	-- Read the header
	if f:Read( 4 ) ~= "VBSP" then
		f:Close()
		return nil, NikNaks.BSP_ERROR_NOT_BSP
	end

	-- Create BSP object
	--- @class BSPObject
	local BSP = setmetatable( {}, meta )
	BSP._mapfile = fileName
	BSP._size	 = f:Size()
	BSP._mapname = string.GetFileFromFilename( fileName )
	BSP._mapname = string.match( BSP._mapname, "(.+).bsp$" ) or BSP._mapname
	BSP._version = f:ReadLong()
	BSP._fileobj = f

	if BSP._version > 21 then
		f:Close()
		return nil, NikNaks.BSP_ERROR_TOO_NEW
	end

	-- Read Lump Header
	--- @type BSPLumpHeader[]
	BSP._lumpheader = {}
	for i = 0, 63 do
		BSP._lumpheader[i] = read_lump_h( BSP, f )
	end

	--- @type BitBuffer[]
	BSP._lumpstream = {}
	BSP._gamelumps = {}
	f:Close()

	if thisMap == fileName then
		thisMapObject = BSP
	end

	return BSP
end


-- Smaller functions
do
	--- Returns the mapname.
	--- @return string
	function meta:GetMapName()
		return self._mapname or "Unknown"
	end

	--- Returns the mapfile.
	--- @return string
	function meta:GetMapFile()
		return self._mapfile or "No file"
	end

	--- Returns the map-version.
	--- @return number
	function meta:GetVersion()
		return self._version
	end

	--- Returns the size of the map in bytes.
	--- @return number
	function meta:GetSize()
		return self._size
	end
end


-- Lump functions
do
	if SERVER then
		---Returns true if the server has a lumpfile for the given lump
		---@param lump_id number
		---@return bool
		---@server
		function meta:HasLumpFile( lump_id )
			if self._lumpfile and self._lumpfile[lump_id] ~= nil then
				return self._lumpfile[lump_id]
			end
			self._lumpfile = self._lumpfile or {}
			self._lumpfile[lump_id] = file.Exists("maps/" .. self._mapname .. "_l_" .. lump_id .. ".lmp", "GAME")
			return self._lumpfile[lump_id]
		end
	end

	-- A list of lumps that are known to be LZMA compressed for TF2 / other. In theory we could apply it to everything
	-- However there might be some rare cases where the data start with "LZMA", and trigger this.

	--- Returns the data lump as a bytebuffer. This will also be cached onto the BSP object.
	--- @param lump_id number
	--- @return BitBuffer
	function meta:GetLump( lump_id )
		local lumpStream = self._lumpstream[lump_id]
		if lumpStream then
			lumpStream:Seek( 0 ) -- Reset the read position
			return lumpStream
		end

		local lump_h = self._lumpheader[lump_id]
		assert( lump_h, "Tried to read invalid lumpheader!" )

		-- The raw lump data
		local data

		-- Check for LUMPs
		local lumpPath = "maps/" .. self._mapname .. "_l_" .. lump_id .. ".lmp"
		if file.Exists( lumpPath, "GAME" ) then -- L4D has _s_ and _h_ files too. Depending on the gamemode.
			data = file.Read( lumpPath, "GAME" )
		elseif lump_h.filelen > 0 then
			local f = openFile( self )
			f:Seek( lump_h.fileofs )
			data = f:Read( lump_h.filelen )
			f:Close()
		else
			data = ""
		end

		-- TF2 have some maps that are LZMA compressed.
		data = LZMADecompress( data )

		-- Create bytebuffer object with the data and return it
		--- @type BitBuffer
		self._lumpstream[lump_id] = NikNaks.BitBuffer( data or "" )

		return self._lumpstream[lump_id]
	end

	--- Deletes lump_data cached in the BSP module.
	--- @param lump_id number
	function meta:ClearLump( lump_id )
		self._lumpstream[lump_id] = nil
	end

	--- Returns the lump version.
	--- @return number
	function meta:GetLumpVersion( lump_id )
		return self._lumpheader[ lump_id ].version
	end

	--- Returns the data lump as a datastring. 
	--- This won't be cached or saved, but it is faster than to parse the data into a bytebuffer and useful if you need the raw data.
	--- @param lump_id number
	--- @return string
	function meta:GetLumpString( lump_id )
		local lump_h = self._lumpheader[lump_id]
		assert( lump_h, "Tried to read invalid lumpheader!" )

		-- The raw lump data
		local data

		local lumpPath = "maps/" .. self._mapname .. "_l_" .. lump_id .. ".lmp"
		if file.Exists( lumpPath, "GAME" ) then
			data = file.Read( lumpPath, "GAME" )
		elseif lump_h.filelen > 0 then
			local f = openFile( self )
			f:Seek( lump_h.fileofs )
			data = f:Read( lump_h.filelen )
			f:Close()
		else
			data = ""
		end

		-- TF2 have some maps that are LZMA compressed.
		data = LZMADecompress( data )
		return data
	end

	--- Returns a list of gamelumps.
	--- @return BSPLumpHeader[]
	function meta:GetGameLumpHeaders()
		if self._gamelump then return self._gamelump end
		self._gamelump = {}

		local lump = self:GetLump( 35 )

		for i = 0, math.min( 63, lump:ReadLong() ) do
			--- @class BSPLumpHeader
			local t = {
				id = lump:ReadLong(),
				flags = lump:ReadUShort(),
				version = lump:ReadUShort(),
				fileofs = lump:ReadLong(),
				filelen = lump:ReadLong()
			}

			self._gamelump[i] = t
		end

		self:ClearLump( 35 )
		return self._gamelump
	end

	--- Returns gamelump number, matching the gLumpID.
	--- @param GameLumpID number
	--- @return BSPLumpHeader
	function meta:FindGameLump( GameLumpID )
		for _, v in pairs( self:GetGameLumpHeaders() ) do
			if v.id == GameLumpID then
				return v
			end
		end
	end

	--- @class GameLump
	--- @field buffer BitBuffer
	--- @field version number
	--- @field flags number

	--- Returns the game lump as a bytebuffer. This will also be cached on the BSP object.
	--- @param gameLumpID any
	---	@return GameLump
	function meta:GetGameLump( gameLumpID )
		local gameLump = self._gamelumps[gameLumpID]
		if gameLump then
			gameLump.buffer:Seek( 0 )
			return gameLump
		end

		-- Locate the gamelump.
		local t = self:FindGameLump( gameLumpID )

		-- No data found, or lump has no data.
		if not t or t.filelen <= 0 then
			-- Create an empty bitbuffer with -1 version and 0 flag
			gameLump = {
				flags = t and t.flags or 0,
				version = t and t.version or -1,
				buffer = NikNaks.BitBuffer.Create(),
			}

			self._gamelumps[gameLumpID] = gameLump
			return gameLump
		else
			local f = openFile( self )
			f:Seek( t.fileofs )

			gameLump = {
				flags = t.flags,
				version = t.version,
				buffer = NikNaks.BitBuffer.Create( LZMADecompress( f:Read( t.filelen ) ) ),
			}

			self._gamelumps[gameLumpID] = gameLump
			return gameLump
		end
	end
end

-- Word Data
do
	local default = [[detail\detailsprites.vmt]]

	--- Returns the detail-metail the map uses.
	--- @return string
	function meta:GetDetailMaterial()
		local wEnt = self:GetEntities()[0]
		return wEnt and wEnt.detailmaterial or default
	end

	--- Returns true if the map is a cold world. ( Flag set in the BSP )
	--- @return boolean
	function meta:IsColdWorld()
		local wEnt = self:GetEntity( 0 )
		return wEnt and wEnt.coldworld == 1 or false
	end

	--- Returns the min-positions where brushes are within the map.
	--- @return Vector
	function meta:WorldMin()
		if self._wmin then return self._wmin end

		local wEnt = self:GetEntity( 0 )
		if not wEnt then
			self._wmin = NikNaks.vector_zero
			return self._wmin
		end

		self._wmin = util.StringToType( wEnt.world_mins or "0 0 0", "Vector" ) or NikNaks.vector_zero
		return self._wmin
	end

	--- Returns the max-position where brushes are within the map.
	--- @return Vector
	function meta:WorldMax()
		if self._wmax then return self._wmax end

		local wEnt = self:GetEntity( 0 )
		if not wEnt then
			self._wmax = NikNaks.vector_zero
			return self._wmax
		end

		self._wmax = util.StringToType( wEnt.world_maxs or "0 0 0", "Vector" ) or NikNaks.vector_zero
		return self._wmax
	end

	--- Returns the map-bounds. These are not the size of the map, but the bounds where brushes are within.
	--- @return Vector
	--- @return Vector
	function meta:GetBrushBounds()
		return self:WorldMin(), self:WorldMax()
	end

	--- Returns the skybox position. Returns [0,0,0] if none are found.
	--- @return Vector
	function meta:GetSkyBoxPos()
		if self._skyCamPos then return self._skyCamPos end

		local t = self:FindByClass( "sky_camera" )
		if #t < 1 then
			self._skyCamPos = NikNaks.vector_zero
		else
			self._skyCamPos = t[1].origin
		end

		return self._skyCamPos
	end

	--- Returns the skybox scale. Returns 1 if none are found.
	--- @return number
	function meta:GetSkyBoxScale()
		if self._skyCamScale then return self._skyCamScale end

		local t = self:FindByClass( "sky_camera" )
		if #t < 1 then
			self._skyCamScale = 1
		else
			self._skyCamScale = t[1].scale
		end

		return self._skyCamScale
	end

	--- Returns true if the map has a 3D skybox.
	--- @return boolean
	function meta:HasSkyBox()
		if self._skyCam ~= nil then return self._skyCam end
		self._skyCam = #self:FindByClass( "sky_camera" ) > 0
		return self._skyCam
	end

	--- Returns a position in the skybox that matches the one in the world.
	--- @param vec Vector
	--- @return Vector
	function meta:WorldToSkyBox( vec )
		return vec / self:GetSkyBoxScale() + self:GetSkyBoxPos()
	end

	--- Returns a position in the world that matches the one in the skybox.
	--- @param vec Vector
	--- @return Vector
	function meta:SkyBoxToWorld( vec )
		return ( vec - self:GetSkyBoxPos() ) * self:GetSkyBoxScale()
	end
end

-- Cubemaps
do
	--- @class CubeMap
	local cubemeta = {}
	cubemeta.__index = cubemeta
	cubemeta.__tostring = function( self ) return
		format( obj_tostring, "Cubemap", "Index: " .. self:GetIndex() )
	end

	function cubemeta:GetPos() return self.origin end
	function cubemeta:GetSize() return self.size end
	function cubemeta:GetIndex() return self.id or -1 end
	function cubemeta:GetTexture() return self.texture end

	--- Returns the CubeMaps in the map.
	--- @return CubeMap[]
	function meta:GetCubemaps()
		if self._cubemaps then return self._cubemaps end

		local b = self:GetLump( 42 )
		local len = b:Size()

		--- @type CubeMap[]
		self._cubemaps = {}
		for _ = 1, math.min( 1024, len / 128 ) do
			--- @class CubeMap
			local t = setmetatable( {}, cubemeta )
			t.origin = Vector( b:ReadLong(), b:ReadLong(), b:ReadLong() )
			t.size = b:ReadLong()
			t.texture = ""

			local texturePath = "maps/" .. self:GetMapName() .. "/c" .. t.origin.x .. "_" .. t.origin.y .. "_" .. t.origin.z
			t.texturePath = texturePath
			if self:GetVersion() > 19 then
				t.texturePath = texturePath .. ".hdr"
			end

			if t.size == 0 then
				t.size = 32
			end

			t.id = table.insert( self._cubemaps, t ) - 1
		end

		self:ClearLump( 42 )
		return self._cubemaps
	end

	--- Returns the nearest cubemap to a position.
	--- @param pos Vector
	--- @return CubeMap
	function meta:FindNearestCubemap( pos )
		local lr, lc

		for _, v in ipairs( self:GetCubemaps() ) do
			local cd = v:GetPos():DistToSqr( pos )

			if not lc then
				lc = v
				lr = cd
			elseif lr > cd then
				lc = v
				lr = cd
			end
		end

		return lc
	end
end

-- Textures and materials
do
	-- local max_data = 256000
	--- @return number[]
	function meta:GetTexdataStringTable()
		if self._texstab then return self._texstab end

		self._texstab = {}

		local data = self:GetLump( 44 )
		for i = 0, data:Size() / 32 - 1 do
			self._texstab[i] = data:ReadLong()
		end

		self:ClearLump( 44 )
		return self._texstab
	end

	--- @return string[]
	function meta:GetTexdataStringData()
		if self._texstr then return self._texstr end

		--- @type string[]
		self._texstr = {}

		--- @type string[]
		self._texstr.id = {}

		local data = self:GetLump( 43 )
		for i = 0, data:Size() / 8 - 1 do
			local _id = data:Tell() / 8
			local str = data:ReadStringNull()
			self._texstr.id[_id] = str

			if #str == 0 then break end

			self._texstr[i] = string.lower( str )
		end

		self:ClearLump( 43 )
		return self._texstr
	end

	--- @return string[]
	function meta:GetTextures()
		local c = {}
		local q = self:GetTexdataStringData()
		for i = 1, #q do
			c[i] = q[i]
		end
		return c
	end

	local function getTexByID( self, id )
		id = self:GetTexdataStringTable()[id]
		return self:GetTexdataStringData().id[id]
	end

	--- Returns a list of material-data used by the map.
	--- @return TextureData[]
	function meta:GetTexData()
		if self._tdata then return self._tdata end

		--- @type TextureData[]
		self._tdata = {}

		-- Load TexdataStringTable		
		self:GetTextures()
		local b = self:GetLump( 2 )
		local count = b:Size() / 256 + 1

		for i = 0, count - 1 do
			--- @class TextureData
			local t = {}
			t.reflectivity = b:ReadVector()
			local n = b:ReadLong()
			t.nameStringTableID = getTexByID( self, n ) or tostring( n )
			t.width = b:ReadLong()
			t.height = b:ReadLong()
			t.view_width = b:ReadLong()
			t.view_height = b:ReadLong()
			self._tdata[i] = t
		end

		self:ClearLump( 2 )
		return self._tdata
	end

	--- Returns a list of material-data used by the map.
	--- @return TextureInfo[]
	function meta:GetTexInfo()
		if self._tinfo then return self._tinfo end

		self._tinfo = {}
		local data = self:GetLump( 6 )

		for i = 0, data:Size() / 576 - 1 do
			--- @class TextureInfo
			--- @field textureVects table<number, number[]>
			--- @field lightmapVecs table<number, number[]>
			local t = {}

			t.textureVects	= {}
			t.textureVects[0] = { [0] = data:ReadFloat(), data:ReadFloat(), data:ReadFloat(), data:ReadFloat() }
			t.textureVects[1] = { [0] = data:ReadFloat(), data:ReadFloat(), data:ReadFloat(), data:ReadFloat() }
			t.lightmapVecs	= {}
			t.lightmapVecs[0] = { [0] = data:ReadFloat(), data:ReadFloat(), data:ReadFloat(), data:ReadFloat() }
			t.lightmapVecs[1] = { [0] = data:ReadFloat(), data:ReadFloat(), data:ReadFloat(), data:ReadFloat() }
			t.flags			= data:ReadLong()
			t.texdata 		= data:ReadLong()
			self._tinfo[i] = t
		end

		self:ClearLump( 6 )
		return self._tinfo
	end
end

-- Planes, Vertex and Edges
do
	--- Returns a list of all planes
	--- @return Plane[]
	function meta:GetPlanes()
		if self._plane then return self._plane end

		--- @type Plane[]
		self._plane = {}

		local data = self:GetLump( 1 )
		for i = 0, data:Size() / 160 - 1 do
			--- @class Plane
			local t = {}

			t.normal = data:ReadVector() -- Normal vector
			t.dist = data:ReadFloat() -- distance form origin
			t.type = data:ReadLong() -- plane axis indentifier

			self._plane[i] = t
		end

		self:ClearLump( 1 )
		return self._plane
	end

	local MAX_MAP_VERTEXS = 65536
	--- Returns an array of coordinates of all the vertices (corners) of brushes in the map geometry.
	--- @return Vector[]
	function meta:GetVertex()
		if self._vertex then return self._vertex end

		--- @type Vector[]
		self._vertex = {}
		local data = self:GetLump( 3 )

		for i = 0, math.min( data:Size() / 96, MAX_MAP_VERTEXS ) - 1 do
			self._vertex[i] = data:ReadVector()
		end

		self:ClearLump( 3 )
		return self._vertex
	end

	local MAX_MAP_EDGES = 256000
	--- Returns all edges. An edge is two points forming a line.
	--- Note: First edge seems to be [0 0 0] - [0 0 0]
	--- @return table<number, Vector[]>
	function meta:GetEdges()
		if self._edge then return self._edge end

		-- @type table<number, Vector[]>
		self._edge = {}
		local data = self:GetLump( 12 )

		local v = self:GetVertex()
		for i = 0, math.min( data:Size() / 32, MAX_MAP_EDGES ) -1 do
			self._edge[i] = { v[data:ReadUShort()], v[data:ReadUShort()] }
		end

		self:ClearLump( 12 )
		return self._edge
	end

	local MAX_MAP_SURFEDGES = 512000
	---Returns all surfedges. A surfedge is an index to edges with a direction. If positive First -> Second, if negative Second -> First.
	---@return number[]
	function meta:GetSurfEdges()
		if self._surfedge then return self._surfedge end

		--- @type number[]
		self._surfedge = {}
		local data = self:GetLump( 13 )

		for i = 0, math.min( data:Size() / 32, MAX_MAP_SURFEDGES ) -1 do
			self._surfedge[i] = data:ReadLong()
		end

		self:ClearLump( 13 )
		return self._surfedge
	end

	local abs = math.abs
	--- Returns the two edge-positions using surf index
	--- @param num number
	--- @return Vector, Vector
	function meta:GetSurfEdgesIndex( num )
		local surf = self:GetSurfEdges()[num]
		local edge = self:GetEdges()[abs( surf )]

		if surf >= 0 then
			return edge[1], edge[2]
		else
			return edge[2], edge[1]
		end
	end
end

-- Visibility, leafbrush and leaf functions
do
	--- Returns the visibility data.
	--- @return VisibilityInfo
	function meta:GetVisibility()
		if self._vis then return self._vis end

		local data = self:GetLump( 4 )
		local num_clusters = data:ReadLong()

		-- Check to see if the num_clusters match
		if num_clusters ~= self:GetLeafsNumClusters() then
			error( "Invalid NumClusters!" )
		end

		--- @class VisibilityInfo
		--- @field VisData VisbilityData[]
		local t = { VisData = {} }
		local visData = t.VisData

		for i = 0, num_clusters - 1 do
			--- @class VisbilityData
			local v = {}
			v.PVS = data:ReadULong()
			v.PAS = data:ReadULong()

			visData[i] = v
		end

		data:Seek( 0 )

		local bytebuff = {}
		for i = 0, bit.rshift( data:Size() - data:Tell(), 3 ) - 1 do
			bytebuff[i] = data:ReadByte()
		end

		t._bytebuff = bytebuff
		t.num_clusters = num_clusters
		self._vis = t

		self:ClearLump( 4 )
		return t
	end

	local TEST_EPSILON			= 0.1
	--- Returns the leaf the point is within. Use 0 If unsure about iNode.
	--- @param iNode number
	--- @param point Vector
	--- @return LeafObject
	function meta:PointInLeaf( iNode, point )
		if iNode < 0 then
			return self:GetLeafs()[-1 -iNode]
		end

		local node = self:GetNodes()[iNode]
		local plane = self:GetPlanes()[node.planenum]
		local dist = point:Dot( plane.normal ) - plane.dist

		if dist > TEST_EPSILON then
			return self:PointInLeaf( node.children[1], point )
		elseif dist < -TEST_EPSILON then
			return self:PointInLeaf( node.children[2], point )
		else
			local pTest = self:PointInLeaf( node.children[1], point )
			if pTest.cluster ~= -1 then
				return pTest
			end

			return self:PointInLeaf( node.children[2], point )
		end
	end

	--- Returns the leaf the point is within, but allows caching by feeding the old VisLeaf. 
	--- Will also return a boolean indicating if the leaf is new.
	--- @param iNode number
	--- @param point Vector
	--- @param lastVis LeafObject?
	--- @return LeafObject
	--- @return boolean newLeaf
	function meta:PointInLeafCache( iNode, point, lastVis )
		if not lastVis then return self:PointInLeaf( iNode, point ), true end
		if point:WithinAABox( lastVis.mins, lastVis.maxs ) then return lastVis, false end
		return self:PointInLeaf( iNode, point ), true
	end

	--- Returns the vis-cluster from said point.
	--- @param position Vector
	--- @return number
	function meta:ClusterFromPoint( position )
		return self:PointInLeaf( 0, position ).cluster or -1
	end

	--- Computes the leaf-id the detail is within. -1 for none.
	--- @param position Vector
	--- @return number
	function meta:ComputeDetailLeaf( position )
		local node = 0
		local nodes = self:GetNodes()
		local planes = self:GetPlanes()

		while node > 0 do
			--- @type MapNode
			local n = nodes[node]
			--- @type Plane
			local p = planes[n.planenum]

			if position:Dot( p.normal ) < p.dist then
				node = n.children[2]
			else
				node = n.children[1]
			end
		end

		return - node - 1
	end

	local MAX_MAP_LEAFFACES = 65536
	--- Returns the leaf_face array. This is used to return a list of faces from a leaf.
	--- FaceID = LeafFace[ Leaf.firstleafface + [0 -> Leaf.numleaffaces - 1] ]
	--- @return number[]
	function meta:GetLeafFaces()
		if self._leaffaces then return self._leaffaces end

		--- @type number[]
		self._leaffaces = {}
		local data = self:GetLump( 16 )

		for i = 1, math.min( data:Size() / 16, MAX_MAP_LEAFFACES ) do
			self._leaffaces[i] = data:ReadUShort()
		end

		self:ClearLump( 16 )
		return self._leaffaces
	end

	local MAX_MAP_LEAFBRUSHES = 65536
	--- Returns an array of leafbrush-data.
	--- @return BrushObject[]
	function meta:GetLeafBrushes()
		if self._leafbrush then return self._leafbrush end

		--- @type BrushObject[]
		self._leafbrush = {}
		local data = self:GetLump( 17 )
		local brushes = self:GetBrushes()

		for i = 1, math.min( data:Size() / 16, MAX_MAP_LEAFBRUSHES ) do
			self._leafbrush[i] = brushes[data:ReadUShort()]
		end

		self:ClearLump( 17 )
		return self._leafbrush
	end

	--- Returns map-leafs in a table with cluster-IDs as key. Note: -1 is no cluster ID.
	--- @return table<number, LeafObject[]>
	function meta:GetLeafClusters()
		if self._clusters then return self._clusters end

		--- @type table<number, LeafObject[]>
		self._clusters = {}
		local leafs = self:GetLeafs()

		for i = 0, #leafs - 1 do
			local leaf = leafs[i]
			local cluster = leaf.cluster
			if self._clusters[cluster] then
				table.insert( self._clusters[cluster], leaf )
			else
				self._clusters[cluster] = { leaf }
			end
		end

		return self._clusters
	end

end

-- Faces and Displacments
do
	--- Returns the DispVerts data.
	--- @return DispVert[]
	function meta:GetDispVerts()
		if self._dispVert then return self._dispVert end

		--- @type DispVert[]
		self._dispVert = {}
		local data = self:GetLump( 33 )

		for i = 0, data:Size() / 160 - 1 do
			--- @class DispVert
			local t = {
				vec = data:ReadVector(),
				dist = data:ReadFloat(),
				alpha = data:ReadFloat()
			}

			self._dispVert[i] = t
		end

		self:ClearLump( 33 )
		return self._dispVert
	end

	--- Holds flags for the triangle in the displacment mesh.
	--- Returns the DispTris data
	--- @return number[]
	function meta:GetDispTris()
		if self._dispTris then return self._dispTris end

		--- @type number[]
		self._dispTris = {}
		local data = self:GetLump( 48 )

		for i = 0, data:Size() / 16 - 1 do
			self._dispTris[i] = data:ReadUShort()
		end

		self:ClearLump( 48 )
		return self._dispTris
	end

	local m_AllowedVerts = 10

	--- Returns the DispInfo data.
	--- @return DispInfo[]
	function meta:GetDispInfo()
		if self._dispinfo then return self._dispinfo end

		--- @type DispInfo[]
		self._dispinfo = {}
		local data = self:GetLump( 26 )

		for i = 0, data:Size() / 1408 - 1 do
			--- @class DispInfo
			local q = {}
			q.startPosition = data:ReadVector()
			q.DispVertStart = data:ReadLong()
			q.DispTriStart = data:ReadLong()
			q.power = data:ReadLong()
			q.minTess = data:ReadLong()
			q.smoothingAngle = data:ReadFloat()
			q.contents = data:ReadLong()
			q.MapFace = data:ReadUShort()
			q.LightmapAlphaStart = data:ReadLong()
			q.LightmapSamplePositionStart = data:ReadLong()
			-- 46 bytes used. 130 bytes left regarding corner neighbors .. ect
			-- allowedVerts are 40 bytes (10 * 4), therefore neighbors are 90 bytes
			data:Skip( 720 ) -- CDispCornerNeighbors + CDispNeighbor
			q.allowedVerts = {}
			for _ = 0, m_AllowedVerts - 1 do
				q.allowedVerts[i] = data:ReadULong()
			end
			self._dispinfo[i] = q
		end

		self:ClearLump( 26 )
		return self._dispinfo
	end

	---!! DEBUG FUNCTIONS !!
	function meta:GetMaterialMeshs()
		if self._materialmesh then return self._materialmesh end
		self._materialmesh = {}
		-- Build a list of faces-mesh data.
		local _meshData = {}
		local faces = self:GetFaces()
		for i = 1, #faces do
			local face = faces[i]
			local s = face:GetTexture()
			if not _meshData[s] then _meshData[s] = {} end
			local _facemesh = face:GenerateVertexTriangleData()
			if not _facemesh then continue end
			for i = 1, #_facemesh do
				table.insert( _meshData[s], _facemesh[i] )
			end
		end
		-- Generate the meshes
		for tex, meshData in pairs( _meshData ) do
			local _mat = Material( tex )
			local _mesh = Mesh( _mat )
			table.insert( _MESHBUILD, _mesh )
			mesh.Begin( _mesh, MATERIAL_TRIANGLES, #meshData )
			for i = 1, #meshData do
				local vert = meshData[i]
				-- > Mesh
				mesh.Normal( vert.normal )
				mesh.Position( vert.pos ) -- Set the position
				mesh.TexCoord( 0, vert.u, vert.v ) -- Set the texture UV coordinates
				mesh.TexCoord( 1, vert.lu, vert.lv ) -- Set the lightmap UV coordinates
				mesh.TexCoord( 2, vert.lu, vert.lv  ) -- Set the lightmap UV coordinates?
				--mesh.TexCoord( 2, self.LightmapTextureSizeInLuxels[1], self.LightmapTextureSizeInLuxels[2] ) -- Set the texture UV coordinates
				--mesh.TexCoord( 2, self.LightmapTextureMinsInLuxels[1], self.LightmapTextureMinsInLuxels[2] ) -- Set the texture UV coordinates
				mesh.AdvanceVertex()
			end
			mesh.End()
			self._materialmesh[_mat] = _mesh
		end
		return self._materialmesh
	end

	-- Do I need thse?
	local function BakeTriangles( triangle )
		for i = 1, #triangle, 3 do
			local A, B, C = triangle[ i ], triangle[ i + 1 ], triangle[ i + 2 ]
			local p = A.pos
			local edge1 = B.pos - p
			local edge2 = C.pos - p
			local deltaUV1 = Vector( B.u - A.u, B.v - A.v )
			local deltaUV2 = Vector( C.u - A.u, C.v - A.v )
			
			local f = 1 / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);
			local tangent = Vector(0,0,0)
			local bitangent = Vector(0,0,0)

			tangent.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x)
			tangent.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y)
			tangent.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z)


			bitangent.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
			bitangent.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
			bitangent.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);

			local binormal = bitangent:GetNormalized()

			tangent = Vector(1,0,0)
			binormal = Vector(0,1,0)

			--bitangent = bitangent:GetNormalized()
			triangle[ i ].tangent = tangent
			triangle[ i + 1 ].tangent =tangent
			triangle[ i + 2 ].tangent = tangent
			
			triangle[ i ].binormal = binormal
			triangle[ i + 1 ].binormal = binormal
			triangle[ i + 2 ].binormal = binormal

			local udata = {0,0,1,1}
			triangle[ i ].userdata = udata
			triangle[ i + 1 ].userdata = udata
			triangle[ i + 2 ].userdata = udata
		end
	end

	--- Returns all original faces. ( Warning, uses a lot of memory )
	--- @return OriginalFace[]
	function meta:GetOriginalFaces()
		if self._originalfaces then return self._originalfaces end

		--- @class OriginalFace[]
		self._originalfaces = {}
		local data = self:GetLump( 27 )

		for i = 1, math.min( data:Size() / 448, MAX_MAP_FACES ) do
			--- @class OriginalFace
			--- @field styles number[]
			--- @LightmapTextureMinsInLuxels number[]
			--- @LightmapTextureSizeInLuxels number[]
			local t = {}
			t.plane 	= self:GetPlanes()[data:ReadUShort()]
			t.side 		= data:ReadByte()
			t.onNode 	= data:ReadByte()
			t.firstedge = data:ReadLong()
			t.numedges 	= data:ReadShort()
			t.texinfo 	= data:ReadShort()
			t.dispinfo				= data:ReadShort()
			t.surfaceFogVolumeID	= data:ReadShort()
			t.styles				= { data:ReadByte(), data:ReadByte(), data:ReadByte(), data:ReadByte() }
			t.lightofs				= data:ReadLong()
			t.area					= data:ReadFloat()
			t.LightmapTextureMinsInLuxels	= { data:ReadLong(), data:ReadLong() }
			t.LightmapTextureSizeInLuxels	= { data:ReadLong(), data:ReadLong() }
			t.origFace			= data:ReadLong()
			t.numPrims			= data:ReadUShort()
			t.firstPrimID		= data:ReadUShort()
			t.smoothingGroups	= data:ReadULong()
			t.__map = self
			t.__id = i
			self._originalfaces[i] = t
		end

		self:ClearLump( 27 )
		return self._originalfaces
	end
end

-- Model The brush-models embedded within the map. 0 is always the entire map.
do
	--- @class BModel
	local meta_bmodel = {}
	meta_bmodel.__index = meta_bmodel

	--- Returns a list of BModels ( Brush Models )
	--- @return BModel
	function meta:GetBModels()
		if self._bmodel then return self._bmodel end
		self._bmodel = {}
		local data = self:GetLump( 14 )
		for i = 0, data:Size() / 384 - 1 do
			--- @class BModel
			local t = {}
			t.mins = data:ReadVector()
			t.maxs = data:ReadVector()
			t.origin = data:ReadVector()
			t.headnode = data:ReadLong()
			t.firstface = data:ReadLong()
			t.numfaces = data:ReadLong()
			t.__map = self
			setmetatable( t, meta_bmodel )
			self._bmodel[i] = t
		end

		self:ClearLump( 14 )
		return self._bmodel
	end

	--- Returns a list of Faces making up this bmodel
	--- @return FaceObject[]
	function meta_bmodel:GetFaces()
		local t = {}
		local c = 1
		local faces = self.__map:GetFaces()

		for i = self.firstface, self.firstface + self.numfaces - 1 do
			t[c] = faces[i]
			c = c + 1
		end

		return t
	end

	--- Locates the BModelIndex for the said faceIndex
	--- @param faceId number
	--- @return number
	function meta:FindBModelIDByFaceIndex( faceId )
		local bModels = self:GetBModels()

		for i = 0, #bModels do
			local q = bModels[i]
			if q.numfaces >= 0 and faceId >= q.firstface and faceId < q.firstface + q.numfaces then
				return i
			end
		end

		return 0
	end
end

-- Special custom functions
do
	local lower = string.lower

	--- Returns true if the position is outside the map
	--- @param position Vector
	--- @return boolean
	function meta:IsOutsideMap( position )
		local leaf = self:PointInLeaf( 0, position )
		if not leaf then return true end -- No leaf? Shouldn't be possible.
		return leaf:IsOutsideMap()
	end

	--- Returns a list of all materials used by the map.
	--- @return IMaterial[]
	function meta:GetMaterials()
		if self._materials then	return self._materials end

		--- @type IMaterial[]
		self._materials = {}

		for _, v in pairs( self:GetTextures() ) do
			if v then
				local m = Material( v )
				if m then table.insert( self._materials, m ) end
			end
		end

		return self._materials
	end

	--- Returns true if the texture is used by the map.
	--- @param texture string
	--- @return boolean
	function meta:HasTexture( texture )
		texture = lower( texture )

		for _, v in pairs( self:GetTextures() ) do
			if v and lower( v ) == texture then
				return true
			end
		end

		return false
	end

	--- Returns true if the material is used by the map.
	--- @param material IMaterial
	--- @return boolean
	function meta:HasMaterial( material )
		return self:HasTexture( material:GetName() )
	end

	--- Returns true if the skybox is rendering at this position.
	--- Note: Seems to be broken in EP2 and beyond, where the skybox is rendered at all times regardless of position.
	--- @return boolean
	function meta:IsRenderingSkyboxAtPosition( position )
		local leaf = self:PointInLeaf( 0, position )
		if not leaf then return false end -- No leaf? Shouldn't be possible
		return leaf:Has2DSkyboxInPVS()
	end

	--- Returns a list of skybox leafs (If the map has a skybox)
	--- @return LeafObject[]
	function meta:GetSkyboxLeafs()
		if self._skyboxleafs then return self._skyboxleafs end

		--- @type LeafObject[]
		self._skyboxleafs = {}

		local t = self:FindByClass( "sky_camera" )
		if #t < 1 then return self._skyboxleafs end -- Unable to locate skybox leafs

		local p = t[1].origin
		local leaf = self:PointInLeaf( 0, p )
		if not leaf then return self._skyboxleafs end

		local area = leaf.area

		local i = 1
		for _, l in ipairs( self:GetLeafs() ) do
			if l.area == area then
				self._skyboxleafs[i] = l
				i = i + 1
			end
		end

		return self._skyboxleafs
	end

	--- Returns the size of the skybox
	--- @return Vector|nil
	--- @return Vector|nil
	function meta:GetSkyboxSize()
		if self._skyboxmin and self._skyboxmaxs then return self._skyboxmin, self._skyboxmaxs end

		for _, leaf in ipairs( self:GetSkyboxLeafs() ) do
			if not self._skyboxmin then
				self._skyboxmin = Vector( leaf.mins )
			else
				self._skyboxmin.x = math.min( self._skyboxmin.x, leaf.mins.x )
				self._skyboxmin.y = math.min( self._skyboxmin.y, leaf.mins.y )
				self._skyboxmin.z = math.min( self._skyboxmin.z, leaf.mins.z )
			end
			if not self._skyboxmaxs then
				self._skyboxmaxs = Vector( leaf.maxs )
			else
				self._skyboxmaxs.x = math.max( self._skyboxmaxs.x, leaf.maxs.x )
				self._skyboxmaxs.y = math.max( self._skyboxmaxs.y, leaf.maxs.y )
				self._skyboxmaxs.z = math.max( self._skyboxmaxs.z, leaf.maxs.z )
			end
		end

		return self._skyboxmin, self._skyboxmaxs
	end
end

-- Old debug code below
if true then return end

function TESTVPS()
	--map:PVSForOrigin(Vector(0,0,0)) -- Warmup
	local vec = LocalPlayer():GetPos()
	local s = SysTime()
	local map = NikNaks.Map()
	local vec = map:PVSForOrigin(vec)
	print(string.format("%fs", SysTime() - s))
end


local cTrace, i = nil, 0

local oldLeaf
local matList = {}
PVS_DEBUG = false
hook.Add("PostDrawOpaqueRenderables", "TEST2", function(a, b)
	if a then return end
	local leaf, new = NikNaks.CurrentMap:PointInLeafCache(0, LocalPlayer():GetShootPos(), oldLeaf)
	--if not leaf then return end
	--leaf:DebugRender()
	if a or not PVS_DEBUG then return end
	--for id, leaf in pairs( NikNaks.CurrentMap:GetLeafClusters()[0] ) do
	--	leaf:DebugRender()
	--end
	local leaf, new = NikNaks.CurrentMap:PointInLeafCache(0, LocalPlayer():GetShootPos(), oldLeaf)
	
	oldLeaf = leaf
	if new then
		print(string.format("Leaf %i, Area %i, Cluster %i, Flags %i", leaf.__id, leaf.area, leaf.cluster, leaf.flags))
	end
	local PVS = NikNaks.CurrentMap:PVSForOrigin( EyePos())

	for _, leaf in pairs( PVS:GetLeafs() ) do
		if type(leaf) == "number" then continue end
		leaf:DebugRender( leaf:Has2DSkyInPVS() and Color(0,0,255) or leaf:Has3DSkyInPVS() and Color(255,0,0) or Color(0,255,0))
	end

	--for id, leaf2 in pairs( NikNaks.CurrentMap:GetLeafs() ) do
	--	if type(leaf2) == "number" then continue end
	--	if leaf2.area ~= leaf.area then continue end
	--	leaf2:DebugRender()
	--end
end)

hook.Add("PostDrawOpaqueRenderables", "TEST", function()
	if not NikNaks or not NikNaks.CurrentMap then return end
	if true then return end
	i = i + 1
	local a, b = Entity(497):GetPos(), Entity(498):GetPos()
	if i > 10 then
		cTrace = NikNaks.CurrentMap:TestTraceLeaf(a, b)
		i = 0
	end
	if not cTrace then return end
	--render.DrawLine( a, b, color_white, true )
	render.DrawLine( a, cTrace.hitpos, cTrace.hit and Color(0,255,0) or color_white, true )
	render.SetMaterial(mat)
	render.DrawQuadEasy( cTrace.hitpos, cTrace.hitnormal, 64, 64, color_white, (CurTime() * -20) % 360 )

	debugRender(cTrace)
end)


local mat = Material("effects/wheel_ring")
local cMat = Material("vgui/avatar_default")
local qMat = cMat
local lMat
local function GetMat( str )
	if lMat and lMat == str then return cMat end
	lMat = str
	cMat = Material(str)
	return cMat
end

local function debugRender( trace )
	local angle = EyeAngles()

	-- Only use the Yaw component of the angle
	angle = Angle( 0, angle.y - 90, 90 )
	local mat = cMat
	if trace.surface and trace.surface.nameStringTableID then
		mat = GetMat(trace.surface.nameStringTableID)
	end

	cam.Start3D2D( trace.hitpos, angle, .5 )
		draw.SimpleText( "Fraction: " .. trace.fraction, "Default", 0, 0, color_white )
		draw.SimpleText( "Hit: " .. tostring(trace.hit), "Default", 0, 14, color_white )
	cam.End3D2D()
	if not trace.hit then return end
	qMat:SetTexture("$basetexture", mat:GetTexture("$basetexture"))
	render.SetMaterial(qMat)
	render.DrawQuadEasy(trace.hitpos + Vector(0,0,32) -EyeAngles():Forward() * 10, -EyeAngles():Forward(), 32, 32, color_white, 180)
	qMat:SetTexture("$basetexture", "effects/wheel_ring")
end


local function emptyTrace()
	return {
		mins = Vector(0,0,0),
		maxs = Vector(0,0,0),
		extents = Vector(0,0,0)
	}
end

local NEVER_UPDATED = -9999
local DIST_EPSILON = 0.03125
function DM_ClipBoxToBrush(trace, mins, maxs, p1, p2, self)
	if self.numsides < 1 then return end
	
	local ofs = Vector(0,0,0)
	local dist = 0

	local enterfrac = NEVER_UPDATED
	local leavefrac = 1
	local clipplane

	local getout = false
	local startout = false
	local leadside = nil
	
	if not trace.ispoint then
		for i = 1, self.numsides - 1 do
			local side = self.sides[i]
			local plane = side.plane

			ofs.x = plane.normal.x < 0 and maxs.x or mins.x
			ofs.y = plane.normal.y < 0 and maxs.y or mins.y
			ofs.z = plane.normal.z < 0 and maxs.z or mins.z

			dist = ofs:Dot( plane.normal )
			dist = plane.dist - dist

			d1 = p1:Dot( plane.normal ) - dist
			d2 = p2:Dot( plane.normal ) - dist

			if d1 > 0 and d2 > 0 then return end

			if d2 > 0 then getout = true end
			if d1 > 0 then startout = true end

			if d1 <= 0 and d2 <= 0 then continue end

			if d1 > d2 then
				local f = (d1 - DIST_EPSILON) / (d1 - d2)
				if f > enterfrac then
					enterfrac = f
					clipplane = plane
					leadside = side
				end
			else
				f = (d1+DIST_EPSILON) / (d1 - d2)
				if f < leavefrac then leavefrac = f end
			end
		end
	else
		for i = 1, self.numsides - 1 do
			local side = self.sides[i]
			local plane = side.plane

			local texinfo = self.__map:GetTexInfo()[ side.texinfo ]
			local surfaces = self.__map:GetTexData()[ texinfo.texdata ]
			--if surfaces and surfaces.nameStringTableID == "TOOLS/TOOLSNODRAW" then continue end
			
			if side.bevel == 1 then continue end

			dist = plane.dist
			d1 = p1:Dot(plane.normal) - dist
			d2 = p2:Dot(plane.normal) - dist
			
			if d1 > 0 and d2 > 0 then return end

			if d2 > 0 then getout = true end
			if d1 > 0 then startout = true end
			
			if d1 < 0 and d2 < 0 then continue end
			if d1 > d2 then
				f = (d1 - DIST_EPSILON) / (d1-d2)
				if f > enterfrac then
					enterfrac = f
					clipplane = plane
					leadside = side
				end
			else
				f = (d1+DIST_EPSILON) / (d1-d2)
				if f < leavefrac then leavefrac = f end
			end
		end
	end
	if not startout then
		trace.startspolid = true
		if not getout then
			trace.allsolid = true
		end
		return
	end
	if enterfrac < leavefrac then
		if enterfrac > NEVER_UPDATED and enterfrac < trace.fraction then
			if enterfrac < 0 then
				enterfrac = 0
			end
			trace.fraction = enterfrac
			trace.plane = clipplane
			trace.type = clipplane.type
			if leadside.texinfo ~= -1 then
				local texinfo = self.__map:GetTexInfo()[ leadside.texinfo ]
				trace.surface = self.__map:GetTexData()[ texinfo.texdata ]
			else
				trace.surface = 0
			end
			trace.side = leadside
			trace.contents = self.contents
		end
	end
	return trace
end

function meta:TestTraceLeaf(from, to)
	local leaf = self:PointInLeaf(0, from)
	local flb = leaf.firstleafbrush

	local trace = emptyTrace()
	trace.startsolid = false
	trace.fraction = 1
	trace.ispoint = true
	trace.hitnormal = Vector(0,0,0)
	for i = 0, leaf.numleafbrushes - 1 do
		local v = self:GetLeafBrushes()[flb + i]
		--If brush is solid
		if bit.band(v.contents, 1) == 0 then continue end
		DM_ClipBoxToBrush( trace, trace.mins, trace.maxs, from, to, v)
		if trace.fraction ~= 1 or trace.startsolid then
			if trace.startsolid then
				trace.fraction = 0
			end
			trace.hit = true
			trace.hitnormal = trace.plane.normal
			trace.hitpos = from + (to - from) * trace.fraction
			return trace
		end
	end
	trace.hitpos = to
	trace.hit = false
	return trace
end

function meta:TestTrace(from, to)
	local trace = emptyTrace()
	trace.startsolid = false
	trace.fraction = 1
	trace.ispoint = false
	trace.hitnormal = Vector(0,0,0)
	for k, v in pairs( self:GetBrushes() ) do
		--local v = self:GetLeafBrushes()[flb + i]
		--If brush is solid
		if bit.band(v.contents, 1) == 0 then continue end
		DM_ClipBoxToBrush( trace, trace.mins, trace.maxs, from, to, v)
		if trace.fraction ~= 1 or trace.startsolid then
			if trace.startsolid then
				trace.fraction = 0
			end
			trace.hit = true
			trace.hitnormal = trace.plane.normal
			trace.hitpos = from + (to - from) * trace.fraction
			return trace
		end
	end
	trace.hitpos = to
	trace.hit = false
	return trace
end

-- Can't load these maps normally
local brokenMaps = {
	["d2_coast_02.bsp"] = true,
	["ep1_citadel_00_demo.bsp"] = true
}

local function assertMap( BSP )
	-- Check Entity
	local worldent = BSP:GetEntities()[0]
	assert(worldent.classname == "worldspawn", "Invalid MapEntity!")
	-- Check leaf
	local leaf = BSP:GetLeafs()[0]
	assert(leaf.numleaffaces == 0, "Invalid Leaf")
	assert(leaf.leafWaterDataID == -1, "Invalid Leaf")
	assert(leaf.maxs.x == 0, "Invalid Leaf")
	-- Check face
	local face = BSP:GetFaces()[0]
	assert(face, "Invalid Face")
	BSP:GetTextures()
end

function NikNaks.MapDebug( areYouSure )
	if not areYouSure or areYouSure ~= "iamsure" then return end
	print("Scanning all maps for any parse / data errors")
	local t = file.Find("maps/*.BSP", "GAME")
	local list = {}
	for _, mapString in pairs( t ) do
		if brokenMaps[mapString] then continue end
		table.insert(list, mapString)
	end
	local maps = #list
	local thread = coroutine.create(function()
		while true do
			if #list < 1 then coroutine.yield(true) return end
			local mapname = table.remove(list, 1)
			print("> Testing", mapname)
			local s = SysTime()
			local BSP = NikNaks.Map( mapname )
			assert( BSP, "Unable to parse MAP" )
			assertMap( BSP )
			print(string.format("%s took %fs", string.NiceSize(BSP._size), SysTime() - s))
			coroutine.wait( 0.1 )
		end
	end)
	hook.Add("Think", "NikNaks.BSPDEBUG", function()
		local noerr, yield = coroutine.resume( thread )
		if yield or not noerr then
			print(string.format("Scanned %i maps.", maps))
			hook.Remove("Think", "NikNaks.BSPDEBUG")
		end
	end)		
end
