/* map-proc.m,
 */

#include <allegro.h>
#include <assert.h>
#include <math.h>
#include "adime/include/adime.h"
#include "common.h"
#include "group.h"
#include "gui/gui-clip.h"
#include "gui/gui-dirty.h"
#include "gui/gui-raid.h"
#include "gui/gui-widgets.h"
#include "gui/gui.h"
#include "map-editor/map-proc.h"
#include "map-editor/palette-proc.h"
#include "map-save.h"
#include "map.h"
#include "music.h"
#include "player.h"
#include "projectile.h"
#include "seborrhea/seborrhea.h"
#include "sound.h"
#include "tile.h"
#include "unit-intern.h"
#include "unit.h"
#include "units/all-units.h"


#define SCROLL_RATE	(TILE_H/2)
#define GRID_SIZE	16
#define GRID_SIZE_2	(GRID_SIZE/2)


/*--------------------------------------------------------------*/
/* Map panning.							*/
/*--------------------------------------------------------------*/

static void shift_list(List *list, double deltax, double deltay)
{
    ListIterator *it;
    Unit *unit;
    double ux, uy;

    foreach (it, list) {
        unit = [it getItem];
	[unit getX:&ux Y:&uy];
	ux += deltax;
	uy += deltay;
	if (uy < 0)
	    uy = 0;
	[unit setX:ux Y:uy];
    }
}

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

@implementation MapProc
- (void) setScrollbarPrecision
{
    if (3*screen_w > w)
	[hscrollbar setSliderPrecision:3*screen_w - w];
    [vscrollbar setSliderPrecision:[current_map length] * TILE_H - h/2];
}

- (id) setX:(int)x_ Y:(int)y_ W:(int)w_ H:(int)h_
{
    int sw, sh;
    [super setX:x_ Y:y_ W:w_ H:h_];

    sw = [vscrollbar maxWidth];
    sh = [hscrollbar maxHeight];

    [hscrollbar setX:x+sw Y:y+h-sh W:w-sw H:sh];
    [vscrollbar setX:x    Y:y      W:sw   H:h-sh];
    [self setScrollbarPrecision];

    return self;
}

- (int) minWidth  { return [hscrollbar minWidth]  + MAX(min_width,  [vscrollbar minWidth]); }
- (int) minHeight { return [vscrollbar minHeight] + MAX(min_height, [hscrollbar minHeight]); }

- (Widget *) getWidgetBounding:(int)mx :(int)my
{
    Widget *widget;

    widget = [hscrollbar getWidgetBounding:mx :my];
    if (widget) return widget;

    widget = [vscrollbar getWidgetBounding:mx :my];
    if (widget) return widget;

    return [super getWidgetBounding:mx :my];
}

- init
{
    the_map = [Map new];
    tagged_list = [LIST_NEW];
    assert(the_map && tagged_list);

    [[(Map*)[the_map resize:100] reset] setOffsetX:0 Y:0];
    if (!current_map)
        current_map = the_map;

    /* Scrollbars. */
    hscrollbar = [[[RaidHScrollbar newWithName:"MapProc HScrollBar"]
		       setHPadding:0 VPadding:1]
		     setParentWidget:self];
    vscrollbar = [[[RaidVScrollbar newWithName:"MapProc VScrollBar"]
		      setHPadding:1 VPadding:0]
		     setParentWidget:self];

    h_padding = 0;
    v_padding = 0;

    return [super init];
}

- free
{
    hscrollbar = [hscrollbar free];
    vscrollbar = [vscrollbar free];
    the_map = [the_map free];

    if (tagged_list) {
	[tagged_list removeAllItems];
	tagged_list = [tagged_list free];
    }

    return [super free];
}

- (BOOL) draw:(BITMAP *)bmp :(BOOL)marked_dirty
{
    ListIterator *it;
    clip_t old_clip;

    marked_dirty = [super draw:bmp :marked_dirty];
    
    /* Draw the scrollbars. */
    [hscrollbar draw:bmp :marked_dirty];
    [vscrollbar draw:bmp :marked_dirty];

    if (!marked_dirty)
	return NO;

    save_clip(bmp, &old_clip);
    set_clip(bmp, x+[vscrollbar width], y, x+w-1, y+h-[hscrollbar height]-1);

    /* Draw the map. */
    if ([the_map stars]) {
	[the_map drawParallaxLayer:bmp :NO];

	/* Don't draw the map tiles if we are in parallax mode. */
	if (editing_mode >= TILE_MODE)
	    [the_map drawTiles:bmp];
    }
    else
	[the_map drawTiles:bmp];

    /* Draw the units. */
    draw_units(bmp, ACTIVE_GROUND_LIST);
    draw_unit_shadows(bmp);
    draw_units(bmp, ACTIVE_AIR_LIST);

    /* Extra stats for the tagged unit. */
    foreach (it, tagged_list) {
        int rtm = text_mode(-1);
        [[it getItem] drawMapEditorExtras:bmp];
        text_mode(rtm);
    }

    if (snap_to_grid)
	[self drawGrid:bmp];

    /* Centre/Boundaries. */
    vline(bmp, x-offsetX+screen_w/2, y, y+h, makecol(0xff, 0x80, 0x80));
    vline(bmp, x-offsetX+screen_w+160, y, y+h, makecol(0xff, 0x80, 0x80));
    vline(bmp, x-offsetX-160, y, y+h, makecol(0xff, 0x80, 0x80));

    /* If sufficiently large, show how big the screen is. */
    if (h > screen_h) {
        int yy = y + h / 2;
        hline(bmp, x, yy - screen_h / 2, x + w - 1, makecol(0xff, 0x80, 0x80));
        hline(bmp, x, yy + screen_h / 2, x + w - 1, makecol(0xff, 0x80, 0x80));
    }

    /* Will we spawn stars? */
    if ([current_map stars])
	textout(bmp, font, "Stars", x+20, y, makecol(0xff, 0x80, 0x80));

    restore_clip(bmp, &old_clip);

    return YES;
}

- (void) drawGrid:(BITMAP *)bmp
{
    int c1 = makecol(0xff, 0xc0, 0x00);
    int c2 = makecol(0x40, 0x40, 0x40);
    int xx, yy, x_, y_;

    xx = (int)[the_map offsetX] % GRID_SIZE;
    yy = (int)[the_map offsetY] % GRID_SIZE;

    for (y_ = -yy; y_ < y + h; y_ += GRID_SIZE)
	for (x_ = -xx; x_ < x + w; x_ += GRID_SIZE) {
	    putpixel(bmp, x_,   y_,   c1);
	    putpixel(bmp, x_-1, y_,   c2);
	    putpixel(bmp, x_+1, y_,   c2);
	    putpixel(bmp, x_,   y_-1, c2);
	    putpixel(bmp, x_,   y_+1, c2);
	}
}

- (void) deleteUnit:(Unit *)unit
{
    if (!unit)
        return;

    [tagged_list removeItem:unit];

    destroy_unit(unit);
    flags |= FLAG_DIRTY;
}

- (void) doResizeMap
{
    int new_height = [the_map length];

    start_autopoll_music();
    if (adime_dialogf("Resize Map",
		      ADIME_ALIGN_CENTRE, ADIME_ALIGN_CENTRE, 50,
		      "New map length (>=50):%int[50,]", &new_height) != 2) {
	[the_map resize:new_height];
	[self setScrollbarPrecision];
	flags |= FLAG_DIRTY;
    }
    stop_autopoll_music();
}

- (void) doPanMap
{
    int deltay = 0;

    start_autopoll_music();
    if (adime_dialogf("Pan Map",
		      ADIME_ALIGN_CENTRE, ADIME_ALIGN_CENTRE, 100,
		      "Pan by n tiles:%int[,]", &deltay) != 2) {
	[the_map pan:deltay];
	shift_list(active_air_list,    0, deltay * TILE_H);
	shift_list(active_ground_list, 0, deltay * TILE_H);
	flags |= FLAG_DIRTY;
    }
    stop_autopoll_music();
}

- (void) processSpecialKeys:(int)data
{
#define TOGGLE_FLAG_FOR_TAGGED(f)			\
    foreach (it, tagged_list)				\
	[[it getItem] toggleFlag:f]

    ListIterator *it;
    Unit *unit;
    int i = 0;

    /* There must be a better was to do this.  Oh well. */

    /* [a] Set activation line. */
    if (data == KEY_A) {
	int abs = 1;
	clear_keybuf();
	start_autopoll_music();
	if (adime_dialogf("Set Activation Line",
			  ADIME_ALIGN_CENTRE, ADIME_ALIGN_CENTRE, 100,
			  "Activation Line (-480, 480):%int[-480,480]"
			  "Absolute:%bool[]",
			  &i, &abs) != 2) {
	    foreach (it, tagged_list) {
		unit = [it getItem];
		if ([unit conformsTo:@protocol(VariableActivationLine)])
		    [(<VariableActivationLine>)unit setActivationLine:i :abs];
	    }
	}
	stop_autopoll_music();
    }

    /* [d] Set travel range. */
    elif (data == KEY_D) {
	clear_keybuf();
	start_autopoll_music();
	if (adime_dialogf("Set Travel Range",
			  ADIME_ALIGN_CENTRE, ADIME_ALIGN_CENTRE, 100,
			  "Range:%int[0,]", &i) != 2) {
	    foreach (it, tagged_list) {
		unit = [it getItem];
		if ([unit conformsTo:@protocol(VariableTravelRange)])
		    [(<VariableTravelRange>)unit setTravelRange:i];
	    }
	}
	stop_autopoll_music();
    }

    /* [g] Set unit group. */
    elif (data == KEY_G) {
	/* Find first unit which can be grouped, and take it's y value
	   as the default group number. */
	foreach (it, tagged_list) {
	    unit = [it getItem];
	    if ([unit conformsTo:@protocol(Groupable)]) {
		double x_, y_;
		[unit getX:&x_ Y:&y_];
		i = y_;
		break;
	    }
	}

	clear_keybuf();
	start_autopoll_music();
	if (adime_dialogf("Set Group",
			  ADIME_ALIGN_CENTRE, ADIME_ALIGN_CENTRE, 100,
			  "Group number:%int[,]", &i) != 2) {
	    foreach (it, tagged_list) {
		unit = [it getItem];
		if ([unit conformsTo:@protocol(Groupable)])
		    [(<Groupable>)unit setGroup:i];
	    }
	}
	stop_autopoll_music();
    }

    /* [1-5] Primary/secondary/tertiary/health/nuke powerups. */
    elif (data == KEY_1) TOGGLE_FLAG_FOR_TAGGED(FLAG_DEATH_SPAWN_PRIMARY_POWERUP);
    elif (data == KEY_2) TOGGLE_FLAG_FOR_TAGGED(FLAG_DEATH_SPAWN_SECONDARY_POWERUP);
    elif (data == KEY_3) TOGGLE_FLAG_FOR_TAGGED(FLAG_DEATH_SPAWN_TERTIARY_POWERUP);
    elif (data == KEY_4) TOGGLE_FLAG_FOR_TAGGED(FLAG_DEATH_SPAWN_HEALTH_POWERUP);
    elif (data == KEY_5) TOGGLE_FLAG_FOR_TAGGED(FLAG_DEATH_SPAWN_NUKE_POWERUP);
    elif (data == KEY_6) TOGGLE_FLAG_FOR_TAGGED(FLAG_ONLY_SPAWN_IN_TWO_PLAYER);
    else return;

    flags |= FLAG_DIRTY;

#undef TOGGLE_FLAG_FOR_TAGGED
}

- (void) loadMap:(const char *)filename
{
    [tagged_list removeAllItems];
    load_map(filename, YES);
    [self setScrollbarPrecision];
    flags |= FLAG_DIRTY;
}

- (void) loadMapDialog
{
    char filename[PATH_MAX] = "maps/";

    start_autopoll_music();
    if (adime_file_select("Load...", filename, RAID_MAP_EXTENSION, sizeof filename, 600, 400))
	[self loadMap:filename];
    stop_autopoll_music();
}

- (void) saveMap
{
    char filename[PATH_MAX] = "maps/";

    start_autopoll_music();
    if (adime_file_select("Save as...", filename, RAID_MAP_EXTENSION, sizeof filename, 600, 400))
	save_map(filename);
    stop_autopoll_music();
}

- (void) selectMapMusic 
{
    char filename[PATH_MAX] = "music/";

    start_autopoll_music();
    if (adime_file_select("Play which directory during gameplay?", filename, "mp3", sizeof filename, 600, 400)) {
	/* I assume the music is inside music/. */
	[current_map setMusicDirname:filename];
    }
    stop_autopoll_music();
}

- (int) HSliderToOffset:(int)data { return data - screen_w; }
- (void) offsetToHSlider:(int)off { [hscrollbar setHandlePosition:off + screen_w]; }
- (int) VSliderToOffset:(int)data { return data; }
- (void) offsetToVSlider:(int)off { [vscrollbar setHandlePosition:off]; }

- (void) receiveMessage:(int)msg :(int)data
{
    ListIterator *it;
    Unit *unit;
    int x_, offset_x, prev_offset_x;
    int y_, offset_y, prev_offset_y;

    offset_x = prev_offset_x = [the_map offsetX];
    offset_y = prev_offset_y = [the_map offsetY];
    x_ = mouse_x + offset_x;
    y_ = mouse_y + offset_y;

    if (snap_to_grid) {
        x_ += GRID_SIZE_2 * ((x_ > 0) ? 1 : -1);
        y_ += GRID_SIZE_2 * ((y_ > 0) ? 1 : -1);
	x_ -= x_ % GRID_SIZE;
	y_ -= y_ % GRID_SIZE;
    }

    switch (msg) {
	case MSG_KEY_PRESSED:
	    if (data == KEY_SPACE) {
		snap_to_grid = !snap_to_grid;
                flags |= FLAG_DIRTY;
            }
            elif (data == KEY_DEL) {
		ListIterator *nx;
		foreach_nx (it, nx, tagged_list) {
		    unit = [it getItem];
		    nx = [it next];
		    [self deleteUnit:unit];
		}
	    }
            elif (data == KEY_ESC) {
		[tagged_list removeAllItems];
                flags |= FLAG_DIRTY;
            }
	    elif (data == KEY_L) [self loadMapDialog];
	    elif (data == KEY_S) [self saveMap];
	    elif (data == KEY_R) [self doResizeMap];
	    elif (data == KEY_P) [self doPanMap];
	    elif (data == KEY_F3) [self selectMapMusic];
	    elif (data == KEY_F4) {
		[current_map setStars:![current_map stars]];
		flags |= FLAG_DIRTY;
	    }
		
            elif (![tagged_list isEmpty])
                [self processSpecialKeys:data];
	    break;

	case MSG_KEY_HELD:
	    if (data == KEY_HOME)  offset_y = 0;
	    if (data == KEY_END)   offset_y = [the_map length] * 64 - h;
	    if (data == KEY_PGUP)  offset_y -= SCROLL_RATE * 5;
	    if (data == KEY_PGDN)  offset_y += SCROLL_RATE * 5;
	    if (data == KEY_LEFT)  offset_x -= SCROLL_RATE;
	    if (data == KEY_RIGHT) offset_x += SCROLL_RATE;
	    if (data == KEY_UP)    offset_y -= SCROLL_RATE;
	    if (data == KEY_DOWN)  offset_y += SCROLL_RATE;

	    if (![tagged_list isEmpty]) {
                double ux, uy, ua;	/* Unit x, y, angle. */
		double xx, yy, aa;	/* Displacements. */

		xx = yy = aa = 0.0;
		if (data == KEY_4_PAD) xx--;
		if (data == KEY_6_PAD) xx++;
		if (data == KEY_8_PAD) yy--;
		if (data == KEY_2_PAD) yy++;
		if (data == KEY_7_PAD) aa += deg2rad(1.0);
		if (data == KEY_9_PAD) aa -= deg2rad(1.0);

		if (!xx && !yy && !aa)
		    break;

		foreach (it, tagged_list) {
		    unit = [it getItem];

		    [unit getX:&ux Y:&uy];
		    ua = [unit angle];

		    ux += xx;
		    uy += yy;
		    ua += aa;

		    if (ua <= -M_PI) ua += 2 * M_PI;
		    if (ua >   M_PI) ua -= 2 * M_PI;

		    [[unit setX:ux Y:uy] setAngle:ua];
		}
                flags |= FLAG_DIRTY;
            }
	    break;

	case MSG_MOUSE_PRESSED:
	    last_mouse_x = mouse_x;
	    last_mouse_y = mouse_y;

	    if (editing_mode <= TILE_MODE)
		break;

	    if (data == 0) {	/* Left button. */
		Unit *unit = findUnitAt(x_, y_);
		
                flags |= FLAG_DIRTY;

		if (unit) {
		    /* Shift-click toggles this unit's tagged status. */
		    if (key_shifts & KB_SHIFT_FLAG) {
			if (![tagged_list removeItem:unit])
			    [tagged_list insertItem:unit];
		    }
		    /* CTRL-click selects the clicked unit class. */
		    elif (key_shifts & KB_CTRL_FLAG) {
			if (editing_mode == GROUND_MODE)
			    selected_ground_class = [unit class];
			else
			    selected_air_class = [unit class];

			[map_palette receiveMessage:MSG_RESELECT];
		    }
		    /* Plain-click replaces the tagged units to current. */
		    else {
			[tagged_list removeAllItems];
			[tagged_list insertItem:unit];
		    }
		}
		else {
                    if (editing_mode == GROUND_MODE)
                        unit = spawn_unit(selected_ground_class, x_, y_, ACTIVE_GROUND_LIST, YES);
                    else
                        unit = spawn_unit(selected_air_class, x_, y_, ACTIVE_AIR_LIST, YES);

		    /* Non-shift-click replaces tagged list. */
		    if (!(key_shifts & KB_SHIFT_FLAG))
			[tagged_list removeAllItems];
		    [tagged_list insertItem:unit];
                }
	    }

	    elif (data == 1) {	/* Right button. */
		Unit *unit = findUnitAt(x_, y_);
		[self deleteUnit:unit];
            }
	    break;

	case MSG_MOUSE_HELD:
	    if (data == 0) {	/* Left button. */
		if (ABS(mouse_x - last_mouse_x) < 5 &&
		    ABS(mouse_y - last_mouse_y) < 5)
		    break;

		if (editing_mode == PARALLAX_MODE) {
		    if ([the_map setParallaxTileAt:x_/TILE_W :y_/TILE_H to:lmb_tile])
			flags |= FLAG_DIRTY;
		}
		elif (editing_mode == TILE_MODE) {
		    if ([the_map setTileAt:x_/TILE_W :y_/TILE_H to:lmb_tile])
			flags |= FLAG_DIRTY;
		}
		elif (![tagged_list isEmpty]) {
		    double xx = -999, yy = -999, ux, uy;

		    foreach (it, tagged_list) {
			unit = [it getItem];

			[unit getX:&ux Y:&uy];
			if (xx == -999)
			    xx = ux, yy = uy;

			if ([unit reditSetX:x_ + ux - xx Y:y_ + uy - yy])
			    flags |= FLAG_DIRTY;
		    }
		}
	    }

	    elif (data == 1) {	/* Right button. */
		if (editing_mode == PARALLAX_MODE) {
		    if ([the_map setParallaxTileAt:x_/TILE_W :y_/TILE_H to:nil])
			flags |= FLAG_DIRTY;
		}
		elif (editing_mode == TILE_MODE) {
		    if ([the_map setTileAt:x_/TILE_W :y_/TILE_H to:rmb_tile])
			flags |= FLAG_DIRTY;
		}
		else {
		    double xx, yy, theta;
		    
		    if (ABS(mouse_x - last_mouse_x) < 5 &&
			ABS(mouse_y - last_mouse_y) < 5)
			break;
		    
		    if (!tagged_list)
			break;
		    
		    /* Rotate units to mouse. */
		    foreach (it, tagged_list) {
			unit = [it getItem];
			
			[unit getX:&xx Y:&yy];
			theta = atan2(yy - y_, x_ - xx);
			theta = deg2rad((int)rad2deg(theta)); /* Bluddy dogdy. */
			[unit setAngle:theta];
		    }
		    
		    flags |= FLAG_DIRTY;
		}
            }

	    elif (data == 2) {	/* Middle button. */
		offset_x += (mouse_x - last_mouse_x) / 10;
		offset_y += (mouse_y - last_mouse_y) / 10;
	    }
	    break;

	case MSG_MOUSE_WHEEL:
	    offset_y += SCROLL_RATE * data;
	    break;

	case MSG_MOUSE_ENTER:	/* XXX: what's this for, multiple maps? */
	    current_map = the_map;
	    /* Fall through. */

	default:
	    [super receiveMessage:msg :data];
	    break;
    }

    /* Clamp the offsets so we don't go too far off the map. */
    offset_x = MID(-screen_w, offset_x, 2*screen_w - w);
    offset_y = MID(-14, offset_y, [current_map length] * TILE_H - h/2);

    if ((offset_x != prev_offset_x) || (offset_y != prev_offset_y)) {
        [the_map setOffsetX:offset_x Y:offset_y];
	[self offsetToHSlider:offset_x];
	[self offsetToVSlider:offset_y];
	flags |= FLAG_DIRTY;
    }
}

- (void) receiveMessage:(int)msg :(int)data fromChild:(Widget *)child
{
    int offset_x = [the_map offsetX];
    int offset_y = [the_map offsetY];
    (void)child;

    if (msg == MSG_HSLIDER)
	offset_x = [self HSliderToOffset:data];
    elif (msg == MSG_VSLIDER)
	offset_y = [self VSliderToOffset:data];

    [the_map setOffsetX:offset_x Y:offset_y];
    flags |= FLAG_DIRTY;
}

- (void) setSelectedLMBItem:(int)data
{
    if (editing_mode <= TILE_MODE)
	lmb_tile = get_tile_by_index(data);
    elif (editing_mode == GROUND_MODE)
	selected_ground_class = ground_units_table[data];
    elif (editing_mode == AIR_MODE)
	selected_air_class = air_units_table[data];
}

- (void) setSelectedRMBItem:(int)data
{
    if (editing_mode <= TILE_MODE)
	rmb_tile = get_tile_by_index(data);
}

- (Class) getSelectedUnitClass
{
    if (editing_mode == GROUND_MODE)
	return selected_ground_class;
    else
	return selected_air_class;
}
@end

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

@interface Phony: Projectile
@end

@implementation Phony
- init
{
    alliance = YES;
    return self;
}

- (void) getX:(double *)x_ Y:(double *)y_ W:(int *)w_ H:(int *)h_;
{
    if (x_) *x_ = x;
    if (y_) *y_ = y;
    if (w_) *w_ = 2;
    if (h_) *h_ = 2;
}
@end

static Unit *unit_in_contact_with(Thing<DetectsCollision> *object, List *list)
{
    ListIterator *it;

    foreach (it, list) {
	Unit *unit = [it getItem];

	/* Use "obj collidesWith:unit" so that projectiles will
	   determine if a collision takes place.  This allows railguns
	   to do stuff. */
	if ([object collidesWith:unit]) {
	    [it free];
	    return unit;
	}
    }

    return nil;
}

Unit *findUnitAt(double x, double y)
{
    Phony *phony = [[Phony new] setX:x Y:y];
    Unit *unit;

    if (editing_mode == GROUND_MODE)
	unit = unit_in_contact_with(phony, active_ground_list);
    else
	unit = unit_in_contact_with(phony, active_air_list);

    [phony free];
    return unit;
}
