package sfs.cluster.lobby;

import it.gotoandplay.smartfoxserver.SmartFoxServer;
import it.gotoandplay.smartfoxserver.data.User;
import it.gotoandplay.smartfoxserver.extensions.AbstractExtension;

import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Logger;

import sfs.cluster.lobby.data.ClusterRoom;

import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

/**
 * Time-based updater. This class provides a way to update the game room list at specified time intervals instead of real-time.
 * This approach is more optimized in larger applications were thousands of game rooms are running at any time. 
 */
public class GameListUpdater implements Runnable
{
	// The update frequency
	static final long UPDATE_FREQUENCY = 3000L; // ms.
	boolean running = false;
	
	// Here's where we aggregate the room changes
	ConcurrentMap<String, ConcurrentMap<String, Object>> gameChanges;
	
	// The GameListUpdater runs on its own dedicated thread
	Thread gameListUpdaterThread;
	ClusterLobbyExtension parentExtension;
	Logger log;
	
	public GameListUpdater(AbstractExtension parentExtension)
    {
		this.parentExtension = (ClusterLobbyExtension) parentExtension;
		gameListUpdaterThread = new Thread(this, "__::gameUpdaterThread::__");
		log = SmartFoxServer.log;
		
		init();
		gameListUpdaterThread.start();
    }
	
	private void init()
	{
		gameChanges = new ConcurrentHashMap<String, ConcurrentMap<String,Object>>();
	}
	
	/**
	 * Set that a certain game of a certain type was updated
	 * We accumulate changes for a number of seconds and then fire the changes to all clients
	 * 
	 * @param gameType		the type of the game
	 * @param gameName		the name of the game
	 */
	public synchronized void setGameChanged(String gameType, String gameName)
	{
		ConcurrentMap<String, Object> gameList = gameChanges.get(gameType);
		
		if (gameList == null)
		{
			gameList = new ConcurrentHashMap<String, Object>();
			gameChanges.put(gameType, gameList);
		}
		
		/*
		 * We put an empty object as a place-holder
		 */
		gameList.put(gameName, new Object());
	}
	
	public void dispose()
	{
		this.running = false;
	}
	
	public void run()
	{
	    this.running = true;
	    
	    while( running )
	    {
	    	try
	    	{
	    		Thread.sleep(UPDATE_FREQUENCY);
	    		
	    		Map<String, List<SocketChannel>> usersByGameType = getUsersByGameType();
	    		Map<String, JSONArray> gameChangesByType = getGameChangesByType();
	    		
	    		// Send an update for each game type
	    		for (Map.Entry<String, JSONArray> entry : gameChangesByType.entrySet())
	    		{
	    			JSONObject update = new JSONObject();
	    			update.put("_cmd", "gameListUp");
	    			update.put("list", entry.getValue());
	    			
	    			LinkedList recipients = (LinkedList) usersByGameType.get(entry.getKey());
	    			
	    			if (recipients != null)
		    			parentExtension.sendResponse(	update, 
		    											-1,
		    											null, 
		    											recipients
		    										);
	    			
	    		}
	    	}
	    	
	    	catch(InterruptedException ie)
	    	{
	    		log.warning("GameUpdater Interrupted");
	    	}
	    	catch(Exception e)
	    	{
	    		e.printStackTrace();
	    	}
	    }
	    
	}
	
	/*
	 * We lock on {this} object to avoid changes in the main MAP as we populate the
	 * array of changes.
	 * 
	 * If this is changed concurrently we might loose our data
	 * At the end of the process the main MAP is destroyed
	 * 
	 */
	private synchronized Map<String, JSONArray> getGameChangesByType()
	{
		Map<String, JSONArray> gameChangesByType = new HashMap<String, JSONArray>();
		
		// Cycle through all game types
		for (Map.Entry<String, ConcurrentMap<String, Object>> entry : gameChanges.entrySet())
		{
			JSONArray gameList = new JSONArray();
			
			// Cycle through all games that were changed for the current game-type
			for (String gameName : entry.getValue().keySet())
			{
				JSONObject jsRoomObj = new JSONObject();
				jsRoomObj.put("name", gameName);
				
				ClusterRoom gameRoom = DataStore.instance().clusterRoomsByName.get(gameName);
				
				// room exists -> populate data
				if (gameRoom != null)
				{
					jsRoomObj.put("id", gameRoom.getLocalID());
					jsRoomObj.put("pCount", gameRoom.getPlayerCount().intValue() + "/" + gameRoom.getMaxPlayers());
				}
				
				// room doesn't exist -> mark as removed
				else
				{
					jsRoomObj.put("id", -1);
				}
				
				gameList.add(jsRoomObj);
			}
			
			gameChangesByType.put(entry.getKey(), gameList);
		}
		
		// Destroy local data
		init();
		
		return gameChangesByType;
	}
	
	/**
	 * Creates a map of the connected users grouped by gameType.
	 * Each user is currently listening for updates of one of the many games available in the system.
	 * This method groups these listeners by game-type. It is used to fire updates to each specific group.
	 * 
	 * @return the map of users
	 */
	private Map<String, List<SocketChannel>> getUsersByGameType()
	{
		Map<String, List<SocketChannel>> usersByGameType = new HashMap<String, List<SocketChannel>>();
		
		List allUsers = parentExtension.currentZone.getUserList();
		
		for (Object o : allUsers)
		{
			User user = (User) o;
			String gameType = (String) user.properties.get("gameType");
			
			if (gameType != null)
			{
				List<SocketChannel> recipients = usersByGameType.get(gameType);
			
				if (recipients == null)
				{
					recipients = new LinkedList<SocketChannel>();
					usersByGameType.put(gameType, recipients);
				}
				
				recipients.add(user.getChannel());
			}
		}
		
		return usersByGameType;
	}
}
