package be.ac.ulb.code.wit.resources.serializers;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

import org.apache.abdera.Abdera;
import org.apache.abdera.factory.Factory;
import org.apache.abdera.model.Entry;
import org.apache.abdera.model.Feed;

import be.ac.ulb.code.wit.contacts.Contact;
import be.ac.ulb.code.wit.contacts.Contacts;
import be.ac.ulb.code.wit.contacts.Contacts.IdentifiedContact;
import be.ac.ulb.code.wit.resources.ServicePaths;
import be.ac.ulb.code.wit.resources.serializers.AtomHelpers.Relations;

import com.sun.jersey.api.uri.UriTemplate;

public class ContactSerializer {
	private final Contacts contacts;
	private final static Factory ABDERA_FACTORY = Abdera.getInstance().getFactory();
	private static final UriTemplate ContactIdTempate = 
			new UriTemplate(ServicePaths.getFullPath(ServicePaths.CONTACT));
	private String group = null;

	/**
	 * Retrieves a contact in a contacts list by its identifier.
	 * @param contactId The identifier of the contact to be retrieved. 
	 * @param contacts The contacts list in which the specified contact is to be found.
	 * @return The contact as it appears in the contacts list.
	 * @throws NotFoundException The specified contact could not be found in the contacts list.
	 */
	private static IdentifiedContact getContact(final int contactId, Contacts contacts) throws NotFoundException {
		IdentifiedContact result = contacts.getContact(contactId);
		if (result == null) {
			throw new NotFoundException("The specified id could not be matched to a contact.");
		}
		return result;
	}

	/**
	 * Retrieves a contact in a contacts list by its identifier.
	 * @param contactId The identifier of the contact to be retrieved. 
	 * @param contacts The contacts list in which the specified contact is to be found.
	 * @return The contact as it appears in the contacts list.
	 * @throws NotFoundException The specified contact could not be found in the contacts list.
	 */
	public static IdentifiedContact getContact(final String contactId, Contacts contacts) throws NotFoundException {
		try {
			final int cid = Integer.parseInt(contactId);
			return getContact(cid, contacts);
		}
		catch (NumberFormatException e) {
			throw new NotFoundException("The specified id has an incorrect format.", e);
		}
	}
	
	/**
	 * Retrieves a contact in a contacts list by and url-identifier, as it occurs in atom entry documents.
	 * @param url The url-identifier of the contact to be retrieved. 
	 * @param contacts The contacts list in which the specified contact is to be found.
	 * @return The contact as it appears in the contacts list.
	 * @throws NotFoundException The specified contact could not be found in the contacts list.
	 */
	public static IdentifiedContact getContactFromId(final String url, Contacts contacts) throws NotFoundException {
		Map<String,String> groupValues = new HashMap<String, String>();
		if (ContactIdTempate.match(url, groupValues)) {
			return getContact(groupValues.get("contactid"), contacts);
		}
		else {
			throw new NotFoundException("The specified id has an incorrect format.");	
		}
	}
	
	/**
	 * Ensures that specified contact belongs to the group used by this serializer.
	 * @param contact The contact to be checked
	 * @return The contact passed as an argument if it belongs to the group of the serializer. Throws a NotFoundException otherwise.
	 * @throws NotFoundException The specified contact does not belong to the group.
	 */
	private IdentifiedContact checkContactInGroup(IdentifiedContact contact) throws NotFoundException {
		if (group != Contacts.GROUP_ALLCONTACTS) {
			if (!contacts.getContactGroups(contact).contains(group)) {
				throw new NotFoundException("The specified contact does no belong to the requested group");
			}
		}
		return contact;
	}
	
	/**
	 * Retrieves a contact in a contacts list by its identifier.
	 * @param contactId The identifier of the contact to be retrieved. 
	 * @return The contact as it appears in the contacts list.
	 * @throws NotFoundException The specified contact could not be found in the contacts list.
	 */
	public IdentifiedContact getContact(final String contactId) throws NotFoundException {
		return checkContactInGroup(getContact(contactId, contacts));
	}

	/**
	 * Construct a new ContactSerializer, using information
	 * from the given Contacts list and working in the context
	 * of the given group.
	 * 
	 * @param contacts The contacts list to be used.
	 * @param group The contacts group in which the serialization occurs.
	 * @throws NotFoundException if the specified group is not valid.
	 */
	public ContactSerializer(final Contacts contacts, final String group) throws NotFoundException {
		super();
		this.contacts = contacts;
		checkGroup(group);
		
		this.group = group;
	}

	/**
	 * Check that a given group has a valid name, or is the default group
	 * with all contacts. Otherwise, throw a NotFoundException.
	 * @param group The group to be checked.
	 * @throws NotFoundException if the specified group is not valid.
	 */
	public static void checkGroup(String group) {
		if (group != Contacts.GROUP_ALLCONTACTS) {
			if (!Contact.validGroupName(group)) {
				throw new NotFoundException();
			}
		}
	}

	/**
	 * Creates an atom entry representing a contact.
	 * @param identifiedContact The contact to be serialized as an atom entry.
	 * @param summary Whether only a summary of the information (to include into
	 * 				a feed) is to be generated.
	 * @return an atom entry representing the given contact.
	 */
	private Entry makeEntry(final IdentifiedContact identifiedContact, final boolean summary) {
		final Entry entry = ABDERA_FACTORY.newEntry();
		final Contact contact = identifiedContact.getContact();
		final String contactId = Integer.toString(identifiedContact.getContactId());

		entry.setTitle(contact.getDisplayName());
		entry.setEdited(contact.getLastModified());
		entry.setId(ServicePaths.getURI(ServicePaths.CONTACT,
			"contactid", contactId));
		entry.addLink(getURL(identifiedContact).toString(), Relations.EDIT);
		entry.addLink(getURL(identifiedContact).toString(), Relations.SELF);

		if (!summary) {
			entry.addExtension(AtomHelpers.createExtensionElement(
				AtomHelpers.ExtensionElements.CONTACT_FIRSTNAME,
				contact.getFirstName()));

			entry.addExtension(AtomHelpers.createExtensionElement(
				AtomHelpers.ExtensionElements.CONTACT_LASTNAME,
				contact.getLastName()));

			entry.addExtension(AtomHelpers.createExtensionElement(
				AtomHelpers.ExtensionElements.CONTACT_EMAIL,
				contact.getEmail()));

			for (String contactGroup: contacts.getContactGroups(identifiedContact)) {
				entry.addLink(ServicePaths.getURI(ServicePaths.GROUP,
						"group", contactGroup), AtomHelpers.Relations.GROUP);
			}
		}

		return entry;
	}
	
	/**
	 * Make a String representation of a contact, as an atom entry.
	 * @param contact The contact to be represented as an atom entry.
	 * @return a String representation of the given contact, as an atom entry.
	 */
	public String makeStringEntry(IdentifiedContact contact) {
		return AtomHelpers.stringify(makeEntry(contact, false));
	}

	
	/**
	 * Creates an atom entry representing a contact as part of a feed
	 */
	private Entry makeFeedEntry(IdentifiedContact contact) {
		return makeEntry(contact, true);
	}
	
	/**
	 * Generates an atom feed containing every contacts in the context group.
	 * @return an atom feed containing every contacts in the context group.
	 */
	public String makeFeed() {
		final Feed contactsFeed = Abdera.getInstance().getFactory().newFeed();
		String url;
		
		if (group == Contacts.GROUP_ALLCONTACTS) {
			url = ServicePaths.getURI(ServicePaths.CONTACTS); 
			contactsFeed.setTitle("Contacts List");
		}
		else {
			contactsFeed.setTitle("Contacts in group " + group);
			url = ServicePaths.getURI(ServicePaths.GROUP,
						"group", group);
		}
		
		contactsFeed.addLink(url, Relations.SELF);
		contactsFeed.setId(url);
		contactsFeed.setUpdated(contacts.getLastModified());
		
		for (IdentifiedContact contact: contacts.getContacts(group)) {
			contactsFeed.addEntry(makeFeedEntry(contact));
		}
		
		return AtomHelpers.stringify(contactsFeed);
	}

	/**
	 * Retrieves the URI corresponding to the given contact, in the context group.
	 * @param contact The contact whose URI is to be retrieved.
	 * @return The URI of the contact.
	 */
    public URI getURL(IdentifiedContact contact) {
    	String uri;
    
    	if (group == Contacts.GROUP_ALLCONTACTS) {
    		uri = ServicePaths.getURI(
				ServicePaths.CONTACT,
				"contactid", Integer.toString(contact.getContactId()));
    	}
    	else {
    		uri = ServicePaths.getURI(
				ServicePaths.GROUP_CONTACT,
				"contactid", Integer.toString(contact.getContactId()),
				"group", group);
    	}
    	try {
			return new URI(uri);
		} catch (URISyntaxException e) {
			//Should not happen because the URI are well know. Cannot recover from this
			throw new IllegalStateException(e);
		}
    }
}
