/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.sword2;

import org.apache.abdera.i18n.iri.IRI;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.handle.HandleManager;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Context;
import org.swordapp.server.SwordError;

import java.sql.SQLException;
import java.net.URL;
import java.net.MalformedURLException;

/**
 * @author Richard Jones
 *
 * Class responsible for constructing and de-constructing sword url space
 * urls
 */
public class SwordUrlManager
{
	/** the sword configuration */
	private SwordConfigurationDSpace config;

	/** the active dspace context */
	private Context context;

	public SwordUrlManager(SwordConfigurationDSpace config, Context context)
	{
		this.config = config;
		this.context = context;
	}

	/**
	 * Obtain the deposit URL for the given collection.  These URLs
	 * should not be considered persistent, but will remain consistent
	 * unless configuration changes are made to DSpace
	 *
	 * @param collection
	 * @return	The Deposit URL
	 * @throws DSpaceSwordException
	 */
	public String getDepositLocation(Collection collection)
		throws DSpaceSwordException
	{
		return this.getBaseCollectionUrl() + "/" + collection.getHandle();
	}

	/**
	 * Obtain the deposit URL for the given community.  These URLs
	 * should not be considered persistent, but will remain consistent
	 * unless configuration changes are made to DSpace
	 *
	 * @param community
	 * @return	The Deposit URL
	 * @throws DSpaceSwordException
	 */
	public String getDepositLocation(Community community)
		throws DSpaceSwordException
	{
		if (this.config.allowCommunityDeposit())
		{
			return this.getBaseCollectionUrl() + "/" + community.getHandle();
		}
		return null;
	}

	public String getSwordBaseUrl()
			throws DSpaceSwordException
	{
		String sUrl = ConfigurationManager.getProperty("swordv2-server", "url");
		if (sUrl == null || "".equals(sUrl))
		{
			String dspaceUrl = ConfigurationManager.getProperty("dspace.baseUrl");
			if (dspaceUrl == null || "".equals(dspaceUrl))
			{
				throw new DSpaceSwordException("Unable to construct service document urls, due to missing/invalid " +
						"config in sword2.url and/or dspace.baseUrl");
			}

            try
            {
                URL url = new URL(dspaceUrl);
                sUrl = new URL(url.getProtocol(), url.getHost(), url.getPort(), "/swordv2").toString();
            }
            catch (MalformedURLException e)
            {
                throw new DSpaceSwordException("Unable to construct service document urls, due to invalid dspace.baseUrl " +
						e.getMessage(),e);
            }
		}
		return sUrl;
	}

    public Item getItem(Context context, String location)
            throws DSpaceSwordException, SwordError
    {
        try
		{
			String baseUrl = this.getSwordBaseUrl();
            String emBaseUrl = baseUrl + "/edit-media/";
			String eBaseUrl = baseUrl + "/edit/";
			String sBaseUrl = baseUrl + "/statement/";
			String cBaseUrl = null;
			if (location.startsWith(emBaseUrl))
			{
				cBaseUrl = emBaseUrl;
			}
			else if (location.startsWith(eBaseUrl))
			{
				cBaseUrl = eBaseUrl;
			}
			else if (location.startsWith(sBaseUrl))
			{
				cBaseUrl = sBaseUrl;
			}
            else
            {
				throw new SwordError(DSpaceUriRegistry.BAD_URL, "The item URL is invalid");
			}

            String iid = location.substring(cBaseUrl.length());
            if (iid.endsWith(".atom"))
            {
                // this is the atom url, so we need to strip that to ge tthe item id
                iid = iid.substring(0, iid.length() - ".atom".length());
            }
			else if (iid.endsWith(".rdf"))
			{
				// this is the rdf url so we need to strip that to get the item id
				iid = iid.substring(0, iid.length() - ".rdf".length());
			}
			
            int itemId = Integer.parseInt(iid);
            Item item = Item.find(context, itemId);
			return item;
		}
		catch (SQLException e)
		{
			// log.error("Caught exception:", e);
			throw new DSpaceSwordException("There was a problem resolving the collection", e);
		}
    }

	public String getTypeSuffix(Context context, String location)
	{
		String tail = location.substring(location.lastIndexOf("/"));
		int typeSeparator = tail.lastIndexOf(".");
		if (typeSeparator == -1)
		{
			return null;
		}
		return tail.substring(typeSeparator + 1);
	}

    public boolean isFeedRequest(Context context, String url)
    {
        return url.endsWith(".atom");
    }

	/**
	 * Obtain the collection which is represented by the given
	 * URL
	 *
	 * @param context	the DSpace context
	 * @param location	the URL to resolve to a collection
	 * @return		The collection to which the url resolves
	 * @throws DSpaceSwordException
	 */
	// FIXME: we need to generalise this to DSpaceObjects, so that we can support
	// Communities, Collections and Items separately
	public Collection getCollection(Context context, String location)
		throws DSpaceSwordException, SwordError
	{
		try
		{
			String baseUrl = this.getBaseCollectionUrl();
			if (baseUrl.length() == location.length())
			{
				throw new SwordError(DSpaceUriRegistry.BAD_URL, "The deposit URL is incomplete");
			}
			String handle = location.substring(baseUrl.length());
			if (handle.startsWith("/"))
			{
				handle = handle.substring(1);
			}
			if ("".equals(handle))
			{
				throw new SwordError(DSpaceUriRegistry.BAD_URL, "The deposit URL is incomplete");
			}

			DSpaceObject dso = HandleManager.resolveToObject(context, handle);

			if (!(dso instanceof Collection))
			{
				throw new SwordError(DSpaceUriRegistry.BAD_URL, "The deposit URL does not resolve to a valid collection");
			}

			return (Collection) dso;
		}
		catch (SQLException e)
		{
			// log.error("Caught exception:", e);
			throw new DSpaceSwordException("There was a problem resolving the collection", e);
		}
	}

	/**
	 * Construct the service document URL for the given object, which will
	 * be supplied in the sword:service element of other service document
	 * entries.
	 *
	 * @param community
	 * @throws DSpaceSwordException
	 */
	public String constructSubServiceUrl(Community community)
			throws DSpaceSwordException
	{
		String base = this.getBaseServiceDocumentUrl();
		String handle = community.getHandle();
		return base + "/" + handle;
	}

	/**
	 * Construct the service document URL for the given object, which will
	 * be supplied in the sword:service element of other service document
	 * entries.
	 *
	 * @param collection
	 * @throws DSpaceSwordException
	 */
	public String constructSubServiceUrl(Collection collection)
			throws DSpaceSwordException
	{
		String base = this.getBaseServiceDocumentUrl();
		String handle = collection.getHandle();
		return base + "/" + handle;
	}

	/**
	 * Extract a DSpaceObject from the given URL.  If this method is unable to
	 * locate a meaningful and appropriate DSpace object it will throw the
	 * appropriate SWORD error.
	 * @param url
	 * @throws DSpaceSwordException
	 * @throws SwordError
	 */
	public DSpaceObject extractDSpaceObject(String url)
			throws DSpaceSwordException, SwordError
	{
		try
		{
			String sdBase = this.getBaseServiceDocumentUrl();
			// String mlBase = this.getBaseMediaLinkUrl();

			if (url.startsWith(sdBase))
			{
				// we are dealing with a service document request
				
				// first, let's find the beginning of the handle
				url = url.substring(sdBase.length());
				if (url.startsWith("/"))
				{
					url = url.substring(1);
				}
				if (url.endsWith("/"))
				{
					url = url.substring(0, url.length() - 1);
				}

				DSpaceObject dso = HandleManager.resolveToObject(context, url);
				if (dso instanceof Collection || dso instanceof Community)
				{
					return dso;
				}
				else
				{
					throw new SwordError(DSpaceUriRegistry.BAD_URL,
							"Service Document request does not refer to a DSpace Collection or Community");
				}
			}
			else
			{
				throw new SwordError(DSpaceUriRegistry.BAD_URL,
						"Unable to recognise URL as a valid service document: " + url);
			}
		}
		catch (SQLException e)
		{
			throw new DSpaceSwordException(e);
		}
	}

	/**
	 * Get the base URL for service document requests.
	 *
	 * @throws DSpaceSwordException
	 */
	public String getBaseServiceDocumentUrl()
			throws DSpaceSwordException
	{
		String sdUrl = ConfigurationManager.getProperty("swordv2-server", "servicedocument.url");
		if (sdUrl == null || "".equals(sdUrl))
		{
			String dspaceUrl = ConfigurationManager.getProperty("dspace.baseUrl");
			if (dspaceUrl == null || "".equals(dspaceUrl))
			{
				throw new DSpaceSwordException("Unable to construct service document urls, due to missing/invalid " +
						"config in swordv2-server.cfg servicedocument.url and/or dspace.baseUrl");
			}

            try
            {
                URL url = new URL(dspaceUrl);
                sdUrl = new URL(url.getProtocol(), url.getHost(), url.getPort(), "/swordv2/servicedocument").toString();
            }
            catch (MalformedURLException e)
            {
                throw new DSpaceSwordException("Unable to construct service document urls, due to invalid dspace.baseUrl " +
						e.getMessage(),e);
            }
		}
		return sdUrl;
	}

	/**
	 * Get the base deposit URL for the DSpace SWORD implementation.  This
	 * is effectively the URL of the servlet which deals with deposit
	 * requests, and is used as the basis for the individual Collection
	 * URLs
	 *
	 * If the configuration sword.deposit.url is set, this will be returned,
	 * but if not, it will construct the url as follows:
	 *
	 * [dspace.baseUrl]/sword/deposit
	 *
	 * where dspace.baseUrl is also in the configuration file.
	 *
	 * @return	the base URL for sword deposit
	 * @throws DSpaceSwordException
	 */
	public String getBaseCollectionUrl()
		throws DSpaceSwordException
	{
		String depositUrl = ConfigurationManager.getProperty("swordv2-server", "collection.url");
		if (depositUrl == null || "".equals(depositUrl))
		{
			String dspaceUrl = ConfigurationManager.getProperty("dspace.baseUrl");
			if (dspaceUrl == null || "".equals(dspaceUrl))
			{
				throw new DSpaceSwordException("Unable to construct deposit urls, due to missing/invalid config in " +
						"swordv2-server.cfg deposit.url and/or dspace.baseUrl");
			}

            try
            {
                URL url = new URL(dspaceUrl);
                depositUrl = new URL(url.getProtocol(), url.getHost(), url.getPort(), "/swordv2/collection").toString();
            }
            catch (MalformedURLException e)
            {
                throw new DSpaceSwordException("Unable to construct deposit urls, due to invalid dspace.baseUrl " +
						e.getMessage(),e);
            }


		}
		return depositUrl;
	}

	/**
	 * Is the given URL the base service document URL?
	 *
	 * @param url
	 * @throws DSpaceSwordException
	 */
	public boolean isBaseServiceDocumentUrl(String url)
			throws DSpaceSwordException
	{
		return this.getBaseServiceDocumentUrl().equals(url);
	}

	/**
	 * Central location for constructing usable URLs for DSpace bitstreams.
	 * There is no place in the main DSpace code base for doing this.
	 *
	 * @param bitstream
	 * @throws DSpaceSwordException
	 */
	public String getBitstreamUrl(Bitstream bitstream)
			throws DSpaceSwordException
	{
		try
		{
			Bundle[] bundles = bitstream.getBundles();
			Bundle parent = null;
			if (bundles.length > 0)
			{
				parent = bundles[0];
			}
			else
			{
				throw new DSpaceSwordException("Encountered orphaned bitstream");
			}

			Item[] items = parent.getItems();
			Item item;
			if (items.length > 0)
			{
				item = items[0];
			}
			else
			{
				throw new DSpaceSwordException("Encountered orphaned bundle");
			}

			String handle = item.getHandle();
			String bsLink = ConfigurationManager.getProperty("dspace.url");

			if (handle != null && !"".equals(handle))
			{
				bsLink = bsLink + "/bitstream/" + handle + "/" + bitstream.getSequenceID() + "/" + bitstream.getName();
			}
			else
			{
				bsLink = bsLink + "/retrieve/" + bitstream.getID() + "/" + bitstream.getName();
			}

			return bsLink;
		}
		catch (SQLException e)
		{
			throw new DSpaceSwordException(e);
		}
	}

	public String getActionableBitstreamUrl(Bitstream bitstream)
			throws DSpaceSwordException
	{
		return this.getSwordBaseUrl() + "/edit-media/bitstream/" + bitstream.getID() + "/" + bitstream.getName();
	}

	public boolean isActionableBitstreamUrl(Context context, String url)
	{
		return url.contains("/edit-media/bitstream/");
	}

	public Bitstream getBitstream(Context context, String location)
			throws DSpaceSwordException, SwordError
	{
		try
		{
			String baseUrl = this.getSwordBaseUrl();
            String emBaseUrl = baseUrl + "/edit-media/bitstream/";
			if (!location.startsWith(emBaseUrl))
			{
				throw new SwordError(DSpaceUriRegistry.BAD_URL, "The bitstream URL is invalid");
			}

            String bitstreamParts = location.substring(emBaseUrl.length());

			// the bitstream id is the part up to the first "/"
			int firstSlash = bitstreamParts.indexOf("/");
			int bid = Integer.parseInt(bitstreamParts.substring(0, firstSlash));
			String fn = bitstreamParts.substring(firstSlash + 1);

            Bitstream bitstream = Bitstream.find(context, bid);
			return bitstream;
		}
		catch (SQLException e)
		{
			// log.error("Caught exception:", e);
			throw new DSpaceSwordException("There was a problem resolving the collection", e);
		}
	}

	/**
	 * Get the media link URL for the given bitstream.
	 *
	 * @param bitstream
	 * @throws DSpaceSwordException
	 */
	public String getMediaLink(Bitstream bitstream)
			throws DSpaceSwordException
	{
//		try
//		{
//			Bundle[] bundles = bitstream.getBundles();
//			Bundle parent = null;
//			if (bundles.length > 0)
//			{
//				parent = bundles[0];
//			}
//			else
//			{
//				throw new DSpaceSwordException("Encountered orphaned bitstream");
//			}
//
//			Item[] items = parent.getItems();
//			Item item;
//			if (items.length > 0)
//			{
//				item = items[0];
//			}
//			else
//			{
//				throw new DSpaceSwordException("Encountered orphaned bundle");
//			}
//
//			String itemUrl = this.getMediaLink(item);
//			if (itemUrl.equals(this.getBaseMediaLinkUrl()))
//			{
//				return itemUrl;
//			}
//
//			String bsUrl = itemUrl + "/bitstream/" + bitstream.getID();
//
//			return bsUrl;
//		}
//		catch (SQLException e)
//		{
//			throw new DSpaceSWORDException(e);
//		}
		return null;
	}

	// FIXME: we need a totally new kind of URL scheme; perhaps we write the identifier into the item
	public String getAtomStatementUri(Item item)
			throws DSpaceSwordException
	{
		return this.getSwordBaseUrl() + "/statement/" + item.getID() + ".atom";
	}

	public String getOreStatementUri(Item item)
			throws DSpaceSwordException
	{
		return this.getSwordBaseUrl() + "/statement/" + item.getID() + ".rdf";
	}

	public String getAggregationUrl(Item item)
			throws DSpaceSwordException
	{
		return this.getOreStatementUri(item) + "#aggregation";
	}

	public IRI getEditIRI(Item item)
			throws DSpaceSwordException
	{
		return new IRI(this.getSwordBaseUrl() + "/edit/" + item.getID());
	}

	public String getSplashUrl(Item item)
	{
        // FIXME: this appears not to return the item's handle
		return HandleManager.getCanonicalForm(item.getHandle());
	}

	public IRI getContentUrl(Item item)
			throws DSpaceSwordException
	{
		return new IRI(this.getSwordBaseUrl() + "/edit-media/" + item.getID());
	}

    public IRI getMediaFeedUrl(Item item)
			throws DSpaceSwordException
	{
		return new IRI(this.getSwordBaseUrl() + "/edit-media/" + item.getID() + ".atom");
	}
}
