"use strict";

var DEFINE_GAMEOBJECT_INTERACTIONS = {
	GRAB : 1, // this object implements grab(), drop(), .held (bool), .weight (from MGO_Projectile), .impact (damage) and .hoistpoint[{x,y},{x,y}] and may be carried/thrown by having its Update() temporarily overridden (its IndisposedUpdate(), if present, will still be called while grabbed/thrown)
	PUSH : 2, // this object implements push(dx), which will be called when the player collides with it
	CHECK : 4, // this object implements checkEvent(source_GO), which will be called when the player inspects it, passed the player game object, and which returns a ScriptedEvent name (from DATA_SDS_SCRIPTED_EVENTS)
	TRIGGER : 8, // this object implements triggerEvent(source_GO), which will be called when the player touches it, passed the player game object, and which returns a ScriptedEvent name (from DATA_SDS_SCRIPTED_EVENTS)
	SIGNALABLE : 16, // this object implements signal(source_GO,...), a dedicated placeholder intended to process and react to the actions of e.g. switches and timers.  Does not necessarily accept or return a value, but may.
	DAMAGE : 32, // this object implements damage(source_GO,points), which will be called when a damage source collides with it, and which should return true if the object was destroyed by the damage, false otherwise
	INFLICT : 64, // this object implements inflict(), which returns the amount of {hp,chi} damage it deals to the player
	FORCE_CRUSH : 128, // this object will be moved when solid objects move into it, and implements crush(), which will be called when that move fails to change its position
	COLLECT : 256, // this object implements collect(source_GO), which will be called when the player touches it; collect() should remove this object from the scene after applying desired effects
	PORT : 512, // this object implements getDestName() which returns the name of the SDS_MAP_ROOM to load next, and relocate(sprite) which sets the passed sprite to its new entry/spawn point
	AUTOPORT : 1024, // as above, but triggers automatically on contact rather than on inspection
	CONTROL : 2048 // this object has a .controls (Controls) which determine some/all of its behavior, allowing player control and puppeting
}

/*
Container class for all the intractive scripting of things in the game.
All GameObject-derived classes must implement an Update() routine
which performs relevant game logic and will be called every frame.
*/
var GameObject = function(sprite, name){
	this.sprite = sprite;
	this.sprite.game_object = this; // WARNING: MUTUAL REFERNCE.  BE SURE TO BREAK THIS MANUALLY FOR GC!
	this.name = name ? name : "Nameless GO";
	this.manager = null;
	this.Update = noop;

	this.interactions = 0;

	this.sfx = null;

	this._checkevent = null;

	// KEEP IN MIND that anything initialized here but not overridden in
	// individual derivitive constructors will be shared across all instances
	// with the same prototype!  This shouldn't matter if you're using the
	// "heap" strictly for within-function local storage and initializing
	// all your heap vars correctly before use each time, but do beware
	// of odd value leaks if you don't!
	this._heap = {};
	this._heap.spriteMoveRes = new SpriteMoveResult(); // just generically useful
	this._heap.spritePhysicsMoveRes = new SpriteMoveResult(); // for applying e.g. gravity independent of controls

	this._heap.Update = {}; // for future
	this._heap.Update.spriteMoveRes = new SpriteMoveResult(); // just generically useful
}
	// TODO: update legacy room population so this is standard
	GameObject.prototype.addToRoom = function(mapRoom){
		mapRoom.addSprite(this.sprite);
	}
	GameObject.prototype.clearFromRoom = function(mapRoom){
		mapRoom.removeSprite(this.sprite);
	}
	// used by GameObjectManager after object is removed form management.
	// override if any more un-linking is needed than just the sprite
	GameObject.prototype.cleanup = function(){
		if (this.sprite.map_room) this.sprite.map_room.removeSprite(this.sprite);
		//if (this.manager) this.manager.remove(this); // will re-call cleanup jic, hence conditional checks
		if (this.sprite) this.sprite.game_object = null;
	}
	GameObject.prototype.playSound = function(sfxname){
		if (this.sfx){
			this.sfx.stop();
			this.sfx=null;
		}
		this.sfx = new Chiptune(sfxname,SDS.sfxchip);
		this.sfx.play(false);
	}
	
	GameObject.prototype.drawIntoCanvas = function(canvas){
		this.sprite.drawIntoCanvas(canvas);
	}

	GameObject.prototype.spawnOnSprite = function(sprite,objMgr,offx,offy){
		offx = offx ? offx : 0;
		offy = offy ? offy : 0;
		this.sprite.x = offx + sprite.x 
			//+ sprite.box_left
			//- this.sprite.box_left
			//+ ((sprite.box_right - sprite.box_left) - (this.sprite.box_right - this.sprite.box_left))>>1;
			
			//+ sprite.box_left>>1
			//+ sprite.box_right>>1
			//- this.sprite.box_left>>1
			//- this.sprite.box_right>>1;

			+ ((sprite.box_left
			+ sprite.box_right
			- this.sprite.box_left
			- this.sprite.box_right)>>1);

		this.sprite.y = offy + sprite.y 
			//+ sprite.box_top
			//- this.sprite.box_top
			//+ ((sprite.box_bottom - sprite.box_top) - (this.sprite.box_bottom - this.sprite.box_top))>>1;
			+ ((sprite.box_top
			+ sprite.box_bottom
			- this.sprite.box_top
			- this.sprite.box_bottom)>>1);

		sprite.map_room.addSprite(this.sprite);
		objMgr.add(this);
	};

	GameObject.prototype._makeCheckable = function(checkevent){
		this._checkevent = checkevent;
		this.interactions |= (checkevent ? DEFINE_GAMEOBJECT_INTERACTIONS.CHECK : 0);
	};
	GameObject.prototype.checkEvent = function(sourceGO){
		return this._checkevent;
	};

	// pushes/pops new override context layers onto an object,
	// cacheing old values in a metaVars object.
	// useful for metagameobjects and temporary takeovers.
	// this can get VERY ugly; the only suggestion I offer by way of
	// consistent best practice is to pushContext before you take over,
	// then put all your takeover vars in a new metaVars object,
	// which you clean up before you yourself pop context.
	// try VERY hard not to set up situations where one object might pop another's context :P
	// these functions use vars instead of _heap because no _heap is necessarily safe around a context shift.
	// Fortunately, they're called very rarely and shouldn't build up undue overhead for the GC.
	GameObject.prototype.pushContext = function(properties){
		if (this.onPushContext) this.onPushContext();
		this.contextVars = {
			"contextVars" : this.contextVars
		};

		for (var i=0; i<properties.length; ++i){
			this.contextVars[properties[i]] = this[properties[i]];
		}
	};
	GameObject.prototype.popContext = function(){
		if (this.contextVars){
			for (var i in this.contextVars){
				if (i!=="contextVars"){
					this[i] = this.contextVars[i];
				}
			}
			this.contextVars = this.contextVars.contextVars;
			if (this.onPopContext) this.onPopContext();
		}
	};
	GameObject.prototype.popAllContext = function(){
		while (this.contextVars) this.popContext();
	};
	GameObject.prototype.onPushContext = function(){
		// called before a context layer is pushed, in case the parent layer wants to respond
		// (implementations should override this as needed)
	};
	GameObject.prototype.onPopContext = function(){
		// called after a context layer is popped, in case the parent layer wants to respond
		// (implementations should override this as needed)
	};


	// if overridden to return true,
	// GameObjectManager's update loop will skip calling Update,
	// instead putting this GO into a bin to be checked again after all
	// other GOs have Updated.
	// The bin is not guaranteed to be ordered.
	// shouldDeferUpdate will be called again, and the GO may defer again,
	// an indefinite number of times.  I.e. this CAN cause a softlock
	// if a GO never enters a state where its shouldDeferUpdate returns false!
	// Note that because deferred updates are resolved after the main object list
	// has been traversed, objects added by a deferred Update will not be Updated
	// themselves until the next frame.
	// Removing objects in a deferred Update has not been tested.  The new code
	// in GameObjectManager._remove _should_ handle it ok, but I make no promises,
	// and in general do not advise trying unless you're ready to debug.
	GameObject.prototype.shouldDeferUpdate = function(){
		return false;
	};


	// helpers to apply common behavior to many GOs.
	// works in part due to the magic of ._heap and making
	// GOs carry their local Update vars with them.

	// swimming
	// requires movement arrays 
	//	leftward_move_water, 
	//	rightward_move_water, 
	//	upward_move_water,
	//	downward_move_water
	// requires animation states
	//	bob,
	//	swim
	// requires properties
	//	controls
	//	anim_state
	//	horizontal_move_index
	//	vertical_move_index
	//	direction
	GameObject.prototype.NPCSwim = function(initstate){

		this._heap.dx = 0;
		this._heap.dy = 0;


		this._heap.leftMoveArr = null;
		this._heap.rightMoveArr = null;
		this._heap.upMoveArr = null;
		this._heap.downMoveArr = null;

		this._heap.i=0;

		
		this.anim_state = (typeof(initstate)!=='undefined' && initstate!=null) ? initstate : this.ANIM_STATES.bob;

		this._heap.leftMoveArr = this.leftward_move_water;
		this._heap.rightMoveArr = this.rightward_move_water;
		this._heap.upMoveArr = this.upward_move_water;
		this._heap.downMoveArr = this.downward_move_water;

		if (this.controls.held.left){
			this.horizontal_move_index--;
			if (this.horizontal_move_index>0){
				this.horizontal_move_index--;
			}
			this.horizontal_move_index = Math.max(this.horizontal_move_index, -(this._heap.leftMoveArr.length)+1);
			this.direction=-1;
			this.anim_state = this.ANIM_STATES.swim;
		}
		else if (this.controls.held.right){
			this.horizontal_move_index++;
			if (this.horizontal_move_index<0){
				this.horizontal_move_index++;
			}
			this.horizontal_move_index = Math.min(this.horizontal_move_index, this._heap.rightMoveArr.length-1);
			this.direction=1;
			this.anim_state = this.ANIM_STATES.swim;
		}
		else{
			if (this.horizontal_move_index>0){
				this.horizontal_move_index--;
			}
			else if (this.horizontal_move_index<0){
				this.horizontal_move_index++;
			}
		}

		if (this.controls.held.up){
			this.vertical_move_index--;
			if (this.vertical_move_index>0){
				this.vertical_move_index--;
			}
			this.vertical_move_index = Math.max(this.vertical_move_index, -(this._heap.upMoveArr.length)+1);
			this.anim_state = this.ANIM_STATES.swim;
		}
		else if (this.controls.held.down){
			this.vertical_move_index++;
			if (this.vertical_move_index<0){
				this.vertical_move_index++;
			}
			this.vertical_move_index = Math.min(this.vertical_move_index, this._heap.downMoveArr.length-1);
			this.anim_state = this.ANIM_STATES.swim;
		}
		else{
			if (this.vertical_move_index>0){
				this.vertical_move_index--;
			}
			else if (this.vertical_move_index<0){
				this.vertical_move_index++;
			}
		}

		this._heap.dx = (this.horizontal_move_index>0) 
			? this._heap.rightMoveArr[this.horizontal_move_index]
			: this._heap.leftMoveArr[-(this.horizontal_move_index)];
		this._heap.dy = (this.vertical_move_index>0) 
			? this._heap.downMoveArr[this.vertical_move_index]
			: this._heap.upMoveArr[-(this.vertical_move_index)];

		this.sprite.move(this._heap.dx,this._heap.dy,  this._heap.spriteMoveRes);


		if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_H){
			this.horizontal_move_index = 0;

			if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_FLAGS.EJECTED_SH){
				for (this._heap.i=0; this._heap.i<this._heap.spriteMoveRes.ejections.length; this._heap.i++){
					if (this._heap.spriteMoveRes.ejections[this._heap.i].game_object.interactions & DEFINE_GAMEOBJECT_INTERACTIONS.PUSH){
						this._heap.spriteMoveRes.ejections[this._heap.i].game_object.push(this._heap.dx);
						//this.pushing=4;
					}
				}
			}

		}
		if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_V){
			this.vertical_move_index = 0;
		}

		if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_FLAGS.COLLIDING_S){
			
		}
	}

	// walking/jumping/falling
	// requires movement arrays 
	//	leftward_move_land, 
	//	rightward_move_land, 
	//	jump_rise_air,
	//	fall_air,
	//	leftward_move_water, 
	//	rightward_move_water, 
	//	jump_rise_water,
	//	fall_water,
	// requires animation states
	//	stand,
	//	walk,
	//	jump,
	//	fall
	// requires properties
	//	controls
	//	anim_state
	//	grounded
	//	in_water_cache
	//	horizontal_move_index
	//	jump_index
	//	fall_index
	//	direction
	GameObject.prototype.NPCWalk = function(initstate){

		this._heap.dx = 0;
		this._heap.dy = 0;


		this._heap.leftMoveArr = null;
		this._heap.rightMoveArr = null;
		this._heap.jumpArr = null;
		this._heap.fallArr = null;

		this._heap.i=0;

		this._heap.wasGrounded = this.grounded;
		
		this.anim_state = (typeof(initstate)!=='undefined' && initstate!=null) ? initstate : this.ANIM_STATES.stand;

		this._heap.wasInWater = this.in_water_cache;
		this.in_water_cache = this.sprite.isTouchingTerrain(DEFINE_MAP_TERRAIN_TYPES.WATER);
		if (this.in_water_cache != this._heap.wasInWater){
			this._heap.splash = new GO_Splash(0);
			this._heap.splash.spawnOnSprite(this.sprite,this.manager);
			this._heap.splash.sprite.y+=9;
			this._heap.splash=null; // until pooling implemented
		}

		this._heap.leftMoveArr = this.in_water_cache ? this.leftward_move_water : this.leftward_move_land;
		this._heap.rightMoveArr = this.in_water_cache ? this.rightward_move_water : this.rightward_move_land;
		this._heap.jumpArr = this.in_water_cache ? this.jump_rise_water : this.jump_rise_air;
		this._heap.fallArr = this.in_water_cache ? this.fall_water : this.fall_air;

		if (this.controls.held.left){
			this.horizontal_move_index--;
			if (this.horizontal_move_index>0){
				this.horizontal_move_index--;
			}
			this.direction=-1;
			this.anim_state = this.ANIM_STATES.walk;
		}
		else if (this.controls.held.right){
			this.horizontal_move_index++;
			if (this.horizontal_move_index<0){
				this.horizontal_move_index++;
			}
			this.direction=1;
			this.anim_state = this.ANIM_STATES.walk;
		}
		else{
			if (this.horizontal_move_index>0){
				this.horizontal_move_index--;
			}
			else if (this.horizontal_move_index<0){
				this.horizontal_move_index++;
			}
		}
	
		// in case our moveArr changed to a smaller one between frames and we're still coasting at a higher value
		this.horizontal_move_index = Math.max(this.horizontal_move_index, -(this._heap.leftMoveArr.length)+1);
		this.horizontal_move_index = Math.min(this.horizontal_move_index, this._heap.rightMoveArr.length-1);

		this._heap.dx = (this.horizontal_move_index>0) 
			? this._heap.rightMoveArr[this.horizontal_move_index]
			: this._heap.leftMoveArr[-(this.horizontal_move_index)];
		this._heap.dy = 0;


		this.sprite.move(this._heap.dx,this._heap.dy,  this._heap.spriteMoveRes);

		if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_H){
			this.horizontal_move_index = 0;
		}

		// quick check for grounded
		this.grounded = 
			!(this._heap.oldAnimState==this.ANIM_STATES.jump && this.jump_index < this._heap.jumpArr.length-1 && this.controls.held.A) &&
			(
				(this.sprite.map_room.findVertically(
					DEFINE_MAP_TERRAIN_TYPES.GROUND,
					this.sprite.x+this.sprite.bottom_left.x, this.sprite.y+this.sprite.bottom_left.y,
					1,
					1,
					this.sprite.constraints.x==DEFINE_SPRITE_MOVE_CONSTRAINT.WRAP
				) !=-1) ||
				(this.sprite.map_room.findVertically(
					DEFINE_MAP_TERRAIN_TYPES.GROUND,
					this.sprite.x+this.sprite.bottom_right.x, this.sprite.y+this.sprite.bottom_right.y,
					1,
					1,
					this.sprite.constraints.x==DEFINE_SPRITE_MOVE_CONSTRAINT.WRAP
				) !=-1)
			);

		if ((this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_FLAGS.EJECTED_SH) && this.grounded){
				for (this._heap.i=0; this._heap.i<this._heap.spriteMoveRes.ejections_h.length; this._heap.i++){
					if (this._heap.spriteMoveRes.ejections_h[this._heap.i].game_object.interactions & DEFINE_GAMEOBJECT_INTERACTIONS.PUSH){
						this._heap.spriteMoveRes.ejections_h[this._heap.i].game_object.push(this._heap.dx);
						//this.pushing=4;
					}
				}
			}

		// check for jump transition
		if (this.controls.pressed.A && (this._heap.wasGrounded || this._heap.in_water_cache)){
			this.jump_index = 0;
			this.anim_state = this.ANIM_STATES.jump;
		}
		else if (this._heap.oldAnimState == this.ANIM_STATES.jump && this.controls.held.A){
			this.jump_index++;
			this.anim_state = this.ANIM_STATES.jump;
		}
		else if (!this.grounded){
			this.jump_index = this._heap.jumpArr.length-1;
			this.anim_state = this.ANIM_STATES.fall;
			if (this._heap.oldAnimState!=this.ANIM_STATES.fall){
				this.fall_index = 0;
			}
		}

		if (this.anim_state == this.ANIM_STATES.jump){
			if (this.jump_index >= this._heap.jumpArr.length-1){
				this.anim_state = this.ANIM_STATES.fall;
				if (this._heap.oldAnimState!=this.ANIM_STATES.fall){
					this.fall_index = 0;
				}
			}
			else{
				// apply jump dy
				this._heap.dx = 0;
				this._heap.dy = this._heap.jumpArr[this.jump_index];

				this.sprite.move(this._heap.dx,this._heap.dy,  this._heap.spritePhysicsMoveRes);
				this.grounded=false; // to not confuse the flounder check

				if (this._heap.spritePhysicsMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_V){
					this.jump_index = this._heap.jumpArr.length-1;
					//...and trust physics will sort itself out next frame
				}
			}
		}
		if (this.anim_state == this.ANIM_STATES.fall){
			if (this._heap.oldAnimState==this.ANIM_STATES.fall){
				this.fall_index++;
				//this.fall_index = Math.min(this.fall_index, this._heap.fallArr.length-1);
			}
			// in case our moveArr changed to a smaller one between frames and we're still coasting at a higher value
			this.fall_index = Math.min(this.fall_index, this._heap.fallArr.length-1);
			// apply fall dy
			this._heap.dx = 0;
			this._heap.dy = this._heap.fallArr[this.fall_index];

			this.sprite.move(this._heap.dx,this._heap.dy,  this._heap.spritePhysicsMoveRes);

			// and next frame, if we're touching water or hit the grounded check, we'll transition anyway
			// no need to do it expressly here.
			if ((this._heap.spritePhysicsMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_V) ||
				(this.sprite.map_room.findVertically(
					DEFINE_MAP_TERRAIN_TYPES.GROUND,
					this.sprite.x+this.sprite.bottom_left.x, this.sprite.y+this.sprite.bottom_left.y,
					1,
					1,
					this.sprite.constraints.x==DEFINE_SPRITE_MOVE_CONSTRAINT.WRAP
				) !=-1) ||
				(this.sprite.map_room.findVertically(
					DEFINE_MAP_TERRAIN_TYPES.GROUND,
					this.sprite.x+this.sprite.bottom_right.x, this.sprite.y+this.sprite.bottom_right.y,
					1,
					1,
					this.sprite.constraints.x==DEFINE_SPRITE_MOVE_CONSTRAINT.WRAP
				) !=-1)){
				this.grounded = true;
				this.anim_state = (this.controls.held.left || this.controls.held.right) ? this.ANIM_STATES.walk : this.ANIM_STATES.stand;
			}
		}
	}

	// just motion, no state or direction
	// requires movement arrays 
	//	leftward_move, 
	//	rightward_move, 
	//	upward_move,
	//	downward_move
	// requires properties
	//	controls
	//	anim_state
	//	horizontal_move_index
	//	vertical_move_index
	GameObject.prototype.SimpleMove = function(){

		this._heap.dx = 0;
		this._heap.dy = 0;


		this._heap.leftMoveArr = null;
		this._heap.rightMoveArr = null;
		this._heap.upMoveArr = null;
		this._heap.downMoveArr = null;

		this._heap.i=0;

		this._heap.leftMoveArr = this.leftward_move;
		this._heap.rightMoveArr = this.rightward_move;
		this._heap.upMoveArr = this.upward_move;
		this._heap.downMoveArr = this.downward_move;

		if (this.controls.held.left){
			this.horizontal_move_index--;
			if (this.horizontal_move_index>0){
				this.horizontal_move_index--;
			}
			this.horizontal_move_index = Math.max(this.horizontal_move_index, -(this._heap.leftMoveArr.length)+1);
		}
		else if (this.controls.held.right){
			this.horizontal_move_index++;
			if (this.horizontal_move_index<0){
				this.horizontal_move_index++;
			}
			this.horizontal_move_index = Math.min(this.horizontal_move_index, this._heap.rightMoveArr.length-1);
		}
		else{
			if (this.horizontal_move_index>0){
				this.horizontal_move_index--;
			}
			else if (this.horizontal_move_index<0){
				this.horizontal_move_index++;
			}
		}

		if (this.controls.held.up){
			this.vertical_move_index--;
			if (this.vertical_move_index>0){
				this.vertical_move_index--;
			}
			this.vertical_move_index = Math.max(this.vertical_move_index, -(this._heap.upMoveArr.length)+1);
		}
		else if (this.controls.held.down){
			this.vertical_move_index++;
			if (this.vertical_move_index<0){
				this.vertical_move_index++;
			}
			this.vertical_move_index = Math.min(this.vertical_move_index, this._heap.downMoveArr.length-1);
		}
		else{
			if (this.vertical_move_index>0){
				this.vertical_move_index--;
			}
			else if (this.vertical_move_index<0){
				this.vertical_move_index++;
			}
		}

		this._heap.dx = (this.horizontal_move_index>0) 
			? this._heap.rightMoveArr[this.horizontal_move_index]
			: this._heap.leftMoveArr[-(this.horizontal_move_index)];
		this._heap.dy = (this.vertical_move_index>0) 
			? this._heap.downMoveArr[this.vertical_move_index]
			: this._heap.upMoveArr[-(this.vertical_move_index)];

		this.sprite.move(this._heap.dx,this._heap.dy,  this._heap.spriteMoveRes);


		if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_H){
			this.horizontal_move_index = 0;

			if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_FLAGS.EJECTED_SH){
				for (this._heap.i=0; this._heap.i<this._heap.spriteMoveRes.ejections.length; this._heap.i++){
					if (this._heap.spriteMoveRes.ejections[this._heap.i].game_object.interactions & DEFINE_GAMEOBJECT_INTERACTIONS.PUSH){
						this._heap.spriteMoveRes.ejections[this._heap.i].game_object.push(this._heap.dx);
					}
				}
			}

		}
		if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_V){
			this.vertical_move_index = 0;
		}

		if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_FLAGS.COLLIDING_S){
			
		}
	}

