/*
 * Wolfenstein: Enemy Territory GPL Source Code
 * Copyright (C) 1999-2010 id Software LLC, a ZeniMax Media company.
 *
 * ET: Legacy
 * Copyright (C) 2012-2018 ET:Legacy team <mail@etlegacy.com>
 *
 * This file is part of ET: Legacy - http://www.etlegacy.com
 *
 * ET: Legacy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ET: Legacy is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with ET: Legacy. If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, Wolfenstein: Enemy Territory GPL Source Code is also
 * subject to certain additional terms. You should have received a copy
 * of these additional terms immediately following the terms and conditions
 * of the GNU General Public License which accompanied the source code.
 * If not, please request a copy in writing from id Software at the address below.
 *
 * id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.
 */
/**
 * @file renderer/tr_bsp.c
 * @brief Loads and prepares a map file for scene rendering.
 *
 * A single entry point: void RE_LoadWorldMap( const char *name );
 */

#include "tr_local.h"

static world_t s_worldData;
static byte    *fileBase;

surfaceType_t skipData = SF_SKIP;

//===============================================================================

/**
 * @brief HSVtoRGB
 * @param[in] h
 * @param[in] s
 * @param[in] v
 * @param[out] rgb
 */
static void HSVtoRGB(float h, float s, float v, float rgb[3])
{
	int   i;
	float f;
	float p, q, t;

	h *= 5;

	i = floor(h);
	f = h - i;

	p = v * (1 - s);
	q = v * (1 - s * f);
	t = v * (1 - s * (1 - f));

	switch (i)
	{
	case 0:
		rgb[0] = v;
		rgb[1] = t;
		rgb[2] = p;
		break;
	case 1:
		rgb[0] = q;
		rgb[1] = v;
		rgb[2] = p;
		break;
	case 2:
		rgb[0] = p;
		rgb[1] = v;
		rgb[2] = t;
		break;
	case 3:
		rgb[0] = p;
		rgb[1] = q;
		rgb[2] = v;
		break;
	case 4:
		rgb[0] = t;
		rgb[1] = p;
		rgb[2] = v;
		break;
	case 5:
		rgb[0] = v;
		rgb[1] = p;
		rgb[2] = q;
		break;
	}
}

/**
 * @brief R_ColorShiftLightingBytes
 * @param[in] in
 * @param[out] out
 */
static void R_ColorShiftLightingBytes(byte in[4], byte out[4])
{
	// shift the color data based on overbright range
	int shift = r_mapOverBrightBits->integer - tr.overbrightBits;
	// shift the data based on overbright range
	int r = in[0] << shift;
	int g = in[1] << shift;
	int b = in[2] << shift;

	// normalize by color instead of saturating to white
	if ((r | g | b) > 255)
	{
		int max = r > g ? r : g;

		max = max > b ? max : b;
		r   = r * 255 / max;
		g   = g * 255 / max;
		b   = b * 255 / max;
	}

	out[0] = (byte)r;
	out[1] = (byte)g;
	out[2] = (byte)b;
	out[3] = in[3];
}

/**
 * @brief R_ProcessLightmap
 * @param[in] pic
 * @param[in] in_padding
 * @param[in] width
 * @param[in] height
 * @param[out] pic_out
 * @return maxIntensity
 */
float R_ProcessLightmap(byte *pic, int in_padding, int width, int height, byte *pic_out)
{
	int   j;
	float maxIntensity = 0;

	if (r_lightMap->integer > 1)     // color code by intensity as development tool (FIXME: check range)
	{
		//double sumIntensity = 0;
		float r, g, b, intensity;
		float out[3] = { 0, 0, 0 };

		for (j = 0; j < width * height; j++)
		{
			r = pic[j * in_padding + 0];
			g = pic[j * in_padding + 1];
			b = pic[j * in_padding + 2];

			intensity = 0.33f * r + 0.685f * g + 0.063f * b;

			if (intensity > 255)
			{
				intensity = 1.0f;
			}
			else
			{
				intensity /= 255.0f;
			}

			if (intensity > maxIntensity)
			{
				maxIntensity = intensity;
			}

			HSVtoRGB(intensity, 1.00, 0.50, out);

			if (r_lightMap->integer == 3)
			{
				// Arnout: artists wanted the colours to be inversed
				pic_out[j * 4 + 0] = (byte)(out[2] * 255);
				pic_out[j * 4 + 1] = (byte)(out[1] * 255);
				pic_out[j * 4 + 2] = (byte)(out[0] * 255);
			}
			else
			{
				pic_out[j * 4 + 0] = (byte)(out[0] * 255);
				pic_out[j * 4 + 1] = (byte)(out[1] * 255);
				pic_out[j * 4 + 2] = (byte)(out[2] * 255);
			}
			pic_out[j * 4 + 3] = 255;

			//sumIntensity += intensity;
		}
	}
	else
	{
		for (j = 0 ; j < width * height; j++)
		{
			R_ColorShiftLightingBytes(&pic[j * in_padding], &pic_out[j * 4]);
			pic_out[j * 4 + 3] = 255;
		}
	}

	return maxIntensity;
}

#define LIGHTMAP_SIZE   128

/**
 * @brief R_LoadLightmaps
 * @param[in] l
 */
static void R_LoadLightmaps(lump_t *l)
{
	byte         *buf;
	unsigned int len;
	byte         image[LIGHTMAP_SIZE * LIGHTMAP_SIZE * 4];
	int          i; //, j;
	float        intensity, maxIntensity = 0;

	// clear lightmaps first
	tr.numLightmaps = 0;
	Com_Memset(tr.lightmaps, 0, sizeof(*tr.lightmaps) * MAX_LIGHTMAPS);

	len = l->filelen;
	if (!len)
	{
		return;
	}
	buf = fileBase + l->fileofs;

	// we are about to upload textures
	R_IssuePendingRenderCommands();

	// create all the lightmaps
	tr.numLightmaps = len / (LIGHTMAP_SIZE * LIGHTMAP_SIZE * 3);
	if (tr.numLightmaps == 1)
	{
		// FIXME: HACK: maps with only one lightmap turn up fullbright for some reason.
		// this avoids this, but isn't the correct solution.
		tr.numLightmaps++;
	}

	for (i = 0 ; i < tr.numLightmaps ; i++)
	{
		// expand the 24 bit on-disk to 32 bit
		intensity = R_ProcessLightmap(buf + i * LIGHTMAP_SIZE * LIGHTMAP_SIZE * 3, 3, LIGHTMAP_SIZE, LIGHTMAP_SIZE, image);
		if (intensity > maxIntensity)
		{
			maxIntensity = intensity;
		}

		tr.lightmaps[i] = R_CreateImage(va("*lightmap%d", i), image,
		                                LIGHTMAP_SIZE, LIGHTMAP_SIZE, qfalse, qfalse, GL_CLAMP_TO_EDGE);
	}

	if (r_lightMap->integer > 1)
	{
		Ren_Print("Brightest lightmap value: %d\n", ( int ) (maxIntensity * 255));
	}
}

/**
 * @brief This is called by the clipmodel subsystem so we can share the 1.8 megs of
 * space in big maps...
 * @param[in] vis
 */
void RE_SetWorldVisData(const byte *vis)
{
	tr.externalVisData = vis;
}

/**
 * @brief R_LoadVisibility
 * @param[in] l
 */
static void R_LoadVisibility(lump_t *l)
{
	int  len;
	byte *buf;

	len = PAD(s_worldData.numClusters, 64);

	s_worldData.novis = ri.Hunk_Alloc(len, h_low);
	Com_Memset(s_worldData.novis, 0xff, len);

	len = l->filelen;
	if (!len)
	{
		return;
	}
	buf = fileBase + l->fileofs;

	s_worldData.numClusters  = LittleLong(((int *)buf)[0]);
	s_worldData.clusterBytes = LittleLong(((int *)buf)[1]);

	// CM_Load should have given us the vis data to share, so
	// we don't need to allocate another copy
	if (tr.externalVisData)
	{
		s_worldData.vis = tr.externalVisData;
	}
	else
	{
		byte *dest;

		dest = ri.Hunk_Alloc(len - 8, h_low);
		Com_Memcpy(dest, buf + 8, len - 8);
		s_worldData.vis = dest;
	}
}

//===============================================================================

/**
 * @brief ShaderForShaderNum
 * @param[in] shaderNum
 * @param[in] lightmapNum
 * @return
 */
static shader_t *ShaderForShaderNum(int shaderNum, int lightmapNum)
{
	shader_t  *shader;
	dshader_t *dsh;

	shaderNum = LittleLong(shaderNum);

	if (shaderNum < 0 || shaderNum >= s_worldData.numShaders)
	{
		Ren_Drop("ShaderForShaderNum: bad num %i", shaderNum);
	}
	dsh = &s_worldData.shaders[shaderNum];

	shader = R_FindShader(dsh->shader, lightmapNum, qtrue);

	// if the shader had errors, just use default shader
	if (shader->defaultShader)
	{
		return tr.defaultShader;
	}

	return shader;
}

// optimizations here
// memory block for use by surfaces
static byte *surfHunkPtr;
static int  surfHunkSize;
#define SURF_HUNK_MAXSIZE 0x40000

/**
 * @brief R_InitSurfMemory
 */
void R_InitSurfMemory(void)
{
	// allocate a new chunk
	surfHunkPtr  = ri.Hunk_Alloc(SURF_HUNK_MAXSIZE, h_low);
	surfHunkSize = 0;
}

/**
 * @brief R_GetSurfMemory
 * @param[in] size
 * @return
 */
void *R_GetSurfMemory(int size)
{
	byte *retval;

	// round to cacheline
	size = PAD(size, 32);

	surfHunkSize += size;
	if (surfHunkSize >= SURF_HUNK_MAXSIZE)
	{
		// allocate a new chunk
		R_InitSurfMemory();
		surfHunkSize += size;   // since it just got reset
	}
	retval       = surfHunkPtr;
	surfHunkPtr += size;

	return (void *)retval;
}

/**
 * @brief Creates a bounding sphere from a bounding box
 * @param[in] mins
 * @param[in] maxs
 * @param[in] origin
 * @param[out] radius
 */
static void SphereFromBounds(vec3_t mins, vec3_t maxs, vec3_t origin, float *radius)
{
	vec3_t temp;

	VectorAdd(mins, maxs, origin);
	VectorScale(origin, 0.5f, origin);
	VectorSubtract(maxs, origin, temp);
	*radius = VectorLength(temp);
}

/**
 * @brief Handles final surface classification
 * @param[in] ds
 * @param[in,out] gen
 * @param[in] pt
 */
static void FinishGenericSurface(dsurface_t *ds, srfGeneric_t *gen, vec3_t pt)
{
	// set bounding sphere
	SphereFromBounds(gen->bounds[0], gen->bounds[1], gen->origin, &gen->radius);

	// take the plane normal from the lightmap vector and classify it
	gen->plane.normal[0] = LittleFloat(ds->lightmapVecs[2][0]);
	gen->plane.normal[1] = LittleFloat(ds->lightmapVecs[2][1]);
	gen->plane.normal[2] = LittleFloat(ds->lightmapVecs[2][2]);
	gen->plane.dist      = DotProduct(pt, gen->plane.normal);
	SetPlaneSignbits(&gen->plane);
	gen->plane.type = PlaneTypeForNormal(gen->plane.normal);
}

/**
 * @brief ParseMesh
 * @param[in] ds
 * @param[in,out] verts
 * @param[in,out] surf
 */
static void ParseMesh(dsurface_t *ds, drawVert_t *verts, msurface_t *surf)
{
	srfGridMesh_t *grid;
	int           i, j;
	int           width, height, numPoints;
	drawVert_t    points[MAX_PATCH_SIZE * MAX_PATCH_SIZE];
	int           lightmapNum;
	vec3_t        bounds[2];
	vec3_t        tmpVec;

	lightmapNum = LittleLong(ds->lightmapNum);

	// get fog volume
	surf->fogIndex = LittleLong(ds->fogNum) + 1;

	// get shader value
	surf->shader = ShaderForShaderNum(ds->shaderNum, lightmapNum);
	if (r_singleShader->integer && !surf->shader->isSky)
	{
		surf->shader = tr.defaultShader;
	}

	// we may have a nodraw surface, because they might still need to
	// be around for movement clipping
	if (s_worldData.shaders[LittleLong(ds->shaderNum)].surfaceFlags & SURF_NODRAW)
	{
		surf->data = &skipData;
		return;
	}

	width  = LittleLong(ds->patchWidth);
	height = LittleLong(ds->patchHeight);

	verts    += LittleLong(ds->firstVert);
	numPoints = width * height;
	for (i = 0 ; i < numPoints ; i++)
	{
		for (j = 0 ; j < 3 ; j++)
		{
			points[i].xyz[j]    = LittleFloat(verts[i].xyz[j]);
			points[i].normal[j] = LittleFloat(verts[i].normal[j]);
		}
		for (j = 0 ; j < 2 ; j++)
		{
			points[i].st[j]       = LittleFloat(verts[i].st[j]);
			points[i].lightmap[j] = LittleFloat(verts[i].lightmap[j]);
		}
		R_ColorShiftLightingBytes(verts[i].color, points[i].color);
	}

	// pre-tesseleate
	grid       = R_SubdividePatchToGrid(width, height, points);
	surf->data = (surfaceType_t *) grid;

	// copy the level of detail origin, which is the center
	// of the group of all curves that must subdivide the same
	// to avoid cracking
	for (i = 0 ; i < 3 ; i++)
	{
		bounds[0][i] = LittleFloat(ds->lightmapVecs[0][i]);
		bounds[1][i] = LittleFloat(ds->lightmapVecs[1][i]);
	}
	VectorAdd(bounds[0], bounds[1], bounds[1]);
	VectorScale(bounds[1], 0.5f, grid->lodOrigin);
	VectorSubtract(bounds[0], grid->lodOrigin, tmpVec);
	grid->lodRadius = VectorLength(tmpVec);

	// finish surface
	FinishGenericSurface(ds, (srfGeneric_t *) grid, grid->verts[0].xyz);
}

/**
 * @brief ParseTriSurf
 * @param[in] ds
 * @param[in,out] verts
 * @param[in,out] surf
 * @param[in,out] indexes
 */
static void ParseTriSurf(dsurface_t *ds, drawVert_t *verts, msurface_t *surf, int *indexes)
{
	srfTriangles_t *tri;
	int            i, j;
	int            numVerts, numIndexes;
	int            lightmapNum;

	// get lightmap num
	lightmapNum = LittleLong(ds->lightmapNum);

	// get fog volume
	surf->fogIndex = LittleLong(ds->fogNum) + 1;

	// get shader
	surf->shader = ShaderForShaderNum(ds->shaderNum, lightmapNum);      //% LIGHTMAP_BY_VERTEX );
	if (r_singleShader->integer && !surf->shader->isSky)
	{
		surf->shader = tr.defaultShader;
	}

	numVerts   = LittleLong(ds->numVerts);
	numIndexes = LittleLong(ds->numIndexes);

	//tri = ri.Hunk_Alloc( sizeof( *tri ) + numVerts * sizeof( tri->verts[0] )
	//  + numIndexes * sizeof( tri->indexes[0] ) );
	tri = R_GetSurfMemory(sizeof(*tri) + numVerts * sizeof(tri->verts[0])
	                      + numIndexes * sizeof(tri->indexes[0]));

	tri->surfaceType = SF_TRIANGLES;
	tri->numVerts    = numVerts;
	tri->numIndexes  = numIndexes;
	tri->verts       = ( drawVert_t * )(tri + 1);
	tri->indexes     = ( int * )(tri->verts + tri->numVerts);

	surf->data = (surfaceType_t *)tri;

	// copy vertexes
	ClearBounds(tri->bounds[0], tri->bounds[1]);
	verts += LittleLong(ds->firstVert);
	for (i = 0 ; i < numVerts ; i++)
	{
		for (j = 0 ; j < 3 ; j++)
		{
			tri->verts[i].xyz[j]    = LittleFloat(verts[i].xyz[j]);
			tri->verts[i].normal[j] = LittleFloat(verts[i].normal[j]);
		}
		AddPointToBounds(tri->verts[i].xyz, tri->bounds[0], tri->bounds[1]);
		for (j = 0 ; j < 2 ; j++)
		{
			tri->verts[i].st[j]       = LittleFloat(verts[i].st[j]);
			tri->verts[i].lightmap[j] = LittleFloat(verts[i].lightmap[j]);
		}

		R_ColorShiftLightingBytes(verts[i].color, tri->verts[i].color);
	}

	// copy indexes
	indexes += LittleLong(ds->firstIndex);
	for (i = 0 ; i < numIndexes ; i++)
	{
		tri->indexes[i] = LittleLong(indexes[i]);
		if (tri->indexes[i] < 0 || tri->indexes[i] >= numVerts)
		{
			Ren_Drop("Bad index in triangle surface");
		}
	}

	// finish surface
	FinishGenericSurface(ds, (srfGeneric_t *) tri, tri->verts[0].xyz);
}

/**
 * @brief parses a foliage drawsurface
 * @param[in] ds
 * @param[in,out] verts
 * @param[in,out] surf
 * @param[in,out] indexes
 */
static void ParseFoliage(dsurface_t *ds, drawVert_t *verts, msurface_t *surf, int *indexes)
{
	srfFoliage_t *foliage;
	int          i, j, numVerts, numIndexes, numInstances, size;
	vec3_t       bounds[2], boundsTranslated[2];
	float        scale;

	// get fog volume
	surf->fogIndex = LittleLong(ds->fogNum) + 1;

	// get shader
	surf->shader = ShaderForShaderNum(ds->shaderNum, LIGHTMAP_BY_VERTEX);
	if (r_singleShader->integer && !surf->shader->isSky)
	{
		surf->shader = tr.defaultShader;
	}

	// foliage surfaces have their actual vert count in patchHeight
	// and the instance count in patchWidth
	// the instances are just additional drawverts

	// get counts
	numVerts     = LittleLong(ds->patchHeight);
	numIndexes   = LittleLong(ds->numIndexes);
	numInstances = LittleLong(ds->patchWidth);

	// calculate size
	size = sizeof(*foliage) +
	       numVerts * (sizeof(foliage->xyz[0]) + sizeof(foliage->normal[0]) + sizeof(foliage->texCoords[0]) + sizeof(foliage->lmTexCoords[0])) +
	       numIndexes * sizeof(foliage->indexes[0]) +
	       numInstances * sizeof(foliage->instances[0]);

	// get memory
	foliage = R_GetSurfMemory(size);

	// set up surface
	foliage->surfaceType  = SF_FOLIAGE;
	foliage->numVerts     = numVerts;
	foliage->numIndexes   = numIndexes;
	foliage->numInstances = numInstances;
	foliage->xyz          = ( vec4_t * )(foliage + 1);
	foliage->normal       = ( vec4_t * )(foliage->xyz + foliage->numVerts);
	foliage->texCoords    = ( vec2_t * )(foliage->normal + foliage->numVerts);
	foliage->lmTexCoords  = ( vec2_t * )(foliage->texCoords + foliage->numVerts);
	foliage->indexes      = ( unsigned int * )(foliage->lmTexCoords + foliage->numVerts);
	foliage->instances    = ( foliageInstance_t * )(foliage->indexes + foliage->numIndexes);

	surf->data = (surfaceType_t *) foliage;

	// get foliage drawscale
	scale = r_drawFoliage->value;
	if (scale < 0.0f)
	{
		scale = 1.0f;
	}
	else if (scale > 2.0f)
	{
		scale = 2.0f;
	}

	// copy vertexes
	ClearBounds(bounds[0], bounds[1]);
	verts += LittleLong(ds->firstVert);
	for (i = 0; i < numVerts; i++)
	{
		// copy xyz and normal
		for (j = 0; j < 3; j++)
		{
			foliage->xyz[i][j]    = LittleFloat(verts[i].xyz[j]);
			foliage->normal[i][j] = LittleFloat(verts[i].normal[j]);
		}

		// scale height
		foliage->xyz[i][2] *= scale;

		// finish
		foliage->xyz[i][3] = foliage->normal[i][3] = 0;
		AddPointToBounds(foliage->xyz[i], bounds[0], bounds[1]);

		// copy texture coordinates
		for (j = 0; j < 2; j++)
		{
			foliage->texCoords[i][j]   = LittleFloat(verts[i].st[j]);
			foliage->lmTexCoords[i][j] = LittleFloat(verts[i].lightmap[j]);
		}
	}

	// copy indexes
	indexes += LittleLong(ds->firstIndex);
	for (i = 0; i < numIndexes; i++)
	{
		foliage->indexes[i] = LittleLong(indexes[i]);
		if (foliage->indexes[i] >= numVerts)
		{
			Ren_Drop("Bad index in triangle surface");
		}
	}

	// copy origins and colors
	ClearBounds(foliage->bounds[0], foliage->bounds[1]);
	verts += numVerts;
	for (i = 0; i < numInstances; i++)
	{
		// copy xyz
		for (j = 0; j < 3; j++)
		{
			foliage->instances[i].origin[j] = LittleFloat(verts[i].xyz[j]);
		}
		VectorAdd(bounds[0], foliage->instances[i].origin, boundsTranslated[0]);
		VectorAdd(bounds[1], foliage->instances[i].origin, boundsTranslated[1]);
		AddPointToBounds(boundsTranslated[0], foliage->bounds[0], foliage->bounds[1]);
		AddPointToBounds(boundsTranslated[1], foliage->bounds[0], foliage->bounds[1]);

		// copy color
		R_ColorShiftLightingBytes(verts[i].color, foliage->instances[i].color);
	}

	// finish surface
	FinishGenericSurface(ds, (srfGeneric_t *) foliage, foliage->xyz[0]);
}

/**
 * @brief ParseFlare
 * @param[in] ds
 * @param verts - unused
 * @param[in,out] surf
 * @param indexes - unused
 */
static void ParseFlare(dsurface_t *ds, drawVert_t *verts, msurface_t *surf, int *indexes)
{
	srfFlare_t *flare;
	int        i;

	// get fog volume
	surf->fogIndex = LittleLong(ds->fogNum) + 1;

	// get shader
	surf->shader = ShaderForShaderNum(ds->shaderNum, LIGHTMAP_BY_VERTEX);
	if (r_singleShader->integer && !surf->shader->isSky)
	{
		surf->shader = tr.defaultShader;
	}

	flare              = ri.Hunk_Alloc(sizeof(*flare), h_low);
	flare->surfaceType = SF_FLARE;

	surf->data = (surfaceType_t *)flare;

	for (i = 0 ; i < 3 ; i++)
	{
		flare->origin[i] = LittleFloat(ds->lightmapOrigin[i]);
		flare->color[i]  = LittleFloat(ds->lightmapVecs[0][i]);
		flare->normal[i] = LittleFloat(ds->lightmapVecs[2][i]);
	}
}

/**
 * @brief R_MergedWidthPoints
 * @param[in] grid
 * @param[in] offset
 * @return true if there are grid points merged on a width edge
 */
int R_MergedWidthPoints(srfGridMesh_t *grid, int offset)
{
	int i, j;

	for (i = 1; i < grid->width - 1; i++)
	{
		for (j = i + 1; j < grid->width - 1; j++)
		{
			if (Q_fabs(grid->verts[i + offset].xyz[0] - grid->verts[j + offset].xyz[0]) > .1f)
			{
				continue;
			}
			if (Q_fabs(grid->verts[i + offset].xyz[1] - grid->verts[j + offset].xyz[1]) > .1f)
			{
				continue;
			}
			if (Q_fabs(grid->verts[i + offset].xyz[2] - grid->verts[j + offset].xyz[2]) > .1f)
			{
				continue;
			}
			return qtrue;
		}
	}
	return qfalse;
}

/**
 * @brief R_MergedHeightPoints
 * @param[in] grid
 * @param[in] offset
 * @return true if there are grid points merged on a height edge
 */
int R_MergedHeightPoints(srfGridMesh_t *grid, int offset)
{
	int i, j;

	for (i = 1; i < grid->height - 1; i++)
	{
		for (j = i + 1; j < grid->height - 1; j++)
		{
			if (Q_fabs(grid->verts[grid->width * i + offset].xyz[0] - grid->verts[grid->width * j + offset].xyz[0]) > .1f)
			{
				continue;
			}
			if (Q_fabs(grid->verts[grid->width * i + offset].xyz[1] - grid->verts[grid->width * j + offset].xyz[1]) > .1f)
			{
				continue;
			}
			if (Q_fabs(grid->verts[grid->width * i + offset].xyz[2] - grid->verts[grid->width * j + offset].xyz[2]) > .1f)
			{
				continue;
			}
			return qtrue;
		}
	}
	return qfalse;
}

/**
 * @brief R_FixSharedVertexLodError_r
 * @param start
 * @param grid1
 *
 * @note Never sync LoD through grid edges with merged points!
 *
 * @todo FIXME: write generalized version that also avoids cracks between a patch and one that meets half way?
 */
void R_FixSharedVertexLodError_r(int start, srfGridMesh_t *grid1)
{
	int           j, k, l, m, n, offset1, offset2, touch;
	srfGridMesh_t *grid2;

	for (j = start; j < s_worldData.numsurfaces; j++)
	{
		//
		grid2 = (srfGridMesh_t *) s_worldData.surfaces[j].data;
		// if this surface is not a grid
		if (grid2->surfaceType != SF_GRID)
		{
			continue;
		}
		// if the LOD errors are already fixed for this patch
		if (grid2->lodFixed == 2)
		{
			continue;
		}
		// grids in the same LOD group should have the exact same lod radius
		if (grid1->lodRadius != grid2->lodRadius)
		{
			continue;
		}
		// grids in the same LOD group should have the exact same lod origin
		if (grid1->lodOrigin[0] != grid2->lodOrigin[0])
		{
			continue;
		}
		if (grid1->lodOrigin[1] != grid2->lodOrigin[1])
		{
			continue;
		}
		if (grid1->lodOrigin[2] != grid2->lodOrigin[2])
		{
			continue;
		}

		touch = qfalse;
		for (n = 0; n < 2; n++)
		{
			if (n)
			{
				offset1 = (grid1->height - 1) * grid1->width;
			}
			else
			{
				offset1 = 0;
			}
			if (R_MergedWidthPoints(grid1, offset1))
			{
				continue;
			}
			for (k = 1; k < grid1->width - 1; k++)
			{
				for (m = 0; m < 2; m++)
				{
					if (m)
					{
						offset2 = (grid2->height - 1) * grid2->width;
					}
					else
					{
						offset2 = 0;
					}
					if (R_MergedWidthPoints(grid2, offset2))
					{
						continue;
					}
					for (l = 1; l < grid2->width - 1; l++)
					{
						//
						if (Q_fabs(grid1->verts[k + offset1].xyz[0] - grid2->verts[l + offset2].xyz[0]) > .1f)
						{
							continue;
						}
						if (Q_fabs(grid1->verts[k + offset1].xyz[1] - grid2->verts[l + offset2].xyz[1]) > .1f)
						{
							continue;
						}
						if (Q_fabs(grid1->verts[k + offset1].xyz[2] - grid2->verts[l + offset2].xyz[2]) > .1f)
						{
							continue;
						}
						// ok the points are equal and should have the same lod error
						grid2->widthLodError[l] = grid1->widthLodError[k];
						touch                   = qtrue;
					}
				}
				for (m = 0; m < 2; m++)
				{
					if (m)
					{
						offset2 = grid2->width - 1;
					}
					else
					{
						offset2 = 0;
					}
					if (R_MergedHeightPoints(grid2, offset2))
					{
						continue;
					}
					for (l = 1; l < grid2->height - 1; l++)
					{
						//
						if (Q_fabs(grid1->verts[k + offset1].xyz[0] - grid2->verts[grid2->width * l + offset2].xyz[0]) > .1f)
						{
							continue;
						}
						if (Q_fabs(grid1->verts[k + offset1].xyz[1] - grid2->verts[grid2->width * l + offset2].xyz[1]) > .1f)
						{
							continue;
						}
						if (Q_fabs(grid1->verts[k + offset1].xyz[2] - grid2->verts[grid2->width * l + offset2].xyz[2]) > .1f)
						{
							continue;
						}
						// ok the points are equal and should have the same lod error
						grid2->heightLodError[l] = grid1->widthLodError[k];
						touch                    = qtrue;
					}
				}
			}
		}
		for (n = 0; n < 2; n++)
		{
			if (n)
			{
				offset1 = grid1->width - 1;
			}
			else
			{
				offset1 = 0;
			}
			if (R_MergedHeightPoints(grid1, offset1))
			{
				continue;
			}
			for (k = 1; k < grid1->height - 1; k++)
			{
				for (m = 0; m < 2; m++)
				{
					if (m)
					{
						offset2 = (grid2->height - 1) * grid2->width;
					}
					else
					{
						offset2 = 0;
					}
					if (R_MergedWidthPoints(grid2, offset2))
					{
						continue;
					}
					for (l = 1; l < grid2->width - 1; l++)
					{
						if (Q_fabs(grid1->verts[grid1->width * k + offset1].xyz[0] - grid2->verts[l + offset2].xyz[0]) > .1f)
						{
							continue;
						}
						if (Q_fabs(grid1->verts[grid1->width * k + offset1].xyz[1] - grid2->verts[l + offset2].xyz[1]) > .1f)
						{
							continue;
						}
						if (Q_fabs(grid1->verts[grid1->width * k + offset1].xyz[2] - grid2->verts[l + offset2].xyz[2]) > .1f)
						{
							continue;
						}
						// ok the points are equal and should have the same lod error
						grid2->widthLodError[l] = grid1->heightLodError[k];
						touch                   = qtrue;
					}
				}
				for (m = 0; m < 2; m++)
				{
					if (m)
					{
						offset2 = grid2->width - 1;
					}
					else
					{
						offset2 = 0;
					}
					if (R_MergedHeightPoints(grid2, offset2))
					{
						continue;
					}
					for (l = 1; l < grid2->height - 1; l++)
					{
						if (Q_fabs(grid1->verts[grid1->width * k + offset1].xyz[0] - grid2->verts[grid2->width * l + offset2].xyz[0]) > .1f)
						{
							continue;
						}
						if (Q_fabs(grid1->verts[grid1->width * k + offset1].xyz[1] - grid2->verts[grid2->width * l + offset2].xyz[1]) > .1f)
						{
							continue;
						}
						if (Q_fabs(grid1->verts[grid1->width * k + offset1].xyz[2] - grid2->verts[grid2->width * l + offset2].xyz[2]) > .1f)
						{
							continue;
						}
						// ok the points are equal and should have the same lod error
						grid2->heightLodError[l] = grid1->heightLodError[k];
						touch                    = qtrue;
					}
				}
			}
		}
		if (touch)
		{
			grid2->lodFixed = 2;
			R_FixSharedVertexLodError_r(start, grid2);
			//NOTE: this would be correct but makes things really slow
			//grid2->lodFixed = 1;
		}
	}
}

/**
 * @brief This function assumes that all patches in one group are nicely stitched together for the highest LoD.
 * If this is not the case this function will still do its job but won't fix the highest LoD cracks.
 */
void R_FixSharedVertexLodError(void)
{
	int           i;
	srfGridMesh_t *grid1;

	for (i = 0; i < s_worldData.numsurfaces; i++)
	{
		grid1 = (srfGridMesh_t *) s_worldData.surfaces[i].data;
		// if this surface is not a grid
		if (grid1->surfaceType != SF_GRID)
		{
			continue;
		}

		if (grid1->lodFixed)
		{
			continue;
		}

		grid1->lodFixed = 2;
		// recursively fix other patches in the same LOD group
		R_FixSharedVertexLodError_r(i + 1, grid1);
	}
}

/**
 * @brief R_StitchPatches
 * @param[in] grid1num
 * @param[in] grid2num
 * @return
 */
int R_StitchPatches(int grid1num, int grid2num)
{
	int           k, l, m, n, offset1, offset2, row, column;
	float         *v1, *v2;
	srfGridMesh_t *grid1 = (srfGridMesh_t *) s_worldData.surfaces[grid1num].data;
	srfGridMesh_t *grid2 = (srfGridMesh_t *) s_worldData.surfaces[grid2num].data;

	for (n = 0; n < 2; n++)
	{
		if (n)
		{
			offset1 = (grid1->height - 1) * grid1->width;
		}
		else
		{
			offset1 = 0;
		}
		if (R_MergedWidthPoints(grid1, offset1))
		{
			continue;
		}

		for (k = 0; k < grid1->width - 2; k += 2)
		{
			for (m = 0; m < 2; m++)
			{
				if (grid2->width >= MAX_GRID_SIZE)
				{
					break;
				}
				if (m)
				{
					offset2 = (grid2->height - 1) * grid2->width;
				}
				else
				{
					offset2 = 0;
				}
				//if (R_MergedWidthPoints(grid2, offset2))
				//  continue;
				for (l = 0; l < grid2->width - 1; l++)
				{
					v1 = grid1->verts[k + offset1].xyz;
					v2 = grid2->verts[l + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}

					v1 = grid1->verts[k + 2 + offset1].xyz;
					v2 = grid2->verts[l + 1 + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}

					v1 = grid2->verts[l + offset2].xyz;
					v2 = grid2->verts[l + 1 + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) < .01f &&
					    Q_fabs(v1[1] - v2[1]) < .01f &&
					    Q_fabs(v1[2] - v2[2]) < .01f)
					{
						continue;
					}

					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert column into grid2 right after after column l
					if (m)
					{
						row = grid2->height - 1;
					}
					else
					{
						row = 0;
					}
					grid2 = R_GridInsertColumn(grid2, l + 1, row,
					                           grid1->verts[k + 1 + offset1].xyz, grid1->widthLodError[k + 1]);
					grid2->lodStitched                  = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
			for (m = 0; m < 2; m++)
			{
				if (grid2->height >= MAX_GRID_SIZE)
				{
					break;
				}
				if (m)
				{
					offset2 = grid2->width - 1;
				}
				else
				{
					offset2 = 0;
				}
				//if (R_MergedHeightPoints(grid2, offset2))
				//  continue;
				for (l = 0; l < grid2->height - 1; l++)
				{
					v1 = grid1->verts[k + offset1].xyz;
					v2 = grid2->verts[grid2->width * l + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}

					v1 = grid1->verts[k + 2 + offset1].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}

					v1 = grid2->verts[grid2->width * l + offset2].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) < .01f &&
					    Q_fabs(v1[1] - v2[1]) < .01f &&
					    Q_fabs(v1[2] - v2[2]) < .01f)
					{
						continue;
					}

					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert row into grid2 right after after row l
					if (m)
					{
						column = grid2->width - 1;
					}
					else
					{
						column = 0;
					}
					grid2 = R_GridInsertRow(grid2, l + 1, column,
					                        grid1->verts[k + 1 + offset1].xyz, grid1->widthLodError[k + 1]);
					grid2->lodStitched                  = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
		}
	}
	for (n = 0; n < 2; n++)
	{
		if (n)
		{
			offset1 = grid1->width - 1;
		}
		else
		{
			offset1 = 0;
		}
		if (R_MergedHeightPoints(grid1, offset1))
		{
			continue;
		}
		for (k = 0; k < grid1->height - 2; k += 2)
		{
			for (m = 0; m < 2; m++)
			{
				if (grid2->width >= MAX_GRID_SIZE)
				{
					break;
				}
				if (m)
				{
					offset2 = (grid2->height - 1) * grid2->width;
				}
				else
				{
					offset2 = 0;
				}
				//if (R_MergedWidthPoints(grid2, offset2))
				//  continue;
				for (l = 0; l < grid2->width - 1; l++)
				{
					//
					v1 = grid1->verts[grid1->width * k + offset1].xyz;
					v2 = grid2->verts[l + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}

					v1 = grid1->verts[grid1->width * (k + 2) + offset1].xyz;
					v2 = grid2->verts[l + 1 + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}
					//
					v1 = grid2->verts[l + offset2].xyz;
					v2 = grid2->verts[(l + 1) + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) < .01f &&
					    Q_fabs(v1[1] - v2[1]) < .01f &&
					    Q_fabs(v1[2] - v2[2]) < .01f)
					{
						continue;
					}

					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert column into grid2 right after after column l
					if (m)
					{
						row = grid2->height - 1;
					}
					else
					{
						row = 0;
					}
					grid2 = R_GridInsertColumn(grid2, l + 1, row,
					                           grid1->verts[grid1->width * (k + 1) + offset1].xyz, grid1->heightLodError[k + 1]);
					grid2->lodStitched                  = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
			for (m = 0; m < 2; m++)
			{
				if (grid2->height >= MAX_GRID_SIZE)
				{
					break;
				}
				if (m)
				{
					offset2 = grid2->width - 1;
				}
				else
				{
					offset2 = 0;
				}
				//if (R_MergedHeightPoints(grid2, offset2))
				//  continue;
				for (l = 0; l < grid2->height - 1; l++)
				{
					v1 = grid1->verts[grid1->width * k + offset1].xyz;
					v2 = grid2->verts[grid2->width * l + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}

					v1 = grid1->verts[grid1->width * (k + 2) + offset1].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}

					v1 = grid2->verts[grid2->width * l + offset2].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) < .01f &&
					    Q_fabs(v1[1] - v2[1]) < .01f &&
					    Q_fabs(v1[2] - v2[2]) < .01f)
					{
						continue;
					}

					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert row into grid2 right after after row l
					if (m)
					{
						column = grid2->width - 1;
					}
					else
					{
						column = 0;
					}
					grid2 = R_GridInsertRow(grid2, l + 1, column,
					                        grid1->verts[grid1->width * (k + 1) + offset1].xyz, grid1->heightLodError[k + 1]);
					grid2->lodStitched                  = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
		}
	}
	for (n = 0; n < 2; n++)
	{
		if (n)
		{
			offset1 = (grid1->height - 1) * grid1->width;
		}
		else
		{
			offset1 = 0;
		}
		if (R_MergedWidthPoints(grid1, offset1))
		{
			continue;
		}
		for (k = grid1->width - 1; k > 1; k -= 2)
		{
			for (m = 0; m < 2; m++)
			{
				if (!grid2 || grid2->width >= MAX_GRID_SIZE)
				{
					break;
				}
				if (m)
				{
					offset2 = (grid2->height - 1) * grid2->width;
				}
				else
				{
					offset2 = 0;
				}
				//if (R_MergedWidthPoints(grid2, offset2))
				//  continue;
				for (l = 0; l < grid2->width - 1; l++)
				{
					//
					v1 = grid1->verts[k + offset1].xyz;
					v2 = grid2->verts[l + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}

					v1 = grid1->verts[k - 2 + offset1].xyz;
					v2 = grid2->verts[l + 1 + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}
					//
					v1 = grid2->verts[l + offset2].xyz;
					v2 = grid2->verts[(l + 1) + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) < .01f &&
					    Q_fabs(v1[1] - v2[1]) < .01f &&
					    Q_fabs(v1[2] - v2[2]) < .01f)
					{
						continue;
					}

					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert column into grid2 right after after column l
					if (m)
					{
						row = grid2->height - 1;
					}
					else
					{
						row = 0;
					}
					grid2 = R_GridInsertColumn(grid2, l + 1, row,
					                           grid1->verts[k - 1 + offset1].xyz, grid1->widthLodError[k + 1]);
					grid2->lodStitched                  = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
			for (m = 0; m < 2; m++)
			{
				if (!grid2 || grid2->height >= MAX_GRID_SIZE)
				{
					break;
				}
				if (m)
				{
					offset2 = grid2->width - 1;
				}
				else
				{
					offset2 = 0;
				}
				//if (R_MergedHeightPoints(grid2, offset2))
				//  continue;
				for (l = 0; l < grid2->height - 1; l++)
				{
					v1 = grid1->verts[k + offset1].xyz;
					v2 = grid2->verts[grid2->width * l + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}

					v1 = grid1->verts[k - 2 + offset1].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}

					v1 = grid2->verts[grid2->width * l + offset2].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) < .01f &&
					    Q_fabs(v1[1] - v2[1]) < .01f &&
					    Q_fabs(v1[2] - v2[2]) < .01f)
					{
						continue;
					}

					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert row into grid2 right after after row l
					if (m)
					{
						column = grid2->width - 1;
					}
					else
					{
						column = 0;
					}
					grid2 = R_GridInsertRow(grid2, l + 1, column,
					                        grid1->verts[k - 1 + offset1].xyz, grid1->widthLodError[k + 1]);
					if (!grid2)
					{
						break;
					}
					grid2->lodStitched                  = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
		}
	}
	for (n = 0; n < 2; n++)
	{
		if (n)
		{
			offset1 = grid1->width - 1;
		}
		else
		{
			offset1 = 0;
		}
		if (R_MergedHeightPoints(grid1, offset1))
		{
			continue;
		}
		for (k = grid1->height - 1; k > 1; k -= 2)
		{
			for (m = 0; m < 2; m++)
			{
				if (!grid2 || grid2->width >= MAX_GRID_SIZE)
				{
					break;
				}
				if (m)
				{
					offset2 = (grid2->height - 1) * grid2->width;
				}
				else
				{
					offset2 = 0;
				}
				//if (R_MergedWidthPoints(grid2, offset2))
				//  continue;
				for (l = 0; l < grid2->width - 1; l++)
				{
					v1 = grid1->verts[grid1->width * k + offset1].xyz;
					v2 = grid2->verts[l + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}

					v1 = grid1->verts[grid1->width * (k - 2) + offset1].xyz;
					v2 = grid2->verts[l + 1 + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}
					//
					v1 = grid2->verts[l + offset2].xyz;
					v2 = grid2->verts[(l + 1) + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) < .01f &&
					    Q_fabs(v1[1] - v2[1]) < .01f &&
					    Q_fabs(v1[2] - v2[2]) < .01f)
					{
						continue;
					}

					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert column into grid2 right after after column l
					if (m)
					{
						row = grid2->height - 1;
					}
					else
					{
						row = 0;
					}
					grid2 = R_GridInsertColumn(grid2, l + 1, row,
					                           grid1->verts[grid1->width * (k - 1) + offset1].xyz, grid1->heightLodError[k + 1]);
					grid2->lodStitched                  = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
			for (m = 0; m < 2; m++)
			{
				if (!grid2 || grid2->height >= MAX_GRID_SIZE)
				{
					break;
				}
				if (m)
				{
					offset2 = grid2->width - 1;
				}
				else
				{
					offset2 = 0;
				}
				//if (R_MergedHeightPoints(grid2, offset2))
				//  continue;
				for (l = 0; l < grid2->height - 1; l++)
				{
					v1 = grid1->verts[grid1->width * k + offset1].xyz;
					v2 = grid2->verts[grid2->width * l + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}

					v1 = grid1->verts[grid1->width * (k - 2) + offset1].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[1] - v2[1]) > .1f)
					{
						continue;
					}
					if (Q_fabs(v1[2] - v2[2]) > .1f)
					{
						continue;
					}

					v1 = grid2->verts[grid2->width * l + offset2].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if (Q_fabs(v1[0] - v2[0]) < .01f &&
					    Q_fabs(v1[1] - v2[1]) < .01f &&
					    Q_fabs(v1[2] - v2[2]) < .01f)
					{
						continue;
					}

					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert row into grid2 right after after row l
					if (m)
					{
						column = grid2->width - 1;
					}
					else
					{
						column = 0;
					}
					grid2 = R_GridInsertRow(grid2, l + 1, column,
					                        grid1->verts[grid1->width * (k - 1) + offset1].xyz, grid1->heightLodError[k + 1]);
					grid2->lodStitched                  = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
		}
	}
	return qfalse;
}

/**
 * @brief This function will try to stitch patches in the same LoD group together for the highest LoD.
 *
 * @details Only single missing vertice cracks will be fixed.
 *
 * Vertices will be joined at the patch side a crack is first found, at the other side
 * of the patch (on the same row or column) the vertices will not be joined and cracks
 * might still appear at that side.
 *
 * @param[in] grid1num
 * @return
 */
int R_TryStitchingPatch(int grid1num)
{
	int           j, numstitches = 0;
	srfGridMesh_t *grid1 = (srfGridMesh_t *) s_worldData.surfaces[grid1num].data;
	srfGridMesh_t *grid2;

	for (j = 0; j < s_worldData.numsurfaces; j++)
	{
		grid2 = (srfGridMesh_t *) s_worldData.surfaces[j].data;
		// if this surface is not a grid
		if (grid2->surfaceType != SF_GRID)
		{
			continue;
		}
		// grids in the same LOD group should have the exact same lod radius
		if (grid1->lodRadius != grid2->lodRadius)
		{
			continue;
		}
		// grids in the same LOD group should have the exact same lod origin
		if (grid1->lodOrigin[0] != grid2->lodOrigin[0])
		{
			continue;
		}
		if (grid1->lodOrigin[1] != grid2->lodOrigin[1])
		{
			continue;
		}
		if (grid1->lodOrigin[2] != grid2->lodOrigin[2])
		{
			continue;
		}

		while (R_StitchPatches(grid1num, j))
		{
			numstitches++;
		}
	}
	return numstitches;
}

/**
 * @brief R_StitchAllPatches
 */
void R_StitchAllPatches(void)
{
	int           i, numstitches = 0;
	srfGridMesh_t *grid1;
	qboolean      stitched;

	do
	{
		stitched = qfalse;
		for (i = 0; i < s_worldData.numsurfaces; i++)
		{
			grid1 = (srfGridMesh_t *) s_worldData.surfaces[i].data;
			// if this surface is not a grid
			if (grid1->surfaceType != SF_GRID)
			{
				continue;
			}

			if (grid1->lodStitched)
			{
				continue;
			}

			grid1->lodStitched = qtrue;
			stitched           = qtrue;

			numstitches += R_TryStitchingPatch(i);
		}
	}
	while (stitched);
	Ren_Print("stitched %d LoD cracks\n", numstitches);
}

/**
 * @brief R_MovePatchSurfacesToHunk
 */
void R_MovePatchSurfacesToHunk(void)
{
	int           i, size;
	srfGridMesh_t *grid, *hunkgrid;

	for (i = 0; i < s_worldData.numsurfaces; i++)
	{
		grid = (srfGridMesh_t *) s_worldData.surfaces[i].data;
		// if this surface is not a grid
		if (grid->surfaceType != SF_GRID)
		{
			continue;
		}

		size     = (grid->width * grid->height - 1) * sizeof(drawVert_t) + sizeof(*grid);
		hunkgrid = ri.Hunk_Alloc(size, h_low);
		Com_Memcpy(hunkgrid, grid, size);

		hunkgrid->widthLodError = ri.Hunk_Alloc(grid->width * 4, h_low);
		Com_Memcpy(hunkgrid->widthLodError, grid->widthLodError, grid->width * 4);

		hunkgrid->heightLodError = ri.Hunk_Alloc(grid->height * 4, h_low);
		// rain - copy into hunkgrid instead of copying grid overtop of itself
		Com_Memcpy(hunkgrid->heightLodError, grid->heightLodError, grid->height * 4);

		R_FreeSurfaceGridMesh(grid);

		s_worldData.surfaces[i].data = (void *) hunkgrid;
	}
}

/**
 * @brief R_LoadSurfaces
 * @param[in] surfs
 * @param[in] verts
 * @param[in] indexLump
 */
static void R_LoadSurfaces(lump_t *surfs, lump_t *verts, lump_t *indexLump)
{
	dsurface_t *in;
	msurface_t *out;
	drawVert_t *dv;
	int        *indexes;
	int        count;
	int        numFaces = 0, numMeshes = 0, numTriSurfs = 0, numFlares = 0, numFoliages = 0;
	int        i;

	in = ( void * )(fileBase + surfs->fileofs);
	if (surfs->filelen % sizeof(*in))
	{
		Ren_Drop("LoadMap: funny lump size in %s", s_worldData.name);
	}
	count = surfs->filelen / sizeof(*in);

	dv = ( void * )(fileBase + verts->fileofs);
	if (verts->filelen % sizeof(*dv))
	{
		Ren_Drop("LoadMap: funny lump size in %s", s_worldData.name);
	}

	indexes = ( void * )(fileBase + indexLump->fileofs);
	if (indexLump->filelen % sizeof(*indexes))
	{
		Ren_Drop("LoadMap: funny lump size in %s", s_worldData.name);
	}

	out = ri.Hunk_Alloc(count * sizeof(*out), h_low);

	s_worldData.surfaces    = out;
	s_worldData.numsurfaces = count;

	// init the surface memory. This is optimization, so we don't have to
	// look for memory for each surface, we allocate a big block and just chew it up
	// as we go
	R_InitSurfMemory();

	for (i = 0 ; i < count ; i++, in++, out++)
	{
		switch (LittleLong(in->surfaceType))
		{
		case MST_PATCH:
			ParseMesh(in, dv, out);
			numMeshes++;
			break;
		case MST_TRIANGLE_SOUP:
			ParseTriSurf(in, dv, out, indexes);
			numTriSurfs++;
			break;
		case MST_PLANAR:
			// faces and triangle surfaces are now homogenous
			ParseTriSurf(in, dv, out, indexes);
			numFaces++;
			break;
		case MST_FLARE:
			ParseFlare(in, dv, out, indexes);
			numFlares++;
			break;
		case MST_FOLIAGE:
			ParseFoliage(in, dv, out, indexes);
			numFoliages++;
			break;
		default:
			Ren_Drop("Bad surfaceType");
		}
	}

#ifdef PATCH_STITCHING
	R_StitchAllPatches();
#endif

	R_FixSharedVertexLodError();

#ifdef PATCH_STITCHING
	R_MovePatchSurfacesToHunk();
#endif

	Ren_Print("...loaded %d faces, %i meshes, %i trisurfs, %i flares, %i foliages\n",
	          numFaces, numMeshes, numTriSurfs, numFlares, numFoliages);
}

/**
 * @brief R_LoadSubmodels
 * @param[in] l
 */
static void R_LoadSubmodels(lump_t *l)
{
	dmodel_t *in;
	bmodel_t *out;
	int      i, j, count;

	in = ( void * )(fileBase + l->fileofs);
	if (l->filelen % sizeof(*in))
	{
		Ren_Drop("LoadMap: funny lump size in %s", s_worldData.name);
	}
	count = l->filelen / sizeof(*in);

	s_worldData.numBModels = count;

	s_worldData.bmodels = out = ri.Hunk_Alloc(count * sizeof(*out), h_low);

	for (i = 0 ; i < count ; i++, in++, out++)
	{
		model_t *model;

		model = R_AllocModel();

		etl_assert(model != NULL);              // this should never happen

		if (model)   // stop clang to complain about dereference
		{
			model->type         = MOD_BRUSH;
			model->model.bmodel = out;
			Com_sprintf(model->name, sizeof(model->name), "*%d", i);

			for (j = 0 ; j < 3 ; j++)
			{
				out->bounds[0][j] = LittleFloat(in->mins[j]);
				out->bounds[1][j] = LittleFloat(in->maxs[j]);
			}

			out->firstSurface = s_worldData.surfaces + LittleLong(in->firstSurface);
			out->numSurfaces  = LittleLong(in->numSurfaces);

			// for attaching fog brushes to models
			out->firstBrush = LittleLong(in->firstBrush);
			out->numBrushes = LittleLong(in->numBrushes);

			// allocate decal memory
			j           = (i == 0 ? MAX_WORLD_DECALS : MAX_ENTITY_DECALS);
			out->decals = ri.Hunk_Alloc(j * sizeof(*out->decals), h_low);
			Com_Memset(out->decals, 0, j * sizeof(*out->decals));
		}
	}
}

//==================================================================

/**
 * @brief R_SetParent
 * @param[in,out] node
 * @param[in] parent
 */
static void R_SetParent(mnode_t *node, mnode_t *parent)
{
	//  set parent
	node->parent = parent;

	// handle leaf nodes
	if (node->contents != -1)
	{
		// add node surfaces to bounds
		if (node->nummarksurfaces > 0)
		{
			int          c;
			msurface_t   **mark;
			srfGeneric_t *gen;

			// add node surfaces to bounds
			mark = node->firstmarksurface;
			c    = node->nummarksurfaces;
			while (c--)
			{
				gen = ( srfGeneric_t * )(**mark).data;
				if (gen->surfaceType != SF_FACE &&
				    gen->surfaceType != SF_GRID &&
				    gen->surfaceType != SF_TRIANGLES &&
				    gen->surfaceType != SF_FOLIAGE)
				{
					mark++;
					continue;
				}
				AddPointToBounds(gen->bounds[0], node->surfMins, node->surfMaxs);
				AddPointToBounds(gen->bounds[1], node->surfMins, node->surfMaxs);
				mark++;
			}
		}

		// go back
		return;
	}

	// recurse to child nodes
	R_SetParent(node->children[0], node);
	R_SetParent(node->children[1], node);

	// surface bounds
	BoundsAdd(node->surfMins, node->surfMaxs, node->children[0]->surfMins, node->children[0]->surfMaxs);
	BoundsAdd(node->surfMins, node->surfMaxs, node->children[1]->surfMins, node->children[1]->surfMaxs);
}

/**
 * @brief R_LoadNodesAndLeafs
 * @param[in] nodeLump
 * @param[in] leafLump
 */
static void R_LoadNodesAndLeafs(lump_t *nodeLump, lump_t *leafLump)
{
	int     i, j, p;
	dnode_t *in;
	dleaf_t *inLeaf;
	mnode_t *out;
	int     numNodes, numLeafs;

	in = ( void * )(fileBase + nodeLump->fileofs);
	if (nodeLump->filelen % sizeof(dnode_t) ||
	    leafLump->filelen % sizeof(dleaf_t))
	{
		Ren_Drop("LoadMap: funny lump size in %s", s_worldData.name);
	}
	numNodes = nodeLump->filelen / sizeof(dnode_t);
	numLeafs = leafLump->filelen / sizeof(dleaf_t);

	out = ri.Hunk_Alloc((numNodes + numLeafs) * sizeof(*out), h_low);

	s_worldData.nodes            = out;
	s_worldData.numnodes         = numNodes + numLeafs;
	s_worldData.numDecisionNodes = numNodes;

	// skybox optimization
	s_worldData.numSkyNodes = 0;
	s_worldData.skyNodes    = ri.Hunk_Alloc(WORLD_MAX_SKY_NODES * sizeof(*s_worldData.skyNodes), h_low);

	// load nodes
	for (i = 0 ; i < numNodes; i++, in++, out++)
	{
		for (j = 0 ; j < 3 ; j++)
		{
			out->mins[j] = LittleLong(in->mins[j]);
			out->maxs[j] = LittleLong(in->maxs[j]);
		}

		// surface bounds
		VectorCopy(out->mins, out->surfMins);
		VectorCopy(out->maxs, out->surfMaxs);

		p          = LittleLong(in->planeNum);
		out->plane = s_worldData.planes + p;

		out->contents = CONTENTS_NODE;  // differentiate from leafs

		for (j = 0 ; j < 2 ; j++)
		{
			p = LittleLong(in->children[j]);
			if (p >= 0)
			{
				out->children[j] = s_worldData.nodes + p;
			}
			else
			{
				out->children[j] = s_worldData.nodes + numNodes + (-1 - p);
			}
		}
	}

	// load leafs
	inLeaf = ( void * )(fileBase + leafLump->fileofs);
	for (i = 0 ; i < numLeafs ; i++, inLeaf++, out++)
	{
		for (j = 0 ; j < 3 ; j++)
		{
			out->mins[j] = LittleLong(inLeaf->mins[j]);
			out->maxs[j] = LittleLong(inLeaf->maxs[j]);
		}

		// surface bounds
		ClearBounds(out->surfMins, out->surfMaxs);

		out->cluster = LittleLong(inLeaf->cluster);
		out->area    = LittleLong(inLeaf->area);

		if (out->cluster >= s_worldData.numClusters)
		{
			s_worldData.numClusters = out->cluster + 1;
		}

		out->firstmarksurface = s_worldData.marksurfaces +
		                        LittleLong(inLeaf->firstLeafSurface);
		out->nummarksurfaces = LittleLong(inLeaf->numLeafSurfaces);
	}

	// chain decendants
	R_SetParent(s_worldData.nodes, NULL);
}

//=============================================================================

/**
 * @brief R_LoadShaders
 * @param[in] l
 */
static void R_LoadShaders(lump_t *l)
{
	int       i, count;
	dshader_t *in, *out;

	in = ( void * )(fileBase + l->fileofs);
	if (l->filelen % sizeof(*in))
	{
		Ren_Drop("LoadMap: funny lump size in %s", s_worldData.name);
	}
	count = l->filelen / sizeof(*in);
	out   = ri.Hunk_Alloc(count * sizeof(*out), h_low);

	s_worldData.shaders    = out;
	s_worldData.numShaders = count;

	Com_Memcpy(out, in, count * sizeof(*out));

	for (i = 0 ; i < count ; i++)
	{
		out[i].surfaceFlags = LittleLong(out[i].surfaceFlags);
		out[i].contentFlags = LittleLong(out[i].contentFlags);
	}
}

/**
 * @brief R_LoadMarksurfaces
 * @param[in] l
 */
static void R_LoadMarksurfaces(lump_t *l)
{
	int        i, j, count;
	int        *in;
	msurface_t **out;

	in = ( void * )(fileBase + l->fileofs);
	if (l->filelen % sizeof(*in))
	{
		Ren_Drop("LoadMap: funny lump size in %s", s_worldData.name);
	}
	count = l->filelen / sizeof(*in);
	out   = ri.Hunk_Alloc(count * sizeof(*out), h_low);

	s_worldData.marksurfaces    = out;
	s_worldData.nummarksurfaces = count;

	for (i = 0 ; i < count ; i++)
	{
		j      = LittleLong(in[i]);
		out[i] = s_worldData.surfaces + j;
	}
}

/**
 * @brief R_LoadPlanes
 * @param[in] l
 */
static void R_LoadPlanes(lump_t *l)
{
	int      i, j;
	cplane_t *out;
	dplane_t *in;
	int      count;
	byte     bits;

	in = ( void * )(fileBase + l->fileofs);
	if (l->filelen % sizeof(*in))
	{
		Ren_Drop("LoadMap: funny lump size in %s", s_worldData.name);
	}
	count = l->filelen / sizeof(*in);
	out   = ri.Hunk_Alloc(count * 2 * sizeof(*out), h_low);

	s_worldData.planes    = out;
	s_worldData.numplanes = count;

	for (i = 0 ; i < count ; i++, in++, out++)
	{
		bits = 0;
		for (j = 0 ; j < 3 ; j++)
		{
			out->normal[j] = LittleFloat(in->normal[j]);
			if (out->normal[j] < 0)
			{
				bits |= 1 << j;
			}
		}

		out->dist     = LittleFloat(in->dist);
		out->type     = PlaneTypeForNormal(out->normal);
		out->signbits = bits;
	}
}

/**
 * @brief R_LoadFogs
 * @param[in] l
 * @param[in] brushesLump
 * @param[in] sidesLump
 */
static void R_LoadFogs(lump_t *l, lump_t *brushesLump, lump_t *sidesLump)
{
	int          i, j;
	fog_t        *out;
	dfog_t       *fogs;
	dbrush_t     *brushes, *brush;
	dbrushside_t *sides;
	int          count, brushesCount, sidesCount;
	int          sideNum;
	int          planeNum;
	shader_t     *shader;
	int          firstSide;

	fogs = ( void * )(fileBase + l->fileofs);
	if (l->filelen % sizeof(*fogs))
	{
		Ren_Drop("LoadMap: funny lump size in %s", s_worldData.name);
	}
	count = l->filelen / sizeof(*fogs);

	// create fog strucutres for them
	s_worldData.numfogs = count + 1;
	s_worldData.fogs    = ri.Hunk_Alloc(s_worldData.numfogs * sizeof(*out), h_low);
	out                 = s_worldData.fogs + 1;

	// reset global fog
	s_worldData.globalFog = -1;

	if (!count)
	{
		return;
	}

	brushes = ( void * )(fileBase + brushesLump->fileofs);
	if (brushesLump->filelen % sizeof(*brushes))
	{
		Ren_Drop("LoadMap: funny lump size in %s", s_worldData.name);
	}
	brushesCount = brushesLump->filelen / sizeof(*brushes);

	sides = ( void * )(fileBase + sidesLump->fileofs);
	if (sidesLump->filelen % sizeof(*sides))
	{
		Ren_Drop("LoadMap: funny lump size in %s", s_worldData.name);
	}
	sidesCount = sidesLump->filelen / sizeof(*sides);

	for (i = 0 ; i < count ; i++, fogs++)
	{
		out->originalBrushNumber = LittleLong(fogs->brushNum);

		// global fog has a brush number of -1, and no visible side
		if (out->originalBrushNumber == -1)
		{
			VectorSet(out->bounds[0], MIN_WORLD_COORD, MIN_WORLD_COORD, MIN_WORLD_COORD);
			VectorSet(out->bounds[1], MAX_WORLD_COORD, MAX_WORLD_COORD, MAX_WORLD_COORD);
			firstSide = 0;
		}
		else
		{
			if ((unsigned)out->originalBrushNumber >= brushesCount)
			{
				Ren_Drop("fog brushNumber out of range");
			}

			// find which bsp submodel the fog volume belongs to
			for (j = 0; j < s_worldData.numBModels; j++)
			{
				if (out->originalBrushNumber >= s_worldData.bmodels[j].firstBrush &&
				    out->originalBrushNumber < (s_worldData.bmodels[j].firstBrush + s_worldData.bmodels[j].numBrushes))
				{
					out->modelNum = j;
					break;
				}
			}

			brush = brushes + out->originalBrushNumber;

			firstSide = LittleLong(brush->firstSide);

			if ((unsigned)firstSide > sidesCount - 6)
			{
				Ren_Drop("fog brush sideNumber out of range");
			}

			// brushes are always sorted with the axial sides first
			sideNum           = firstSide + 0;
			planeNum          = LittleLong(sides[sideNum].planeNum);
			out->bounds[0][0] = -s_worldData.planes[planeNum].dist;

			sideNum           = firstSide + 1;
			planeNum          = LittleLong(sides[sideNum].planeNum);
			out->bounds[1][0] = s_worldData.planes[planeNum].dist;

			sideNum           = firstSide + 2;
			planeNum          = LittleLong(sides[sideNum].planeNum);
			out->bounds[0][1] = -s_worldData.planes[planeNum].dist;

			sideNum           = firstSide + 3;
			planeNum          = LittleLong(sides[sideNum].planeNum);
			out->bounds[1][1] = s_worldData.planes[planeNum].dist;

			sideNum           = firstSide + 4;
			planeNum          = LittleLong(sides[sideNum].planeNum);
			out->bounds[0][2] = -s_worldData.planes[planeNum].dist;

			sideNum           = firstSide + 5;
			planeNum          = LittleLong(sides[sideNum].planeNum);
			out->bounds[1][2] = s_worldData.planes[planeNum].dist;
		}

		// get information from the shader for fog parameters
		shader = R_FindShader(fogs->shader, LIGHTMAP_NONE, qtrue);

		// colorInt is now set in the shader so we can modify it
		out->shader = shader;

		// global fog sets clearcolor/zfar
		if (out->originalBrushNumber == -1)
		{
			s_worldData.globalFog = i + 1;
			VectorCopy(shader->fogParms.color, s_worldData.globalOriginalFog);
			s_worldData.globalOriginalFog[3] = shader->fogParms.depthForOpaque;
		}

		// set the gradient vector
		sideNum = LittleLong(fogs->visibleSide);

		// made this check a little more strenuous (was sideNum == -1)
		if (sideNum < 0 || sideNum >= sidesCount)
		{
			out->hasSurface = qfalse;
		}
		else
		{
			out->hasSurface = qtrue;
			planeNum        = LittleLong(sides[firstSide + sideNum].planeNum);
			VectorSubtract(vec3_origin, s_worldData.planes[planeNum].normal, out->surface);
			out->surface[3] = -s_worldData.planes[planeNum].dist;
		}

		out++;
	}
}

/**
 * @brief R_LoadLightGrid
 * @param[in] l
 */
void R_LoadLightGrid(lump_t *l)
{
	int     i;
	vec3_t  maxs;
	int     numGridPoints;
	world_t *w = &s_worldData;
	float   *wMins, *wMaxs;

	w->lightGridInverseSize[0] = 1.0f / w->lightGridSize[0];
	w->lightGridInverseSize[1] = 1.0f / w->lightGridSize[1];
	w->lightGridInverseSize[2] = 1.0f / w->lightGridSize[2];

	wMins = w->bmodels[0].bounds[0];
	wMaxs = w->bmodels[0].bounds[1];

	for (i = 0 ; i < 3 ; i++)
	{
		w->lightGridOrigin[i] = w->lightGridSize[i] * ceil(wMins[i] / w->lightGridSize[i]);
		maxs[i]               = w->lightGridSize[i] * floor(wMaxs[i] / w->lightGridSize[i]);
		w->lightGridBounds[i] = (maxs[i] - w->lightGridOrigin[i]) / w->lightGridSize[i] + 1;
	}

	numGridPoints = w->lightGridBounds[0] * w->lightGridBounds[1] * w->lightGridBounds[2];

	if (l->filelen != numGridPoints * 8)
	{
		Ren_Warning("WARNING: light grid mismatch\n");
		w->lightGridData = NULL;
		return;
	}

	w->lightGridData = ri.Hunk_Alloc(l->filelen, h_low);
	Com_Memcpy(w->lightGridData, ( void * )(fileBase + l->fileofs), l->filelen);

	// deal with overbright bits
	for (i = 0 ; i < numGridPoints ; i++)
	{
		R_ColorShiftLightingBytes(&w->lightGridData[i * 8], &w->lightGridData[i * 8]);
		R_ColorShiftLightingBytes(&w->lightGridData[i * 8 + 3], &w->lightGridData[i * 8 + 3]);
	}
}

/**
 * @brief R_LoadEntities
 * @param[in] l
 */
void R_LoadEntities(lump_t *l)
{
	char    *p, *token, *s;
	char    keyname[MAX_TOKEN_CHARS];
	char    value[MAX_TOKEN_CHARS];
	world_t *w = &s_worldData;

	w->lightGridSize[0] = 64;
	w->lightGridSize[1] = 64;
	w->lightGridSize[2] = 128;

	p = ( char * )(fileBase + l->fileofs);

	// store for reference by the cgame
	w->entityString = ri.Hunk_Alloc(l->filelen + 1, h_low);
	strcpy(w->entityString, p);
	w->entityParsePoint = w->entityString;

	token = COM_ParseExt(&p, qtrue);
	if (*token != '{')
	{
		return;
	}

	// only parse the world spawn
	while (1)
	{
		// parse key
		token = COM_ParseExt(&p, qtrue);

		if (!*token || *token == '}')
		{
			break;
		}
		Q_strncpyz(keyname, token, sizeof(keyname));

		// parse value
		token = COM_ParseExt(&p, qtrue);

		if (!*token || *token == '}')
		{
			break;
		}
		Q_strncpyz(value, token, sizeof(value));

		// check for remapping of shaders
		s = "remapshader";
		if (!Q_strncmp(keyname, s, strlen(s)))
		{
			s = strchr(value, ';');
			if (!s)
			{
				Ren_Warning("WARNING: no semi colon in shaderremap '%s'\n", value);
				break;
			}
			*s++ = 0;
			R_RemapShader(value, s, "0");
			continue;
		}
		// check for a different grid size
		if (!Q_stricmp(keyname, "gridsize"))
		{
			sscanf(value, "%f %f %f", &w->lightGridSize[0], &w->lightGridSize[1], &w->lightGridSize[2]);
			continue;
		}
	}
}

/**
 * @brief R_GetEntityToken
 * @param[out] buffer
 * @param[in] size
 * @return
 */
qboolean R_GetEntityToken(char *buffer, size_t size)
{
	const char *s;

	s = COM_Parse(&s_worldData.entityParsePoint);
	Q_strncpyz(buffer, s, size);
	if (!s_worldData.entityParsePoint || !s[0])
	{
		s_worldData.entityParsePoint = s_worldData.entityString;
		return qfalse;
	}
	else
	{
		return qtrue;
	}
}


/**
 * @brief RE_StripExtensionForWorldDir
 * @param[in] in
 * @param[out] out
 */
void RE_StripExtensionForWorldDir(const char *in, char *out)
{
	while (*in && *in != '.')
	{
		*out++ = *in++;
	}
	*out = 0;
}

/**
 * @brief RE_LoadWorldMap
 * @param[in] name
 */
void RE_LoadWorldMap(const char *name)
{
	int       i;
	dheader_t *header;
	byte      *buffer;
	byte      *startMarker;
	size_t    nameLength;

	skyboxportal = 0;

	if (tr.worldMapLoaded)
	{
		Ren_Drop("ERROR: attempted to redundantly load world map\n");
	}

	// set default sun direction to be used if it isn't
	// overridden by a shader
	tr.sunDirection[0] = 0.45f;
	tr.sunDirection[1] = 0.3f;
	tr.sunDirection[2] = 0.9f;

	tr.sunShader = 0;   // clear sunshader so it's not there if the level doesn't specify it

	// inalidate fogs (likely to be re-initialized to new values by the current map)
	// TODO:(SA)this is sort of silly.  I'm going to do a general cleanup on fog stuff
	//          now that I can see how it's been used.  (functionality can narrow since
	//          it's not used as much as it's designed for.)
	R_SetFog(FOG_SKY, 0, 0, 0, 0, 0, 0);
	R_SetFog(FOG_PORTALVIEW, 0, 0, 0, 0, 0, 0);
	R_SetFog(FOG_HUD, 0, 0, 0, 0, 0, 0);
	R_SetFog(FOG_MAP, 0, 0, 0, 0, 0, 0);
	R_SetFog(FOG_CURRENT, 0, 0, 0, 0, 0, 0);
	R_SetFog(FOG_TARGET, 0, 0, 0, 0, 0, 0);
	R_SetFog(FOG_WATER, 0, 0, 0, 0, 0, 0);
	R_SetFog(FOG_SERVER, 0, 0, 0, 0, 0, 0);

	VectorNormalize(tr.sunDirection);

	tr.worldMapLoaded = qtrue;
	tr.worldDir       = NULL;

	// load it
	ri.FS_ReadFile(name, (void **)&buffer);
	if (!buffer)
	{
		Ren_Drop("RE_LoadWorldMap: %s not found", name);
	}

	// set map meta dir for lightmaps
	// THIS MUST BE IN SYNC WITH Q3MAP2
	nameLength  = strlen(name) + 1;
	tr.worldDir = ri.Hunk_Alloc(sizeof(char) * nameLength, h_low);
	Q_strncpyz(tr.worldDir, name, sizeof(char) * nameLength);
	RE_StripExtensionForWorldDir(tr.worldDir, tr.worldDir);

	// clear tr.world so if the level fails to load, the next
	// try will not look at the partially loaded version
	tr.world = NULL;

	Com_Memset(&s_worldData, 0, sizeof(s_worldData));
	Q_strncpyz(s_worldData.name, name, sizeof(s_worldData.name));

	Q_strncpyz(s_worldData.baseName, COM_SkipPath(s_worldData.name), sizeof(s_worldData.name));
	COM_StripExtension(s_worldData.baseName, s_worldData.baseName, sizeof(s_worldData.baseName));

	startMarker = ri.Hunk_Alloc(0, h_low);

	header   = (dheader_t *)buffer;
	fileBase = (byte *)header;

	i = LittleLong(header->version);
	if (i != BSP_VERSION)
	{
		Ren_Drop("RE_LoadWorldMap: %s has wrong version number (%i should be %i)",
		         name, i, BSP_VERSION);
	}

	// swap all the lumps
	for (i = 0 ; i < sizeof(dheader_t) / 4 ; i++)
	{
		((int *)header)[i] = LittleLong(((int *)header)[i]);
	}

	// load into heap
	Ren_UpdateScreen();
	R_LoadShaders(&header->lumps[LUMP_SHADERS]);
	Ren_UpdateScreen();
	R_LoadLightmaps(&header->lumps[LUMP_LIGHTMAPS]);
	Ren_UpdateScreen();
	R_LoadPlanes(&header->lumps[LUMP_PLANES]);
	Ren_UpdateScreen();
	//% R_LoadFogs( &header->lumps[LUMP_FOGS], &header->lumps[LUMP_BRUSHES], &header->lumps[LUMP_BRUSHSIDES] );
	//% Ren_UpdateScreen();
	R_LoadSurfaces(&header->lumps[LUMP_SURFACES], &header->lumps[LUMP_DRAWVERTS], &header->lumps[LUMP_DRAWINDEXES]);
	Ren_UpdateScreen();
	R_LoadMarksurfaces(&header->lumps[LUMP_LEAFSURFACES]);
	Ren_UpdateScreen();
	R_LoadNodesAndLeafs(&header->lumps[LUMP_NODES], &header->lumps[LUMP_LEAFS]);
	Ren_UpdateScreen();
	R_LoadSubmodels(&header->lumps[LUMP_MODELS]);
	Ren_UpdateScreen();

	// moved fog lump loading here, so fogs can be tagged with a model num
	R_LoadFogs(&header->lumps[LUMP_FOGS], &header->lumps[LUMP_BRUSHES], &header->lumps[LUMP_BRUSHSIDES]);
	Ren_UpdateScreen();

	R_LoadVisibility(&header->lumps[LUMP_VISIBILITY]);
	Ren_UpdateScreen();
	R_LoadEntities(&header->lumps[LUMP_ENTITIES]);
	Ren_UpdateScreen();
	R_LoadLightGrid(&header->lumps[LUMP_LIGHTGRID]);
	Ren_UpdateScreen();

	s_worldData.dataSize = (byte *)ri.Hunk_Alloc(0, h_low) - startMarker;

	// only set tr.world now that we know the entire level has loaded properly
	tr.world = &s_worldData;

	// reset fog to world fog (if present)
	R_SetFog(FOG_CMD_SWITCHFOG, FOG_MAP, 20, 0, 0, 0, 0);

	// set the sun shader if there is one
	if (tr.sunShaderName)
	{
		tr.sunShader = R_FindShader(tr.sunShaderName, LIGHTMAP_NONE, qtrue);
	}

	ri.FS_FreeFile(buffer);
}
