// world.cpp: core map management stuff

#include "cube.h"

#import "Command.h"
#import "Entity.h"
#import "Monster.h"
#import "Player.h"

extern OFString *entnames[]; // lookup from map entities above to strings

struct sqr *world = NULL;
int sfactor, ssize, cubicsize, mipsize;

struct header hdr;

// set all cubes with "tag" to space, if tag is 0 then reset ALL tagged cubes
// according to type
void
settag(int tag, int type)
{
	int maxx = 0, maxy = 0, minx = ssize, miny = ssize;
	for (int x = 0; x < ssize; x++) {
		for (int y = 0; y < ssize; y++) {
			struct sqr *s = S(x, y);

			if (s->tag) {
				if (tag) {
					if (tag == s->tag)
						s->type = SPACE;
					else
						continue;
				} else
					s->type = type ? SOLID : SPACE;

				if (x > maxx)
					maxx = x;
				if (y > maxy)
					maxy = y;
				if (x < minx)
					minx = x;
				if (y < miny)
					miny = y;
			}
		}
	}

	if (maxx) {
		// remip minimal area of changed geometry
		struct block b = { minx, miny, maxx - minx + 1,
			maxy - miny + 1 };
		remip(&b, 0);
	}
}

// reset for editing or map saving
void
resettagareas()
{
	settag(0, 0);
}

// set for playing
void
settagareas()
{
	settag(0, 1);

	[ents enumerateObjectsUsingBlock: ^ (Entity *e, size_t i, bool *stop) {
		if (ents[i].type == CARROT)
			setspawn(i, true);
	}];
}

void
trigger(int tag, int type, bool savegame)
{
	if (!tag)
		return;

	settag(tag, type);

	if (!savegame && type != 3)
		playsound(S_RUMBLE, NULL);

	OFString *aliasname = [OFString stringWithFormat:
	    @"level_trigger_%d", tag];

	if (identexists(aliasname))
		execute(aliasname, true);

	if (type == 2)
		[Monster endSinglePlayerWithAllKilled: false];
}

COMMAND(trigger, ARG_2INT, ^ (int tag, int type, bool savegame) {
	trigger(tag, type, savegame);
})

// main geometric mipmapping routine, recursively rebuild mipmaps within block
// b. tries to produce cube out of 4 lower level mips as well as possible, sets
// defer to 0 if mipped cube is a perfect mip, i.e. can be rendered at this mip
// level indistinguishable from its constituent cubes (saves considerable
// rendering time if this is possible).

void
remip(const struct block *b, int level)
{
	if (level >= SMALLEST_FACTOR)
		return;

	int lighterr = getvar(@"lighterror") * 3;
	struct sqr *w = wmip[level];
	struct sqr *v = wmip[level + 1];
	int ws = ssize >> level;
	int vs = ssize >> (level + 1);
	struct block s = *b;
	if (s.x & 1) {
		s.x--;
		s.xs++;
	}
	if (s.y & 1) {
		s.y--;
		s.ys++;
	}
	s.xs = (s.xs + 1) & ~1;
	s.ys = (s.ys + 1) & ~1;
	for (int x = s.x; x < s.x + s.xs; x += 2)
		for (int y = s.y; y < s.y + s.ys; y += 2) {
			struct sqr *o[4];
			o[0] = SWS(w, x, y, ws); // the 4 constituent cubes
			o[1] = SWS(w, x + 1, y, ws);
			o[2] = SWS(w, x + 1, y + 1, ws);
			o[3] = SWS(w, x, y + 1, ws);
			// the target cube in the higher mip level
			struct sqr *r = SWS(v, x / 2, y / 2, vs);
			*r = *o[0];
			unsigned char nums[MAXTYPE];
			for (int i = 0; i < MAXTYPE; i++)
				nums[i] = 0;
			for (int j = 0; j < 4; j++)
				nums[o[j]->type]++;
			// cube contains both solid and space, treated
			// specially in the renderer
			r->type = SEMISOLID;
			for (int k = 0; k < MAXTYPE; k++)
				if (nums[k] == 4)
					r->type = k;
			if (!SOLID(r)) {
				int floor = 127, ceil = -128;
				for (int i = 0; i < 4; i++) {
					if (!SOLID(o[i])) {
						int fh = o[i]->floor;
						int ch = o[i]->ceil;
						if (r->type == SEMISOLID) {
							if (o[i]->type == FHF)
								// crap hack,
								// needed for
								// rendering
								// large mips
								// next to hfs
								fh -=
								    o[i]->vdelta /
								    4 + 2;
							if (o[i]->type == CHF)
								// FIXME: needs
								// to somehow
								// take into
								// account
								// middle
								// vertices on
								// higher mips
								ch +=
								    o[i]->vdelta /
								    4 + 2;
						}
						if (fh < floor)
							// take lowest floor and
							// highest ceil, so we
							// never have to see
							// missing lower/upper
							// from the side
							floor = fh;
						if (ch > ceil)
							ceil = ch;
					}
				}
				r->floor = floor;
				r->ceil = ceil;
			}
			if (r->type == CORNER)
				// special case: don't ever split even if
				// textures etc are different
				goto mip;
			r->defer = 1;
			if (SOLID(r)) {
				for (int i = 0; i < 3; i++) {
					if (o[i]->wtex != o[3]->wtex)
						// on an all solid cube, only
						// thing that needs to be equal
						// for a perfect mip is the
						// wall texture
						goto c;
				}
			} else {
				for (int i = 0; i < 3; i++) {
					// perfect mip even if light is not
					// exactly equal
					if (o[i]->type != o[3]->type ||
					    o[i]->floor != o[3]->floor ||
					    o[i]->ceil != o[3]->ceil ||
					    o[i]->ftex != o[3]->ftex ||
					    o[i]->ctex != o[3]->ctex ||
					    abs(o[i + 1]->r - o[0]->r) >
					    lighterr ||
					    abs(o[i + 1]->g - o[0]->g) >
					    lighterr ||
					    abs(o[i + 1]->b - o[0]->b) >
					    lighterr ||
					    o[i]->utex != o[3]->utex ||
					    o[i]->wtex != o[3]->wtex)
						goto c;
				}

				// can make a perfect mip out of a hf if slopes
				// lie on one line
				if (r->type == CHF || r->type == FHF) {
					if (o[0]->vdelta - o[1]->vdelta !=
					    o[1]->vdelta -
					    SWS(w, x + 2, y, ws)->vdelta ||
					    o[0]->vdelta - o[2]->vdelta !=
					    o[2]->vdelta -
					    SWS(w, x + 2, y + 2, ws)->vdelta ||
					    o[0]->vdelta - o[3]->vdelta !=
					    o[3]->vdelta -
					    SWS(w, x, y + 2, ws)->vdelta ||
					    o[3]->vdelta - o[2]->vdelta !=
					    o[2]->vdelta -
					    SWS(w, x + 2, y + 1, ws)->vdelta ||
					    o[1]->vdelta - o[2]->vdelta !=
					    o[2]->vdelta -
					    SWS(w, x + 1, y + 2, ws)->vdelta)
						goto c;
				}
			}
			{
				// if any of the constituents is not perfect,
				// then this one isn't either
				for (int i = 0; i < 4; i++)
					if (o[i]->defer)
						goto c;
			}
		mip:
			r->defer = 0;
		c:;
		}
	s.x /= 2;
	s.y /= 2;
	s.xs /= 2;
	s.ys /= 2;
	remip(&s, level + 1);
}

void
remipmore(const struct block *b, int level)
{
	struct block bb = *b;

	if (bb.x > 1)
		bb.x--;
	if (bb.y > 1)
		bb.y--;
	if (bb.xs < ssize - 3)
		bb.xs++;
	if (bb.ys < ssize - 3)
		bb.ys++;

	remip(&bb, level);
}

int
closestent() // used for delent and edit mode ent display
{
	if (noteditmode())
		return -1;

	__block int best;
	__block float bdist = 99999;
	[ents enumerateObjectsUsingBlock: ^ (Entity *e, size_t i, bool *stop) {
		if (e.type == NOTUSED)
			return;

		OFVector3D v = OFMakeVector3D(e.x, e.y, e.z);
		float dist = OFDistanceOfVectors3D(v, Player.player1.origin);
		if (dist < bdist) {
			best = i;
			bdist = dist;
		}
	}];

	return (bdist == 99999 ? -1 : best);
}

COMMAND(entproperty, ARG_2INT, ^ (int prop, int amount) {
	int e = closestent();
	if (e < 0)
		return;
	switch (prop) {
	case 0:
		ents[e].attr1 += amount;
		break;
	case 1:
		ents[e].attr2 += amount;
		break;
	case 2:
		ents[e].attr3 += amount;
		break;
	case 3:
		ents[e].attr4 += amount;
		break;
	}
})

COMMAND(delent, ARG_NONE, ^ {
	int e = closestent();
	if (e < 0) {
		conoutf(@"no more entities");
		return;
	}
	int t = ents[e].type;
	conoutf(@"%@ entity deleted", entnames[t]);
	ents[e].type = NOTUSED;
	addmsg(1, 10, SV_EDITENT, e, NOTUSED, 0, 0, 0, 0, 0, 0, 0);
	if (t == LIGHT)
		calclight();
})

int
findtype(OFString *what)
{
	for (int i = 0; i < MAXENTTYPES; i++)
		if ([what isEqual: entnames[i]])
			return i;
	conoutf(@"unknown entity type \"%@\"", what);
	return NOTUSED;
}

Entity *
newentity(int x, int y, int z, OFString *what, int v1, int v2, int v3, int v4)
{
	int type = findtype(what);

	Entity *e = [Entity entity];
	e.x = x;
	e.y = y;
	e.z = z;
	e.attr1 = v1;
	e.type = type;
	e.attr2 = v2;
	e.attr3 = v3;
	e.attr4 = v4;

	switch (type) {
	case LIGHT:
		if (v1 > 32)
			v1 = 32;
		if (!v1)
			e.attr1 = 16;
		if (!v2 && !v3 && !v4)
			e.attr2 = 255;
		break;
	case MAPMODEL:
		e.attr4 = e.attr3;
		e.attr3 = e.attr2;
	case MONSTER:
	case TELEDEST:
		e.attr2 = (unsigned char)e.attr1;
	case PLAYERSTART:
		e.attr1 = (int)Player.player1.yaw;
		break;
	}
	addmsg(1, 10, SV_EDITENT, ents.count, type, e.x, e.y, e.z, e.attr1,
	    e.attr2, e.attr3, e.attr4);

	[ents addObject: e];

	if (type == LIGHT)
		calclight();

	return e;
}

COMMAND(clearents, ARG_1STR, ^ (OFString *name) {
	int type = findtype(name);

	if (noteditmode() || multiplayer())
		return;

	for (Entity *e in ents)
		if (e.type == type)
			e.type = NOTUSED;

	if (type == LIGHT)
		calclight();
})

static unsigned char
scalecomp(unsigned char c, int intens)
{
	int n = c * intens / 100;
	if (n > 255)
		n = 255;
	return n;
}

COMMAND(scalelights, ARG_2INT, ^ (int f, int intens) {
	for (Entity *e in ents) {
		if (e.type != LIGHT)
			continue;

		e.attr1 = e.attr1 * f / 100;
		if (e.attr1 < 2)
			e.attr1 = 2;
		if (e.attr1 > 32)
			e.attr1 = 32;

		if (intens) {
			e.attr2 = scalecomp(e.attr2, intens);
			e.attr3 = scalecomp(e.attr3, intens);
			e.attr4 = scalecomp(e.attr4, intens);
		}
	}

	calclight();
})

int
findentity(int type, int index)
{
	for (int i = index; i < ents.count; i++)
		if (ents[i].type == type)
			return i;
	for (int j = 0; j < index; j++)
		if (ents[j].type == type)
			return j;
	return -1;
}

struct sqr *wmip[LARGEST_FACTOR * 2];

void
setupworld(int factor)
{
	ssize = 1 << (sfactor = factor);
	cubicsize = ssize * ssize;
	mipsize = cubicsize * 134 / 100;
	struct sqr *w = world =
	    OFAllocZeroedMemory(mipsize, sizeof(struct sqr));
	for (int i = 0; i < LARGEST_FACTOR * 2; i++) {
		wmip[i] = w;
		w += cubicsize >> (i * 2);
	}
}

// main empty world creation routine, if passed factor -1 will enlarge old
// world by 1
void
empty_world(int factor, bool force)
{
	if (!force && noteditmode())
		return;
	cleardlights();
	pruneundos(0);
	struct sqr *oldworld = world;
	bool copy = false;
	if (oldworld && factor < 0) {
		factor = sfactor + 1;
		copy = true;
	}
	if (factor < SMALLEST_FACTOR)
		factor = SMALLEST_FACTOR;
	if (factor > LARGEST_FACTOR)
		factor = LARGEST_FACTOR;
	setupworld(factor);

	for (int x = 0; x < ssize; x++) {
		for (int y = 0; y < ssize; y++) {
			struct sqr *s = S(x, y);
			s->r = s->g = s->b = 150;
			s->ftex = DEFAULT_FLOOR;
			s->ctex = DEFAULT_CEIL;
			s->wtex = s->utex = DEFAULT_WALL;
			s->type = SOLID;
			s->floor = 0;
			s->ceil = 16;
			s->vdelta = 0;
			s->defer = 0;
		}
	}

	strncpy(hdr.head, "CUBE", 4);
	hdr.version = MAPVERSION;
	hdr.headersize = sizeof(struct header);
	hdr.sfactor = sfactor;

	if (copy) {
		for (int x = 0; x < ssize / 2; x++) {
			for (int y = 0; y < ssize / 2; y++) {
				*S(x + ssize / 4, y + ssize / 4) =
				    *SWS(oldworld, x, y, ssize / 2);
			}
		}

		for (Entity *e in ents) {
			e.x += ssize / 4;
			e.y += ssize / 4;
		}
	} else {
		char buffer[128] = "Untitled Map by Unknown";
		memcpy(hdr.maptitle, buffer, 128);
		hdr.waterlevel = -100000;
		for (int i = 0; i < 15; i++)
			hdr.reserved[i] = 0;
		for (int k = 0; k < 3; k++)
			for (int i = 0; i < 256; i++)
				hdr.texlists[k][i] = i;
		[ents removeAllObjects];
		struct block b = { 8, 8, ssize - 16, ssize - 16 };
		edittypexy(SPACE, &b);
	}

	calclight();
	startmap(@"base/unnamed");
	if (oldworld) {
		OFFreeMemory(oldworld);
		toggleedit();
		execute(@"fullbright 1", true);
	}
}

COMMAND(mapenlarge, ARG_NONE, ^ {
	empty_world(-1, false);
})

COMMAND(newmap, ARG_1INT, ^ (int i) {
	empty_world(i, false);
})

COMMAND(recalc, ARG_NONE, ^ {
	calclight();
})
