package interfaces;

import java.awt.Color;
import java.awt.Graphics2D;
import java.util.HashSet;

import engine.Game;
import engine.Handler;
import mobs.Goblin;
import player.HealthAndDurability;
import player.Player;
import player.PlayerAttackSequence;
import utils.Circle;
import utils.Point2f;
import utils.Utils;
import world.Boulder;
import world.DroppedItem;
import world.Tile;
import world.Tree;

/**  ------------- Nihilne ------------
    *** @author Mitch Weaver - 2016 ***
      ~~~~ All Rights Reserved ~~~~ */

public abstract class Mob implements GameObject, Directions, ItemIDs, MobData, Python, Biomes, Colors {
	
	/** @param PLAYER: A default object to be a placeholder for keeping track of the player. This is necessary as the player is not an object, it is used staticaly.
	 *  @param GET_HIT_ANIMATION_TIMER: The length of time in ticks to show the get hit animation frame */
	public static enum MOB_ID { RABBIT, GOBLIN }
	public static enum ANIMATION { idleWalk, idle1, idle2, idle3, runAway, sleep; }
	public static final Object PLAYER = new Object();
	public static final int GET_HIT_ANIMATION_TIMER_AMOUNT = 40;
	
	/** @param destroyingTimer: needs to be AT LEAST your [death animation speed * death animation frames]
	 *  @param territory = is the total circle of territory each mob
	 *  @param hostile: isn't used yet! ... keeping it because it might be nice to have later 
	 *  @flag 'public' needs to be used to be accessible from Jython, methods that do not need to be accessed from Jython, but do need to be accessed from its java class, use 'protected'  */
	public final int MAX_HEALTH, width, height, combatLevel;
	public final float radius, aggroRadius;
	public int damage, facing = 3, destroyingTimer = 0, howFarToRunAwayTimer = 20;
	public float health, velX, velY, distanceToMove, walkingSpeed, runningSpeed;
	public boolean attacking, hostile, runningAway, discovered, sleeping, inCombat;
	public Point2f pos, center;
	public Circle home, territory, attackBounds;
	public MOB_ID id;
	public ANIMATION currentAnimation = ANIMATION.idleWalk;
	
	public Mob(MOB_ID id, Point2f pos, int width, int height, int homeRadius, int aggroRadius, boolean hostile, int damage, int combatLevel, int TERRITORY_ZONE_DISTANCE, int health){
		this.pos = pos; this.width = width; this.height = height; this.hostile = hostile; this.damage = damage; this.id = id; this.aggroRadius = aggroRadius; this.combatLevel = combatLevel;
		this.health = MAX_HEALTH = health;
		center = new Point2f(pos.x + width/2, pos.y + height/2);
		radius = width/2;
		
		home = new Circle(new Point2f(pos.x + width/2, pos.y + height/2), homeRadius);
		territory = new Circle(home.center, TERRITORY_ZONE_DISTANCE);
		distanceToMove = Utils.rand.nextInt((int)(home.radius * 1.25));
		Handler.mobs.add(this);
	}

// ------------------------------------- ABSTRACT METHODS ------------------------------------------------------------------------------------------------------------------------------------ //
	public abstract void tick();
	public abstract void render(Graphics2D g2d);
	public abstract void destroy();
	public abstract void runAnimations();
// ------------------------------------ END ABSTRACT METHODS ---------------------------------------------------------------------------------------------------------------------------------- //
	
// ----------------------------------- JYTHON STUFF ------------------------------------------------------------------------------------------------------------------------------------------//
	public static final void initAllPyScripts() {
		Goblin.initPyScripts();
	}
// -------------------------------- END JYTHON STUFF -----------------------------------------------------------------------------------------------------------------------------------------//

// ------------------------------------------------------- COLLISION ---------------------------------------------------------------------------------------------------------------------- //
	public boolean collided, justCollided, playerCollided, playerAggroCollided, treeCollided, boulderCollided, itemCollided, structureCollided, waterCollided;
	private int alternateCollision; private static final boolean STOP = true, DONT_STOP = false;
	public void collision() {
		playerAggroCollided = calcCollision(aggroRadius, Player.center, Player.radius, DONT_STOP);
		if(!playerAggroCollided) playerCollided = calcCollision(radius, Player.center, Player.radius, STOP);
		
		collideWithOtherMobs();
		
		if(--alternateCollision <= 0) alternateCollision = 3; else return; // helps cut down on the lag
		
		treeCollided = boulderCollided = structureCollided = itemCollided = false;
		
		for(final Structure i : Handler.structures) if(calcCollision(radius, i.center, i.radius, STOP)) { structureCollided = true; break; }
		if(!(velX == 0 && velY == 0)) { /* no need to calc hard collision if they're just chillin */
			for(final Tree i : Handler.trees)               if(i.discovered && Handler.AREA_RECT.contains(i.center.x, i.center.y) && calcCollision(radius, i.center, i.radius, STOP)) { treeCollided = true; break; }
			for(final Boulder i : Handler.boulders)         if(i.discovered && Handler.AREA_RECT.contains(i.center.x, i.center.y) && calcCollision(radius, i.center, i.radius, STOP)) { boulderCollided = true; break; }
			for(final DroppedItem i : Handler.droppedItems) if(i.discovered && Handler.AREA_RECT.contains(i.center.x, i.center.y) && calcCollision(radius, i.center, i.radius, DONT_STOP)) { itemCollided = true; break; }
		}
		
		if(playerCollided || treeCollided || boulderCollided || collided) justCollided = collided = true; else collided = false;
	}
	
	private float diffX, diffY, COMBINED_RADII; private int DISTANCEX, DISTANCEY;
	private boolean calcCollision(float RADIUS, Point2f p, float r, final boolean STOP) {
			if(Utils.intersects(center, RADIUS, p, r)) {
				if (STOP) {
					
					COMBINED_RADII = RADIUS + r;
					DISTANCEX = (int)Utils.abs(center.x - p.x);
					DISTANCEY = (int)Utils.abs(center.y - p.y);
					diffX = Utils.sqrt(COMBINED_RADII - DISTANCEX) / 2;
					diffY = Utils.sqrt(COMBINED_RADII - DISTANCEY) / 2;
					
					if (center.x > p.x) pos.x += diffX;  else pos.x -= diffX;
					if (center.y > p.y) pos.y += diffY;  else pos.y -= diffY;
					 
					center.x = pos.x + width/2;
					center.y = pos.y + height/2;
				}
				return true; 
			} return false;
	}
	
	private final void collideWithOtherMobs() {
		/** @loop != this ~~~ don't check for collision against themselves */
		for(final Mob i : Handler.mobs) if(i != this && i.discovered && Handler.AREA_RECT.contains(i.center.x, i.center.y))
			switch(id){
				/** @default: DEFAULT == STOP AGAINST ALL OTHER MOBS EXCEPT FOR SMALL CREATURES! */
				// NOTE: small animals should collide with themselves!
				case RABBIT: calcCollision(radius, i.center, i.radius, STOP);
				default: if(i.id != MOB_ID.RABBIT) { calcCollision(radius, i.center, i.radius, STOP); }
			}
	}
	
	public void fixBuggyCollision() {
		if(!justCollided) return;
	    justCollided = collided = false;
	    idleMove(walkingSpeed);
	}
// ------------------------------------------------------ END COLLISION -------------------------------------------------------------------------------------------------------------------- //

// ------------------------------------------------------ POSITION UPDATING ----------------------------------------------------------------------------------------------------------------- //
	/** @param alternateUpdatePosition: the alternate helps them slow down and stop bugging out all random directions 
	 *  @param tempFacing: Saves the last facing from the last successful tick. Keeps mobs from spinning around  */
	private int tempFacing = 1, alternateUpdatePosition;
	public void updatePosition() {
		if(collided) velX = velY = 0;
		
		// NO NEED TO UPDATE POSITION IF NOT MOVING
		if(!(velX == 0 && velY == 0)) {
		
			// LOOP THROUGH ALL TILES TO CHECK IF GOING INTO WATER
			outerLoop:
			for(final Tile i : Handler.currentTileSection(center.x, center.y)) {
				if(!territory.contains(i.x, i.y)) continue;
				switch(i.biome) {
					case Tile.OCEAN: { if(i.getBounds().contains(center.x + velX, center.y + velY)) { velX = velY = 0; collided = waterCollided = true; break outerLoop; }}
					case Tile.LAKE: {  if(i.getBounds().contains(center.x + velX, center.y + velY)) { velX = velY = 0; collided = waterCollided = true; break outerLoop; }}
					case Tile.RIVER: { if(i.getBounds().contains(center.x + velX, center.y + velY)) { velX = velY = 0; collided = waterCollided = true; break outerLoop; }}
				}
			}
			
			// IF MOVING IN BOTH X AND Y, NORMALIZE SPEED
			if (velX != 0 && velY != 0) { pos.x += velX / Utils.sqrtOf2; pos.y += velY / Utils.sqrtOf2; }
			// ELSE JUST ADD THE SINGULAR VELOCITY
			else { pos.x += velX; pos.y += velY; }
		
			// UPDATE CENTER
			center.x = pos.x + (width * 0.5f); center.y = pos.y + (height * 0.5f);
		}
			
			if(!inCombat) { // else facing is calc'd in attack sequence
				if(alternateUpdatePosition <= 0) {
					if(!(velX == 0 && velY == 0)){ /* if not moving, keep the tempFacing value */
						if (velX == 0 && velY < 0) 		 facing = NORTH;       else if (velX == 0 && velY > 0) facing = SOUTH;
						else if ( velX > 0 && velY == 0) facing = EAST;        else if (velX < 0 && velY == 0)        facing = WEST;
						else if (velX > 0 && velY > 0)   facing = SOUTHEAST;   else if (velX < 0 && velY < 0)  facing = NORTHWEST;
						else if (velX > 0 && velY < 0)   facing = NORTHEAST;   else if (velX < 0 && velY > 0)  facing = SOUTHWEST;
						tempFacing = facing;
					} alternateUpdatePosition = 5;
				} else alternateUpdatePosition--;
				facing = tempFacing;
				
				facing += Game.rotation - 1;
				if(facing >= 9) facing -= 8;
			} 
	}

	public void idleMove(final float SPEED) {
		if(inCombat) return;
		/* Get which direction + speed to move */
		final float angle = Utils.toRadians(Utils.rand.nextInt(360));
		velX = (float)Utils.rotateX(SPEED, SPEED, angle);
		velY = (float)Utils.rotateY(SPEED, SPEED, angle);
		
		/* Get the distance to move */
		distanceToMove = Utils.rand.nextInt((int)home.radius);
		
		/* switch the animation */
		currentAnimation = ANIMATION.idleWalk;
	}
	
	public void goHome(final float SPEED){
		if(inCombat) return; runningAway = false;
		distanceToMove = center.distance(home.center) - randomizeNum(home.radius / 2);
		switch(Utils.direction(center, home.center)) {
			case NORTH: 	velX = 0;      velY = -SPEED; break;
			case NORTHEAST: velX = SPEED;  velY = -SPEED; break;
			case EAST: 		velX = SPEED;  velY = 0;      break;
			case SOUTHEAST: velX = SPEED;  velY = SPEED;  break;
			case SOUTH: 	velX = 0;      velY = SPEED;  break;
			case SOUTHWEST: velX = -SPEED; velY = SPEED;  break;
			case WEST: 		velX = -SPEED; velY = 0;      break;
			case NORTHWEST: velX = -SPEED; velY = -SPEED; break;
		}
	}
	/** @param TICKS_TO_RUN_AWAY: the number of ticks to keep running away from player,
	 *         without yet stopping after having cleared its collision */
	private static final int TICKS_TO_RUN_AWAY = 30;
	public void runAway(final float SPEED, final Point2f centerOfWhereToRunFrom){
		if(inCombat) return;    runningAway = true;
		switch(Utils.direction(center, centerOfWhereToRunFrom)){
			case NORTH: 	velX = 0;      velY = SPEED;  break;
			case NORTHEAST: velX = -SPEED; velY = SPEED;  break;
			case EAST: 		velX = -SPEED; velY = 0;      break;
			case SOUTHEAST: velX = -SPEED; velY = -SPEED; break;
			case SOUTH: 	velX = 0;      velY = -SPEED; break;
			case SOUTHWEST: velX = SPEED;  velY = -SPEED; break;
			case WEST: 		velX = SPEED;  velY = 0;      break;
			case NORTHWEST: velX = SPEED;  velY = SPEED;  break;
		}
		howFarToRunAwayTimer = randomizeNum(TICKS_TO_RUN_AWAY);
	}
	
	public void fixBuggyMovement(final float SPEED) { if(currentAnimation == ANIMATION.idleWalk && velX == 0 && velY == 0 && !inCombat) idleMove(SPEED); }
	
//	public void runTowards(final float speed, final Point2f centerOfWhereToRunTo) {
//		velX = velY = 0;
//		switch (Utils.direction(center, centerOfWhereToRunTo)) {
//			case NORTH: 	velX = 0;      velY = -speed; break;
//			case NORTHEAST: velX = speed;  velY = -speed; break;
//			case EAST: 		velX = speed;  velY = 0; break;
//			case SOUTHEAST: velX = speed;  velY = speed; break;
//			case SOUTH: 	velX = 0;      velY = speed; break;
//			case SOUTHWEST: velX = -speed; velY = speed; break;
//			case WEST: 		velX = -speed; velY = 0; break;
//			case NORTHWEST: velX = -speed; velY = -speed; break;
//		}
//		// put a way to stop here
//	}
	
// -------------------------------------------------- END POSITION UPDATING ------------------------------------------------------------------------------------------------------------ //

// -------------------------------------------------- COMBAT --------------------------------------------------------------------------------------------------------------------------- //
	/*********************************** VARIABLES ************************************************/
		/** @param TARGET: The point the mob should run to
		 *  @param hitObjects: The objects that have been hit since start of attack sequence
		 *  @param HITBOX_DELAY_AMOUNT: The delay from the start of attack to when hitbox becomes effective
		 *  @param attackTimer: The length of time in ticks the full attack will last
		 *  @param ATTACK_RANGE_FUDGE_DISTANCE: The length away from intersecting ones needs to be to still be considered intersecting
		 *  @param GET_HIT_TIMER: The time to show the 'get hit' animation frame
		 *  @param GET_HIT_TIMER_AMOUNT: A const to set the above timer to */
		private Point2f TARGET; public HashSet<Object> hitObjects = new HashSet<Object>();
		private static final int HITBOX_DELAY_AMOUNT = 26, ATTACK_RANGE_FUDGE_DISTANCE = 20, GET_HIT_TIMER_AMOUNT = 25;
		protected int GET_HIT_TIMER; private int hitboxDelay, attackTimer;
	/********************************* END VARIABLES  *******************************************/
	
	public void combat() { // NOTE: THIS IS THE GENERIC COMBAT, OVERRIDE THIS FOR SPECIFIC MOB NEEDS
		if(!attacking || hitboxDelay > 0) return; // (don't keep spamming hits if already hit an entity
		
		// IF HIT PLAYER
		if(!hitObjects.contains(PLAYER) && Utils.intersects(attackBounds, Player.center, Player.radius)) {
			hitObjects.add(PLAYER);
			HealthAndDurability.hurtPlayer(damage);
		}
	}
	
	public void startAttackSequence( /* some kind of object? */ ) {
		if(attacking || inCombat) return; // don't restart sequence if already in combat
		inCombat = true;
		TARGET = Player.center;
		hitObjects.clear();
		hitboxDelay = HITBOX_DELAY_AMOUNT;
		GET_HIT_TIMER = 0;
		currentAnimation = ANIMATION.idleWalk; // XXX: YOU MAY NEED TO OVERRIDE THIS FOR SOME MOBS!
	}
	
	public void attackSequence(final float SPEED) {
		// NULL CHECK IN CASE TARGET HAS DIED / ETC OR  IF TARGET HAS GONE OUTSIDE ITS TERRITORY AGGRO, STOP ATTACK SEQUENCE
		if (TARGET == null || Player.health <= 0 || !territory.contains(TARGET)) { STOP_ATTACK_SEQUENCE(); return; }
		
		// MOVE TOWARDS TARGET
		if (!attacking) {
			final double angle = Math.atan2(TARGET.y - center.y, TARGET.x - center.x);
			velX = (float)((Math.cos(angle) * SPEED) * Utils.sqrtOf2);
			velY = (float)((Math.sin(angle) * SPEED) * Utils.sqrtOf2);
			
			// DIRECTION / FACING TOWARD TARGET
			int tempFacing = Utils.direction(center, TARGET) + (Game.rotation - 1);
			if (tempFacing > 8) tempFacing -= 8;
			facing = tempFacing;
		} else velX = velY = 0f;
		
		// STOP MOB WHEN REACHES THE TARGET, THEN ATTACK!
		if (attacking) {
			if(hitboxDelay > 0) hitboxDelay--; else combat();
			if(attackTimer > 0) attackTimer--; else STOP_ATTACK_SEQUENCE();
		} else if (Utils.distance(attackBounds.center, TARGET) < ATTACK_RANGE_FUDGE_DISTANCE) {
			attacking = true;
			attackTimer = calcAttackTimerAmount();
		}
		
	}
	
	private int calcAttackTimerAmount() {
		switch (id) {
			case GOBLIN: return 55;
			
			default: return 25;
		}
	}
	
	private void STOP_ATTACK_SEQUENCE() { TARGET = null; hitObjects.clear(); attacking = inCombat = false; currentAnimation = ANIMATION.idleWalk; }
	
	public void HIT_BY_PLAYER() { // XXX: WILL NEED TO RETROFIT THIS TO WORK WITH MOBS NOT JUST PLAYER
		hitboxDelay += HITBOX_DELAY_AMOUNT/4;
		attackTimer += HITBOX_DELAY_AMOUNT/4;
		
		// DEDUCT DURABILITY FROM PLAYER'S WEAPON
		if(!PlayerAttackSequence.hitMobs.contains(this)) {
			HealthAndDurability.HIT_MOB();
		
			// SUBTRACT FROM CURRENT HEALTH
			health -= Player.currentDamage; // NOTE: For mobs strong against certain attacks, etc, override this method.
			
			// SET THE GET HIT TIMER TO RENDER THE GET-HIT ANIMATION IN RENDER
			GET_HIT_TIMER = GET_HIT_TIMER_AMOUNT;
		}
	}
	
	public void getHitClock() { if(GET_HIT_TIMER > 0) GET_HIT_TIMER--; }
// ------------------------------------------------- END COMBAT ------------------------------------------------------------------------------------------------------------------------- //
	
// -------------------------------------------------------------- DISCOVERING ---------------------------------------------------------------------------------------------------------- //
	private static final int DISCOVERY_STATUS_DELAY = 30;
	private int getDiscoveredStatusDelay = DISCOVERY_STATUS_DELAY;
	public boolean getDiscoveredStatus() {
		if(!discovered) { return discovered = Player.DISCOVERY.contains(center.x, center.y); }
		
		// IF MOB WALKS OUTSIDE OF DISCOVERED TILES, SET THE MOB TO BE NOT DISCOVERED
		// DELAY THIS BECAUSE ITS EXPENSIVE!
		if(getDiscoveredStatusDelay < 0) {
			
			for(final Tile t : Handler.currentTileSection(center.x, center.y)) if(!t.discovered && t.getBounds().contains(center.x, center.y)) discovered = false;
				
			getDiscoveredStatusDelay = DISCOVERY_STATUS_DELAY;
		} else getDiscoveredStatusDelay--;
		
		return discovered;
	}
// ---------------------------------------------------------- END DISCOVERING ------------------------------------------------------------------------------------------------------------- //

// ---------------------------------------------------------- MISC METHODS ----------------------------------------------------------------------------------------------------------------- //
	public void drawCollisionBounds(Graphics2D copy){
		if(Game.drawCollisionBounds){ copy.setColor(Color.green); copy.drawOval((int)(center.x - radius), (int)(center.y - radius), (int)(radius*2), (int)(radius*2)); }
		if(Game.drawMobAggroRadii || Game.drawCollisionBounds){ copy.setColor(Color.red); copy.drawOval((int)(center.x - aggroRadius), (int)(center.y - aggroRadius), (int)(aggroRadius*2), (int)(aggroRadius*2)); }
		if(Game.drawMobTerritoryZones || Game.drawCollisionBounds) { copy.setColor(Color.orange); copy.drawOval((int)(home.center.x - aggroRadius), (int)(home.center.y - aggroRadius), (int)(home.diameter()), (int)(home.diameter())); }
		if(Game.drawMobAttackBounds || Game.drawCollisionBounds) { copy.setColor(Color.magenta);  copy.drawOval((int)(attackBounds.center.x - attackBounds.radius), (int)(attackBounds.center.y - attackBounds.radius), (int)(attackBounds.diameter()), (int)(attackBounds.diameter())); }
		if(Game.drawCollisionBounds) { 							   copy.setColor(Color.BLACK); territory.draw(copy); }
	}
	
	public void allBugFixes() {
		fixBuggyCollision();
		fixBuggyMovement(walkingSpeed);
	}
	
	public String toString() { return getClass().toString().substring(getClass().toString().indexOf(".")+1); }
	
	public boolean alive(){ return health > 0; }
	
	public void normalizeHealth() { if(health > MAX_HEALTH) health = MAX_HEALTH; }
	
	protected static final int randomizeNum(float num) { return (int)(Utils.rand.nextInt((int)num/2) + num*0.75f); }
	
	protected final void NULL_ALL_FROM_SUPER_CLASS() {
		home.center = territory.center = attackBounds.center = null;
		home = territory = attackBounds = null;
		center = pos = null;
		id = null; currentAnimation = null;
		hitObjects.clear(); hitObjects = null; TARGET = null;
	}
// --------------------------------------------------------- END MISC METHODS ---------------------------------------------------------------------------------------------------------------- //
	
	
}
