/* unit.m,
 *
 * The base unit class and other unit related stuff like handling
 * updating, shadow creation, etc.
 */

#include <allegro.h>
#include <assert.h>
#include <math.h>
#ifndef NO_FBLEND
# include "fblend/include/fblend.h"
#endif
#include "common.h"
#include "debri.h"
#include "debris/explosion.h"
#include "debris/large-chunk.h"
#include "debug.inc"
#include "init.h"
#include "linklist.h"
#include "map.h"
#include "nuke.h"
#include "player.h"
#include "powerup.h"
#include "projectile.h"
#include "rotate.h"
#include "rottrans.h"
#include "seborrhea/seborrhea-allegro.h"
#include "seborrhea/seborrhea.h"
#include "sound.h"
#include "unit.h"
#include "unit-intern.h"
#include "units/all-units.h"


Unit<Boss> *boss;


#define MAX4(alpha,beta,gamma,delta)		(MAX(alpha, MAX(beta, MAX(gamma, delta))))
#define MIN4(alpha,beta,gamma,delta)		(MIN(alpha, MIN(beta, MIN(gamma, delta))))
#define SWAP(alpha,beta)		{ double gamma = alpha; alpha = beta; beta = gamma; }

/*--------------------------------------------------------------*/

@implementation Unit
        /* Initialization. */
+ (BOOL) loadPrerequisites { return YES; }

- init
{
    [super init];

    angle = -M_PI_2;
    speed = 0.0;

    sprite = [base_sebum getSebumByName:"dummy48x48"];

    flags = 0;
    chunk_colours = CHUNK_GRAY;

    return self;
}

- (id) setAngle:(double)theta { angle = theta; return self; }
- (id) setX:(double)x_ Y:(double)y_ { x = x_; y = y_; return self; }
- (void) delete { flags |= FLAG_DEAD; }

- (void) spawnDyingExplosions
{
    /* Bosses can replace this function for bigger, longer explosions. */
    if (health < -5)
        [self delete];

    if (health % 2 == 0)
	spawn_debris([BigExplosion class], x, y, HIGH_LAYER);

    health--;
    flash_tics += 2;
}

- (void) die
{
#define SPAWN_CHUNK(c)		spawn_debris([c class], x, y, HIGH_LAYER)
#define SPAWN_POWERUP(c)	spawn_powerup([c class], x, y)

    int n;

    for (n = 0; n < debri_amount+1; n++) {
	/* Determine which chunk colours to spawn */
	if (chunk_colours & CHUNK_GRAY)	     SPAWN_CHUNK(LargeChunk);
	if (chunk_colours & CHUNK_BLUE)	     SPAWN_CHUNK(LargeChunkBlue);
	if (chunk_colours & CHUNK_COFFEE)    SPAWN_CHUNK(LargeChunkCoffee);
	if (chunk_colours & CHUNK_GREEN)     SPAWN_CHUNK(LargeChunkGreen);
	if (chunk_colours & CHUNK_RED)	     SPAWN_CHUNK(LargeChunkRed);
	if (chunk_colours & CHUNK_SARDAUKAR) SPAWN_CHUNK(LargeChunkSardaukar);
    }
    health = 0;
    flags |= (FLAG_DYING|FLAG_INVINCIBLE);

    /* Some units give powerups when they die. */
    if (flags & FLAG_DEATH_SPAWN_PRIMARY_POWERUP)   SPAWN_POWERUP(PrimaryPowerup);
    if (flags & FLAG_DEATH_SPAWN_SECONDARY_POWERUP) SPAWN_POWERUP(SecondaryPowerup);
    if (flags & FLAG_DEATH_SPAWN_TERTIARY_POWERUP)  SPAWN_POWERUP(TertiaryPowerup);
    if (flags & FLAG_DEATH_SPAWN_HEALTH_POWERUP)    SPAWN_POWERUP(HealthPowerup);
    if (flags & FLAG_DEATH_SPAWN_NUKE_POWERUP)	    SPAWN_POWERUP(NukePowerup);

    /* Sounds. */
    play_explosion_sample(x);

#undef SPAWN_POWERUP
#undef SPAWN_CHUNK
}

	/* Drawing. */
- (void) draw:(BITMAP *)dest
{
    int x_, y_;

    if (not sprite) {
	printf("%s is missing its sprite!\n", [[self class] name]);
	flags |= FLAG_DEAD;
	game_flags |= FLAG_LEVEL_COMPLETE;
	return;
    }

    if (flash_tics > 0)
	flash_tics--;

    x_ = x - offsetX;
    y_ = y - offsetY;

    if (flash_tics % 4 >= 2) {		/* Flicker. */
	if (rotatable_unit)
	    [sprite drawTo:dest X:x_ Y:y_ Tint:0xff:0xc0:0xa0:0xc0 Angle:angle];
	else {
	    x_ -= [sprite width]/2;
	    y_ -= [sprite height]/2;
	    [sprite drawTo:dest X:x_ Y:y_ Tint:0xff:0xc0:0xa0:0xc0];
	}
    }
    else {				/* Normal drawing routines. */
        if (rotatable_unit)
	    [sprite drawTo:dest X:x_ Y:y_ Angle:angle];
        else
	    [super draw:dest];
    }
}

- (void) drawShadow:(BITMAP *)dest
{
    double x_, y_;
    int w_, h_;

    if (not shadow)
	return;

    x_ = (x * 3 / 4) + (screen_w / 8) - offsetX;
    y_ = y - offsetY;
    y_ = y_ + SHADOW_DISPLACEMENT * (1 - (2*y_/screen_h));

    if (shadow) {
	w_ = [shadow width]/2;
	h_ = [shadow height]/2;

	if (rotatable_unit)
	    [shadow drawTo:dest X:x_ Y:y_ Alpha:0x60 Angle:angle];
	else
	    [shadow drawTo:dest X:x_-w_ Y:y_-h_ Alpha:0x60];
    }
}

	/* Update. */
- (BOOL) readyToActivate
{
    /* Units which are still in the inactive list are queried here
       whether or not they are ready to be moved into the active list
       (ie. activate).

       If any one of the following conditions apply, the unit is
       considered to activate off the screen:
        1) x < 0
	2) x > screen_w
	3) y + activate_y >= top of screen

       These units will delay activation until it they top of the
       screen hits their activation line. This means they are not
       drawn, don't fire, etc. for the moment (intended behaviour). */
    int sprite_w_2 = [sprite width]/2;
    int sprite_h_2 = [sprite height]/2;

    if ((x + sprite_w_2 < 0) ||
	(x - sprite_w_2 > screen_w) ||
	(activate_y <= -screen_h)) {
	if (y-sprite_h_2 + activate_y - SHADOW_DISPLACEMENT >= offsetY)
	    goto activate;
    }

    /* Other units activate as soon as they reach the top of the
       screen or earlier (given by a positive activate_y), but not
       later.  Remember to allow some space for the shadows. */
    else {
        if (y+sprite_h_2 + MAX(0, activate_y) + SHADOW_DISPLACEMENT >= offsetY)
            goto activate;
    }

    return NO;

 activate:
    /* If this unit is marked 2 player only, but only one player is
       alive, kill it. */
    if (flags & FLAG_ONLY_SPAWN_IN_TWO_PLAYER) {
	if (not player[0] || not player[1])
	    [self delete];
    }

    return YES;
}

- (BOOL) mayStillInfluenceGameplay
{
    /* Remember to leave some room for shadows! */
    int x_ = x - offsetX;
    int y_ = y - offsetY + SHADOW_DISPLACEMENT;
    int sprite_w_2 = [sprite width]/2;
    int sprite_h_2 = [sprite height]/2;
    BOOL above_screen = (y_+sprite_h_2 < 0);
    BOOL below_screen = (y_-sprite_h_2 > screen_h + 50);
    double yv = speed*sin(angle);

    if ((x_ + sprite_w_2 < -160) || (x_ - sprite_w_2 > screen_w + 160))
	return NO;

    /* We are facing up, are pass the top of the screen and moving
       faster than the map is scrolling. */
    else if (above_screen && (angle > 0) && (-yv < [current_map scrollRate]))
	return NO;

    else if (below_screen) {
	/* We are facing down. */
	if (angle < 0)
	    return NO;
	
	/* We are travelling up slower than the map scrolls. */
	else if (-yv > [current_map scrollRate])
	    return NO;
    }

    return YES;
}

- (int) update
{
    /* It has now died.  Remove it from the lists. */
    if (flags & FLAG_DEAD)
	return THING_DEAD;

    /* Spawn some explosions while it still dying. */
    if (flags & FLAG_DYING) {
	[self spawnDyingExplosions];

	/* It has now died.  Remove it from the lists. */
	if (flags & FLAG_DEAD)
	    return THING_DEAD;
    }

    /* Only fire if we are not dying, and players alive. */
    else if ((flags & FLAG_FIRING_ENABLED) &&
	     (game_flags & FLAG_PLAYERS_ALIVE))
	[self fire];

    /* Move if we want. */
    if (flags & FLAG_MOVING_ENABLED)
        [self move];
    else
        [self enableMovement];

    /* Don't call [super update] because we don't want to remove
       things who are still spawning explosions, but have 0 health. */
    if ([self mayStillInfluenceGameplay])
	return THING_NORMAL;
    else
	return THING_DEAD;
}

- (void) enableMovement
{
    /* Units with -screen_w < activate_y < 0 want to begin their
       movement not immediately, but before scrolling off the screen.
       Make these move at the right time. */
    int sprite_h_2 = [sprite height] / 2;

    if (y + sprite_h_2 + activate_y + SHADOW_DISPLACEMENT >= offsetY)
        flags |= FLAG_MOVING_ENABLED;

    /* XXX: rotated units. */
}

- (void) move
{
    x += speed * cos(angle);
    y -= speed * sin(angle);
}

- (void) fire {}

	/* Collisions. */
- (int) flags { return flags; }
- (int) collisionLists { return ALLY_LIST|COLLIDES_WITH_PROJECTILES_AND_NUKES; }
- (void) getX:(double *)x_ Y:(double *)y_ { *x_ = x; *y_ = y; }

- (BOOL) collidesWithRotatedUnit:(Thing<DetectsCollision> *)object
{
    /* Collision detection between a rotated unit (self) and a
       non-rotated unit (object).

       XXX: objw/objh not used yet.  All rotated units should use
       this, when objw/h is used.  (Only miniboss1 and boss1 are using
       this ATM). */
    double x1, x2, x3, x4, y1, y2, y3, y4;
    double objx, objy;
    int objw, objh;

    [object getX:&objx Y:&objy W:&objw H:&objh];

    /* Shift origin to self's (x,y) */
    objx -= x;
    objy -= y;

    rotate(-(w/2.0), -(h/2.0), -angle, &x1, &y1); /* Top left.     */
    rotate( (w/2.0), -(h/2.0), -angle, &x2, &y2); /* Top right.    */
    rotate(-(w/2.0),  (h/2.0), -angle, &x3, &y3); /* Bottom left.  */
    rotate( (w/2.0),  (h/2.0), -angle, &x4, &y4); /* Bottom right. */

    /*
		    /\ (x2,y2)
		   /  \ (x4,y4)
		  /   /
		 / . / <-- origin
	(x1,y1)	/   /
		\  /
		 \/ (x3, y3)
    */

    if (y1 > y3) {
	SWAP(x1, x4); SWAP(x2, x3);
	SWAP(y1, y4); SWAP(y2, y3);
    }

    /* Check 1: objy below the line made by (x1,y1) (x2,y2) */
    if (objy < (objx-x1) * (y2-y1)/(x2-x1) + y1)
	return NO;

    /* Check 2: objy above the line made by (x3,y3) (x4,y4) */
    if (objy > (objx-x3) * (y4-y3)/(x4-x3) + y3)
	return NO;

    /* Check 3: objx right of  line made by (x1,y1) (x3,y3) */
    if (objx < (objy-y1) * (x3-x1)/(y3-y1) + x1)
	return NO;

    /* Check 4: objx left  of  line made by (x2,y2) (x4,y4) */
    if (objx > (objy-y2) * (x4-x2)/(y4-y2) + x2)
	return NO;

    return YES;
}

- (BOOL) collidesWith:(Thing<DetectsCollision> *)object
{
    double x1, y1, x2, y2;
    int w1, h1, w2, h2;

    if (flags & FLAG_DYING)
	return NO;

    [self   getX:&x1 Y:&y1 W:&w1 H:&h1];
    [object getX:&x2 Y:&y2 W:&w2 H:&h2];

    return ((x1 - w1 / 2 <= x2 + w2 / 2) && (x1 + w1 / 2 >= x2 - w2 / 2) &&
	    (y1 - h1 / 2 <= y2 + h2 / 2) && (y1 + h1 / 2 >= y2 - h2 / 2));
}

- (void) getW:(int *)w_ H:(int *)h_
{
    /* Don't collide with things if you are dead. */
    if ((flags & FLAG_DYING) || (flags & FLAG_DEAD)) {
	*w_ = 0;
	*h_ = 0;
    }
    else if (rotatable_unit) {
	double x1, y1, x2, y2, x3, y3, x4, y4;

	rotate(x-w/2., y-h/2., angle, &x1, &y1);
	rotate(x+w/2., y-h/2., angle, &x2, &y2);
	rotate(x-w/2., y+h/2., angle, &x3, &y3);
	rotate(x+w/2., y+h/2., angle, &x4, &y4);

	if (w_) *w_ = MAX4(x1, x2, x3, x4) - MIN4(x1, x2, x3, x4);
	if (h_) *h_ = MAX4(y1, y2, y3, y4) - MIN4(y1, y2, y3, y4);
    }
    else {
	if (w_) *w_ = w;
	if (h_) *h_ = h;
    }
}

- (void) getX:(double *)x_ Y:(double *)y_ W:(int *)w_ H:(int *)h_
{
    [self getX:x_ Y:y_];
    [self getW:w_ H:h_];
}

- (int) receiveDamage:(int)damage type:(enum DAMAGE_TYPE)type
{
    (void)type;

    /* Enemy ships can't be repaired. */
    if (damage <= 0 || flags & FLAG_INVINCIBLE || flags & FLAG_DYING)
	return 0;
    else {
	int ret = MIN(damage, health);
	health -= ret;
	flash_tics = 15;
        if (health <= 0)
            [self die];
	return ret;
    }
}

- (int) receiveDamage:(int)damage type:(enum DAMAGE_TYPE)type from:(int)unit_id
{
    int ret = [self receiveDamage:damage type:type];

    if (ret && unit_id >= 0 && player[unit_id]) {
	int score_multiplier;

	if (type == DAMAGE_TYPE_NUKE)
	    score_multiplier = 10;
	else
	    score_multiplier = 100;

	[player[unit_id] increaseScore:ret*score_multiplier];
	if (flags & FLAG_DYING)
	    [player[unit_id] incrementKillsFrom:x :y];
    }

    return ret;
}

	/* Load/Save.  Format: x;y;a[;flags]. */
- (void) importUnitProperties:(char *)str
{
    double x_, y_, theta;
    int f_;

    /* For flags, only keep flags below 0xff.  The others depend on
       whether they were saved or not.  Note: flags is optional.  */
    flags = flags & 0xff;
    if (sscanf(str, "%lf;%lf;%lf;%d", &x_, &y_, &theta, &f_) == 4)
	flags |= f_;

    /* Pass it through setX:Y: since some objects may want to do fancy
       things depending on the initial location (mirroring paths). */
    [[self setX:x_ Y:y_] setAngle:deg2rad(theta)];
}

- (char *) exportUnitProperties:(char *)str
{
    int f;
    snprintf(str, 1024, "%g;%g;%g", x, y, rad2deg(angle));

    f = flags - (flags & 0xff);

    /* Any flags we want to save. */
    if (f) {
	char str2[1024];
	snprintf(str2, sizeof(str2), "%s;%d", str, f);
	strncpy(str, str2, sizeof(str2));
    }

    return str;
}

	/* For use in map editor only! */
- (void) drawTo:(BITMAP *)dest at:(int)x_ :(int)y_
{
    int w_, h_;
    assert(sprite);

    w_ = [sprite width];
    h_ = [sprite height];

    if (w_ <= 64 && h_ <= 64) {
	x_ += (64-w_)/2;
	y_ += (64-h_)/2;
	[sprite drawTo:dest X:x_ Y:y_];
    }
    else {				/* XXX */
	double ratio = MIN(64.0/w_, 64.0/h_);

	if ([sprite isKindOf:[SebImageAllegro class]])
	    stretch_sprite(dest, [(SebImageAllegro *)sprite bitmap], x_, y_, w_*ratio, h_*ratio);
	else
	    [sprite drawTo:dest X:x_ Y:y_ W:64 H:64];
    }
}

- (void) drawMapEditorExtras:(BITMAP *)dest
{
    int c = makecol(0x40, 0xa0, 0x40);
    int x_ = x - offsetX, y_ = y - offsetY;
    int w_, h_, r;

    [self getW:&w_ H:&h_];
    r = MAX(w_, h_) / 2;

    /* Circle to show tagged-ness, and the angle. */
    circle(dest, x_, y_, r+5, makecol(0x80, 0xff, 0xff));
    line(dest, x_, y_, x_+r*cos(angle), y_-r*sin(angle), makecol(0xff, 0x80, 0x80));

    /* Some quantitative information about our unit. */
    textprintf_centre(dest, font, x_, y_+h_/2, c, "%s (%g, %g @ %gdeg)",
		      [self name], x, y, rad2deg(angle));

    {				/* Powerups. */
	char str[1024] = "Powerups: \0";

	if (flags & FLAG_DEATH_SPAWN_PRIMARY_POWERUP)
	    strcat(str, "Primary ");
	if (flags & FLAG_DEATH_SPAWN_SECONDARY_POWERUP)
	    strcat(str, "Secondary ");
	if (flags & FLAG_DEATH_SPAWN_TERTIARY_POWERUP)
	    strcat(str, "Tertiary ");
	if (flags & FLAG_DEATH_SPAWN_HEALTH_POWERUP)
	    strcat(str, "Health ");
	if (flags & FLAG_DEATH_SPAWN_NUKE_POWERUP)
	    strcat(str, "Nuke");
	if (strcmp(str, "Powerups: ") != 0)
	    textout_centre(dest, font, str, x_, y_+h_/2+10, c);
    }

    if (flags & FLAG_ONLY_SPAWN_IN_TWO_PLAYER)
	textout_centre(dest, font, "(2p)", x_, y_-h_/2-10, c);
}

- (BOOL) reditSetX:(double)x_ Y:(double)y_
{
    if ((x == x_) && (y == y_))
	return NO;

    [self setX:x_ Y:y_];
    return YES;
}

- (void) toggleFlag:(int)f { flags ^= f; }
- (double) angle; { return angle; }
@end

/*--------------------------------------------------------------*/
/* Drawing.							*/
/*--------------------------------------------------------------*/

static inline void draw_units_in_list(BITMAP *dest, List *list)
{
    ListIterator *it;

    foreach (it, list) {
	[[it getItem] draw:dest];

#ifdef DEBUG_BOUNDING_BOX
	[[it getItem] drawBoundingBox:dest];
#endif
    }
}

void draw_units(BITMAP *dest, enum UNIT_LIST list)
{
    if (list == ACTIVE_AIR_LIST)
	draw_units_in_list(dest, active_air_list);
    elif (list == ACTIVE_GROUND_LIST)
	draw_units_in_list(dest, active_ground_list);
    elif (list == ALLY_LIST)
	draw_units_in_list(dest, ally_list);
}

void draw_unit_shadows(BITMAP *dest)
{
    ListIterator *it;

    foreach (it, active_air_list)
	[[it getItem] drawShadow:dest];

    foreach (it, ally_list)
	[[it getItem] drawShadow:dest];
}

/*--------------------------------------------------------------*/
/* Updating.							*/
/*--------------------------------------------------------------*/

static inline void update_inactive_list(List *inactive_list, List *active_list)
{
    ListIterator *it, *nx;

    foreach_nx (it, nx, inactive_list) {
	Unit *unit = [it getItem];
	nx = [it next];

        if ([unit readyToActivate]) {
	    [inactive_list removeItem:unit];
	    [active_list insertItem:unit];
	}
    }
}

static void update_active_list(List *list)
{
    ListIterator *it, *nx;
    int collision_lists;

    foreach_nx (it, nx, list) {
	Unit *unit = [it getItem];
	nx = [it next];

	collision_lists = [unit collisionLists];

	/* Check for collision with nukes. */
	if (collision_lists & COLLIDES_WITH_NUKES) {
	    Nuke *nuke = nuke_in_contact_with(unit);

	    if (nuke) {
		int d;

		/* Only receive the damage the unit received. */
		d = [unit receiveDamage:3 type:DAMAGE_TYPE_NUKE from:[nuke parentID]];
		[nuke receiveDamage:d];
	    }
	}

        /* Check for collision with allies in the air. */
	if (collision_lists & ALLY_LIST) {
            ListIterator *it2;

            foreach (it2, ally_list) {
		Unit *ally = [it2 getItem];

		/* I think this is a bogus check:
		    if (not ([ally collisionLists] & ACTIVE_AIR_LIST))
			continue;
		*/

                if ([unit collidesWith:ally]) {
		    /* Enemies receive double damage.  This is mainly
		       for satellites. */
                    [unit receiveDamage:2 type:DAMAGE_TYPE_UNIT];
                    [ally receiveDamage:1 type:DAMAGE_TYPE_UNIT];
                }
            }
	}

	if ([unit update] == THING_DEAD) {
	    if (unit == player[0]) {
		player[0] = NULL;
		/* Force the status bar to redraw. */
		game_flags |= FLAG_REDRAW_STATUSBARS;
	    }
	    else if (unit == player[1]) {
		player[1] = NULL;
		/* Force the status bar to redraw. */
		game_flags |= FLAG_REDRAW_STATUSBARS;
	    }
	    
	    [list removeItem:unit];
	    [unit free];
	}
    }
}

void update_units(void)
{
    /* Move inactive units on the screen to the active list. */
    update_inactive_list(ground_list, active_ground_list);
    update_inactive_list(air_list, active_air_list);

    /* Update the active enemies. */
    update_active_list(active_ground_list);
    update_active_list(active_air_list);

    /* Update the allies. */
    update_active_list(ally_list);
}

/*--------------------------------------------------------------*/
/* Creating/destroying units.					*/
/*--------------------------------------------------------------*/

Unit *spawn_unit(Class class, double x, double y,
		 enum UNIT_LIST list, BOOL at_start)
{
    Unit *unit;
    List *ll;
    assert(class);

    ll = unit_list_number_to_list(list);
    assert(ll && "Attempted to add unit in non-proper list.");

    unit = [[class new] setX:x Y:y];
    if (not unit)
	return nil;

    if (at_start)
	[ll insertItem:unit];
    else
	[ll insertItemAtEnd:unit];

    return unit;
}

void destroy_unit(Unit *unit)
{
    if ([active_air_list removeItem:unit]) {
	[unit delete];
	unit = [unit free];
	return;
    }

    if ([active_ground_list removeItem:unit]) {
	[unit delete];
	unit = [unit free];
	return;
    }
}

/*--------------------------------------------------------------*/
/* Init/shutdown.						*/
/*--------------------------------------------------------------*/

void unit_reset(void)
{
    unit_intern_shutdown();
    unit_intern_init();
    boss = nil;
}

void unit_init(void)
{
    unit_intern_init();
}

void unit_shutdown(void)
{
    boss = nil;
    unit_intern_shutdown();
    mark_all_unit_data_unnecessary();
}
