/**
 * 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.sword;

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.purl.sword.base.SWORDErrorException;

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 SWORDConfiguration config;

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

	public SWORDUrlManager(SWORDConfiguration config, Context context)
	{
		this.config = config;
		this.context = context;
	}

	/**
	 * Get the generator url for atom entry documents.  This can be
	 * overridden from the default in configuration
	 *
	 * @return
	 */
	public String getGeneratorUrl()
	{
		String cfg = ConfigurationManager.getProperty("sword.generator.url");
		if (cfg == null || "".equals(cfg))
		{
			return SWORDProperties.SOFTWARE_URI;
		}
		return cfg;
	}

	/**
	 * 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.getBaseDepositUrl() + "/" + collection.getHandle();
	}

	/**
	 * Obtain the deposit URL for the given item.  These URLs
	 * should not be considered persistent, but will remain consistent
	 * unless configuration changes are made to DSpace
	 *
	 * @param item
	 * @return	The Deposit URL
	 * @throws DSpaceSWORDException
	 */
	public String getDepositLocation(Item item)
		throws DSpaceSWORDException
	{
		return this.getBaseDepositUrl() + "/" + item.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
	{
		// FIXME: there is no deposit url for communities yet, so this could
		// be misleading
		return this.getBaseDepositUrl() + "/" + community.getHandle();
	}

	/**
	 * 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, SWORDErrorException
	{
		try
		{
			String baseUrl = this.getBaseDepositUrl();
			if (baseUrl.length() == location.length())
			{
				throw new SWORDErrorException(DSpaceSWORDErrorCodes.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 SWORDErrorException(DSpaceSWORDErrorCodes.BAD_URL, "The deposit URL is incomplete");
			}

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

			if (!(dso instanceof Collection))
			{
				throw new SWORDErrorException(DSpaceSWORDErrorCodes.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);
		}
	}

	/**
	 * 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
	 */
	public DSpaceObject getDSpaceObject(Context context, String location)
		throws DSpaceSWORDException, SWORDErrorException
	{
		try
		{
			String baseUrl = this.getBaseDepositUrl();
			if (baseUrl.length() == location.length())
			{
				throw new SWORDErrorException(DSpaceSWORDErrorCodes.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 SWORDErrorException(DSpaceSWORDErrorCodes.BAD_URL, "The deposit URL is incomplete");
			}

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

			if (!(dso instanceof Collection) && !(dso instanceof Item))
			{
				throw new SWORDErrorException(DSpaceSWORDErrorCodes.BAD_URL, "The deposit URL does not resolve to a valid deposit target");
			}

			return 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
	 * @return
	 * @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
	 * @return
	 * @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
	 * @return
	 * @throws DSpaceSWORDException
	 * @throws SWORDErrorException
	 */
	public DSpaceObject extractDSpaceObject(String url)
			throws DSpaceSWORDException, SWORDErrorException
	{
		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 SWORDErrorException(DSpaceSWORDErrorCodes.BAD_URL,
							"Service Document request does not refer to a DSpace Collection or Community");
				}
			}
			else if (url.startsWith(mlBase))
			{
				// we are dealing with a bitstream media link

				// find the index of the "/bitstream/" segment of the url
				int bsi = url.indexOf("/bitstream/");

				// subtsring the url from the end of this "/bitstream/" string, to get the bitstream id
				String bsid = url.substring(bsi + 11);

				// strip off extraneous slashes
				if (bsid.endsWith("/"))
				{
					bsid = bsid.substring(0, url.length() - 1);
				}

                return Bitstream.find(context, Integer.parseInt(bsid));
			}
			else
			{
				throw new SWORDErrorException(DSpaceSWORDErrorCodes.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
	 *
	 * @return
	 * @throws DSpaceSWORDException
	 */
	public String getBaseServiceDocumentUrl()
			throws DSpaceSWORDException
	{
		String depositUrl = ConfigurationManager.getProperty("sword.servicedocument.url");
		if (depositUrl == null || "".equals(depositUrl))
		{
			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 sword.servicedocument.url and/or dspace.baseUrl");
			}

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


		}
		return depositUrl;
	}

	/**
	 * 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 getBaseDepositUrl()
		throws DSpaceSWORDException
	{
		String depositUrl = ConfigurationManager.getProperty("sword.deposit.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 " +
						"sword.deposit.url and/or dspace.baseUrl");
			}

            try
            {
                URL url = new URL(dspaceUrl);
                depositUrl = new URL(url.getProtocol(), url.getHost(), url.getPort(), "/sword/deposit").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
	 * @return
	 * @throws DSpaceSWORDException
	 */
	public boolean isBaseServiceDocumentUrl(String url)
			throws DSpaceSWORDException
	{
		return this.getBaseServiceDocumentUrl().equals(url);
	}

	/**
	 * is the given url the base media link url
	 *
	 * @param url
	 * @return
	 * @throws DSpaceSWORDException
	 */
	public boolean isBaseMediaLinkUrl(String url)
			throws DSpaceSWORDException
	{
		return this.getBaseMediaLinkUrl().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
	 * @return
	 * @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();
			}

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

	/**
	 * get the base media link url
	 *
	 * @return
	 * @throws DSpaceSWORDException
	 */
	public String getBaseMediaLinkUrl()
			throws DSpaceSWORDException
	{
		String mlUrl = ConfigurationManager.getProperty("sword.media-link.url");
		if (mlUrl == null || "".equals(mlUrl))
		{
			String dspaceUrl = ConfigurationManager.getProperty("dspace.baseUrl");
			if (dspaceUrl == null || "".equals(dspaceUrl))
			{
				throw new DSpaceSWORDException("Unable to construct media-link urls, due to missing/invalid config in " +
						"sword.media-link.url and/or dspace.baseUrl");
			}

            try
            {
                URL url = new URL(dspaceUrl);
                mlUrl = new URL(url.getProtocol(), url.getHost(), url.getPort(), "/sword/media-link").toString();
            }
            catch (MalformedURLException e)
            {
                throw new DSpaceSWORDException("Unable to construct media-link urls, due to invalid dspace.baseUrl " +
						e.getMessage(),e);
            }


		}
		return mlUrl;
	}

	/**
	 * get the media link url for the given item
	 *
	 * @param dso
	 * @return
	 * @throws DSpaceSWORDException
	 */
	private String getMediaLink(Item dso)
			throws DSpaceSWORDException
	{
		String ml = this.getBaseMediaLinkUrl();
		String handle = dso.getHandle();
		if (handle != null)
		{
			ml = ml + "/" + dso.getHandle();
		}
		return ml;
	}

	/**
	 * get the media link url for the given bitstream
	 * 
	 * @param bitstream
	 * @return
	 * @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);
		}
	}
}
