/************************************************************************
* collision.c
* voxelands - 3d voxel world sandbox game
* Copyright (C) Lisa 'darkrose' Milne 2016 <lisa@ltmnet.com>
*
* This program 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.
*
* This program 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 this program.  If not, see <http://www.gnu.org/licenses/>
************************************************************************/

#include "common.h"
#include "collision.h"

#include "array.h"
#include "list.h"

#include <stdlib.h>
#include <math.h>

static void collision_initinfo(collisioninfo_t *i)
{
	i->type = COLLISION_NODE;
	i->pos.x = -32768;
	i->pos.y = -32768;
	i->pos.z = -32768;
	i->speed = 0.0;
	i->speed_o.x = 0.0;
	i->speed_o.y = 0.0;
	i->speed_o.z = 0.0;
	i->speed_n.x = 0.0;
	i->speed_n.y = 0.0;
	i->speed_n.z = 0.0;
}


/* checks if movingbox collides with staticbox
 * -1 no collision, 0 x collision, 1 y collision, 2 z collision
 * time of collision stored in dtime */
static int collision_aa(aabox_t *staticbox, aabox_t *movingbox, v3_t *speed, float d, float *dtime)
{
	aabox_t relbox;
	float xsize;
	float ysize;
	float zsize;
	float dt;
	v3_t dspeed;

	xsize = (staticbox->max.x - staticbox->min.x);
	ysize = (staticbox->max.y - staticbox->min.y);
	zsize = (staticbox->max.z - staticbox->min.z);

	relbox.min.x = (movingbox->min.x - staticbox->min.x);
	relbox.min.y = (movingbox->min.y - staticbox->min.y);
	relbox.min.z = (movingbox->min.z - staticbox->min.z);
	relbox.max.x = (movingbox->max.x - staticbox->min.x);
	relbox.max.y = (movingbox->max.y - staticbox->min.y);
	relbox.max.z = (movingbox->max.z - staticbox->min.z);

	/* check against x- */
	if (speed->x > 0.0) {
		if (relbox.max.x <= d) {
			dt = -relbox.max.x/speed->x;
			*dtime = dt;
			dspeed.x = speed->x*dt;
			dspeed.y = speed->y*dt;
			dspeed.z = speed->z*dt;
			if (
				((relbox.min.y+dspeed.y) < ysize)
				&& ((relbox.max.y+dspeed.y) > 0.0)
				&& ((relbox.min.z+dspeed.z) < zsize)
				&& ((relbox.max.z+dspeed.z) > 0.0)
			)
				return 0;
		}else if (relbox.min.x > xsize) {
			return -1;
		}
	/* check against x+ */
	}else if (speed->x < 0.0) {
		if (relbox.min.x >= (xsize-d)) {
			dt = (xsize-relbox.min.x)/speed->x;
			*dtime = dt;
			dspeed.x = speed->x*dt;
			dspeed.y = speed->y*dt;
			dspeed.z = speed->z*dt;
			if (
				((relbox.min.y+dspeed.y) < ysize)
				&& ((relbox.max.y+dspeed.y) > 0.0)
				&& ((relbox.min.z+dspeed.z) < zsize)
				&& ((relbox.max.z+dspeed.z) > 0.0)
			)
				return 0;
		}else if (relbox.max.x < 0.0) {
			return -1;
		}
	}

	/* check against y- */
	if (speed->y > 0.0) {
		if (relbox.max.y <= d) {
			dt = -relbox.max.y/speed->y;
			*dtime = dt;
			dspeed.x = speed->x*dt;
			dspeed.y = speed->y*dt;
			dspeed.z = speed->z*dt;
			if (
				((relbox.min.x+dspeed.x) < xsize)
				&& ((relbox.max.x+dspeed.x) > 0.0)
				&& ((relbox.min.z+dspeed.z) < zsize)
				&& ((relbox.max.z+dspeed.z) > 0.0)
			)
				return 1;
		}else if (relbox.min.y > ysize) {
			return -1;
		}
	/* check against y+ */
	}else if (speed->y < 0.0) {
		if (relbox.min.y >= (ysize-d)) {
			dt = (ysize-relbox.min.y)/speed->y;
			*dtime = dt;
			dspeed.x = speed->x*dt;
			dspeed.y = speed->y*dt;
			dspeed.z = speed->z*dt;
			if (
				((relbox.min.x+dspeed.x) < xsize)
				&& ((relbox.max.x+dspeed.x) > 0.0)
				&& ((relbox.min.z+dspeed.z) < zsize)
				&& ((relbox.max.z+dspeed.z) > 0.0)
			)
				return 1;
		}else if (relbox.max.y < 0) {
			return -1;
		}
	}

	/* check against z- */
	if (speed->z > 0.0) {
		if(relbox.max.z <= d) {
			dt = -relbox.max.z/speed->z;
			*dtime = dt;
			dspeed.x = speed->x*dt;
			dspeed.y = speed->y*dt;
			dspeed.z = speed->z*dt;
			if (
				((relbox.min.x+dspeed.x) < xsize)
				&& ((relbox.max.x+dspeed.x) > 0.0)
				&& ((relbox.min.y+dspeed.y) < ysize)
				&& ((relbox.max.y+dspeed.y) > 0)
			)
				return 2;
		}
	/* check against z+ */
	}else if (speed->z < 0.0) {
		if (relbox.min.z >= (zsize-d)) {
			dt = (zsize-relbox.min.z)/speed->z;
			*dtime = dt;
			dspeed.x = speed->x*dt;
			dspeed.y = speed->y*dt;
			dspeed.z = speed->z*dt;
			if (
				((relbox.min.x+dspeed.x) < xsize)
				&& ((relbox.max.x+dspeed.x) > 0.0)
				&& ((relbox.min.y+dspeed.y) < ysize)
				&& ((relbox.max.y+dspeed.y) > 0.0)
			)
				return 2;
		}
	}

	return -1;
}

/* checks if movingbox+y_increase would hit a ceiling */
static int collision_ceiling(array_t *staticboxes, aabox_t *movingbox, float y_increase, float d)
{
	int i;
	aabox_t *s;

	if (y_increase < 0)
		return 0;

	for (i=0; i<staticboxes->length; i++) {
		s = &((collisionblock_t**)staticboxes->data)[i]->box;
		if (!s)
			return 0;
		if (
			((movingbox->max.y-d) <= s->min.y)
			&& ((movingbox->max.y+y_increase) > s->min.y)
			&& (movingbox->min.x < s->max.x)
			&& (movingbox->max.x > s->min.x)
			&& (movingbox->min.z < s->max.z)
			&& (movingbox->max.z > s->min.z)
		)
			return 1;
	}

	return 0;
}

/* gets any block that could be collided with */
static int collision_get_nearby(array_t *hits, v3_t *pos, v3_t *speed, float dtime)
{
	/* TODO: this, once content*.c/h is done */
	return 0;
}

/* move pos by (speed+(accel*dtime))*dtime, checking for collisions */
/* TODO: this will need something changed for the client, so it doesn't use the server map */
collisionresult_t *collision_move(float pos_max_d, aabox_t *box, float stepheight, float dtime, v3_t *pos, v3_t *speed, v3_t accel)
{
	collisionresult_t *result;
	collisionblock_t *cb;
	collisioninfo_t *info = NULL;
	array_t hits;
	aabox_t mbox;
	aabox_t sbox;
	int i;
	int k;
	float d = pos_max_d * 1.1;

	if (d <= pos_max_d)
		return NULL;

	result = malloc(sizeof(collisionresult_t));
	if (!result)
		return NULL;

	result->touching_ground = 0;
	result->in_liquid = 0;
	result->touching_lethal = 0;
	result->collides = 0;
	result->collides_xz = 0;
	result->standing_on_unloaded = 0;
	result->collisions = NULL;

	if (dtime > 0.5)
		dtime = 0.5;

	speed->x += (accel.x*dtime);
	speed->y += (accel.y*dtime);
	speed->z += (accel.z*dtime);

	/* no movement == no collision */
	if (speed->x == 0.0 && speed->y == 0.0 && speed->z == 0.0)
		return result;

	if (speed->x > 5000.0) {
		speed->x = 5000.0;
	}else if (speed->x < -5000.0) {
		speed->x = -5000.0;
	}
	if (speed->y > 5000.0) {
		speed->y = 5000.0;
	}else if (speed->y < -5000.0) {
		speed->y = -5000.0;
	}
	if (speed->z > 5000.0) {
		speed->z = 5000.0;
	}else if (speed->z < -5000.0) {
		speed->z = -5000.0;
	}

	array_init(&hits, ARRAY_TYPE_PTR);

	if (!collision_get_nearby(&hits,pos,speed,dtime)) {
		array_free(&hits,0);
		return result;
	}

	for (i=0; i<100 && dtime>1e-10; ++i) {
		int nearest_collided = -1;
		float nearest_dtime = dtime;
		int nearest_boxindex = -1;

		mbox.min.x = box->min.x+pos->x;
		mbox.min.y = box->min.y+pos->y;
		mbox.min.z = box->min.z+pos->z;
		mbox.max.x = box->max.x+pos->x;
		mbox.max.y = box->max.y+pos->y;
		mbox.max.z = box->max.z+pos->z;

		for (k=0; k<hits.length; k++) {
			float dtime_tmp;
			int collided;
			cb = ((collisionblock_t**)hits.data)[k];
			if (cb->is_stepup)
				continue;

			collided = collision_aa(
				&cb->box,
				&mbox,
				speed,
				d,
				&dtime_tmp
			);

			if (collided == -1 || dtime_tmp >= nearest_dtime)
				continue;

			nearest_dtime = dtime_tmp;
			nearest_collided = collided;
			nearest_boxindex = k;
		}

		/* no collision */
		if (nearest_collided == -1) {
			pos->x += speed->x*dtime;
			pos->y += speed->y*dtime;
			pos->z += speed->z*dtime;
			dtime = 0;
			break;
		/* collision occurred */
		}else{
			cb = ((collisionblock_t**)hits.data)[nearest_boxindex];

			/* steps are not counted as a collision */
			if (
				(nearest_collided != 1)
				&& (mbox.min.y < cb->box.max.y)
				&& ((mbox.min.y+stepheight) > cb->box.max.y)
				&& (!collision_ceiling(&hits, &mbox, cb->box.max.y-mbox.min.y, d))
			) {
				cb->is_stepup = 1;
				continue;
			}else if (nearest_dtime >= 0.0) {
				pos->x += speed->x*nearest_dtime;
				pos->y += speed->y*nearest_dtime;
				pos->z += speed->z*nearest_dtime;
				dtime -= nearest_dtime;
			}else{
				if (nearest_collided == 0)
					pos->x += speed->x*nearest_dtime;
				if (nearest_collided == 1)
					pos->y += speed->y*nearest_dtime;
				if (nearest_collided == 2)
					pos->z += speed->z*nearest_dtime;
			}

			if (cb->is_unloaded)
				continue;

			if (!info)
				info = malloc(sizeof(collisioninfo_t));

			if (!info)
				break;

			collision_initinfo(info);

			info->pos = cb->pos;
			info->speed_o = *speed;

			switch (nearest_collided) {
			case 0:
				speed->x = 0;
				result->collides = 1;
				result->collides_xz = 1;
				break;
			case 1:
				speed->y = 0;
				result->collides = 1;
				break;
			case 2:
				speed->z = 0;
				result->collides = 1;
				result->collides_xz = 1;
				break;
			default:;
			}

			info->speed_n = *speed;
			if (math_distance(&info->speed_n,&info->speed_o) >= 0.1) {
				result->collisions = list_push(&result->collisions,info);
				info = NULL;
			}
		}
	}

	if (info)
		free(info);

	sbox.min.x = box->min.x+pos->x;
	sbox.min.y = box->min.y+pos->y;
	sbox.min.z = box->min.z+pos->z;
	sbox.max.x = box->max.x+pos->x;
	sbox.max.y = box->max.y+pos->y;
	sbox.max.z = box->max.z+pos->z;
	for (i=0; i<hits.length; i++) {
		cb = ((collisionblock_t**)hits.data)[i];

		/*
			See if the object is touching ground.

			Object touches ground if object's minimum Y is near node's
			maximum Y and object's X-Z-area overlaps with the node's
			X-Z-area.

			Use 0.15*BS so that it is easier to get on a node.
		*/
		if (
			(cb->box.max.x-d) > sbox.min.x
			&& (cb->box.min.x+d) < sbox.max.x
			&& (cb->box.max.z-d) > sbox.min.z
			&& (cb->box.min.z+d) < sbox.max.z
		) {
			if (cb->is_stepup) {
				float diff = (cb->box.max.y-sbox.min.y);
				pos->y += diff;
				sbox.min.y += diff;
				sbox.max.y += diff;
			}
			if (fabs(cb->box.max.y-sbox.min.y) < 0.15) {
				result->touching_ground = 1;
				if (cb->is_unloaded)
					result->standing_on_unloaded = 1;
			}
		}
	}

	array_free(&hits,0);

	return result;
}

/* free a collisionresult_t */
void collision_free(collisionresult_t *r)
{
	collisioninfo_t *i;
	if (!r)
		return;

	while ((i = list_pop(&r->collisions))) {
		free(i);
	}

	free(r);
}
