package be.ac.ulb.code.wit.contacts;

import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;

public class Contacts {
	/**
	 * The default group that contains every single contact.
	 */
	public final static String GROUP_ALLCONTACTS = null; 
	
	private final Map<Integer, IdentifiedContact> contacts = new HashMap<Integer, IdentifiedContact>();
	private Date lastModified = new Date();
	private int maxContactId = 0;
	
	/**
	 * Generate a new Identifier for a contact in this contacts list.
	 * @return The newly generated identifier.
	 */
	private int getNewId() {
		maxContactId++;
		return maxContactId;
	}
	
	/**
	 * Describes a contact keyed in a Contacts collection
	 */
	public class IdentifiedContact {
		final private int contactId = getNewId();
		final private Contact inner; 
		
		/**
		 * Build a new reference to a given Contact, associating is with an id.
		 * @param inner The contact to be keyed in the collection
		 */
		public IdentifiedContact(final Contact inner) {
			this.inner = inner;
		}
		
		/**
		 * Retrieves the id of the contact in the collection.
		 */
		public int getContactId() {
			return contactId;
		}
		
		/**
		 * Retrieves the actual contact that is keyed in the collection.
		 */
		public Contact getContact() {
			return inner;
		}
	}

	/**
	 * Add a contact to this contact list, and give this contact a new identifier
	 * @param contact The contact to be added.
	 * @return An IdentifiedContact, associating a unique identifier with 
	 * 		   the contact.
	 */
	public IdentifiedContact addContact(final Contact contact) {
		final IdentifiedContact identifiedContact = new IdentifiedContact(contact);
		contacts.put(identifiedContact.getContactId(), identifiedContact);
		identifiedContact.getContact().addObserver(new Observer() {
			@Override
			public void update(final Observable arg0, final Object arg1) {
				updateContacts();
			}
		});
		updateContacts();
		
		return identifiedContact;
	}
	
	/**
	 * Add a new contact by its display name. The displayName must satisfy
	 * to the same constraints specified in {@link Contact}.
	 * @param displayName The display name of the new contact
	 * @return An IdentifiedContact, associating a unique identifier with 
	 * 		   the new contact. 
	 */
	public IdentifiedContact addContact(final String displayName) {
		return addContact(new Contact(displayName));
	}

	/**
	 * Retrieves the contact associated with a given identifier
	 * @param contactId The identifier of the contact.
	 * @return The contact corresponding to the given identifier.
	 */
	public IdentifiedContact getContact(final int contactId) {
		return contacts.get(contactId);
	}
	
	/**
	 * Remove the contact from the contact list.
	 * @param contact The contact to be removed from the list.
	 */
	public void removeContact(final IdentifiedContact contact) {
		contacts.remove(contact.getContactId());
		updateContacts();
	}
	
	/**
	 * Returns the date at which the contacts list was last modified.
	 * @return The date at which the contacts list was last modified.
	 */
	public Date getLastModified() {
		return new Date(lastModified.getTime());
	}
	
	/**
	 * Add the specified contact to the specified group.
	 * @param contact The contact to be added to a group.
	 * @param group The group to which the contact is to be added. 
	 * 			If the group is GROUP_ALLCONTACTS, addContactToGroup
	 * 			does not perform any change.
	 */
	public void addContactToGroup(final IdentifiedContact contact, final String group) {
		if (group == GROUP_ALLCONTACTS) {
			return;
		}
		
		contact.getContact().addGroup(group);
	}
	
	/**
	 * Remove the specified contact from the specified group.
	 * @param contact The contact to be removed from a group.
	 * @param group The group removed which the contact is to be remove. 
	 * 			If the group is GROUP_ALLCONTACTS, removeFromGroup
	 * 			removes the contact from the contacts list.
	 */
	public void removeFromGroup(final IdentifiedContact contact, final String group) {
		if (group == GROUP_ALLCONTACTS) {
			removeContact(contact);
		}
		else { 
			contact.getContact().deleteGroup(group);
		}
	}

	/**
	 * Retrieves the list of groups to which contacts belong.
	 * @return The list of groups to which contacts belong.
	 */
	public Collection<String> getGroups() {
		Set<String> result = new HashSet<String>();
		
		for (IdentifiedContact contact: contacts.values()) {
			result.addAll(contact.getContact().getGroups());
		}
		
		return result;
	}
	
	/**
	 * Retrieves the list of groups to which a given contact belongs.
	 * @param contact The contact for which groups are to be retrieved.
	 * @return The list of groups to which the given contact belongs.
	 */
	public Set<String> getContactGroups(final IdentifiedContact contact) {
		return contact.getContact().getGroups();
	}
	
	/**
	 * Retrieves a collection of all contacts in the contact list.
	 * @return A collection of all contacts in the contact list
	 */
	public Collection<IdentifiedContact> getContacts() {
		return Collections.unmodifiableCollection(contacts.values());
	}
		
	/**
	 * Retrieves the list of contacts belonging to a given group.
	 * @param group The group for which contacts are to be retrieved.
	 * @return The list of contacts belonging to the given group. 
	 * 			If the group is GROUP_ALLCONTACTS, getContacts
	 * 			acts as its parameterless counterpart.
	 */
	public Collection<IdentifiedContact> getContacts(final String group) {
		Collection<IdentifiedContact> result;
		
		if (group == GROUP_ALLCONTACTS) {
			result = getContacts();
		}
		else {
			Set<IdentifiedContact> contacts = 
					new HashSet<Contacts.IdentifiedContact>();
			
			for (IdentifiedContact contact: this.contacts.values()) {
				if (contact.getContact().isInGroup(group)) {
					contacts.add(contact);
				}
			}
			
			result = contacts;
		}
		
		return result;
	}
	
	/**
	 * Set the last modified date of the contacts list
	 */
	private void updateContacts() {
		lastModified = new Date();
	}
}

