package sfs3.extensions.examples.spacewar2.core;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import com.smartfoxserver.entities.data.ISFSObject;
import com.smartfoxserver.exceptions.ExceptionMessageComposer;

import sfs3.extensions.examples.spacewar2.SW2RoomExtension;
import sfs3.extensions.examples.spacewar2.entities.Velocity;

public class Game implements Runnable
{
	private SW2RoomExtension ext;
	private Planet planet;
	private Map<Integer, Starship> starships;
	private Map<Integer, WeaponShot> weaponShots;

	public Game(SW2RoomExtension ext)
	{
		this.ext = ext;
		
		// Create map for Starship objects
		starships = new ConcurrentHashMap<Integer,Starship>();
		
		// Create map for WeaponShot objects
		weaponShots = new ConcurrentHashMap<Integer, WeaponShot>();
	}
	
	/**
	 * Creates planet upon Extension request (game started).
	 */
	public void createPlanet(ISFSObject planetSettings)
	{
		// Add planet to simulation
		planet = new Planet(planetSettings.getInt("mass"), planetSettings.getInt("radius"));
	}
	
	/**
	 * Creates starship upon Extension request (game started).
	 * User ID is used to identify it.
	 */
	public void createStarship(int ownerId, ISFSObject shipSettings, ISFSObject weaponsCfg)
	{
		Starship ship = new Starship(ownerId, shipSettings);
		
		// Set starship random position within 1000 pixels from the space (0,0) coords
		// If a planet exists, keep the ship far from its surface (four times the planet radius from its center)
		int maxDist = 1000;
		int minDist = planet.radius * 4;
		int dist =  minDist + (int)(Math.random() * (maxDist - minDist));
		int angle = (int) Math.round(Math.random() * 360 * Math.PI / 180);
		
		ship.x = Math.cos(angle) * dist;
		ship.y = Math.sin(angle) * dist;
		
		// Set starship random rotation angle
		ship.rotation = (int) Math.round(Math.random() * 360 * Math.PI / 180);
		
		ship.lastRenderTime = System.currentTimeMillis();
		
		// Init weapons
		ISFSObject weapon1 = weaponsCfg.getSFSObject(shipSettings.getString("weapon1"));
		ship.initWeapon(weapon1.getString("model"), weapon1.getInt("maxShots"));
		
		ISFSObject weapon2 = weaponsCfg.getSFSObject(shipSettings.getString("weapon2"));
		ship.initWeapon(weapon2.getString("model"), weapon2.getInt("maxShots"));
		
		// Add starship to simulation
		starships.put(ownerId, ship);
		
		// Save initial position, velocity and shield to User Variables and position to the SFS2X Proximity Manager system
		// As this is a new starship and the User Variables are set before the position is set in the Proximity Manager system,
		// only the owner user will receive the User Variables update event
		saveStarshipPosition(ship, true);
	}
	
	/**
	 * Removes starship upon Extension's event handler request (user left MMORoom).
	 */
	public void removeStarshipOnPlayerGone(int ownerId)
	{
		if (starships.containsKey(ownerId))
		{
			Starship ship = starships.get(ownerId);
			
			// Remove ship from simulation
			starships.remove(ownerId);
			
			// Send message to all clients to notify starship destruction
			ext.notifyStarshipExplosion(ownerId, ship.x, ship.y);
		}
	}
	
	/**
	 * Makes starship start or stop rotating.
	 * A command is sent to clients using a dedicated User Variable, so that it is received only by users in proximity.
	 */
	public void rotateStarship(int ownerId, int direction)
	{
		Starship ship = starships.get(ownerId);
		
		// Set rotating
		ship.rotatingDir = direction;
		
		// On rotation start, set dedicated User Variable
		if (direction != 0)
			ext.setStarshipRotating(ownerId, direction);
		
		// On stop send full position update to clients
		// This includes the current rotation direction (0)
		else
			saveStarshipPosition(ship, true);
	}
	
	/**
	 * Activates/deactivates starship thruster.
	 * A command is sent to clients using a dedicated User Variable, so that it is received only by users in proximity.
	 */
	public void thrustStarship(int ownerId, boolean activate)
	{
		Starship ship = starships.get(ownerId);
		
		// Set rotating
		ship.thrust = activate;
		
		// Set User Variable within the position update event
		saveStarshipPosition(ship, true);
	}
	
	/**
	 * Creates a weapon shot.
	 */
	public void fireWeaponShot(int ownerId, ISFSObject settings)
	{
		Starship ship = starships.get(ownerId);
		Weapon weapon = ship.getWeapon(settings.getString("model"));
		
		// Check if weapon can fire
		// (at least one shot must be available)
		if (weapon.isShotAvailable())
		{
			// Increase number of fired shots
			weapon.increaseShotsCount();
			
			// Create shot instance
			WeaponShot shot = new WeaponShot(ownerId, settings);
			
			// Set shot position equal to starship's position, shifted to its bow or tail
			int dir = shot.getSpeed() > 0 ? 1 : -1;
			shot.x = ship.x + 15 * dir * Math.cos(ship.rotation);
			shot.y = ship.y + 15 * dir * Math.sin(ship.rotation);
			
			// Set shot velocity summing the starship speed and the shot ejection speed
			double vx = Math.cos(ship.rotation) * shot.getSpeed();
			double vy = Math.sin(ship.rotation) * shot.getSpeed();
			Velocity v = new Velocity(vx + ship.getVX(), vy + ship.getVY());
			shot.velocity = v;
			
			shot.lastRenderTime = System.currentTimeMillis();
			
			// Save initial position in the SFS2X Proximity Manager system
			// As this is a new shot, clients will be notified of its existence in the proximity update event
			// The ID of the MMOItem in the Proximity Manager system is returned, so we can save it in the shot properties for later reference
			int id = ext.addWeaponShot(shot.getModel(), shot.x, shot.y, shot.getVX(), shot.getVY());
			shot.setMMOItemId(id);
			
			// Add shot to simulation
			weaponShots.put(id, shot);
		}
	}

	/**
	 * Runs the simulation calculating the next position of each object on a frame basis.
	 */
	@Override
	public void run()
	{
		try
		{
			// Move weapon shots to next coordinates
			// Self-destruction is also checked
			for (Iterator<Map.Entry<Integer, WeaponShot>> it = weaponShots.entrySet().iterator(); it.hasNext();)
			{
				WeaponShot shot = it.next().getValue();
				
				// Distance from planet center (always 0,0)
				double dist = Math.sqrt(Math.pow(shot.x, 2) + Math.pow(shot.y, 2));
				
				boolean isPlanetCrash = planet.radius > 0 && dist < planet.radius;
				
				// Check self-destruction or planet crash
				if (shot.isSelfDestruct() || isPlanetCrash)
				{
					// Remove shot from simulation
					it.remove();
					
					// Remove shot from Proximity Manager system and notify clients
					removeWeaponShot(shot);
				}
				else
				{
					// Calculate next coordinates
					renderWeaponShot(shot);
					
					// Save position and velocity to User Variables and position to the SFS2X Proximity Manager system
					saveWeaponShotPosition(shot);
				}
			}
			
			List<Integer> gameOverIds = new ArrayList<Integer>();
			
			// Move starships to next coordinates
			// Check starships collisions with weapon shots in proximity
			// Check collision with planet
			for (Iterator<Map.Entry<Integer, Starship>> it = starships.entrySet().iterator(); it.hasNext();)
			{
				Starship ship = it.next().getValue();
				
				// Calculate next coordinates
				renderStarship(ship);
				
				// Retrieve list of MMOItems in proximity to check the collision
				List<Integer> shotIDs = ext.getWeaponShotsList(ship.x, ship.y);
				
				boolean hit = false;
				
				for (int i = 0; i < shotIDs.size(); i++)
				{
					int shotID = shotIDs.get(i);
					WeaponShot shot = weaponShots.get(shotID);
					
					if (shot == null)
						continue;
					
					// Check collision
					if (shot.isActive() && getDistance(ship, shot) <= shot.getHitRadius())
					{
						// Remove shot from simulation
						weaponShots.remove(shotID);
						
						// Remove shot from Proximity Manager system and notify clients
						removeWeaponShot(shot);
						
						// Update starship trajectory
						int dirX = (ship.x > shot.x ? +1 : -1);
						int dirY = (ship.y > shot.y ? +1 : -1);
						ship.velocity.vx += dirX * shot.getHitForce();
						ship.velocity.vy += dirY * shot.getHitForce();
						
						// Update starship shield
						ship.decreaseShield(shot.getDamage());
						
						// Check ship destroyed
						if (ship.getShield() <= 0)
						{
							// Remove ship from simulation
							starships.remove(ship.getOwnerId());
							
							// Notify starship destruction
							ext.notifyStarshipExplosion(ship.getOwnerId(), ship.x, ship.y);
							
							// Add ship's owner id to game over notification list
							gameOverIds.add(ship.getOwnerId());
						}
						
						hit = true;
					}
				}
				
				// Check collision with planet
				if (planet.radius > 0)
				{
					// Distance from planet center (always 0,0)
					double dist = Math.sqrt(Math.pow(ship.x, 2) + Math.pow(ship.y, 2));
					
					if (dist < planet.radius)
					{
						// Remove ship from simulation
						starships.remove(ship.getOwnerId());
						
						// Notify starship destruction
						ext.notifyStarshipExplosion(ship.getOwnerId(), ship.x, ship.y);
						
						// Add ship's owner id to game over notification list
						gameOverIds.add(ship.getOwnerId());
					}	
				}
				
				// Save position, velocity and shield to User Variables and position to the SFS2X Proximity Manager system
				// If trajectory changed due to external forces (weapon shot), then update clients too
				saveStarshipPosition(ship, hit);
			}
			
			// Notify game over to owners of destroyed ships
			// The number of remaining ships is passed, so that the player ranking can be deduced and the overall user xp can be updated
			if (gameOverIds.size() > 0)
				ext.notifyGameOver(gameOverIds.toArray(new Integer[0]), starships.size());
			
			// Check if there's a winner
			if (starships.size() == 1)
			{
				// Retrieve id of the last remaining ship
				int lastShipId = starships.keySet().iterator().next();
				
				// Notify game over
				ext.notifyGameOver(new Integer[]{ lastShipId }, 0);
			}
		}
		catch (Exception e)
		{
			// In case of exceptions this try-catch prevents the task to stop running
			ExceptionMessageComposer emc = new ExceptionMessageComposer(e);
			ext.trace(emc.toString());
		}
	}
	
	/**
	 * Saves starship's (A) position, velocity and more to User Variables and (B) position to SFS2X Proximity Manager system.
	 * Read the Extension's method for more informations.
	 */
	private void saveStarshipPosition(Starship ss, boolean doUpdateClients)
	{
		ext.setStarshipState(ss.getOwnerId(), ss.x, ss.y, ss.getVX(), ss.getVY(), ss.rotation, ss.thrust, ss.rotatingDir, ss.getShield(), doUpdateClients);
	}
	
	/**
	 * Saves starship's (A) position and velocity to User Variables and (B) position to SFS2X Proximity Manager system.
	 * Read the Extension's method for more informations.
	 */
	private void saveWeaponShotPosition(WeaponShot ws)
	{
		ext.setWeaponShotPosition(ws.getMMOItemId(), ws.x, ws.y, ws.getVX(), ws.getVY());
	}
	
	/**
	 * Removes weapon shot in case of self-destruction or collision.
	 * NOTE: the shot is not removed from the weaponShots list because we use an iterator to do it, where the self-destruction/collision is detected.
	 */
	private void removeWeaponShot(WeaponShot shot)
	{
		// Check if ship which fired the shot still exists (could have been destroyed already)
		if (starships.containsKey(shot.ownerId))
		{
			Starship ship = starships.get(shot.ownerId);
			Weapon weapon = ship.getWeapon(shot.getModel());
			
			// Decrease number of fired shots by owner's ship
			weapon.decreaseShotsCount();
		}
		
		// Remove shot from Proximity Manager system
		ext.removeWeaponShot(shot.getMMOItemId());
		
		// Send message to all clients in proximity to remove the shot from the display list
		ext.notifyWeaponShotExplosion(shot.getMMOItemId(), shot.x, shot.y);
	}
	
	private void renderStarship(Starship ship)
	{
		long now = System.currentTimeMillis();
		long elapsed = now - ship.lastRenderTime;
		
		for (long i = 0; i < elapsed; i++)
		{
			// Ship rotation
			ship.rotation += ship.rotatingDir * ship.getRotationSpeed();
			
			// Thruster force
			if (ship.thrust)
			{
				ship.velocity.vx += Math.cos(ship.rotation) * ship.getThrustAcceleration();
				ship.velocity.vy += Math.sin(ship.rotation) * ship.getThrustAcceleration();
			}
			
			// Limit speed
			ship.velocity.limitSpeed(ship.getMaxSpeed());

			// Planet gravitational attraction
			applyGravityForce(ship);
			
			// Update ship position based on its velocity
			ship.x += ship.velocity.vx;
			ship.y += ship.velocity.vy;
		}
		
		ship.lastRenderTime = now;
	}
	
	private void renderWeaponShot(WeaponShot shot)
	{
		long now = System.currentTimeMillis();
		long elapsed = now - shot.lastRenderTime;
		
		for (long i = 0; i < elapsed; i++)
		{
			// Planet gravitational attraction
			applyGravityForce(shot);
			
			// Update shot position based on its velocity
			shot.x += shot.velocity.vx;
			shot.y += shot.velocity.vy;
		}
		
		shot.lastRenderTime = now;
	}
	
	private void applyGravityForce(GameItem gameItem)
	{
		if (planet.mass > 0)
		{
			// Square distance from planet center (always 0,0)
			double dist2 = Math.pow(gameItem.x, 2) + Math.pow(gameItem.y, 2);
			double angle = Math.atan2(gameItem.y, gameItem.x); // In radians
			float attr = planet.mass / (float)dist2;

			gameItem.velocity.vx -= Math.cos(angle) * attr;
			gameItem.velocity.vy -= Math.sin(angle) * attr;
		}
	}
	
	private double getDistance(GameItem simItem1, GameItem simItem2)
	{
		double dist_x = simItem1.x - simItem2.x;
		double dist_y = simItem1.y - simItem2.y;
		
		return Math.sqrt(Math.pow(dist_x, 2) + Math.pow(dist_y, 2));
	}
}
