/*
	This file is part of Warzone 2100.
	Copyright (C) 1999-2004  Eidos Interactive
	Copyright (C) 2005-2012  Warzone 2100 Project

	Warzone 2100 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 2 of the License, or
	(at your option) any later version.

	Warzone 2100 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 Warzone 2100; if not, write to the Free Software
	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
 * @file map.c
 *
 * Utility functions for the map data structure.
 *
 */
#include <time.h>

#include "lib/framework/frame.h"
#include "lib/framework/endian_hack.h"
#include "lib/framework/file.h"
#include "lib/framework/physfs_ext.h"
#include "lib/ivis_opengl/tex.h"
#include "lib/netplay/netplay.h"  // For syncDebug

#include "map.h"
#include "hci.h"
#include "projectile.h"
#include "display3d.h"
#include "game.h"
#include "texture.h"
#include "advvis.h"
#include "random.h"
#include "research.h"
#include "mission.h"
#include "gateway.h"
#include "wrappers.h"
#include "mapgrid.h"
#include "astar.h"
#include "fpath.h"
#include "levels.h"
#include "scriptfuncs.h"
#include "lib/framework/wzapp.h"

#define GAME_TICKS_FOR_DANGER (GAME_TICKS_PER_SEC * 2)

static WZ_THREAD *dangerThread = NULL;
static WZ_SEMAPHORE *dangerSemaphore = NULL;
static WZ_SEMAPHORE *dangerDoneSemaphore = NULL;
struct floodtile { uint8_t x; uint8_t y; };
static struct floodtile *floodbucket = NULL;
static int bucketcounter;
static UDWORD lastDangerUpdate = 0;
static int lastDangerPlayer = -1;

//scroll min and max values
SDWORD		scrollMinX, scrollMaxX, scrollMinY, scrollMaxY;

/* Structure definitions for loading and saving map data */
struct MAP_SAVEHEADER  // : public GAME_SAVEHEADER
{
	char		aFileType[4];
	UDWORD		version;
	UDWORD		width;
	UDWORD		height;
};

struct MAP_SAVETILE
{
	UWORD		texture;
	UBYTE		height;
};

struct GATEWAY_SAVEHEADER
{
	UDWORD		version;
	UDWORD		numGateways;
};

struct GATEWAY_SAVE
{
	UBYTE	x0,y0,x1,y1;
};

/* Sanity check definitions for the save struct file sizes */
#define SAVE_HEADER_SIZE	16
#define SAVE_TILE_SIZE		3

// Maximum expected return value from get height
#define	MAX_HEIGHT			(256 * ELEVATION_SCALE)

/* The size and contents of the map */
SDWORD	mapWidth = 0, mapHeight = 0;
MAPTILE	*psMapTiles = NULL;
uint8_t *psBlockMap[AUX_MAX];
uint8_t *psAuxMap[MAX_PLAYERS + AUX_MAX];        // yes, we waste one element... eyes wide open... makes API nicer

#define WATER_MIN_DEPTH 180
#define WATER_MAX_DEPTH (WATER_MIN_DEPTH + 100)

static void SetGroundForTile(const char *filename, const char *nametype);
static int getTextureType(const char *textureType);
static bool hasDecals(int i, int j);
static void SetDecals(const char *filename, const char *decal_type);
static void init_tileNames(int type);

/// The different ground types
GROUND_TYPE *psGroundTypes;
int numGroundTypes;
char *tileset = NULL;
static int numTile_names;
static char *Tile_names = NULL;
#define ARIZONA 1
#define URBAN 2
#define ROCKIE 3

static int *map;			// 3D array pointer that holds the texturetype
static bool *mapDecals;           // array that tells us what tile is a decal
#define MAX_TERRAIN_TILES 0x0200  // max that we support (for now), see TILE_NUMMASK

/* Look up table that returns the terrain type of a given tile texture */
UBYTE terrainTypes[MAX_TILE_TEXTURES];

/* Create a new map of a specified size */
bool mapNew(UDWORD width, UDWORD height)
{
	MAPTILE *psTile;
	UDWORD	i;

	/* See if a map has already been allocated */
	if (psMapTiles != NULL)
	{
		/* Clear all the objects off the map and free up the map memory */
		gwShutDown();
		releaseAllProxDisp();
		freeAllDroids();
		freeAllStructs();
		freeAllFeatures();
		freeAllFlagPositions();
		proj_FreeAllProjectiles();
		free(psMapTiles);
		psMapTiles = NULL;
		initStructLimits();
		
		free(psGroundTypes);
	}

	if (width*height > MAP_MAXAREA)
	{
		debug(LOG_ERROR, "Map too large : %u %u", width, height);
		return false;
	}

	if (width <=1 || height <=1)
	{
		debug(LOG_ERROR, "Map is too small : %u, %u", width, height);
		return false;
	}

	psMapTiles = (MAPTILE *)calloc(width * height, sizeof(MAPTILE));
	if (psMapTiles == NULL)
	{
		debug(LOG_FATAL, "Out of memory");
		abort();
		return false;
	}

	psTile = psMapTiles;
	for (i = 0; i < width * height; i++)
	{
		psTile->height = MAX_HEIGHT*ELEVATION_SCALE / 4;
		psTile->illumination = 255;
		psTile->level = psTile->illumination;
		memset(psTile->watchers, 0, sizeof(psTile->watchers));
		memset(psTile->sensors, 0, sizeof(psTile->sensors));
		memset(psTile->jammers, 0, sizeof(psTile->jammers));
		psTile->colour= WZCOL_WHITE;
		psTile->tileExploredBits = 0;
		psTile->sensorBits = 0;
		psTile->jammerBits = 0;
		psTile++;
	}

	mapWidth = width;
	mapHeight = height;

	intSetMapPos(mapWidth * TILE_UNITS/2, mapHeight * TILE_UNITS/2);

	/*set up the scroll mins and maxs - set values to valid ones for a new map*/
	scrollMinX = scrollMinY = 0;
	scrollMaxX = mapWidth;
	scrollMaxY = mapHeight;

	gridReset();

	return true;
}

static void init_tileNames(int type)
{
	char	*pFileData = NULL;
	char	name[MAX_STR_LENGTH] = {'\0'};
	int		numlines = 0, i = 0, cnt = 0;
	uint32_t	fileSize = 0;

	pFileData = fileLoadBuffer;

	switch (type)
	{
		case ARIZONA:
		{
			if (!loadFileToBuffer("tileset/arizona_enum.txt", pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
			{
				debug(LOG_FATAL, "tileset/arizona_enum.txt not found.  Aborting.");
				abort();
			}

			sscanf(pFileData, "%255[^,'\r\n],%d%n", name, &numlines, &cnt);
			pFileData += cnt;

			if (strcmp("arizona_enum", name))
			{
				debug(LOG_FATAL, "%s found, but was expecting arizona_enum, aborting.", name);
				abort();
			}
			break;
		}
		case URBAN:
		{
			if (!loadFileToBuffer("tileset/urban_enum.txt", pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
			{
				debug(LOG_FATAL, "tileset/urban_enum.txt not found.  Aborting.");
				abort();
			}

			sscanf(pFileData, "%255[^,'\r\n],%d%n", name, &numlines, &cnt);
			pFileData += cnt;

			if (strcmp("urban_enum", name))
			{
				debug(LOG_FATAL, "%s found, but was expecting urban_enum, aborting.", name);
				abort();
			}
			break;
		}
		case ROCKIE:
		{
			if (!loadFileToBuffer("tileset/rockie_enum.txt", pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
			{
				debug(LOG_FATAL, "tileset/rockie_enum.txt not found.  Aborting.");
				abort();
			}

			sscanf(pFileData, "%255[^,'\r\n],%d%n", name, &numlines, &cnt);
			pFileData += cnt;

			if (strcmp("rockie_enum", name))
			{
				debug(LOG_FATAL, "%s found, but was expecting rockie_enum, aborting.", name);
				abort();
			}
			break;
		}
		default:
		debug(LOG_FATAL, "Unknown type (%d) given.  Aborting.", type);
		abort();
	}

	debug(LOG_TERRAIN, "name: %s, with %d entries", name, numlines);
	if (numlines == 0 || numlines > MAX_TERRAIN_TILES)
	{
		debug(LOG_FATAL, "Rockie_enum parameter is out of range (%d). Aborting.", numlines);
		abort();
	}

	numTile_names = numlines;
	//increment the pointer to the start of the next record
	pFileData = strchr(pFileData,'\n') + 1;

	Tile_names = (char *)malloc(numlines * sizeof(char[MAX_STR_LENGTH]) );
	memset(Tile_names, 0x0, (numlines * sizeof(char[MAX_STR_LENGTH])));

	for (i=0; i < numlines; i++)
	{
		sscanf(pFileData, "%255[^,'\r\n]%n", &Tile_names[i*MAX_STR_LENGTH], &cnt);
		pFileData += cnt;
		//increment the pointer to the start of the next record
		pFileData = strchr(pFileData,'\n') + 1;
	}
}

// This is the main loading routine to get all the map's parameters set.
// Once it figures out what tileset we need, we then parse the files for that tileset.
// Currently, we only support 3 tilesets.  Arizona, Urban, and Rockie
static bool mapLoadGroundTypes(void)
{
	char	*pFileData = NULL;
	char	tilename[MAX_STR_LENGTH] = {'\0'};
	char	textureName[MAX_STR_LENGTH] = {'\0'};
	char	textureType[MAX_STR_LENGTH] = {'\0'};
	double	textureSize = 0.f;
	int		numlines = 0;
	int		cnt = 0, i = 0;
	uint32_t	fileSize = 0;

	pFileData = fileLoadBuffer;

	debug(LOG_TERRAIN, "tileset: %s", tileset);
	// For Arizona
	if (strcmp(tileset, "texpages/tertilesc1hw") == 0)
	{
fallback:
		init_tileNames(ARIZONA);
		if (!loadFileToBuffer("tileset/tertilesc1hwGtype.txt", pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
		{
			debug(LOG_FATAL, "tileset/tertilesc1hwGtype.txt not found, aborting.");
			abort();
		}

		sscanf(pFileData, "%255[^,'\r\n],%d%n", tilename, &numlines, &cnt);
		pFileData += cnt;

		if (strcmp(tilename, "tertilesc1hw"))
		{
			debug(LOG_FATAL, "%s found, but was expecting tertilesc1hw!  Aborting.", tilename);
			abort();
		}

		debug(LOG_TERRAIN, "tilename: %s, with %d entries", tilename, numlines);
		//increment the pointer to the start of the next record
		pFileData = strchr(pFileData,'\n') + 1;
		numGroundTypes = numlines;
		psGroundTypes = (GROUND_TYPE *)malloc(sizeof(GROUND_TYPE)*numlines);

		for (i=0; i < numlines; i++)
		{
			sscanf(pFileData, "%255[^,'\r\n],%255[^,'\r\n],%lf%n", textureType, textureName, &textureSize, &cnt);
			pFileData += cnt;
			//increment the pointer to the start of the next record
			pFileData = strchr(pFileData,'\n') + 1;

			psGroundTypes[getTextureType(textureType)].textureName = strdup(textureName);
			psGroundTypes[getTextureType(textureType)].textureSize = textureSize ;
		}

		SetGroundForTile("tileset/arizonaground.txt", "arizona_ground");
		SetDecals("tileset/arizonadecals.txt", "arizona_decals");
	}
	// for Urban
	else if (strcmp(tileset, "texpages/tertilesc2hw") == 0)
	{
		init_tileNames(URBAN);
		if (!loadFileToBuffer("tileset/tertilesc2hwGtype.txt", pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
		{
			debug(LOG_POPUP, "tileset/tertilesc2hwGtype.txt not found, using default terrain ground types.");
			goto fallback;
		}

		sscanf(pFileData, "%255[^,'\r\n],%d%n", tilename, &numlines, &cnt);
		pFileData += cnt;

		if (strcmp(tilename, "tertilesc2hw"))
		{
			debug(LOG_POPUP, "%s found, but was expecting tertilesc2hw!", tilename);
			goto fallback;
		}

		debug(LOG_TERRAIN, "tilename: %s, with %d entries", tilename, numlines);
		//increment the pointer to the start of the next record
		pFileData = strchr(pFileData,'\n') + 1;
		numGroundTypes = numlines;
		psGroundTypes = (GROUND_TYPE *)malloc(sizeof(GROUND_TYPE)*numlines);

		for (i=0; i < numlines; i++)
		{
			sscanf(pFileData, "%255[^,'\r\n],%255[^,'\r\n],%lf%n", textureType, textureName, &textureSize, &cnt);
			pFileData += cnt;
			//increment the pointer to the start of the next record
			pFileData = strchr(pFileData,'\n') + 1;

			psGroundTypes[getTextureType(textureType)].textureName = strdup(textureName);
			psGroundTypes[getTextureType(textureType)].textureSize = textureSize;
		}

		SetGroundForTile("tileset/urbanground.txt", "urban_ground");
		SetDecals("tileset/urbandecals.txt", "urban_decals");
	}
	// for Rockie
	else if (strcmp(tileset, "texpages/tertilesc3hw") == 0)
	{
		init_tileNames(ROCKIE);
		if (!loadFileToBuffer("tileset/tertilesc3hwGtype.txt", pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
		{
			debug(LOG_POPUP, "tileset/tertilesc3hwGtype.txt not found, using default terrain ground types.");
			goto fallback;
		}

		sscanf(pFileData, "%255[^,'\r\n],%d%n", tilename, &numlines, &cnt);
		pFileData += cnt;

		if (strcmp(tilename, "tertilesc3hw"))
		{
			debug(LOG_POPUP, "%s found, but was expecting tertilesc3hw!", tilename);
			goto fallback;
		}

		debug(LOG_TERRAIN, "tilename: %s, with %d entries", tilename, numlines);
		//increment the pointer to the start of the next record
		pFileData = strchr(pFileData,'\n') + 1;
		numGroundTypes = numlines;
		psGroundTypes = (GROUND_TYPE *)malloc(sizeof(GROUND_TYPE)*numlines);

		for (i=0; i < numlines; i++)
		{
			sscanf(pFileData, "%255[^,'\r\n],%255[^,'\r\n],%lf%n", textureType, textureName, &textureSize, &cnt);
			pFileData += cnt;
			//increment the pointer to the start of the next record
			pFileData = strchr(pFileData,'\n') + 1;

			psGroundTypes[getTextureType(textureType)].textureName = strdup(textureName);
			psGroundTypes[getTextureType(textureType)].textureSize = textureSize;
		}

		SetGroundForTile("tileset/rockieground.txt", "rockie_ground");
		SetDecals("tileset/rockiedecals.txt", "rockie_decals");
	}
	// When a map uses something other than the above, we fallback to Arizona
	else
	{
		debug(LOG_ERROR, "unsupported tileset: %s", tileset);
		debug(LOG_POPUP, "This is a UNSUPPORTED map with a custom tileset.\nDefaulting to tertilesc1hw -- map may look strange!");
		// HACK: / FIXME: For now, we just pretend this is a tertilesc1hw map.
		goto fallback;
	}
	return true;
}

// Parse the file to set up the ground type
static void SetGroundForTile(const char *filename, const char *nametype)
{
	char	*pFileData = NULL;
	char	tilename[MAX_STR_LENGTH] = {'\0'};
	char	val1[MAX_STR_LENGTH], val2[MAX_STR_LENGTH], val3[MAX_STR_LENGTH], val4[MAX_STR_LENGTH];
	int		numlines = 0;
	int		cnt = 0, i = 0;
	uint32_t	fileSize = 0;

	pFileData = fileLoadBuffer;
	if (!loadFileToBuffer(filename, pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
	{
		debug(LOG_FATAL, "%s not found, aborting.", filename);
		abort();
	}

	sscanf(pFileData, "%255[^,'\r\n],%d%n", tilename, &numlines, &cnt);
	pFileData += cnt;

	if (strcmp(tilename, nametype))
	{
		debug(LOG_FATAL, "%s found, but was expecting %s, aborting.", tilename, nametype);
		abort();
	}

	debug(LOG_TERRAIN, "tilename: %s, with %d entries", tilename, numlines);
	//increment the pointer to the start of the next record
	pFileData = strchr(pFileData,'\n') + 1;

	map = (int *)malloc(sizeof(int) * numlines * 2 * 2 );	// this is a 3D array map[numlines][2][2]

	for (i=0; i < numlines; i++)
	{
		sscanf(pFileData, "%255[^,'\r\n],%255[^,'\r\n],%255[^,'\r\n],%255[^,'\r\n]%n", val1, val2, val3, val4, &cnt);
		pFileData += cnt;
		//increment the pointer to the start of the next record
		pFileData = strchr(pFileData,'\n') + 1;

		// inline int iA(int i, int j, int k){ return i*N2*N3 + j*N3 + k; }
		// in case it isn't obvious, this is a 3D array, and using pointer math to access each element.
		// so map[10][0][1] would be map[10*2*2 + 0 + 1] == map[41]
		// map[10][1][0] == map[10*2*2 + 2 + 0] == map[42]
		map[i*2*2+0*2+0] = getTextureType(val1);
		map[i*2*2+0*2+1] = getTextureType(val2);
		map[i*2*2+1*2+0] = getTextureType(val3);
		map[i*2*2+1*2+1] = getTextureType(val4);
	}
}

// getTextureType() -- just returns the value for that texture type.
static int getTextureType(const char *textureType)
{
	int i = 0;
	for (i=0; i < numTile_names; i++)
	{
		if (!strcmp(textureType, &Tile_names[i*MAX_STR_LENGTH]))
		{
			return i;
		}
	}
	debug(LOG_FATAL, "unknown type [%s] found, aborting!", textureType);
	abort();
}

// groundFromMapTile() just a simple lookup table, using pointers to access the 3D map array
//	(quasi) pointer math is: map[num elements][2][2]
//	so map[10][0][1] would be map[10*2*2 + 0*2 + 1] == map[41]
static int groundFromMapTile(int tile, int j, int k)
{
	return map[TileNumber_tile(tile)* 2 * 2 + j * 2 + k];
}

static void rotFlip(int tile, int *i, int *j)
{
	int texture = TileNumber_texture(tile);
	int rot;
	int map[2][2], invmap[4][2];

	if (texture & TILE_XFLIP)
	{
		*i = 1 - *i;
	}
	if (texture & TILE_YFLIP)
	{
		*j = 1 - *j;
	}

	map[0][0] = 0; invmap[0][0] = 0; invmap[0][1] = 0;
	map[1][0] = 1; invmap[1][0] = 1; invmap[1][1] = 0;
	map[1][1] = 2; invmap[2][0] = 1; invmap[2][1] = 1;
	map[0][1] = 3; invmap[3][0] = 0; invmap[3][1] = 1;
	rot = map[*i][*j];
	rot -= (texture & TILE_ROTMASK) >> TILE_ROTSHIFT;
	while(rot < 0) rot += 4;
	*i = invmap[rot][0];
	*j = invmap[rot][1];
}

/// Tries to figure out what ground type a grid point is from the surrounding tiles
static int determineGroundType(int x, int y, const char *tileset)
{
	int ground[2][2];
	int votes[2][2];
	int weight[2][2];
	int i,j, tile;
	int a,b, best;
	MAPTILE *psTile = mapTile(x,y);

	if (x < 0 || y < 0 || x >= mapWidth || y >= mapHeight)
	{
		return 0; // just return the first ground type
	}

	// check what tiles surround this grid point
	for(i=0;i<2;i++)
	{
		for(j=0;j<2;j++)
		{
			if (x+i-1 < 0 || y+j-1 < 0 || x+i-1 >= mapWidth || y+j-1 >= mapHeight)
			{
				psTile = 0;
				tile = 0;
			}
			else
			{
				psTile = mapTile(x+i-1, y+j-1);
				tile = psTile->texture;
			}
			a = i;
			b = j;
			rotFlip(tile, &a, &b);
			ground[i][j] = groundFromMapTile(tile, a, b);

			votes[i][j] = 0;
			// votes are weighted, some tiles have more weight than others
			weight[i][j] = 10;

			if (psTile)
			{
				// cliff tiles have higher priority, to be clearly visible
				if (terrainType(psTile) == TER_CLIFFFACE) 
					weight[i][j] = 100;
				// water bottom has lower priority, to stay inside water
				if (terrainType(psTile) == TER_WATER) 
					weight[i][j] = 1;
			}
		}
	}

	// now vote, because some maps have seams
	for(i=0;i<2;i++)
	{
		for(j=0;j<2;j++)
		{
			for(a=0;a<2;a++)
			{
				for(b=0;b<2;b++)
				{
					if (ground[i][j] == ground[a][b])
					{
						votes[i][j] += weight[a][b];
					}
				}
			}
		}
	}
	// and determine the winner
	best = -1; 
	a = 0;
	b = 0;
	for(i=0;i<2;i++)
	{
		for(j=0;j<2;j++)
		{
			if (votes[i][j] > best || (votes[i][j] == best && ground[i][j]<ground[a][b]))
			{
				best = votes[i][j];
				a = i;
				b = j;
			}
		}
	}
	return ground[a][b];
}


// SetDecals()
// reads in the decal array for the requested tileset.
static void SetDecals(const char *filename, const char *decal_type)
{
	char decalname[MAX_STR_LENGTH], *pFileData;
	int numlines, cnt, i, tiledecal;
	uint32_t fileSize;

	pFileData = fileLoadBuffer;

	if (!loadFileToBuffer(filename, pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
	{
		debug(LOG_POPUP, "%s not found, aborting.", filename);
		abort();
	}

	sscanf(pFileData, "%255[^,'\r\n],%d%n", decalname, &numlines, &cnt);
	pFileData += cnt;

	if (strcmp(decalname, decal_type))
	{
		debug(LOG_POPUP, "%s found, but was expecting %s, aborting.", decalname, decal_type);
		abort();
	}

	debug(LOG_TERRAIN, "reading: %s, with %d entries", filename, numlines);
	//increment the pointer to the start of the next record
	pFileData = strchr(pFileData,'\n') + 1;
	mapDecals = new bool[MAX_TERRAIN_TILES];
	std::fill_n(mapDecals, MAX_TERRAIN_TILES, false);  // set everything to false.

	for (i=0; i < numlines; i++)
	{
		tiledecal = -1;
		sscanf(pFileData, "%d%n", &tiledecal, &cnt);
		pFileData += cnt;
		//increment the pointer to the start of the next record
		pFileData = strchr(pFileData,'\n') + 1;
		if ((unsigned)tiledecal > MAX_TERRAIN_TILES)
		{
			debug(LOG_ERROR, "Tile index is out of range!  Was %d, our max is %d", tiledecal, MAX_TERRAIN_TILES);
			continue;
		}
		mapDecals[tiledecal] = true;
	}
}
// hasDecals()
// Checks to see if the requested tile has a decal on it or not.
static bool hasDecals(int i, int j)
{
	int index = 0;
	index = TileNumber_tile(mapTile(i, j)->texture);
	if (index > MAX_TERRAIN_TILES)
	{
		debug(LOG_FATAL, "Tile index is out of range!  Was %d, our max is %d", index, MAX_TERRAIN_TILES);
		abort();
	}
	return mapDecals[index];
}
// mapSetGroundTypes()
// Sets the ground type to be a decal or not
static bool mapSetGroundTypes(void)
{
	int i,j;

	for (i=0;i<mapWidth;i++)
	{
		for (j=0;j<mapHeight;j++)
		{
			MAPTILE *psTile = mapTile(i, j);

			psTile->ground = determineGroundType(i,j,tileset);

			if (hasDecals(i,j))
			{
				SET_TILE_DECAL(psTile);
			}
			else
			{
				CLEAR_TILE_DECAL(psTile);
			}
		}
	}
	return true;
}

/* Initialise the map structure */
bool mapLoad(char *filename, bool preview)
{
	UDWORD		numGw, width, height;
	char		aFileType[4];
	UDWORD		version;
	UDWORD		i, j, x, y;
	PHYSFS_file	*fp = PHYSFS_openRead(filename);
	MersenneTwister mt(12345);  // 12345 = random seed.

	if (!fp)
	{
		debug(LOG_ERROR, "%s not found", filename);
		return false;
	}
	else if (PHYSFS_read(fp, aFileType, 4, 1) != 1
	    || !PHYSFS_readULE32(fp, &version)
	    || !PHYSFS_readULE32(fp, &width)
	    || !PHYSFS_readULE32(fp, &height)
	    || aFileType[0] != 'm'
	    || aFileType[1] != 'a'
	    || aFileType[2] != 'p')
	{
		debug(LOG_ERROR, "Bad header in %s", filename);
		goto failure;
	}
	else if (version <= VERSION_9)
	{
		debug(LOG_ERROR, "%s: Unsupported save format version %u", filename, version);
		goto failure;
	}
	else if (version > CURRENT_VERSION_NUM)
	{
		debug(LOG_ERROR, "%s: Undefined save format version %u", filename, version);
		goto failure;
	}
	else if (width * height > MAP_MAXAREA)
	{
		debug(LOG_ERROR, "Map %s too large : %d %d", filename, width, height);
		goto failure;
	}

	if (width <=1 || height <=1)
	{
		debug(LOG_ERROR, "Map is too small : %u, %u", width, height);
		goto failure;
	}

	/* See if this is the first time a map has been loaded */
	ASSERT(psMapTiles == NULL, "Map has not been cleared before calling mapLoad()!");

	/* Allocate the memory for the map */
	psMapTiles = (MAPTILE *)calloc(width * height, sizeof(MAPTILE));
	ASSERT(psMapTiles != NULL, "Out of memory" );

	mapWidth = width;
	mapHeight = height;
	
	// FIXME: the map preview code loads the map without setting the tileset
	if (!tileset)
	{
		tileset = strdup("texpages/tertilesc1hw");
	}
	
	// load the ground types
	if (!mapLoadGroundTypes())
	{
		goto failure;
	}
	
	//load in the map data itself
	
	/* Load in the map data */
	for (i = 0; i < mapWidth * mapHeight; i++)
	{
		UWORD	texture;
		UBYTE	height;

		if (!PHYSFS_readULE16(fp, &texture) || !PHYSFS_readULE8(fp, &height))
		{
			debug(LOG_ERROR, "%s: Error during savegame load", filename);
			goto failure;
		}

		psMapTiles[i].texture = texture;
		psMapTiles[i].height = height*ELEVATION_SCALE;

		// Visibility stuff
		memset(psMapTiles[i].watchers, 0, sizeof(psMapTiles[i].watchers));
		memset(psMapTiles[i].sensors, 0, sizeof(psMapTiles[i].sensors));
		memset(psMapTiles[i].jammers, 0, sizeof(psMapTiles[i].jammers));
		psMapTiles[i].sensorBits = 0;
		psMapTiles[i].jammerBits = 0;
		psMapTiles[i].tileExploredBits = 0;
	}

	if (preview)
	{
		// no need to do anything else for the map preview
		goto ok;
	}

	if (!PHYSFS_readULE32(fp, &version) || !PHYSFS_readULE32(fp, &numGw) || version != 1)
	{
		debug(LOG_ERROR, "Bad gateway in %s", filename);
		goto failure;
	}

	for (i = 0; i < numGw; i++)
	{
		UBYTE	x0, y0, x1, y1;

		if (!PHYSFS_readULE8(fp, &x0) || !PHYSFS_readULE8(fp, &y0) || !PHYSFS_readULE8(fp, &x1) || !PHYSFS_readULE8(fp, &y1))
		{
			debug(LOG_ERROR, "%s: Failed to read gateway info", filename);
			goto failure;
		}
		if (!gwNewGateway(x0, y0, x1, y1))
		{
			debug(LOG_ERROR, "%s: Unable to add gateway %d - dropping it", filename, i);
		}
	}
	
	if (!mapSetGroundTypes())
	{
		goto failure;
	}

	// reset the random water bottom heights
	// set the river bed
	for (i = 0; i < mapWidth; i++)
	{
		for (j = 0; j < mapHeight; j++)
		{
			// FIXME: magic number
			mapTile(i, j)->waterLevel = mapTile(i, j)->height - world_coord(1) / 3;
			// lower riverbed
			if (terrainType(mapTile(i, j)) == TER_WATER && terrainType(mapTile(i-1, j)) == TER_WATER && terrainType(mapTile(i, j-1)) == TER_WATER && terrainType(mapTile(i-1, j-1)) == TER_WATER)
			{
				mapTile(i, j)->height -= WATER_MIN_DEPTH - mt.u32()%(WATER_MAX_DEPTH + 1 - WATER_MIN_DEPTH);
			}
		}
	}

	/* set up the scroll mins and maxs - set values to valid ones for any new map */
	scrollMinX = scrollMinY = 0;
	scrollMaxX = mapWidth;
	scrollMaxY = mapHeight;

	/* Allocate aux maps */
	psBlockMap[AUX_MAP] = (uint8_t *)malloc(mapWidth * mapHeight * sizeof(*psBlockMap[0]));
	psBlockMap[AUX_ASTARMAP] = (uint8_t *)malloc(mapWidth * mapHeight * sizeof(*psBlockMap[0]));
	psBlockMap[AUX_DANGERMAP] = (uint8_t *)malloc(mapWidth * mapHeight * sizeof(*psBlockMap[0]));
	for (x = 0; x < MAX_PLAYERS + AUX_MAX; x++)
	{
		psAuxMap[x] = (uint8_t *)malloc(mapWidth * mapHeight * sizeof(*psAuxMap[0]));
	}

	// Set our blocking bits
	for (y = 0; y < mapHeight; y++)
	{
		for (x = 0; x < mapWidth; x++)
		{
			MAPTILE *psTile = mapTile(x, y);

			auxClearBlocking(x, y, AUXBITS_ALL);
			auxClearAll(x, y, AUXBITS_ALL);

			/* All tiles outside of the map and on map border are blocking. */
			if (x < 1 || y < 1 || x > mapWidth - 1 || y > mapHeight - 1)
			{
				auxSetBlocking(x, y, AUXBITS_ALL);	// block everything
			}
			if (terrainType(psTile) == TER_WATER)
			{
				auxSetBlocking(x, y, WATER_BLOCKED);
			}
			else
			{
				auxSetBlocking(x, y, LAND_BLOCKED);
			}
			if (terrainType(psTile) == TER_CLIFFFACE)
			{
				auxSetBlocking(x, y, FEATURE_BLOCKED);
			}
		}
	}

	/* Set continents. This should ideally be done in advance by the map editor. */
	mapFloodFillContinents();
ok:
	PHYSFS_close(fp);
	return true;
	
failure:
	PHYSFS_close(fp);
	return false;
}

/* Save the map data */
bool mapSave(char **ppFileData, UDWORD *pFileSize)
{
	MAP_SAVEHEADER	*psHeader = NULL;
	MAP_SAVETILE	*psTileData = NULL;
	MAPTILE	*psTile = NULL;
	GATEWAY *psCurrGate = NULL;
	GATEWAY_SAVEHEADER *psGateHeader = NULL;
	GATEWAY_SAVE *psGate = NULL;
	SDWORD	numGateways = 0;

	// find the number of non water gateways
	for(psCurrGate = gwGetGateways(); psCurrGate; psCurrGate = psCurrGate->psNext)
	{
		numGateways += 1;
	}

	/* Allocate the data buffer */
	*pFileSize = SAVE_HEADER_SIZE + mapWidth*mapHeight * SAVE_TILE_SIZE;
	// Add on the size of the gateway data.
	*pFileSize += sizeof(GATEWAY_SAVEHEADER) + sizeof(GATEWAY_SAVE)*numGateways;

	*ppFileData = (char*)malloc(*pFileSize);
	if (*ppFileData == NULL)
	{
		debug( LOG_FATAL, "Out of memory" );
		abort();
		return false;
	}

	/* Put the file header on the file */
	psHeader = (MAP_SAVEHEADER *)*ppFileData;
	psHeader->aFileType[0] = 'm';
	psHeader->aFileType[1] = 'a';
	psHeader->aFileType[2] = 'p';
	psHeader->aFileType[3] = ' ';
	psHeader->version = CURRENT_VERSION_NUM;
	psHeader->width = mapWidth;
	psHeader->height = mapHeight;

	/* MAP_SAVEHEADER */
	endian_udword(&psHeader->version);
	endian_udword(&psHeader->width);
	endian_udword(&psHeader->height);

	/* Put the map data into the buffer */
	psTileData = (MAP_SAVETILE *)(*ppFileData + SAVE_HEADER_SIZE);
	psTile = psMapTiles;
	for (int i = 0; i < mapWidth*mapHeight; i++)
	{
		psTileData->texture = psTile->texture;
		if (terrainType(psTile) == TER_WATER)
		{
			psTileData->height = (psTile->waterLevel + world_coord(1) / 3) / ELEVATION_SCALE;
		}
		else
		{
			psTileData->height = psTile->height / ELEVATION_SCALE;
		}

		/* MAP_SAVETILE */
		endian_uword(&psTileData->texture);

		psTileData = (MAP_SAVETILE *)((UBYTE *)psTileData + SAVE_TILE_SIZE);
		psTile++;
	}

	// Put the gateway header.
	psGateHeader = (GATEWAY_SAVEHEADER*)psTileData;
	psGateHeader->version = 1;
	psGateHeader->numGateways = numGateways;

	/* GATEWAY_SAVEHEADER */
	endian_udword(&psGateHeader->version);
	endian_udword(&psGateHeader->numGateways);

	psGate = (GATEWAY_SAVE*)(psGateHeader+1);

	// Put the gateway data.
	for(psCurrGate = gwGetGateways(); psCurrGate; psCurrGate = psCurrGate->psNext)
	{
		psGate->x0 = psCurrGate->x1;
		psGate->y0 = psCurrGate->y1;
		psGate->x1 = psCurrGate->x2;
		psGate->y1 = psCurrGate->y2;
		ASSERT(psGate->x0 == psGate->x1 || psGate->y0 == psGate->y1, "Invalid gateway coordinates (%d, %d, %d, %d)",
		       psGate->x0, psGate->y0, psGate->x1, psGate->y1);
		psGate++;
	}

	return true;
}

/* Shutdown the map module */
bool mapShutdown(void)
{
	int x;

	if (dangerThread)
	{
		wzSemaphoreWait(dangerDoneSemaphore);
		lastDangerPlayer = -1;
		wzSemaphorePost(dangerSemaphore);
		wzThreadJoin(dangerThread);
		wzSemaphoreDestroy(dangerSemaphore);
		wzSemaphoreDestroy(dangerDoneSemaphore);
		dangerThread = NULL;
		dangerSemaphore = NULL;
		dangerDoneSemaphore = NULL;
	}

	free(psMapTiles);
	delete[] mapDecals;
	free(psGroundTypes);
	free(map);
	free(Tile_names);
	free(psBlockMap[AUX_MAP]);
	psBlockMap[AUX_MAP] = NULL;
	free(psBlockMap[AUX_ASTARMAP]);
	psBlockMap[AUX_ASTARMAP] = NULL;
	free(psBlockMap[AUX_DANGERMAP]);
	free(floodbucket);
	psBlockMap[AUX_DANGERMAP] = NULL;
	for (x = 0; x < MAX_PLAYERS + AUX_MAX; x++)
	{
		free(psAuxMap[x]);
		psAuxMap[x] = NULL;
	}

	map = NULL;
	floodbucket = NULL;
	psGroundTypes = NULL;
	mapDecals = NULL;
	psMapTiles = NULL;
	mapWidth = mapHeight = 0;
	numTile_names = 0;
	Tile_names = NULL;
	return true;
}

/** 
 * Intersect a tile with a line and report the points of intersection
 * line is gives as point plus 2d directional vector
 * returned are two coordinates at the edge
 * true if the intersection also crosses the tile split line
 * (which has to be taken into account)
 **/
bool map_Intersect(int* Cx, int* Cy, int* Vx, int* Vy, int* Sx, int* Sy)
{
	int	 x, y, ox, oy, Dx, Dy, tileX, tileY;
	int	 ily,iry,itx,ibx;

	// dereference pointers
	x = *Cx;
	y = *Cy;
	Dx = *Vx;
	Dy = *Vy;

	/* Turn into tile coordinates */
	tileX = map_coord(x);
	tileY = map_coord(y);

	/* Inter tile comp */
	ox = map_round(x);
	oy = map_round(y);

	/* allow backwards tracing */
	if (ox==0 && Dx<0) {
		tileX--;
		ox=TILE_UNITS;
	}
	if (oy==0 && Dy<0) {
		tileY--;
		oy=TILE_UNITS;
	}

	*Cx=-4*TILE_UNITS; // to trigger assertion
	*Cy=-4*TILE_UNITS;
	*Vx=-4*TILE_UNITS;
	*Vy=-4*TILE_UNITS;

	// calculate intersection point on the left and right (if any)
	ily = y - 4 * TILE_UNITS; // make sure initial value is way outside of tile
	iry = y - 4 * TILE_UNITS;
	if ( Dx!=0) {
		ily = y - ox*Dy/Dx;
		iry = y + (TILE_UNITS-ox)*Dy/Dx;
	}
	// calculate intersection point on top and bottom (if any)
	itx = x - 4 * TILE_UNITS; // make sure initial value is way outside of tile
	ibx = x - 4 * TILE_UNITS;
	if (Dy!=0) {
		itx = x - oy*Dx/Dy;
		ibx = x + (TILE_UNITS-oy)*Dx/Dy;
	}

	// line comes from the left?
	if (Dx>=0) {
		if (map_coord(ily)==tileY || map_coord(ily-1)==tileY) {
			*Cx = world_coord(tileX);
			*Cy = ily;
		}
		if (map_coord(iry)==tileY || map_coord(iry-1)==tileY) {
			*Vx = world_coord(tileX+1);
			*Vy = iry;
		}
	} else {
		if (map_coord(ily)==tileY || map_coord(ily-1)==tileY) {
			*Vx = world_coord(tileX);
			*Vy = ily;
		}
		if (map_coord(iry)==tileY || map_coord(iry-1)==tileY) {
			*Cx = world_coord(tileX+1);
			*Cy = iry;
		}
	}
	// line comes from the top?
	if (Dy>=0) {
		if (map_coord(itx)==tileX || map_coord(itx-1)==tileX) {
			*Cx = itx;
			*Cy = world_coord(tileY);
		}
		if (map_coord(ibx)==tileX || map_coord(ibx-1)==tileX) {
			*Vx = ibx;
			*Vy = world_coord(tileY+1);
		}
	} else {
		if (map_coord(itx)==tileX || map_coord(itx-1)==tileX) {
			*Vx = itx;
			*Vy = world_coord(tileY);
		}
		if (map_coord(ibx)==tileX || map_coord(ibx-1)==tileX) {
			*Cx = ibx;
			*Cy = world_coord(tileY+1);
		}
	}
	// assertions, no intersections outside of tile
	ASSERT(*Cx>=world_coord(tileX) && *Cx<=world_coord(tileX+1),"map_Intersect(): tile Bounds %i %i, %i %i -> %i,%i,%i,%i",x,y,Dx,Dy,*Cx,*Cy,*Vx,*Vy);
	ASSERT(*Cy>=world_coord(tileY) && *Cy<=world_coord(tileY+1),"map_Intersect(): tile Bounds %i %i, %i %i -> %i,%i,%i,%i",x,y,Dx,Dy,*Cx,*Cy,*Vx,*Vy);
	ASSERT(*Vx>=world_coord(tileX) && *Vx<=world_coord(tileX+1),"map_Intersect(): tile Bounds %i %i, %i %i -> %i,%i,%i,%i",x,y,Dx,Dy,*Cx,*Cy,*Vx,*Vy);
	ASSERT(*Vy>=world_coord(tileY) && *Vy<=world_coord(tileY+1),"map_Intersect(): tile Bounds %i %i, %i %i -> %i,%i,%i,%i",x,y,Dx,Dy,*Cx,*Cy,*Vx,*Vy);
	ASSERT(tileX>=0 && tileY>=0 && tileX<mapWidth && tileY<mapHeight,"map_Intersect(): map Bounds %i %i, %i %i -> %i,%i,%i,%i",x,y,Dx,Dy,*Cx,*Cy,*Vx,*Vy);

	//calculate midway line intersection points
	if (((map_coord(itx)==tileX ) == (map_coord(ily)==tileY)) && ((map_coord(ibx)==tileX ) == (map_coord(iry)==tileY))) {
		// line crosses diagnonal only
		if (Dx-Dy==0) return false;
		*Sx = world_coord(tileX) + (Dx*oy - Dy*ox)/(Dx - Dy);
		*Sy = world_coord(tileY) + (Dx*oy - Dy*ox)/(Dx - Dy);
		if (map_coord(*Sx)!=tileX || map_coord(*Sy)!=tileY) return false;
		return true;
	} else if (((map_coord(ibx)==tileX) == (map_coord(ily)==tileY)) && ((map_coord(itx)==tileX) == (map_coord(iry)==tileY))) {
		//line crosses anti-diagonal only
		if (Dx+Dy==0) return false;
		*Sx = world_coord(tileX) + (Dx*(TILE_UNITS - oy) + Dy*ox)/(Dx + Dy);
		*Sy = world_coord(tileY) + (Dy*(TILE_UNITS - ox) + Dx*oy)/(Dx + Dy);
		if (map_coord(*Sx)!=tileX || map_coord(*Sy)!=tileY) return false;
		return true;
	} else {
		//line crosses both tile diagonals
		//TODO: trunk divides tiles into 4 parts instead of 2 in 2.3.
		//We would need to check and return both intersections here now,
		//but that would require an additional return parameter!
		//Instead we check only one of them and know it might be wrong!
		if (Dx+Dy!=0) {
			// check anti-diagonal
			*Sx = world_coord(tileX) + (Dx*(TILE_UNITS - oy) + Dy*ox)/(Dx + Dy);
			*Sy = world_coord(tileY) + (Dy*(TILE_UNITS - ox) + Dx*oy)/(Dx + Dy);
			if (map_coord(*Sx)==tileX && map_coord(*Sy)==tileY) return true;
		}
		if (Dx-Dy!=0) {
			// check diagonal
			*Sx = world_coord(tileX) + (Dx*oy - Dy*ox)/(Dx - Dy);
			*Sy = world_coord(tileY) + (Dx*oy - Dy*ox)/(Dx - Dy);
			if (map_coord(*Sx)==tileX && map_coord(*Sy)==tileY) return true;
		}
	}

	return false;
}

// Rotate vector clockwise by quadrant*90° around (TILE_UNITS/2, TILE_UNITS/2). (Considering x to be to the right, and y down.)
static Vector3i rotateWorldQuadrant(Vector3i v, int quadrant)
{
	switch (quadrant & 3)
	{
		default:  // Can't get here.
		case 0: return v;                                                 break;  // 0°.
		case 1: return Vector3i(TILE_UNITS - v.y,              v.x, v.z); break;  // 90° clockwise.
		case 2: return Vector3i(TILE_UNITS - v.x, TILE_UNITS - v.y, v.z); break;  // 180°.
		case 3: return Vector3i(             v.y, TILE_UNITS - v.x, v.z); break;  // 90° anticlockwise.
	}
}

// Returns (0, 0) rotated clockwise quadrant*90° around (½, ½). (Considering x to be to the right, and y down.)
static Vector2i quadrantCorner(int quadrant)
{
	int dx[4] = {0, 1, 1, 0};
	int dy[4] = {0, 0, 1, 1};
	return Vector2i(dx[quadrant & 3], dy[quadrant & 3]);
}

// Returns (0, -1) rotated clockwise quadrant*90° around (0, 0). (Considering x to be to the right, and y down.)
static Vector2i quadrantDelta(int quadrant)
{
	int dx[4] = {0,  1, 0, -1};
	int dy[4] = {-1, 0, 1,  0};
	return Vector2i(dx[quadrant & 3], dy[quadrant & 3]);
}


static inline bool fracTest(int numerA, int denomA, int numerB, int denomB)
{
	return denomA > 0 && numerA >= 0 && (denomB <= 0 || numerB < 0 || (int64_t)numerA*denomB < (int64_t)numerB*denomA);
}

unsigned map_LineIntersect(Vector3i src, Vector3i dst, unsigned tMax)
{
	// Transform src and dst to a coordinate system such that the tile quadrant containing src has
	// corners at (0, 0), (TILE_UNITS, 0), (TILE_UNITS/2, TILE_UNITS/2).
	Vector2i tile = map_coord(removeZ(src));
	src -= Vector3i(world_coord(tile), 0);
	dst -= Vector3i(world_coord(tile), 0);
	//            +0+
	// quadrant = 3×1
	//            +2+
	int quadrant = ((src.x < src.y)*3) ^ (TILE_UNITS - src.x < src.y);
	src = rotateWorldQuadrant(src, -quadrant);
	dst = rotateWorldQuadrant(dst, -quadrant);
	while (true)
	{
		int height[4];
		for (int q = 0; q < 4; ++q)
		{
			Vector2i corner = tile + quadrantCorner(quadrant + q);
			height[q] = map_TileHeightSurface(corner.x, corner.y);
		}
		Vector3i dif = dst - src;
		//     We are considering the volume of a quadrant (the volume above a quarter of a map tile, which is
		// a degenerate tetrahedron with a point at infinity). We have a line segment, and want to know where
		// it exits the quadrant volume.
		//     There are 5 possible cases. Cases 0-2, our line can exit one of the three sides of the quadrant
		// volume (and pass into a neighbouring quadrant volume), or case 3, can exit through the bottom of the
		// quadrant volume (which means intersecting the terrain), or case 4, the line segment can end (which
		// means reaching the destination with no intersection.
		//     Note that the height of the centre of the tile is the average of the corners, such that a tile
		// consists of 4 flat triangles (which are not (in general) parallel to each other).
		// +--0--+
		//  \ 3 /
		//   2 1
		//    +
		// Denominators are positive iff we are going in the direction of the line. First line crossed has the smallest fraction.
		// numer/denom gives the intersection times for the 5 cases.
		int numer[5], denom[5];
		numer[0] = -(-src.y);
		denom[0] =   -dif.y;
		numer[1] = TILE_UNITS - (src.x + src.y);
		denom[1] =               dif.x + dif.y;
		numer[2] = -(-src.x + src.y);
		denom[2] =   -dif.x + dif.y;
		Vector3i normal(2*(height[1] - height[0]), height[2] + height[3] - height[0] - height[1], -2*TILE_UNITS);  // Normal pointing down, and not normalised.
		numer[3] = height[0]*normal.z - src*normal;
		denom[3] =                      dif*normal;
		numer[4] = 1;
		denom[4] = 1;
		int firstIntersection = 0;
		for (int test = 0; test < 5; ++test)
		{
			if (!fracTest(numer[firstIntersection], denom[firstIntersection], numer[test], denom[test]))
			{
				firstIntersection = test;
			}
		}
		switch (firstIntersection)
		{
			case 0:  // Cross top line first (the tile boundary).
				tile += quadrantDelta(quadrant);
				quadrant += 2;
				src = rotateWorldQuadrant(src, -2) + Vector3i(0, -TILE_UNITS, 0);
				dst = rotateWorldQuadrant(dst, -2) + Vector3i(0, -TILE_UNITS, 0);

				if (tile.x < 0 || tile.x >= mapWidth || tile.y < 0 || tile.y >= mapHeight)
				{
					// Intersect edge of map.
					return (int64_t)tMax * numer[firstIntersection]/denom[firstIntersection];
				}
				break;
			case 1:  // Cross bottom-right line first.
				// Change to the new quadrant, and transform appropriately.
				++quadrant;
				src = rotateWorldQuadrant(src, -1);
				dst = rotateWorldQuadrant(dst, -1);
				break;
			case 2:  // Cross bottom-left line first.
				// Change to the new quadrant, and transform appropriately.
				--quadrant;
				src = rotateWorldQuadrant(src, 1);
				dst = rotateWorldQuadrant(dst, 1);
				break;
			case 3:  // Intersect terrain!
				return (int64_t)tMax * numer[firstIntersection]/denom[firstIntersection];
			case 4:  // Line segment ends.
				return UINT32_MAX;
		}
	}
}

/// The max height of the terrain and water at the specified world coordinates
extern int32_t map_Height(int x, int y)
{
	int tileX, tileY;
	int i, j;
	int32_t height[2][2], center;
	int32_t onTileX, onTileY;
	int32_t left, right, middle;
	int32_t onBottom, result;
	int towardsCenter, towardsRight;

	// Clamp x and y values to actual ones
	// Give one tile worth of leeway before asserting, for units/transporters coming in from off-map.
	ASSERT(x >= -TILE_UNITS, "map_Height: x value is too small (%d,%d) in %dx%d",map_coord(x),map_coord(y),mapWidth,mapHeight);
	ASSERT(y >= -TILE_UNITS, "map_Height: y value is too small (%d,%d) in %dx%d",map_coord(x),map_coord(y),mapWidth,mapHeight);
	x = MAX(x, 0);
	y = MAX(y, 0);
	ASSERT(x < world_coord(mapWidth)+TILE_UNITS, "map_Height: x value is too big (%d,%d) in %dx%d",map_coord(x),map_coord(y),mapWidth,mapHeight);
	ASSERT(y < world_coord(mapHeight)+TILE_UNITS, "map_Height: y value is too big (%d,%d) in %dx%d",map_coord(x),map_coord(y),mapWidth,mapHeight);
	x = MIN(x, world_coord(mapWidth) - 1);
	y = MIN(y, world_coord(mapHeight) - 1);

	// on which tile are these coords?
	tileX = map_coord(x);
	tileY = map_coord(y);

	// where on the tile? (scale to (0,1))
	onTileX = x - world_coord(tileX);
	onTileY = y - world_coord(tileY);

	// get the height for the corners and center
	center = 0;
	for (i = 0; i < 2; i++)
	{
		for (j = 0; j < 2; j++)
		{
			height[i][j] = map_TileHeightSurface(tileX+i, tileY+j);
			center += height[i][j];
		}
	}
	center /= 4;

	// we have:
	//   x ->
	// y 0,0--D--1,0
	// | |  \    / |
	// V A  centre C
	//   | /     \ |
	//   0,1--B--1,1

	// get heights for left and right corners and the distances
	if (onTileY > onTileX)
	{
		if (onTileY < TILE_UNITS - onTileX)
		{
			// A
			right = height[0][0];
			left  = height[0][1];
			towardsCenter = onTileX;
			towardsRight  = TILE_UNITS - onTileY;
		}
		else
		{
			// B
			right = height[0][1];
			left  = height[1][1];
			towardsCenter = TILE_UNITS - onTileY;
			towardsRight  = TILE_UNITS - onTileX;
		}
	}
	else
	{
		if (onTileX > TILE_UNITS - onTileY)
		{
			// C
			right = height[1][1];
			left  = height[1][0];
			towardsCenter = TILE_UNITS - onTileX;
			towardsRight  = onTileY;
		}
		else
		{
			// D
			right = height[1][0];
			left  = height[0][0];
			towardsCenter = onTileY;
			towardsRight  = onTileX;
		}
	}
	ASSERT(towardsCenter <= TILE_UNITS/2, "towardsCenter is too high");

	// now we have:
	//    left   m    right
	//         center

	middle = (left + right)/2;
	onBottom = left * (TILE_UNITS - towardsRight) + right * towardsRight;
	result = onBottom + (center - middle) * towardsCenter * 2;

	return (result + TILE_UNITS/2) / TILE_UNITS;
}

/* returns true if object is above ground */
bool mapObjIsAboveGround(const SIMPLE_OBJECT *psObj)
{
	// min is used to make sure we don't go over array bounds!
	// TODO Using the corner of the map instead doesn't make sense. Fix this...
	const int mapsize = mapWidth * mapHeight - 1;
	const int tileX = map_coord(psObj->pos.x);
	const int tileY = map_coord(psObj->pos.y);
	const int tileYOffset1 = (tileY * mapWidth);
	const int tileYOffset2 = ((tileY+1) * mapWidth);
	const int h1 = psMapTiles[MIN(mapsize, tileYOffset1 + tileX)    ].height;
	const int h2 = psMapTiles[MIN(mapsize, tileYOffset1 + tileX + 1)].height;
	const int h3 = psMapTiles[MIN(mapsize, tileYOffset2 + tileX)    ].height;
	const int h4 = psMapTiles[MIN(mapsize, tileYOffset2 + tileX + 1)].height;

	/* trivial test above */
	if (psObj->pos.z > h1 && psObj->pos.z > h2 && psObj->pos.z > h3 && psObj->pos.z > h4)
	{
		return true;
	}

	/* trivial test below */
	if (psObj->pos.z <= h1 && psObj->pos.z <= h2 && psObj->pos.z <= h3 && psObj->pos.z <= h4)
	{
		return false;
	}

	/* exhaustive test */
	return psObj->pos.z > map_Height(psObj->pos.x, psObj->pos.y);
}

/* returns the max and min height of a tile by looking at the four corners
   in tile coords */
void getTileMaxMin(int x, int y, int *pMax, int *pMin)
{
	*pMin = INT32_MAX;
	*pMax = INT32_MIN;

	for (int j = 0; j < 2; ++j)
		for (int i = 0; i < 2; ++i)
	{
		int height = map_TileHeight(x+i, y+j);
		*pMin = std::min(*pMin, height);
		*pMax = std::max(*pMax, height);
	}
}


// -----------------------------------------------------------------------------------
/* This will save out the visibility data */
bool writeVisibilityData(const char* fileName)
{
	unsigned int i;
	VIS_SAVEHEADER fileHeader;

	PHYSFS_file* fileHandle = openSaveFile(fileName);
	if (!fileHandle)
	{
		return false;
	}

	fileHeader.aFileType[0] = 'v';
	fileHeader.aFileType[1] = 'i';
	fileHeader.aFileType[2] = 's';
	fileHeader.aFileType[3] = 'd';

	fileHeader.version = CURRENT_VERSION_NUM;

	// Write out the current file header
	if (PHYSFS_write(fileHandle, fileHeader.aFileType, sizeof(fileHeader.aFileType), 1) != 1
	 || !PHYSFS_writeUBE32(fileHandle, fileHeader.version))
	{
		debug(LOG_ERROR, "writeVisibilityData: could not write header to %s; PHYSFS error: %s", fileName, PHYSFS_getLastError());
		PHYSFS_close(fileHandle);
		return false;
	}

	int planes = (game.maxPlayers + 7)/8;

	for (unsigned plane = 0; plane < planes; ++plane)
	{
		for (i = 0; i < mapWidth * mapHeight; ++i)
		{
			if (!PHYSFS_writeUBE8(fileHandle, psMapTiles[i].tileExploredBits >> (plane*8)))
			{
				debug(LOG_ERROR, "writeVisibilityData: could not write to %s; PHYSFS error: %s", fileName, PHYSFS_getLastError());
				PHYSFS_close(fileHandle);
				return false;
			}
		}
	}

	// Everything is just fine!
	PHYSFS_close(fileHandle);
	return true;
}

// -----------------------------------------------------------------------------------
/* This will read in the visibility data */
bool readVisibilityData(const char* fileName)
{
	VIS_SAVEHEADER fileHeader;
	unsigned int expectedFileSize, fileSize;
	unsigned int i;

	PHYSFS_file* fileHandle = openLoadFile(fileName, false);
	if (!fileHandle)
	{
		// Failure to open the file is no failure to read it
		return true;
	}

	// Read the header from the file
	if (PHYSFS_read(fileHandle, fileHeader.aFileType, sizeof(fileHeader.aFileType), 1) != 1
	 || !PHYSFS_readUBE32(fileHandle, &fileHeader.version))
	{
		debug(LOG_ERROR, "readVisibilityData: error while reading header from file: %s", PHYSFS_getLastError());
		PHYSFS_close(fileHandle);
		return false;
	}

	// Check the header to see if we've been given a file of the right type
	if (fileHeader.aFileType[0] != 'v'
	 || fileHeader.aFileType[1] != 'i'
	 || fileHeader.aFileType[2] != 's'
	 || fileHeader.aFileType[3] != 'd')
	{
		debug(LOG_ERROR, "readVisibilityData: Weird file type found? Has header letters - '%c' '%c' '%c' '%c' (should be 'v' 'i' 's' 'd')",
		      fileHeader.aFileType[0],
		      fileHeader.aFileType[1],
		      fileHeader.aFileType[2],
		      fileHeader.aFileType[3]);

		PHYSFS_close(fileHandle);
		return false;
	}

	int planes = (game.maxPlayers + 7)/8;

	// Validate the filesize
	expectedFileSize = sizeof(fileHeader.aFileType) + sizeof(fileHeader.version) + mapWidth * mapHeight * planes;
	fileSize = PHYSFS_fileLength(fileHandle);
	if (fileSize != expectedFileSize)
	{
		PHYSFS_close(fileHandle);
		ASSERT(!"readVisibilityData: unexpected filesize", "readVisibilityData: unexpected filesize; should be %u, but is %u", expectedFileSize, fileSize);

		return false;
	}

	// For every tile...
	for(i=0; i<mapWidth*mapHeight; i++)
	{
		psMapTiles[i].tileExploredBits = 0;
	}
	for (unsigned plane = 0; plane < planes; ++plane)
	{
		for(i=0; i<mapWidth*mapHeight; i++)
		{
			/* Get the visibility data */
			uint8_t val = 0;
			if (!PHYSFS_readUBE8(fileHandle, &val))
			{
				debug(LOG_ERROR, "readVisibilityData: could not read from %s; PHYSFS error: %s", fileName, PHYSFS_getLastError());
				PHYSFS_close(fileHandle);
				return false;
			}
			psMapTiles[i].tileExploredBits |= val << (plane*8);
		}
	}

	// Close the file
	PHYSFS_close(fileHandle);

	/* Hopefully everything's just fine by now */
	return true;
}

// Convert a direction into an offset.
// dir 0 => x = 0, y = -1
#define NUM_DIR		8
static const Vector2i aDirOffset[] =
{
	Vector2i( 0, 1),
	Vector2i(-1, 1),
	Vector2i(-1, 0),
	Vector2i(-1,-1),
	Vector2i( 0,-1),
	Vector2i( 1,-1),
	Vector2i( 1, 0),
	Vector2i( 1, 1),
};

// Flood fill a "continent".
// TODO take into account scroll limits and update continents on scroll limit changes
static void mapFloodFill(int x, int y, int continent, uint8_t blockedBits, uint16_t MAPTILE::*varContinent)
{
	std::vector<Vector2i> open;
	open.push_back(Vector2i(x, y));
	mapTile(x, y)->*varContinent = continent;  // Set continent value

	while (!open.empty())
	{
		// Pop the first open node off the list for this iteration
		Vector2i pos = open.back();
		open.pop_back();

		// Add accessible neighbouring tiles to the open list
		for (int i = 0; i < NUM_DIR; ++i)
		{
			// rely on the fact that all border tiles are inaccessible to avoid checking explicitly
			Vector2i npos = pos + aDirOffset[i];

			if (npos.x < 1 || npos.y < 1 || npos.x > mapWidth - 2 || npos.y > mapHeight - 2)
			{
				continue;
			}
			MAPTILE *psTile = mapTile(npos);

			if (!(blockTile(npos.x, npos.y, AUX_MAP) & blockedBits) && psTile->*varContinent == 0)
			{
				open.push_back(npos);               // add to open list
				psTile->*varContinent = continent;  // Set continent value
			}
		}
	}
}

void mapFloodFillContinents()
{
	int x, y, limitedContinents = 0, hoverContinents = 0;

	/* Clear continents */
	for (y = 0; y < mapHeight; y++)
	{
		for (x = 0; x < mapWidth; x++)
		{
			MAPTILE *psTile = mapTile(x, y);

			psTile->limitedContinent = 0;
			psTile->hoverContinent = 0;
		}
	}

	/* Iterate over the whole map, looking for unset continents */
	for (y = 1; y < mapHeight - 2; y++)
	{
		for (x = 1; x < mapWidth - 2; x++)
		{
			MAPTILE *psTile = mapTile(x, y);

			if (psTile->limitedContinent == 0 && !fpathBlockingTile(x, y, PROPULSION_TYPE_WHEELED))
			{
				mapFloodFill(x, y, 1 + limitedContinents++, WATER_BLOCKED | FEATURE_BLOCKED, &MAPTILE::limitedContinent);
			}
			else if (psTile->limitedContinent == 0 && !fpathBlockingTile(x, y, PROPULSION_TYPE_PROPELLOR))
			{
				mapFloodFill(x, y, 1 + limitedContinents++, LAND_BLOCKED | FEATURE_BLOCKED, &MAPTILE::limitedContinent);
			}

			if (psTile->hoverContinent == 0 && !fpathBlockingTile(x, y, PROPULSION_TYPE_HOVER))
			{
				mapFloodFill(x, y, 1 + hoverContinents++, FEATURE_BLOCKED, &MAPTILE::hoverContinent);
			}
		}
	}
	debug(LOG_MAP, "Found %d limited and %d hover continents", limitedContinents, hoverContinents);
}

void tileSetFire(int32_t x, int32_t y, uint32_t duration)
{
	const int posX = map_coord(x);
	const int posY = map_coord(y);
	MAPTILE *const tile = mapTile(posX, posY);

	uint16_t currentTime =  gameTime             / GAME_TICKS_PER_UPDATE;
	uint16_t fireEndTime = (gameTime + duration) / GAME_TICKS_PER_UPDATE;
	if (currentTime == fireEndTime)
	{
		return;  // Fire already ended.
	}
	if ((tile->tileInfoBits & BITS_ON_FIRE) != 0 && (uint16_t)(fireEndTime - currentTime) < (uint16_t)(tile->fireEndTime - currentTime))
	{
		return;  // Tile already on fire, and that fire lasts longer.
	}

	// Burn, tile, burn!
	tile->tileInfoBits |= BITS_ON_FIRE;
	tile->fireEndTime = fireEndTime;

	syncDebug("Fire tile{%d, %d} dur%u end%d", posX, posY, duration, fireEndTime);
}

/** Check if tile contained within the given world coordinates is burning. */
bool fireOnLocation(unsigned int x, unsigned int y)
{
	const int posX = map_coord(x);
	const int posY = map_coord(y);
	const MAPTILE *psTile = mapTile(posX, posY);

	ASSERT(psTile, "Checking fire on tile outside the map (%d, %d)", posX, posY);
	return psTile != NULL && TileIsBurning(psTile);
}

// This function runs in a separate thread!
static int dangerFloodFill(int player)
{
	int i;
	Vector2i pos = getPlayerStartPosition(player);
	Vector2i npos;
	uint8_t aux, block;
	int x, y;
	bool start = true;	// hack to disregard the blocking status of any building exactly on the starting position

	// Set our danger bits
	for (y = 0; y < mapHeight; y++)
	{
		for (x = 0; x < mapWidth; x++)
		{
			auxSet(x, y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_DANGER);
			auxClear(x, y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_TEMPORARY);
		}
	}

	pos.x = map_coord(pos.x);
	pos.y = map_coord(pos.y);
	bucketcounter = 0;

	do
	{
		// Add accessible neighbouring tiles to the open list
		for (i = 0; i < NUM_DIR; i++)
		{
			npos.x = pos.x + aDirOffset[i].x;
			npos.y = pos.y + aDirOffset[i].y;
			if (!tileOnMap(npos.x, npos.y))
			{
				continue;
			}
			aux = auxTile(npos.x, npos.y, MAX_PLAYERS + AUX_DANGERMAP);
			block = blockTile(pos.x, pos.y, AUX_DANGERMAP);
			if (!(aux & AUXBITS_TEMPORARY) && !(aux & AUXBITS_THREAT) && (aux & AUXBITS_DANGER))
			{
				// Note that we do not consider water to be a blocker here. This may or may not be a feature...
				if (!(block & FEATURE_BLOCKED) && (!(aux & AUXBITS_NONPASSABLE) || start))
				{
					floodbucket[bucketcounter].x = npos.x;
					floodbucket[bucketcounter].y = npos.y;
					bucketcounter++;
					if (start && !(aux & AUXBITS_NONPASSABLE))
					{
						start = false;
					}
				}
				else
				{
					auxClear(npos.x, npos.y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_DANGER);
				}
				auxSet(npos.x, npos.y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_TEMPORARY); // make sure we do not process it more than once
			}
		}

		// Clear danger
		auxClear(pos.x, pos.y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_DANGER);

		// Pop the last open node off the bucket list for the next iteration
		if (bucketcounter)
		{
			bucketcounter--;
			pos.x = floodbucket[bucketcounter].x;
			pos.y = floodbucket[bucketcounter].y;
		}
	} while (bucketcounter);
	return 0;
}

// This function runs in a separate thread!
static int dangerThreadFunc(WZ_DECL_UNUSED void *data)
{
	while (lastDangerPlayer != -1)
	{
		dangerFloodFill(lastDangerPlayer);	// Do the actual work
		wzSemaphorePost(dangerDoneSemaphore);   // Signal that we are done
		wzSemaphoreWait(dangerSemaphore);	// Go to sleep until needed.
	}
	return 0;
}

static inline void threatUpdateTarget(int player, BASE_OBJECT *psObj, bool ground, bool air)
{
	int i;

	if (psObj->visible[player] || psObj->born == 2)
	{
		for (i = 0; i < psObj->numWatchedTiles; i++)
		{
			const TILEPOS pos = psObj->watchedTiles[i];

			if (ground)
			{
				auxSet(pos.x, pos.y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_THREAT);	// set ground threat for this tile
			}
			if (air)
			{
				auxSet(pos.x, pos.y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_AATHREAT);	// set air threat for this tile
			}
		}
	}
}

static void threatUpdate(int player)
{
	int i, weapon, x, y;

	// Step 1: Clear our threat bits
	for (y = 0; y < mapHeight; y++)
	{
		for (x = 0; x < mapWidth; x++)
		{
			auxClear(x, y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_THREAT | AUXBITS_AATHREAT);
		}
	}

	// Step 2: Set threat bits
	for (i = 0; i < MAX_PLAYERS; i++)
	{
		DROID *psDroid;
		STRUCTURE *psStruct;

		if (aiCheckAlliances(player, i))
		{
			// No need to iterate friendly objects
			continue;
		}

		for (psDroid = apsDroidLists[i]; psDroid; psDroid = psDroid->psNext)
		{
			UBYTE mode = 0;

			if (psDroid->droidType == DROID_CONSTRUCT || psDroid->droidType == DROID_CYBORG_CONSTRUCT
			    || psDroid->droidType == DROID_REPAIR || psDroid->droidType == DROID_CYBORG_REPAIR)
			{
				continue;	// hack that really should not be needed, but is -- trucks can SHOOT_ON_GROUND...!
			}
			for (weapon = 0; weapon < psDroid->numWeaps; weapon++)
			{
				mode |= asWeaponStats[psDroid->asWeaps[weapon].nStat].surfaceToAir;
			}
			if (psDroid->droidType == DROID_SENSOR)	// special treatment for sensor turrets, no multiweapon support
			{
				mode |= SHOOT_ON_GROUND;		// assume it only shoots at ground targets for now
			}
			if (mode > 0)
			{
				threatUpdateTarget(player, (BASE_OBJECT *)psDroid, mode & SHOOT_ON_GROUND, mode & SHOOT_IN_AIR);
			}
		}

		for (psStruct = apsStructLists[i]; psStruct; psStruct = psStruct->psNext)
		{
			UBYTE mode = 0;

			for (weapon = 0; weapon < psStruct->numWeaps; weapon++)
			{
				mode |= asWeaponStats[psStruct->asWeaps[weapon].nStat].surfaceToAir;
			}
			if (psStruct->pStructureType->pSensor && psStruct->pStructureType->pSensor->location == LOC_TURRET)	// special treatment for sensor turrets
			{
				mode |= SHOOT_ON_GROUND;		// assume it only shoots at ground targets for now
			}
			if (mode > 0)
			{
				threatUpdateTarget(player, (BASE_OBJECT *)psStruct, mode & SHOOT_ON_GROUND, mode & SHOOT_IN_AIR);
			}
		}
	}
}

void mapInit()
{
	int player;

	free(floodbucket);
	floodbucket = (struct floodtile *)malloc(mapWidth * mapHeight * sizeof(*floodbucket));

	lastDangerUpdate = 0;
	lastDangerPlayer = -1;

	// Initialize danger maps
	for (player = 0; player < MAX_PLAYERS; player++)
	{
		auxMapStore(player, AUX_DANGERMAP);
		threatUpdate(player);
		dangerFloodFill(player);
		auxMapRestore(player, AUX_DANGERMAP, AUXBITS_DANGER | AUXBITS_THREAT | AUXBITS_AATHREAT);
	}

	// Start thread
	ASSERT(dangerSemaphore == NULL && dangerThread == NULL, "Map data not cleaned up before starting!");
	if (game.type == SKIRMISH)
	{
		lastDangerPlayer = 0;
		dangerSemaphore = wzSemaphoreCreate(0);
		dangerDoneSemaphore = wzSemaphoreCreate(0);
		dangerThread = wzThreadCreate(dangerThreadFunc, NULL);
		wzThreadStart(dangerThread);
	}
}

void mapUpdate()
{
	const uint16_t currentTime = gameTime / GAME_TICKS_PER_UPDATE;
	int posX, posY;

	for (posY = 0; posY < mapHeight; ++posY)
		for (posX = 0; posX < mapWidth; ++posX)
	{
		MAPTILE *const tile = mapTile(posX, posY);

		if ((tile->tileInfoBits & BITS_ON_FIRE) != 0 && tile->fireEndTime == currentTime)
		{
			// Extinguish, tile, extinguish!
			tile->tileInfoBits &= ~BITS_ON_FIRE;

			syncDebug("Extinguished tile{%d, %d}", posX, posY);
		}
	}

	if (gameTime > lastDangerUpdate + GAME_TICKS_FOR_DANGER && game.type == SKIRMISH)
	{
		// Lock if previous job not done yet
		wzSemaphoreWait(dangerDoneSemaphore);

		auxMapRestore(lastDangerPlayer, AUX_DANGERMAP, AUXBITS_THREAT | AUXBITS_AATHREAT | AUXBITS_DANGER);
		lastDangerPlayer = (lastDangerPlayer + 1 ) % game.maxPlayers;
		auxMapStore(lastDangerPlayer, AUX_DANGERMAP);
		threatUpdate(lastDangerPlayer);
		wzSemaphorePost(dangerSemaphore);
	}
}
