package sfs.cluster.lobby;

import it.gotoandplay.smartfoxserver.SmartFoxServer;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;

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

import sfs.cluster.lobby.data.ClusterEvent;
import sfs.cluster.lobby.data.ClusterRoom;
import sfs.cluster.lobby.data.ClusterUser;

/**
 * This is the main datastore class. Here we keep track of all collections that are shared cluster wide 
 */
public class DataStore
{
	private static DataStore _instance;
	
	//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
	// Cluster wide objects (roots)
	//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
	public Map<String, ClusterUser> clusterUsersByName;							// keeps users by name
	public Map<String, ClusterRoom> clusterRoomsByName;							// keeps rooms by name
	public Map<String, BlockingQueue<ClusterEvent>> postOffice;					// the post office, for node message exchange
	public Map<String, AtomicInteger> nodeUserCounts;							// keep a count of the total amount of users in each node
	
	private Logger log;
	
	// Private constructor
	private DataStore()
    {
		log = SmartFoxServer.log;
		
		/*
		 * Lazy instatiation
		 * It is very important that these data structures are initialized once
		 * In fact when a new JVM comes up in the cluster it will have these collections auto-populated by Terracotta
		 */
		
		if (clusterUsersByName == null)
	    	clusterUsersByName = new ConcurrentHashMap<String,ClusterUser>();
	    
	    if (clusterRoomsByName == null)
	    	clusterRoomsByName = new ConcurrentHashMap<String, ClusterRoom>();
	    
	    if (postOffice == null)
	    	postOffice = new ConcurrentHashMap<String, BlockingQueue<ClusterEvent>>();
	    
	    if (nodeUserCounts == null)
	    	nodeUserCounts = new ConcurrentHashMap<String, AtomicInteger>();	  
	}
	
	public static DataStore instance()
	{
		if (_instance == null)
			_instance = new DataStore();
		
		return _instance;
	}
	
	/**
	 * Dispatch a cluster-wide event to a certain node
	 * 
	 * @param event					the event
	 * @param recipientNode			the recipient node
	 *
	 *
	 */
	public void dispatchClusterEvent(ClusterEvent event, String recipientNode)
	{
		BlockingQueue<ClusterEvent> mailBox = _instance.postOffice.get(recipientNode);
		
		if (mailBox != null && event != null)
			mailBox.offer(event);
		else
			log.warning("Could not disptach cluster event. Node: " + recipientNode + ", Event: " + event);
	}
	
	/**
	 * Broadcast event to multiple nodes
	 * 			
	 * @param event
	 * @param nodeList
	 */
	public void broadcastClusterEvent(ClusterEvent event, List<String> nodeList)
	{
		for (String node : nodeList)
		{
			dispatchClusterEvent(event, node);
		}
	}
	
	/**
	 * Grab a list of the lobby nodes in the cluster
	 * @return	the list of nodes
	 */
	public List<String> getLobbyNodes()
	{
		List<String> nodes = new ArrayList<String>();
		
		for (String node : nodeUserCounts.keySet())
		{
			// Get the IP of the node
			int nodeIp = Integer.parseInt(node.substring(node.indexOf(':') + 1));
			
			/*
			 * Grab all Lobby Nodes ( ip < 10000 )
			 * TODO: instead of hard-coded integer, use a constant
			 */
			if (nodeIp < 10000)
				nodes.add(node);
		}
		
		return nodes;
	}
	
	/**
	 * From a gameType, locate the ID of the game server node
	 * 
	 * @param gameType	the type of game
	 * @return			the game server node ID
	 */
	public String locateGameServerNode(String gameType)
	{
		String nodeId = null;
		String gameServerPort = ClusterUtils.getGameServerPortFromType(gameType);
		
		if (gameServerPort != null)
		{
			gameServerPort = ":" + gameServerPort;
			
			for (String node : nodeUserCounts.keySet())
			{
				if (node.indexOf(gameServerPort) > 0)
				{
					nodeId = node;
					break;
				}
			}
		}
		
		return nodeId;
	}
	
	/**
	 * Get a JSON formatted list of games
	 * 
	 * @param gameType	the type of games
	 * @return			the json array of games
	 */
	public JSONArray getGameList(String gameType)
	{
		JSONArray roomList = new JSONArray();
		
		if (ClusterUtils.getGameServerPortFromType(gameType) != null)
		{
			for (ClusterRoom room : clusterRoomsByName.values())
			{
				if (room.getGameType().equals(gameType))
				{
					JSONObject jsRoomObj = new JSONObject();
					jsRoomObj.put("name", room.getName());
					jsRoomObj.put("id", room.getLocalID());
					jsRoomObj.put("pCount", room.getPlayerCount().intValue() + "/" + room.getMaxPlayers());
					
					roomList.add(jsRoomObj);
				}
			}
		}
		
		return roomList;
	}
	
	/**
	 * Return the full list of user names of a certain node
	 * 
	 * @param nodeId	id of the node
	 * @return			a list of names
	 */
	public List<String> getAllUsersFromNode(String nodeId)
	{
		return getOrRemoveAllUsersFromNode(nodeId, false);
	}
	
	/**
	 * Return and remove the full list of user names of a certain node
	 * 
	 * @param nodeId	id of the node
	 * @return			a list of names
	 */
	public List<String> removeAllUsersFromNode(String nodeId)
	{
		return getOrRemoveAllUsersFromNode(nodeId, true);
	}
	
	/**
	 * Get the list of users from a node. If isRemove is true each user is removed
	 * 
	 * @param nodeId		the id of the node
	 * @param isRemove		if true, it will remove all the users from that node (we use it when a node goes down)
	 * @return				a list of names
	 */
	private List<String> getOrRemoveAllUsersFromNode(String nodeId, boolean isRemove)
	{
		List<String> userList = new ArrayList<String>();
		Iterator<ClusterUser> it = clusterUsersByName.values().iterator();
		
		while(it.hasNext())
		{
			ClusterUser user = it.next();
			
			if (user.getNodeId().equals(nodeId))
			{
				userList.add(user.getName());
				
				// Should it be removed?
				if (isRemove)
					it.remove();
			}
		}
		
		return userList;
	}
	
	/**
	 * return a list of game names from a node
	 * 
	 * @param nodeId	the node id
	 * @return			a list of game names
	 */
	public List<String> getAllRoomsFromNode(String nodeId)
	{
		return getOrRemoveAllRoomsFromNode(nodeId, false);
	}
	
	/**
	 * removes the room names from a node
	 * 
	 * @param nodeId	the id of the node
	 * @return			the list of names
	 */
	public List<String> removeAllRoomsFromNode(String nodeId)
	{
		return getOrRemoveAllRoomsFromNode(nodeId, true);
	}
	
	/**
	 * Get a the full list of room names from a certain node
	 * 
	 * @param nodeId	the id of the node
	 * @param remove	if true, it will remove all the names from the collection (used when a node goes down)
	 * @return			a list of game room names
	 */
	private List<String> getOrRemoveAllRoomsFromNode(String nodeId, boolean remove)
	{
		// grab the 2nd part of the nodeID ->  addr:ip
		String gameType = ClusterUtils.getGameTypeFromPort(nodeId.split(":")[1]);
		
		List<String> roomList = new ArrayList<String>();
		Iterator<ClusterRoom> it = clusterRoomsByName.values().iterator();
		
		while(it.hasNext())
		{
			ClusterRoom room = it.next();
			
			if(room.getGameType().equals(gameType)) 
			{
				roomList.add(room.getName());
				
				if (remove)
					it.remove();
			}
		}
		
		return roomList;
	}
	
	/**
	 * Remove a node from the data store
	 * 
	 * @param nodeId	the id of the node
	 */
	public void removeNode(String nodeId)
	{
		// remove message queue for dead node
		postOffice.remove(nodeId);
		
		// remove user-counter for dead node
		nodeUserCounts.remove(nodeId);
		
		// get the full list of user names connected to the dead node
		List<String> usersFromDeadNode = removeAllUsersFromNode(nodeId);
		
		// get the full list of room names that were present in the dead node
		List<String> roomsFromDeadNode = removeAllRoomsFromNode(nodeId);
		
		// Fire a batch_user_remove event to all other lobby-nodes in the cluster
		ClusterEvent event = new ClusterEvent(ClusterEvent.ON_BATCH_USER_REMOVE);
		event.getParams().put("userList", usersFromDeadNode);
		
		// Fire a batch_room_remove event to all other lobby-nodes in the cluster
		//...
		
		broadcastClusterEvent(event, getLobbyNodes());
	}
	
	/**
	 * Removes a game room, globally
	 * 
	 * @param roomId 	the local id of the room (local to the server instance)
	 * @return			the Room object
	 */
	public ClusterRoom removeGameRoom(int roomId)
	{
		ClusterRoom removedRoom = null;
		
		for (ClusterRoom r : clusterRoomsByName.values())
		{
			if (r.getLocalID() == roomId)
			{
				String roomName = r.getName();
				String gameType = r.getGameType();
				
				removedRoom = clusterRoomsByName.remove(roomName);
				break;
			}
		}
		
		return removedRoom;
	}
	
	/**
     * @return the clusterUsersByName
     */
    public Map<String, ClusterUser> getClusterUsersByName()
    {
	    return _instance.clusterUsersByName;
    }


	/**
     * @return the clusterRoomsByName
     */
    Map<String, ClusterRoom> getClusterRoomsByName()
    {
	    return _instance.clusterRoomsByName;
    }

    /**
     * @return the postOffice
     */
    Map<String, BlockingQueue<ClusterEvent>> getPostOffice()
    {
	    return _instance.postOffice;
    }

    /**
     * @return the nodeUserCounts
     */
    Map<String, AtomicInteger> getNodeUserCounts()
    {
	    return _instance.nodeUserCounts;
    }
}
