/** Physics Subsystem
 * 
 * This is a physics for our platformer. It updates the current
 * state of the world (including that of enemies, animals, and the player)
 * given some length of time having passed.
 */

package edu.wisc.cs.games10.chrononaut;

import java.util.*;

public class Physics {
	float gravity = .001f; // tiles / milliseconds^2
	Player player;
	Level level;
	
	Physics(Player p, Level l) {
		player = p;
		level = l;

		// TODO: also track the "world" so level can change
		//  (unless levels contain pointers to next room???)

		// NOTE: bullets should be handled separately,
		//  as they are the only IMovable that care about hitting
		//  non-tile/non-players (maybe...)
	}
	
	public void setLevel(Level l) {
		level = l;
	}

	public void update(float time, Era era, Camera c) {
		handleBulletCollisions(era);
		handleEnemyCollisions();
		handleCreatureCollisions();
		handleItemCollisions();
		
		handlePhysics(player, time);
		for (Enemy enemy : level.getEnemies()) {
			if (enemy.loc.x > c.leftx - 1 && enemy.loc.x < c.rightx + 1 &&
					enemy.loc.y > c.topy - 4 && enemy.loc.y < c.bottomy + 1) {
				handlePhysics(enemy, time);
			}
		}
	}
	
	private void handlePhysics(IMovable o, float time) {
		handleTileCollision(o);
		
		Vector2D loc = o.getLocation();
		Vector2D vel = Vector2D.mult(o.getVelocity(), time);
		if(!o.equals(player))	//If this is an Enemy, remove it from its old location (and in every tile that it partially covers)
			for(int i = 0; i < o.getTileWidth(); i++)		//
				for(int j = 0; j < o.getTileWidth(); j++)	//
					if((int)Math.floor(loc.x) + i < level.width && (int)Math.floor(loc.x) + i > 0 && (int)Math.floor(loc.y) + j < level.height && (int)Math.floor(loc.y) + j > 0)
						level.enemyTiles[(int)Math.floor(loc.x) + i][(int)Math.floor(loc.y) + j].remove(o);
		loc.add(vel);			//Then update its location	//
		if(!o.equals(player))	//Then record it in its new location (and in every tile that it partially covers)
			for(int i = 0; i < o.getTileWidth(); i++)
				for(int j = 0; j < o.getTileWidth(); j++)
					if((int)Math.floor(loc.x) + i < level.width && (int)Math.floor(loc.x) + i > 0 && (int)Math.floor(loc.y) + j < level.height && (int)Math.floor(loc.y) + j > 0)
						level.enemyTiles[(int)Math.floor(loc.x) + i][(int)Math.floor(loc.y) + j].add((Enemy)o);
		
		handleMapEdges(o);
	}
	
	/** Updates velocities based on o's collision with level tiles. **/
	private void handleTileCollision(IMovable o) {
		float width = o.getTileWidth();
		float height = o.getTileHeight();
		Era era = player.getCurrentEra();
		
		// find current tile right beneath character
		boolean falling = true;
		Vector2D loc = o.getLocation();
		int x = (int) Math.floor(loc.x);
		int y = (int) Math.floor(loc.y);
		Vector2D vel = o.getVelocity();
		
		// kill 'o' if out of screen boundsl
		if (y >= level.getHeight()) {
			o.kill();
			return;
		}

		// === determine if falling ===
		// check for a tile immediately below
		x = (int) Math.floor(loc.x);
		y = (int) Math.floor(loc.y);
		int ybottom = y + (int) Math.ceil(height);
		for (int i = 0; i <= width; i++) {
			if (level.getTile(era, x + i, ybottom) == Tile.SOLID && vel.y >= 0 &&
					level.getTile(era, x + i, ybottom - 1) == Tile.EMPTY) {
				vel.y = 0;
				loc.y = ybottom - o.getTileHeight();
				falling = false;
				
				if (o.equals(player)) {
					player.totaljumps = 0;
				}
			} else if (level.getTile(era, x , ybottom - 1) == Tile.SOLID && o instanceof Enemy) {
				// check if the enemy is in a solid tile, and if so, freeze him instead of making him fall
				vel.y = 0;
				falling = false;
				if (level.getTile(era, x+1 , ybottom-1) == Tile.EMPTY) {
					vel.x = ((PatrolEnemy)o).hspeed;
				} else if(level.getTile(era, x-1 , ybottom-1) == Tile.EMPTY) {
					vel.x = -((PatrolEnemy)o).hspeed;
				} else {
					vel.x = 0;
				}
			}
		}
		// check for a tile immediately to the right
		int xright = x + (int) Math.ceil(o.getTileWidth());
		for (int i = y; i < ybottom; i++) {
			if (vel.x > 0 && level.getTile(era, xright, i) == Tile.SOLID &&
					level.getTile(era, xright - 1, i) == Tile.EMPTY) {
				vel.x = 0;
			}
		}
		
		// and to the left
		int xleft = x;
		for (int i = y; i < ybottom; i++) {
			if (vel.x < 0 && level.getTile(era, xleft, i) == Tile.SOLID &&
					level.getTile(era, xleft + 1, i) == Tile.EMPTY) {
				vel.x = 0;
			}
		}
		
		// check for a tile immediately above
		for (int i = 0; i <= o.getTileWidth(); i++) {
			if (level.getTile(era, x + i, y) == Tile.SOLID && vel.y < 0 &&
					level.getTile(era, x + i, y + 1) == Tile.EMPTY) {
				vel.y = 0;
			}
		}
		
		if (falling){
			vel.y += gravity;
			if(vel.y > o.getTerminalVelocity()) {
				vel.y = o.getTerminalVelocity();
			}
		}
	}
	
	private void handleEnemyCollisions() {
		// check all enemies to see if they are colliding with player
		for (Enemy enemy : level.enemies) {
			if(enemy.doesCollideWith(player)) {
				player.collisionWithEnemy(enemy);
			}
		}
		
		/*
		// TODO: instead of checking all enemies, just check nearby ones
		for(int i = 0; i < o.getTileWidth(); i++)
			for(int j = 0; j < o.getTileHeight(); j++)
				for(Enemy foe : level.enemyTiles[i][j])
					{}
		*/
	}
	
	private void handleCreatureCollisions() {
		if (!player.isExtracting)
			return;
		
		// check all creatures to see if they are colliding with player
		for (Creature c : level.creatures) {
			if(c.era == player.currentEra && c.doesCollideWith(player.extractor)) {
				player.addBodyPart(c.getBodyPart());
				c.kill();
			}
		}
		
		// TODO: instead of checking all enemies, just check nearby ones
		/*
		for(int i = 0; i < o.getTileWidth(); i++)
			for(int j = 0; j < o.getTileHeight(); j++)
				for(Enemy foe : level.enemyTiles[i][j])
					{}
		*/
	}
	
	private void handleItemCollisions() {
		if (player.isTasing) {
			for (Enemy e : level.enemies) {
				if (e.doesCollideWith(player.taser)) {
					// brander bots only get hurt in their body
					if (e instanceof BranderBotEnemy) {
						float taserEdge = player.taser.loc.x;
						if (player.taser.playerFacingRight) {
							taserEdge += player.taser.tileWidth;
						}
						
						if ((e.vel.x < 0 && taserEdge > (1.2 + e.loc.x)) ||
						(e.vel.x > 0 && taserEdge < (e.loc.x + e.tileWidth - 1.2))) {
							e.kill();
						}	
					} else {
						e.kill();
					}
				}
			}
		}
		if(player.isTentacleAttacking) {
			for (Enemy e : level.enemies) {
				if(e.doesCollideWith(player.tentacleArms)) {
					e.kill();
				}
			}
		}
		if(player.equippedTail.turtleShell) {
			List<Bullet> bullets;
			if(player.getCurrentEra() == Era.PAST) {
				bullets = level.pastBullets;
			} else if(player.getCurrentEra() == Era.PRESENT) {
				bullets = level.presentBullets;
			} else {
				bullets = level.futureBullets;
			}
			for (Bullet b : bullets) {
				if(b.doesCollideWith(player.turtleShell)) {
					b.timeLeft = 0;
				}
			}
		}
		
	}
	
	private void handleBulletCollisions(Era era) {
		// not working, might be because player getTileHeight/Width == 1? shouldn't it be 2x3?
		List<Bullet> bulletList;
		if(era == Era.PAST) {
			bulletList = level.pastBullets;
		} else if(era == Era.PRESENT) {
			bulletList = level.presentBullets;
		}else {
			bulletList = level.futureBullets;
		}
		
		ListIterator<Bullet> iterBullet = bulletList.listIterator();
		while (iterBullet.hasNext()) {
			Bullet b = iterBullet.next();
			if (b.doesCollideWith(player)) {
				b.kill();
				iterBullet.remove();
				player.hurt();
			}
			
			// don't destroy bullets if they hit solid tiles
			// the game feels too easy to me right now...
			// maybe there should be semi-solid tiles that bullets can pass through?
			/*
			if(b.vel.x > 0 && (level.getTile(era, (int)(b.loc.x + (int)Math.ceil(b.getTileWidth())),  (int)(b.loc.y + (int)Math.ceil(b.getTileHeight())))!= Tile.EMPTY || level.getTile(era, (int)(b.loc.x + (int)Math.ceil(b.getTileWidth())),  (int)(b.loc.y))!= Tile.EMPTY)) {
				b.kill();
			}
			if(b.vel.x < 0 && (level.getTile(era, (int)(b.loc.x),  (int)(b.loc.y + (int)Math.ceil(b.getTileHeight())))!= Tile.EMPTY || level.getTile(era, (int)(b.loc.x),  (int)(b.loc.y))!= Tile.EMPTY)) {
				b.kill();
			}
			*/
		}
	}
	
	/** Makes sure 'o' is always in a valid tile **/
	private void handleMapEdges(IMovable o) {
		Vector2D loc = o.getLocation();
		int width = level.getWidth();
		//int height = level.getHeight();

		// stay on screen!
		if (loc.x < 0) {
			loc.x = 0;
		}
		if (loc.x >= width) {
			loc.x = width - 1;
		}

	}
}
