package sfs.cluster.lobby.friendlist;

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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.json.JSONArray;

import sfs.cluster.lobby.ClusterLobbyExtension;

/**
 * Manages the Friend Lists
 */
public class FriendListManager
{
	// max number of Friends in each user's friend list
	static int MAX_FRIEND_LIST_LEN = 300;
	
	/*
	 * Clustered Map: this is shared globally across the cluster
	 */
	static Map<String, List<Friend>> friendLists;
	
	
	Zone zone;
	ClusterLobbyExtension parentExtension;
	
	public FriendListManager(Zone zone, AbstractExtension parentExtension)
    {
		// lazy instantiation
		if (friendLists == null)
			friendLists = new ConcurrentHashMap<String, List<Friend>>();
		
		this.zone = zone;
		this.parentExtension = (ClusterLobbyExtension) parentExtension;
	}
	
	/**
	 * @param userName 	the name of the user
	 * @return	his/her user list
	 */
	public List<Friend> getFriendList(String userName)
	{
		return friendLists.get(userName);
	}
	
	/**
	 * Add a new friend to a list
	 * 
	 * @param owner		the owner of the list
	 * @param friend	the new Friend to add
	 * 
	 */
	public void addFriend(String owner, Friend friend)
	{
		List<Friend> fList = friendLists.get(owner);
		
		if (fList == null)
		{
			fList = new LinkedList<Friend>();
			friendLists.put(owner, fList);
		}
		
		// Limit exceeded
		if (fList.size() >= MAX_FRIEND_LIST_LEN)
			return;
		
		synchronized (fList)
        {
			if (!fList.contains(friend))
				fList.add(friend);
        }
	}
	
	/**
	 * Remove a friend from a list
	 * 
	 * @param owner		the owner of the list
	 * @param friend	the Friend to be removed
	 */
	public void removeFriend(String owner, Friend friend)
	{
		List<Friend> fList = friendLists.get(owner);
		
		if (fList != null)
		{
			synchronized (fList)
	        {
		        fList.remove(friend);
	        }
		}
	}
	
	/**
	 * Remove a list
	 * 
	 * @param owner	the owner of the List to be removed from the manager
	 */
	public void removeList(String owner)
	{
		friendLists.remove(owner);
	}
	
	/**
	 * 
	 * @param owner			the owner of the list
	 * @param friendName	the name of the Friend to check
	 * @return				true, if the Friend was found
	 */
	public boolean hasFriend(String owner, String friendName)
	{
		boolean found = false;
		
		List<Friend> fList = getFriendList(owner);
		
		if (fList != null)
		{
			for (Friend friend : fList)
			{
				if (friendName.equals(friend.name))
				{
					found = true;
					break;
				}
			}
		}
	
		return found;
	}
	
	/**
	 * This will return a map of Lists containing the users to update divided by nodes
	 * In the local nodes the LinkedList will contain the actual socket channels to update
	 * In the remote nodes the LinkeList will contain the names of the users to update
	 * 
	 * Remote nodes will receive the updates via "post-office" communication
	 * 
	 * @param friendName	the name of the Friend
	 * @return the lists of users interested in the change grouped by node
	 * 
	 */
	public Map<String, LinkedList<Object>> getClientsToUpdate(String friendName)
    {
		Map<String, LinkedList<Object>> updates = new HashMap<String, LinkedList<Object>>();
		Set<String> keyList = null;
		String owner = null;
		
		// Grab the set of buddy lists
	    keyList = friendLists.keySet();
	    
	    // Run through all 
	    for (Iterator<String> it = keyList.iterator(); it.hasNext();)
	    {
			// get key
			owner = it.next();
			
			// get List
			List<Friend> fList = friendLists.get(owner);
			
			// skip empty lists
			if ( fList != null && fList.size() > 0 )
			{
			    synchronized ( fList )
			    {
			    	// cycle through all friends
			    	for (Friend f : fList)
			    	{
			    		// found an item!
			    		if (f.getName().equals(friendName))
			    		{
			    			LinkedList<Object> channelsToUpdate = updates.get(f.getNodeId());
			    			
			    			// lazy instantiation
			    			if (channelsToUpdate == null)
			    			{
			    				channelsToUpdate = new LinkedList<Object>();
			    				updates.put(f.getNodeId(), channelsToUpdate);
			    			}
			    			
			    			// Add local socket channel
			    			if (f.getNodeId().equals(parentExtension.getNodeId()))
			    			{
			    				channelsToUpdate.add( zone.getChannelFromName(owner) );
			    			}
			    			
			    			// Add remote user name
			    			else
			    			{
			    				channelsToUpdate.add(owner);
			    			}
			    			
			    			// Stop inner loop
			    			break;
			    		}
			    	}
			    }
			}
	    }

		return updates;
    }
	
	/*
	 * Find out which local clients should be updated after a batch-user-removal (node went down)
	 * Instead of sending each update separately we send each client a list of users that were disconnected in a batch
	 * 
	 * Each item in the return list is an array with two elements:
	 * 		
	 * 		0 -> LinkedList recipient(s) of the message
	 * 		1 -> the JSON Array of names 
	 */
	public List<Object[]> getLocalClientsForBatchRemoveUpdate(List<String> batchRemovedClients)
	{
		List<Object[]> localClients = new ArrayList<Object[]>();	
		List localUsers = zone.getUserList();
		
		// Cycle through each local-node user
		for (Object o : localUsers)
		{
			User user = (User) o;
			String userName = user.getName();
			
			Object[] listItem = null;
			JSONArray removedUserList = null;
			LinkedList<Object> recipients = null;
			
			// Cycle through all removed users to see if they are present in this local user friendlist
			for (String removedUserName : batchRemovedClients)
			{
				if (hasFriend(userName, removedUserName))
				{
					// lazy object creation
					if (removedUserList == null)
						removedUserList = new JSONArray();
					
					removedUserList.put(removedUserName);
				}
			}
			
			// If the array exits we need to send data to this local user
			if (removedUserList != null)
			{
				recipients = new LinkedList<Object>();
				recipients.add(user.getChannel());
				
				// Create the list item
				listItem = new Object[2];
				listItem[0] = recipients;
				listItem[1] = removedUserList;
				
				// Add it to the returned list
				localClients.add(listItem);
			}	
		}
		
		return localClients;
	}
}
