package sfs3.extensions.examples.spacewar2;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.apache.commons.io.FileUtils;

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

import sfs3.extensions.examples.spacewar2.core.Game;
import sfs3.extensions.examples.spacewar2.handlers.event.UserLeaveRoomEventHandler;
import sfs3.extensions.examples.spacewar2.handlers.requests.ControlRequestHandler;
import sfs3.extensions.examples.spacewar2.handlers.requests.InitRequestHandler;
import sfs3.extensions.examples.spacewar2.tasks.AbortTask;
import sfs3.extensions.examples.spacewar2.tasks.CountdownTask;
import sfs3.extensions.examples.spacewar2.util.Constants;

public class SW2RoomExtension extends SFSExtension
{
	private SmartFoxServer sfs;
	private ISFSMMOApi mmoApi;
	private MMORoom room;
	
	private ISFSObject settings;
	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()
	{
		// Get a reference to the parent MMORoom
		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();
		
		try
		{
			// Get level id from Room Variables
			String levelId = room.getVariable(Constants.ROOMVAR_GAME_LEVEL).getStringValue();
			
			// Load configuration file and extract game configuration
			loadGameConfig(levelId);
			
			// 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), settings.getInt(Constants.SETTING_GAME_ABORT_SECS), TimeUnit.SECONDS);
		}
		catch (IOException e)
		{
			trace(ExtensionLogLevel.ERROR, "SpaceWar 2 configuration could not be loaded");
		}
		
		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 getSettings()
	{
		return settings;
	}
	
	/**
	 * 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), settings.getInt(Constants.SETTING_GAME_START_SECS), TimeUnit.SECONDS);
		
		trace(ExtensionLogLevel.INFO, "Game start countdown launched in Room " + room.getName() + ": game will start in " + settings.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 " + settings.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, TransportType.TCP);
		
		// 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, TransportType.TCP);
		
		// 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;
		
		// Retrieve list of users
		List<User> users = new ArrayList<User>();
		for (int userId : userIds)
		{
			User user = room.getUserById(userId);
			users.add(user);
			
			int userXp = user.getVariable(Constants.USERVAR_EXPERIENCE).getIntValue();
			
			// Winner increases its experience
			if (ranking == 1)
			{
				if (userXp < Constants.SETTING_MAX_USER_XP)
				{
					UserVariable uv = new SFSUserVariable(Constants.USERVAR_EXPERIENCE, userXp++);
					sfsApi.setUserVariables(user, Arrays.asList(new UserVariable[]{uv}));
				}
			}
			
			// Losers (from ranking 4 and below) reduce their experience
			if (ranking <= 4)
			{
				if (userXp > 0)
				{
					UserVariable uv = new SFSUserVariable(Constants.USERVAR_EXPERIENCE, userXp++);
					sfsApi.setUserVariables(user, Arrays.asList(new UserVariable[]{uv}));
				}
			}
		}
		
		ISFSObject params = new SFSObject();
		params.putInt("r", ranking);
		params.putBool("t", isTied);
		
		// Send Extension response
		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());
	}
	
	/**
	 * Load the game configuration from the external configuration file.
	 */
	private void loadGameConfig(String levelId) throws IOException
	{
		// Load configuration file
		String cfgData = JSONUtil.stripComments(FileUtils.readFileToString(new File(this.getCurrentFolder() + "SpaceWar2.config")));
		
		// Convert to temporary SFSObject
		ISFSObject tempCfg = SFSObject.newFromJsonData(cfgData);
		
		// Get global game settings
		settings = tempCfg.getSFSObject(Constants.CONFIG_SETTINGS);
		
		// Selected level (planet)
		ISFSArray tmpLevelsCfg = tempCfg.getSFSArray(Constants.CONFIG_LEVELS);
		levelCfg = new SFSObject();
		
		for (int i = 0; i < tmpLevelsCfg.size(); i++)
		{
			if (tmpLevelsCfg.getSFSObject(i).getString("name").equals(levelId))
			{
				levelCfg = tmpLevelsCfg.getSFSObject(i);
				break;
			}
		}
		
		// Starships
		ISFSArray tmpStarshipsCfg = tempCfg.getSFSArray(Constants.CONFIG_STARSHIPS);
		starshipsCfg = new SFSObject();
		
		for (int i = 0; i < tmpStarshipsCfg.size(); i++)
		{
			ISFSObject starship = tmpStarshipsCfg.getSFSObject(i);
			String model = starship.getString("model");
			
			starshipsCfg.putSFSObject(model, starship);
		}
		
		// Weapons
		ISFSArray tmpWeaponsCfg = tempCfg.getSFSArray(Constants.CONFIG_WEAPONS);
		weaponsCfg = new SFSObject();
		
		for (int i = 0; i < tmpWeaponsCfg.size(); i++)
		{
			ISFSObject weapon = tmpWeaponsCfg.getSFSObject(i);
			String model = weapon.getString("model");
			
			weaponsCfg.putSFSObject(model, weapon);
		}
		
		trace(ExtensionLogLevel.INFO, "SpaceWar 2 configuration loaded by Room " + room.getName());
	}
}
