package sfs2x.extensions.games.spacewar2.game;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import com.smartfoxserver.cluster.core.SFSCluster;
import com.smartfoxserver.cluster.data.IGlobals;
import com.smartfoxserver.v2.SmartFoxServer;
import com.smartfoxserver.v2.api.ISFSMMOApi;
import com.smartfoxserver.v2.core.SFSEventType;
import com.smartfoxserver.v2.entities.User;
import com.smartfoxserver.v2.entities.data.ISFSObject;
import com.smartfoxserver.v2.entities.data.SFSObject;
import com.smartfoxserver.v2.entities.variables.RoomVariable;
import com.smartfoxserver.v2.entities.variables.SFSRoomVariable;
import com.smartfoxserver.v2.entities.variables.SFSUserVariable;
import com.smartfoxserver.v2.entities.variables.UserVariable;
import com.smartfoxserver.v2.extensions.ExtensionLogLevel;
import com.smartfoxserver.v2.extensions.SFSExtension;
import com.smartfoxserver.v2.mmo.BaseMMOItem;
import com.smartfoxserver.v2.mmo.IMMOItemVariable;
import com.smartfoxserver.v2.mmo.MMOItem;
import com.smartfoxserver.v2.mmo.MMOItemVariable;
import com.smartfoxserver.v2.mmo.MMORoom;
import com.smartfoxserver.v2.mmo.Vec3D;
import com.smartfoxserver.v2.util.TaskScheduler;

import sfs2x.extensions.games.spacewar2.game.core.Game;
import sfs2x.extensions.games.spacewar2.game.handlers.event.UserLeaveRoomEventHandler;
import sfs2x.extensions.games.spacewar2.game.handlers.requests.ControlRequestHandler;
import sfs2x.extensions.games.spacewar2.game.handlers.requests.InitRequestHandler;
import sfs2x.extensions.games.spacewar2.game.tasks.AbortTask;
import sfs2x.extensions.games.spacewar2.game.tasks.CountdownTask;
import sfs2x.extensions.games.spacewar2.shared.util.Constants;

public class SW2RoomExtension extends SFSExtension
{
	private SmartFoxServer sfs;
	private ISFSMMOApi mmoApi;
	private MMORoom room;
	
	private ISFSObject gameCfg;
	private ISFSObject levelCfg;
	private ISFSObject starshipsCfg;
	private ISFSObject weaponsCfg;
	
	private Game game;
	
	private TaskScheduler scheduler;
	private ScheduledFuture<?> abortTask;
	private ScheduledFuture<?> countdownTask;
	private ScheduledFuture<?> gameTask;
	
	@Override
	public void init()
	{
		room = (MMORoom) this.getParentRoom();
		
		// Get a reference to the SmartFoxServer instance
		sfs = SmartFoxServer.getInstance();
		
		// Get a reference to the MMO dedicated API
		mmoApi = sfs.getAPIManager().getMMOApi();
		
		// Get level id from Room Variables
		String levelId = room.getVariable(Constants.ROOMVAR_GAME_LEVEL).getStringValue();
		
		// Get configuration from cluster globals
		IGlobals globals = SFSCluster.getGameService().getGlobals();
		gameCfg = (SFSObject)globals.get(Constants.CONFIG_SETTINGS);
		levelCfg = ((SFSObject)globals.get(Constants.CONFIG_LEVELS)).getSFSObject(levelId);
		starshipsCfg = (SFSObject)globals.get(Constants.CONFIG_STARSHIPS);
		weaponsCfg = (SFSObject)globals.get(Constants.CONFIG_WEAPONS);
		
		// Register handler for user leave room events
		addEventHandler(SFSEventType.USER_LEAVE_ROOM, UserLeaveRoomEventHandler.class);
		addEventHandler(SFSEventType.USER_DISCONNECT, UserLeaveRoomEventHandler.class);
		
		// Register handlers for client requests
		addRequestHandler(Constants.REQ_INIT, InitRequestHandler.class);
		addRequestHandler(Constants.REQ_CONTROL, ControlRequestHandler.class);
		
		// A task scheduler is needed to control the game start flow initially, and the game logic later
		// We use a single thread to avoid concurrency issues in the game logic when checking game objects collisions
		scheduler = new TaskScheduler(1);
		
		// Schedule a task to abort the game if it's not yet started after a number of seconds
		abortTask = scheduler.schedule(new AbortTask(this), gameCfg.getInt(Constants.SETTING_GAME_ABORT_SECS), TimeUnit.SECONDS);
		
		trace(ExtensionLogLevel.INFO, "SpaceWar 2 Room Extension initialized for Room " + room.getName());
	}
	
	@Override
	public void destroy()
	{
		super.destroy();
		
		if (abortTask != null)
			abortTask.cancel(true);

		if (countdownTask != null)
			countdownTask.cancel(true);

		if (gameTask != null)
			gameTask.cancel(true);
	}
	
	/* -------------- PUBLIC METHODS CALLED BY TASK, INTERNAL HANDLERS AND REQUEST HANDLERS -------------- */

	/**
	 * Returns the game settings.
	 */
	public ISFSObject getGameCfg()
	{
		return gameCfg;
	}
	
	/**
	 * Returns the level configuration data.
	 */
	public ISFSObject getLevelCfg()
	{
		return levelCfg;
	}
	
	/**
	 * Returns the starships configuration data.
	 * Method called by the InitRequestHandler to send the starships configuration to users joining the game.
	 */
	public ISFSObject getStarshipsCfg()
	{
		return starshipsCfg;
	}
	
	/**
	 * Returns the weapons configuration data.
	 * Method called by the InitRequestHandler to send the weapons configuration to users joining the game.
	 */
	public ISFSObject getWeaponsCfg()
	{
		return weaponsCfg;
	}
	
	/**
	 * Returns true if the game is running.
	 */
	public boolean isGameRunning()
	{
		return gameTask != null;
	}
	
	/**
	 * Returns the countdown task.
	 * Method called by the InitRequestHandler to send the countdown state to users joining the game.
	 */
	public ScheduledFuture<?> getCountdownTask()
	{
		return countdownTask;
	}
	
	/**
	 * Getter used by handler classes to give inputs to the game core.
	 */
	public Game getGame()
	{
		return game;
	}
	
	/**
	 * Returns the list of users who already sent the READY signal (see InitRequestHandler).
	 */
	public List<User> getReadyUsers()
	{
		List<User> readyUsers = new ArrayList<User>();
		
		for (User user : room.getUserList())
		{
			boolean isReady = (Boolean) user.getProperty("ready");
			
			if (isReady)
				readyUsers.add(user);
		}
		
		return readyUsers;
	}
	
	/**
	 * Method called by the InitRequestHandler when the minimum number of players has been reached.
	 * Begins a countdown at the end of which the game is started.
	 */
	public void startCountdown()
	{
		// Cancel abort task
		abortTask.cancel(true);
		
		// Start countdown
		countdownTask = scheduler.schedule(new CountdownTask(this), gameCfg.getInt(Constants.SETTING_GAME_START_SECS), TimeUnit.SECONDS);
		
		trace(ExtensionLogLevel.INFO, "Game start countdown launched in Room " + room.getName() + ": game will start in " + gameCfg.getInt(Constants.SETTING_GAME_START_SECS) + " seconds");
	}
	
	/**
	 * Method called by the abort task after a number of seconds have passed and the game is not yet started.
	 */
	public void abortGame()
	{
		trace(ExtensionLogLevel.WARN, "Aborting game in Room " + room.getName() + " due to minimum number of players not yet reached within " + gameCfg.getInt(Constants.SETTING_GAME_ABORT_SECS) + " seconds");
		
		// Deactivate Room to avoid new players join it
		room.setActive(false);
		
		// Send notification to clients
		sfsApi.sendExtensionResponse(Constants.RESP_LEAVE, null, room.getUserList(), room, false);
		
		// Kick all users out of the Room
		sfsApi.emptyRoom(room);
	}
	
	/**
	 * Method called by the countdown task to start the game.
	 */
	public void startGame()
	{
		trace(ExtensionLogLevel.WARN, "Starting game in Room " + room.getName());
		
		// Set game as running in Room Variables
		RoomVariable rv = new SFSRoomVariable(Constants.ROOMVAR_GAME_STATE, Constants.GAMESTATE_RUNNING);
		sfsApi.setRoomVariables(null, room, Arrays.asList(new RoomVariable[]{rv}));
		
		// Send start notification to all ready clients
		sfsApi.sendExtensionResponse(Constants.RESP_START, null, getReadyUsers(), room, false);
		
		// Create main game core
		game = new Game(this);
		
		// Add planet
		game.createPlanet(levelCfg);
		
		// Get ready users
		List<User> users = getReadyUsers();
		
		for (User user : users)
		{
			// Check if selected a starship; if not, assign one randomly
			if (user.getVariable(Constants.USERVAR_SHIP_MODEL) == null)
			{
				// Add random ship model to User Variables
				List<String> shipModels = new ArrayList<String>(starshipsCfg.getKeys());
				Collections.shuffle(shipModels);
				
				UserVariable shipModelUV = new SFSUserVariable(Constants.USERVAR_SHIP_MODEL, shipModels.get(0));
				List<UserVariable> userVars = new ArrayList<UserVariable>();
				userVars.add(shipModelUV);
				
				sfsApi.setUserVariables(user, userVars);
			}
			
			// Add user to game
			addStarship(user);
		}
		
		// Schedule task: executes the game logic on the same frame basis (25 fps) used by the client
		gameTask = scheduler.scheduleAtFixedRate(game, 0, 40, TimeUnit.MILLISECONDS);
	}
	
	/**
	 * Method called when the game is started after the minimum number of players has been reached (or when a user enters the game after it was started).
	 * Creates the starship object for the new user and adds it to the game.
	 */
	public void addStarship(User user)
	{
		// Get starship model selected by user and saved in User Variables
		String shipModel = user.getVariable(Constants.USERVAR_SHIP_MODEL).getStringValue();
		
		// Add starship to game
		game.createStarship(user.getId(), starshipsCfg.getSFSObject(shipModel), weaponsCfg);
	}
	
	/* -------------- PUBLIC METHODS CALLED BY CORE GAME ENGINE -------------- */
	
	/**
	 * Saves starship's (A) position, velocity, direction, thrust, rotation state, shield and primary weapon energy to User Variables and (B) position in SFS2X Proximity Manager system.
	 * 
	 * (A) If the starship is newly created or its state changed due to thrust force being applied, rotation completion or due to a missile hit,
	 * the User Variables update is notified to the clients. In the first case (starship newly created), as the position in the Proximity Manager system
	 * has not been set yet, only the starship owner will receive the event, allowing his client to create his own starship in the
	 * right place and start the simulation; in the second case all clients having the starship owner in their proximity list can
	 * synchronize the simulated trajectory with the server's one.
	 * 
	 * (B) Setting the position in the Proximity Manager system makes the clients aware that a user entered their Area of Interest (AoI),
	 * so that they can create his starship and run the simulation based on the position and velocity set in his User Variables (see A).
	 *
	 * @param userId The id of the starship's owner
	 * @param x The starship's x coordinate
	 * @param y The starship's y coordinate
	 * @param vx The starship's velocity along the x axis
	 * @param vy The starship's velocity along the y axis
	 * @param direction The starship's direction
	 * @param thruster True it the starship's thruster is on
	 * @param rotation The starship's rotation direction (-1 = counterclockwise; 0 = no rotation; +1 clockwise)
	 * @param shield The starship's shield energy amount
	 * @param fireClientEvent Send update to clients
	 */
	public void setStarshipState(int userId, double x, double y, double vx, double vy, double direction, boolean thrust, int rotation, int shield, boolean fireClientEvent)
	{
		User user = room.getUserById(userId);
		
		if (user != null)
		{
			// (A) Set User Variables
			List<UserVariable> vars = new ArrayList<UserVariable>();
			vars.add(new SFSUserVariable(Constants.USERVAR_X, x));
			vars.add(new SFSUserVariable(Constants.USERVAR_Y, y));
			vars.add(new SFSUserVariable(Constants.USERVAR_VX, vx));
			vars.add(new SFSUserVariable(Constants.USERVAR_VY, vy));
			vars.add(new SFSUserVariable(Constants.USERVAR_DIR, direction));
			vars.add(new SFSUserVariable(Constants.USERVAR_THRUST, thrust));
			vars.add(new SFSUserVariable(Constants.USERVAR_ROTATE, rotation));
			vars.add(new SFSUserVariable(Constants.USERVAR_SHIELD, shield));
			
			getApi().setUserVariables(user, vars, fireClientEvent, false);
			
			// (B) Set user position in Proximity Manager system
			// Note that we convert the coordinates (expressed as double) to integers as we don't need the proximity to be very precise
			int intX = (int)Math.round(x);
			int intY = (int)Math.round(y);
			Vec3D pos = new Vec3D(intX, intY, 0);
			mmoApi.setUserPosition(user, pos, this.getParentRoom());
		}
	}
	
	/**
	 * Saves starship's current rotation direction.
	 * In case the rotation is stopped, the same User Variable is set in the position update.
	 */
	public void setStarshipRotating(int userId, int direction)
	{
		// Set User Variables
		User user = room.getUserById(userId);
		
		if (user != null)
		{
			List<UserVariable> vars = new ArrayList<UserVariable>();
			vars.add(new SFSUserVariable(Constants.USERVAR_ROTATE, direction));
			
			getApi().setUserVariables(user, vars, true, false);
		}
	}
	
	/**
	 * Sends Extension response to clients to notify the a starship exploded.
	 */
	public void notifyStarshipExplosion(int userId, double x, double y)
	{
		// Retrieve list of users which "see" the ship (in other words the shot coordinates are in their AoI)
		int intX = (int)Math.round(x);
		int intY = (int)Math.round(y);
		Vec3D pos = new Vec3D(intX, intY, 0);
		
		List<User> users = room.getProximityList(pos);
		
		ISFSObject params = new SFSObject();
		params.putInt("id", userId);
		
		// Send Extension response
		this.send(Constants.RESP_SHIP_XPLODE, params, users);
	}
	
	/**
	 * Creates an MMOItem corresponding to a weapon shot.
	 * The weapon model is set as an MMOItem Variable.
	 */
	public int addWeaponShot(String model, double x, double y, double vx, double vy)
	{
		// Create MMOItem with its Variables
		List<IMMOItemVariable> vars = buildWeaponShotMMOItemVars(x, y, vx, vy);
		vars.add(new MMOItemVariable(Constants.ITEMVAR_MODEL, model));
		vars.add(new MMOItemVariable(Constants.ITEMVAR_TYPE, Constants.ITEMTYPE_WEAPON));
		
		MMOItem item = new MMOItem(vars);
		
		// Set MMOItem position in Proximity Manager system
		setMMOItemPosition(item, x, y);
		
		// Return item ID
		return item.getId();
	}
	
	/**
	 * Removes the MMOItem corresponding to a exploded weapon shot.
	 */
	public void removeWeaponShot(int mmoItemId)
	{
		BaseMMOItem item = room.getMMOItemById(mmoItemId);
		
		mmoApi.removeMMOItem(item);
	}
	
	/**
	 * Saves weapon shot's (A) position and velocity to MMOItem Variables and (B) position in SFS2X Proximity Manager system.
	 * 
	 * (A) MMOItem Variables update is never notified to clients because they don't change after the shot is fired.
	 * They are used to store the weapon type and velocity to be transmitted to the clients as soon as the MMOItem enters their AoI.
	 * 
	 * (B) Setting the position in the Proximity Manager system makes the clients aware that a weapon shot entered their Area of Interest (AoI),
	 * so that they can create its sprite and run the simulation based on the position and velocity set in its MMOItem Variables (see A).
	 * 
	 * @param mmoItemId The ID of the MMOItem corresponding to the weapon being fired
	 * @param x The shot's x coordinate
	 * @param y The shot's y coordinate
	 * @param vx The shot's velocity along the x axis
	 * @param vy The shot's velocity along the y axis
	 */
	public void setWeaponShotPosition(int mmoItemId, double x, double y, double vx, double vy)
	{
		BaseMMOItem item = room.getMMOItemById(mmoItemId);
		
		// (A) Set MMOItem Variables
		List<IMMOItemVariable> vars = buildWeaponShotMMOItemVars(x, y, vx, vy);
		mmoApi.setMMOItemVariables(item, vars, false);
		
		// (B) Set MMOItem position in Proximity Manager system
		setMMOItemPosition(item, x, y);
	}
	
	/**
	 * Sends Extension response to clients to notify the a weapon shot exploded.
	 */
	public void notifyWeaponShotExplosion(int mmoItemId, double x, double y)
	{
		// Retrieve list of users which "see" the weapon shot (in other words the shot coordinates are in their AoI)
		int intX = (int)Math.round(x);
		int intY = (int)Math.round(y);
		Vec3D pos = new Vec3D(intX, intY, 0);
		
		List<User> users = room.getProximityList(pos);
		
		ISFSObject params = new SFSObject();
		params.putInt("id", mmoItemId);
		params.putInt("x", intX);
		params.putInt("y", intY);
		
		// Send Extension response
		this.send(Constants.RESP_SHOT_XPLODE, params, users);
	}
	
	/**
	 * Retrieves the list of MMOItems in proximity of the passed coordinates.
	 * This is used to check the collisions of the starships with the weapon shots flying around.
	 */
	public List<Integer> getWeaponShotsList(double x, double y)
	{
		List<Integer> shots = new ArrayList<Integer>();
		
		// Get MMOItems in proximity
		int intX = (int)Math.round(x);
		int intY = (int)Math.round(y);
		Vec3D pos = new Vec3D(intX, intY, 0);
		
		List<BaseMMOItem> items = room.getProximityItems(pos);
		
		// Get all MMOItems of type "weapon"
		for (BaseMMOItem item : items)
		{
			boolean isWeapon = item.getVariable(Constants.ITEMVAR_TYPE).getStringValue().equals(Constants.ITEMTYPE_WEAPON);
			
			if (isWeapon)
				shots.add(item.getId());
		}
		
		return shots;
	}
	
	/**
	 * Sends Extension response to client/s to notify the game is over.
	 */
	public void notifyGameOver(Integer[] userIds, int remainingPlayers)
	{
		int ranking = remainingPlayers + 1;
		boolean isTied = userIds.length > 1;
		
		/*
		 * The ranking determines the user experience change: the winner increases their experience,
		 * while losers (from ranking 4 and below) reduce their experience.
		 * The user experience should be saved on a central database where it could be accessed both by the lobby for matchmaking purpose
		 * (see createOrJoinRoom method in Zone Extension) and here by the game node to update it.
		 * Given that, for sake of simplicity, this example doesn't feature a database, experience is saved in a User Variable on the lobby node.
		 * The lobby is notified of the experience variation by means of a game node event. 
		 */
		
		// Retrieve list of users
		List<User> users = new ArrayList<User>();
		for (int userId : userIds)
		{
			User user = room.getUserById(userId);
			users.add(user);
			
			// Update user experience
			int changeXp = 0;
			
			if (ranking == 1)
				changeXp = +1;
			else if (ranking <= 4)
				changeXp = -1;
			
			if (changeXp != 0)
			{
				// Dispatch event to lobby node
				Map<String, Object> nodeEvtParams = new HashMap<String, Object>();
				nodeEvtParams.put("id","gameover");
				nodeEvtParams.put("u", user.getName());
				nodeEvtParams.put("xpc", changeXp);
		        
		        SFSCluster.getGameService().getApi().dispatchGameEventToLobby(nodeEvtParams);
			}
		}
		
		// Send Extension response with game ranking
		ISFSObject params = new SFSObject();
		params.putInt("r", ranking);
		params.putBool("t", isTied);
		
		this.send(Constants.RESP_GAME_OVER, params, users);
			
		// Stop game
		if (remainingPlayers == 0)
		{
			// Set game as over in Room Variables
			RoomVariable rv = new SFSRoomVariable(Constants.ROOMVAR_GAME_STATE, Constants.GAMESTATE_OVER);
			sfsApi.setRoomVariables(null, room, Arrays.asList(new RoomVariable[]{rv}));
			
			// Stop scheduled task
			gameTask.cancel(false);
			gameTask = null;
			
			trace(ExtensionLogLevel.INFO, "Game in Room " + room.getName() + " is over");
		}
	}
	
	/* -------------- PRIVATE METHODS -------------- */
	
	private List<IMMOItemVariable> buildWeaponShotMMOItemVars(double x, double y, double vx, double vy)
	{
		List<IMMOItemVariable> vars = new ArrayList<IMMOItemVariable>();
		
		vars.add(new MMOItemVariable(Constants.ITEMVAR_X, x));
		vars.add(new MMOItemVariable(Constants.ITEMVAR_Y, y));
		vars.add(new MMOItemVariable(Constants.ITEMVAR_VX, vx));
		vars.add(new MMOItemVariable(Constants.ITEMVAR_VY, vy));
		
		return vars;
	}
	
	private void setMMOItemPosition(BaseMMOItem item, double x, double y)
	{
		// Note that we convert the coordinates (expressed as double) to integers as we don't need the proximity to be very precise
		// the integer position (corresponding to the pixel position on the Flash stage) is more than enough
		int intX = (int)Math.round(x);
		int intY = (int)Math.round(y);
		Vec3D pos = new Vec3D(intX, intY, 0);
		mmoApi.setMMOItemPosition(item, pos, this.getParentRoom());
	}
}
