/*(LGPL)
 * Project Spitfire sprites + scrolling engine for SDL
----------------------------------------------------------------------
	cs.c - Simplistic Control System
----------------------------------------------------------------------
 * Copyright (C) 2001, 2003, 2007 David Olofson
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#define	DBG(x)

#include "cs.h"
#include "logger.h"
#include <stdio.h>
#include <math.h>
#include <string.h>

/*#define ABS(x)   (((x)>=0) ? (x) : (-(x)))*/
#define ABS(x)	labs(x)


static inline void __wrap_point(cs_point_t *p, int wx, int wy)
{
	if(wx)
	{
		if(p->gx < 0)
			p->gx += (-p->gx / wx + 1) * wx;
		p->gx %= wx;
	}
	if(wy)
	{
		if(p->gy < 0)
			p->gy += (-p->gy / wy + 1) * wy;
		p->gy %= wy;
	}
}


/*
 * Make one point do it's thing...
 */
static inline void __move_point(cs_point_t *p, int wx, int wy)
{
	/* 
	 * Push filter
	 */
	p->ox = p->v.x;
	p->oy = p->v.y;

	/* 
	 * Update position and speed
	 */
	p->v.x += p->v.xv;
	p->v.y += p->v.yv;
	p->v.xv += p->v.xa;
	p->v.yv += p->v.ya;

	__wrap_point(p, wx, wy);
}


/*
 * Update the motion filtered output of a point.
 */
static inline void __update_point_f(cs_point_t *p, int ff, int wx, int wy)
{
	int ogx = p->gx;
	int ogy = p->gy;
	int ox = p->ox;
	int oy = p->oy;

	/* Unwrap filter for correct interpolation */
	if(wx | wy)
	{
		if((ox - p->v.x) > (wx >> 1))
			ox -= wx;
		else if((p->v.x - ox) > (wx >> 1))
			ox += wx;
		if((oy - p->v.y) > (wy >> 1))
			oy -= wy;
		else if((p->v.y - oy) > (wy >> 1))
			oy += wy;
	}

	/* 
	 * Interpolate output graphics coordinate
	 */
	p->gx = (ff * p->v.x + (256 - ff) * ox) >> 8;
	p->gy = (ff * p->v.y + (256 - ff) * oy) >> 8;

	/* 
	 * Wrap interpolated output
	 */
	if(wx)
	{
		while(p->gx < 0)
			p->gx += wx;
		p->gx %= wx;
	}
	if(wy)
	{
		while(p->gy < 0)
			p->gy += wy;
		p->gy %= wy;
	}

	/* 
	 * Generate "changed" flag mask.
	 */
	p->changed = (ogx ^ p->gx) | (ogy ^ p->gy);
}


static inline void __update_point(cs_point_t *p, int ff)
{
	int ogx = p->gx;
	int ogy = p->gy;

	/* 
	 * Interpolate output graphics coordinate
	 */
	p->gx = p->v.x;
	p->gy = p->v.y;

	/* 
	 * Generate "changed" flag mask.
	 */
	p->changed = (ogx ^ p->gx) | (ogy ^ p->gy);
}


/*
----------------------------------------------------------------------
 * cs_obj_t
----------------------------------------------------------------------
 */

/*
 * Final adjustment of interpolated graphics coords to deal
 * with the display window wrapping over the edge of the world.
 */
static inline void __fix_wrap(cs_engine_t *e, cs_obj_t *o)
{
	if(o->point.gx + (o->w << __CS_SHIFT) < 0)
		o->point.gx += e->wx;
	else if(o->point.gx > (e->w << __CS_SHIFT))
		o->point.gx -= e->wx;
	if(o->point.gy + (o->h << __CS_SHIFT) < 0)
		o->point.gy += e->wy;
	else if(o->point.gy > (e->h << __CS_SHIFT))
		o->point.gy -= e->wy;
}


static inline int __onscreen(cs_engine_t *e, cs_obj_t *o)
{
	int gx = CS2PIXEL(o->point.gx);
	int gy = CS2PIXEL(o->point.gy);
	if((gx + o->w < 0) || (gx > e->w))
		return 0;
	if((gy + o->h < 0) || (gy > e->h))
		return 0;
	return 1;
}


static inline void __obj_set_layer(cs_obj_t *o, unsigned int layer)
{
	if(o->layer < 0)
		o->layer = CS_DEFAULT_LAYER;
	if(layer >= CS_LAYERS)
		layer = CS_LAYERS - 1;
	o->layer = layer;
}

/*
 * Add object *first*, ie with lowest priority.
 * (Hmmm... Isn't that how some h/w sprite generators do it,
 * if you allocate in increasing channel order?)
 */
static inline void __obj_attach(cs_obj_t *o)
{
	if(o->head || o->next || o->prev)
	{
		log_printf(ELOG, "cs: HEEEELP! Someone's trying"
				" to short-circuit my guts!\n");
		return;
	}

	if(o->layer < 0)
		o->layer = CS_DEFAULT_LAYER;
	o->next = o->owner->objects[o->layer];
	o->head = &o->owner->objects[o->layer];
	if(o->next)
		o->next->prev = o;
	o->owner->objects[o->layer] = o;
}

/*
 * BEWARE! There's a nasty hack in here; these objects need to
 * keep track of the addresses of their list heads in order to
 * remove themselves properly!
 */
static inline void __obj_detach(cs_obj_t *o)
{
	/* "Bypass" */
	if(o->next)
		o->next->prev = o->prev;
	if(o->prev)
		o->prev->next = o->next;
	/* In case we're the first object in the list... */
	if(o->head)
		if(*(o->head) == o)
			*(o->head) = o->next;

	o->head = NULL;
	o->next = NULL;
	o->prev = NULL;
}


void cs_obj_layer(cs_obj_t *o, unsigned int layer)
{
	if(layer == o->layer)
		return;

	__obj_set_layer(o, layer);
	if(o->flags | CS_OBJ_ACTIVE)
	{
		__obj_detach(o);
		__obj_attach(o);
	}
}


void cs_obj_activate(cs_obj_t *o)
{
	if(o->flags | CS_OBJ_ACTIVE)
	{
		DBG(log_printf(DLOG, "cs: Tried to activate active object!\n");)
		return;
	}

	__obj_attach(o);
	o->flags |= CS_OBJ_ACTIVE;
}


void cs_obj_deactivate(cs_obj_t *o)
{
	if(!(o->flags | CS_OBJ_ACTIVE))
	{
		DBG(log_printf(DLOG, "cs: Tried to deactivate passive object!\n");)
		return;
	}

	__obj_detach(o);
	o->flags &= ~CS_OBJ_ACTIVE;
}


/* Basic methods */

void cs_obj_pos(cs_obj_t *o, int x, int y)
{
	o->point.v.x = PIXEL2CS(x);
	o->point.v.y = PIXEL2CS(y);
}


void cs_obj_vel(cs_obj_t *o, int xs, int ys)
{
	o->point.v.xv = xs;
	o->point.v.yv = ys;
}


/*Set movement acceleration*/
void cs_obj_acc(cs_obj_t *o, int xa, int ya)
{
	o->point.v.xa = xa;
	o->point.v.ya = ya;
}


/*Disable display*/
void cs_obj_hide(cs_obj_t *o)
{
	o->flags &= ~CS_OBJ_VISIBLE;
}


/*Enable display*/
void cs_obj_show(cs_obj_t *o)
{
	if(o->flags & CS_OBJ_VISIBLE)
		return;

	o->flags |= CS_OBJ_VISIBLE;
	/* 
	 * Kludge? I'm not sure this is really the right place...
	 */
	cs_point_force(&o->point);
}


/*
 * Remove and throw back in the pool
 */
void cs_obj_free(cs_obj_t *o)
{
	if(o->head == &o->owner->pool)
	{
		DBG(log_printf(DLOG, "cs: Tried to free free object %p!\n",
					o);)
		return;
	}
	cs_obj_deactivate(o);
	if(o->on_free)
		o->on_free(o);
	cs_obj_clear(o);

	/* Set up head pointer for error checking. */
	o->head = &o->owner->pool;

	/* Link to pool; singly only */
	o->next = o->owner->pool;
	o->owner->pool = o;

	++o->owner->pool_free;
}

void cs_obj_clear(cs_obj_t *o)
{
	o->flags = 0;
	o->layer = -1;
	o->fire.rate = 0;
	o->score = 0;
	o->health = 0;
}

void cs_obj_vector(cs_obj_t *o, cs_vector_t * v)
{
	if(v)
		o->point.v = *v;
	else
		memset(&(o->point.v), 0, sizeof(o->point.v));
}



/* Animation methods */
void cs_obj_anim(cs_obj_t *o, int bank,
		int first, int num, int start, int spd)
{
	o->anim.bank = bank;
	o->anim.frame = first;
	o->anim.fframe = first;
	o->anim.aframes = num;
	o->anim.cframe = start << 4;
	o->anim.aspeed = spd;
	o->flags |= CS_OBJ_DOANIM;
}


void cs_obj_image(cs_obj_t *o, int bank, int frame)
{
	o->anim.bank = bank;
	o->anim.frame = frame;
	o->flags &= ~CS_OBJ_DOANIM;
	if((bank >= 0) && (bank < o->owner->nimageinfo))
	{
		o->w = o->owner->imageinfo[bank].w;
		o->h = o->owner->imageinfo[bank].h;
	}
	else
		o->w = o->h = 1;
}


/*Start death sequence*/
void cs_obj_explode(cs_obj_t *o)
{
	o->flags |= CS_OBJ_DYING;
/*
FIXME: Hardcoded bank for explosion!!!
*/
	cs_obj_anim(o, 4, 0, 15, 0, 12);
	if(o->on_dying)
		o->on_dying(o);
}

void cs_obj_shoot(cs_obj_t *o, int xo, int yo, int xs,
		int ys, int rate, int delay, int ammo)
{
	o->fire.v.x = xo;
	o->fire.v.y = yo;
	o->fire.v.xv = xs;
	o->fire.v.yv = ys;
	o->fire.rate = rate;
	o->fire.timer = delay;
	o->fire.ammo = ammo;
}



/*
 * cs_engine_t
 */

static cs_obj_t *__new_obj(cs_engine_t *e)
{
	cs_obj_t *o = calloc(1, sizeof(cs_obj_t));
	if(!o)
		return NULL;

	o->owner = e;
	o->prev = NULL;
	o->next = NULL;
	cs_obj_clear(o);
	++e->pool_total;
	return o;
}

cs_engine_t *cs_engine_create(int w, int h, int objects)
{
	cs_engine_t *e = calloc(1, sizeof(cs_engine_t));
	if(!e)
		return NULL;

	cs_engine_set_size(e, w, h);
	cs_engine_set_wrap(e, 0, 0);

	/* 
	 * Create some objects and throw into the pool.
	 * Note that "pool_free" gets initialized by
	 * cs_obj_die(), objects are initialized and so
	 * on, this way - automatically!
	 */
	while(objects--)
	{
		cs_obj_t *o = __new_obj(e);
		if(o)
			cs_obj_free(o);
		else
		{
			/* Oops, no memory... */
			cs_engine_delete(e);
			return NULL;
		}
	}
	return e;
}


cs_obj_t *cs_engine_get_obj(cs_engine_t *e)
{
	cs_obj_t *o = e->pool;
	if(o)
	{
		e->pool = o->next;
		o->head = NULL;
		o->prev = NULL;
		o->next = NULL;
		--e->pool_free;
	}
	return o;
}


void cs_engine_set_size(cs_engine_t *e, int w, int h)
{
	e->w = w;
	e->h = h;
}


void cs_engine_set_image_size(cs_engine_t *e, int bank, int w, int h)
{
	if(bank < 0)
		return;

	if(bank >= e->nimageinfo)
	{
		int nnii = bank + 16;
		image_info_t *nii = realloc(e->imageinfo,
				sizeof(image_info_t) * nnii);
		if(!nii)
			return;

		e->nimageinfo = nnii;
		e->imageinfo = nii;
	}

	e->imageinfo[bank].w = w;
	e->imageinfo[bank].h = h;
}


void cs_engine_set_wrap(cs_engine_t *e, int x, int y)
{
	e->wx = PIXEL2CS(x);
	e->wy = PIXEL2CS(y);
}


void cs_engine_delete(cs_engine_t *e)
{
	cs_obj_t *o;
	/* First get all objects to the pool... */
	cs_engine_reset(e);
	/* ...then empty the pool. */
	while((o = cs_engine_get_obj(e)))
		free(o);
	free(e->imageinfo);
	free(e);
}


void cs_engine_reset(cs_engine_t *e)
{
	int i;
	for(i = 0; i < CS_LAYERS; ++i)
		while(e->objects[i])
			cs_obj_free(e->objects[i]);
	e->time = 0.0;
}



/*
----------------------------------------------------------------------
 * The actual *engine* :-)
----------------------------------------------------------------------
 */

void __enemy_fire(cs_obj_t *o)
{
	log_printf(DLOG, "fire!\n");
}

void __make_move(cs_obj_t *o, int wx, int wy)
{
	/* 
	 * Movement
	 */
	__move_point(&o->point, wx, wy);

	/* 
	 * Animation
	 */
	if(o->flags & CS_OBJ_DOANIM)
	{
/*
FIXME: Dirty hack here... Should play *any* death animation one-shot and then die.
*/
		if((o->flags & CS_OBJ_DYING)
				&& (o->anim.cframe >> 4 == 14))
			cs_obj_free(o);

		o->anim.cframe += o->anim.aspeed;
		o->anim.cframe %= o->anim.aframes << 4;
		o->anim.frame = (o->anim.cframe >> 4) + o->anim.fframe;
	}

	/* 
	 * Fire / spawning
	 */
	if(!(o->flags & CS_OBJ_DYING) && o->fire.rate)
	{
		if(o->fire.timer > 0)
			--o->fire.timer;
		else
		{
			o->fire.timer = o->fire.rate;
			__enemy_fire(o);
			if(o->on_fire)
				o->on_fire(o);
		}
	}
}


void __update_points(cs_engine_t *e, float frac_frame)
{
	cs_obj_t *o;
	int i;
	int ff = frac_frame * 256.0;

	if(ff < 0)
		ff = 0;
	else if(ff > 256)
		ff = 256;

	if(e->filter)
		for(i = 0; i < CS_USER_POINTS; ++i)
			__update_point_f(&e->points[i], ff, e->wx, e->wy);
	else
		for(i = 0; i < CS_USER_POINTS; ++i)
			__update_point(&e->points[i], ff);

	for(i = 0; i < CS_LAYERS; ++i)
	{
		if(e->filter)
			__update_point_f(&e->offsets[i], ff, e->wx, e->wy);
		else
			__update_point(&e->offsets[i], ff);
		e->changed[i] = 0;
		o = e->objects[i];
		while(o)
		{
			if(e->filter)
				__update_point_f(&o->point, ff, e->wx,
						e->wy);
			else
				__update_point(&o->point, ff);
			o->point.gx -= e->offsets[i].gx;
			o->point.gy -= e->offsets[i].gy;
			e->changed[i] |= o->point.changed;
			__fix_wrap(e, o);
			o = o->next;
		}
	}
}


void __run_all(cs_engine_t *e)
{
	int i;
	cs_obj_t *o;

	for(i = 0; i < CS_USER_POINTS; ++i)
		__move_point(&e->points[i], e->wx, e->wy);

	for(i = 0; i < CS_LAYERS; ++i)
	{
		__move_point(&e->offsets[i], e->wx, e->wy);
		o = e->objects[i];
		while(o)
		{
			__make_move(o, e->wx, e->wy);
			o = o->next;
		}
	}
}


void __wrap_all(cs_engine_t *e)
{
	int i;
	cs_obj_t *o;

	for(i = 0; i < CS_USER_POINTS; ++i)
		__wrap_point(&e->points[i], e->wx, e->wy);

	for(i = 0; i < CS_LAYERS; ++i)
	{
		__wrap_point(&e->offsets[i], e->wx, e->wy);
		o = e->objects[i];
		while(o)
		{
			__wrap_point(&o->point, e->wx, e->wy);
			o = o->next;
		}
	}
}


void cs_engine_advance(cs_engine_t *e, double to_frame)
{
	if(to_frame > 0)
	{
		int frames = floor(to_frame) - floor(e->time);
		if(frames > 0)
		{
			while(frames--)
			{
				__run_all(e);
				e->on_frame(e);
			}
		}
	}
	e->time = to_frame;
	if(e->wx || e->wy)
		__wrap_all(e);
	__update_points(e, to_frame - floor(to_frame));
}


void cs_engine_render(cs_engine_t *e)
{
	cs_obj_t *o;
	int i;

	for(i = CS_LAYERS - 1; i >= 0; --i)
	{
		o = e->objects[i];
		while(o)
		{
			if(o->flags & CS_OBJ_VISIBLE)
				if(o->render)
					if(__onscreen(e, o))
						o->render(o);
			o = o->next;
		}
	}
}
