/**
 * 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.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Bitstream;
import org.dspace.content.BitstreamFormat;
import org.dspace.content.Bundle;
import org.dspace.content.Collection;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.core.*;
import org.swordapp.server.AuthCredentials;
import org.swordapp.server.Deposit;
import org.swordapp.server.SwordAuthException;
import org.swordapp.server.SwordError;
import org.swordapp.server.SwordServerException;
import org.swordapp.server.UriRegistry;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class DSpaceSwordAPI
{
    private static Logger log = Logger.getLogger(DSpaceSwordAPI.class);

	public SwordContext noAuthContext()
			throws DSpaceSwordException
	{
		try
		{
			SwordContext sc = new SwordContext();
			Context context = new Context();
			sc.setContext(context);
			return sc;
		}
		catch (SQLException e)
		{
			throw new DSpaceSwordException(e);
		}
	}

    public SwordContext doAuth(AuthCredentials authCredentials)
            throws SwordAuthException, SwordError, DSpaceSwordException
    {
        // first authenticate the request
        // note: this will build our various DSpace contexts for us
        SwordAuthenticator auth = new SwordAuthenticator();
        SwordContext sc = auth.authenticate(authCredentials);

        // log the request
        String un = authCredentials.getUsername() != null ? authCredentials.getUsername() : "NONE";
        String obo = authCredentials.getOnBehalfOf() != null ? authCredentials.getOnBehalfOf() : "NONE";
        log.info(LogManager.getHeader(sc.getContext(), "sword_auth_request", "username=" + un + ",on_behalf_of=" + obo));

        return sc;
    }

    public String getHeader(Map<String, String> map, String header, String def)
    {
        for (String key : map.keySet())
        {
            if (key.toLowerCase().equals(header.toLowerCase()))
            {
                return map.get(key);
            }
        }
        return def;
    }

    public TreeMap<Float, List<String>> analyseAccept(String acceptHeader)
    {
        if (acceptHeader == null)
        {
            return null;
        }

        String[] parts = acceptHeader.split(",");

        List<Object[]> unsorted = new ArrayList<Object[]>();
        float highest_q = 0;
        int counter = 0;
        for (String part : parts)
        {
            counter += 1;

            // the components of the part can be "type;params;q" "type;params", "type;q" or just "type"
            String[] components = part.split(";");

            // the first part is always the type (see above comment)
            String type = components[0].trim();

            // create some default values for the other parts.  If there is no params, we will use None, if there is
            // no q we will use a negative number multiplied by the position in the list of this part.  This allows us
            // to later see the order in which the parts with no q value were listed, which is important
            String params = null;
            float q = -1 * counter;

            // There are then 3 possibilities remaining to check for: "type;q", "type;params" and "type;params;q"
            // ("type" is already handled by the default cases set up above)
            if (components.length == 2)
            {
                // "type;q" or "type;params"
                if (components[1].trim().startsWith("q="))
                {
                    // "type;q"
                    q = Float.parseFloat(components[1].trim().substring(2)); //strip the "q=" from the start of the q value

                    // if the q value is the highest one we've seen so far, record it
                    if (q > highest_q)
                    {
                        highest_q = q;
                    }
                }
                else
                {
                    // "type;params"
                    params = components[1].trim();
                }
            }
            else if (components.length == 3)
            {
                // "type;params;q"
                params = components[1].trim();
                q = Float.parseFloat(components[1].trim().substring(2)); // strip the "q=" from the start of the q value

                // if the q value is the highest one we've seen so far, record it
                if (q > highest_q)
                {
                    highest_q = q;
                }
            }

            Object[] res = new Object[] { type, params, q };
            unsorted.add(res);
        }

        // once we've finished the analysis we'll know what the highest explicitly requested q will be.  This may leave
        // us with a gap between 1.0 and the highest requested q, into which we will want to put the content types which
        // did not have explicitly assigned q values.  Here we calculate the size of that gap, so that we can use it
        // later on in positioning those elements.  Note that the gap may be 0.0.
        float q_range = 1 - highest_q;

        // set up a dictionary to hold our sorted results.  The dictionary will be keyed with the q value, and the
        // value of each key will be a list of content type strings (in no particular order)
        TreeMap<Float, List<String>> sorted = new TreeMap<Float, List<String>>();

        // go through the unsorted list
        for (Object[] oa : unsorted)
        {
            String contentType = (String) oa[0];
            String p = (String) oa[1];
            if (p != null)
            {
                contentType += ";" + p;
            }
            Float qv = (Float) oa[2];

            if (qv > 0)
            {
                // if the q value is greater than 0 it was explicitly assigned in the Accept header and we can just place
                // it into the sorted dictionary
                if (sorted.containsKey(qv))
                {
                    sorted.get(qv).add(contentType);
                }
                else
                {
                    List<String> cts = new ArrayList<String>();
                    cts.add(contentType);
                    sorted.put(qv, cts);
                }
            }
            else
            {
                // otherwise, we have to calculate the q value using the following equation which creates a q value "qv"
                // within "q_range" of 1.0 [the first part of the eqn] based on the fraction of the way through the total
                // accept header list scaled by the q_range [the second part of the eqn]
                float nq = (1 - q_range) + (((-1 * qv)/counter) * q_range);
                if (sorted.containsKey(nq))
                {
                    sorted.get(nq).add(contentType);
                }
                else
                {
                    List<String> cts = new ArrayList<String>();
                    cts.add(contentType);
                    sorted.put(nq, cts);
                }
            }
        }

        return sorted;
    }

    public void isAcceptable(SwordConfigurationDSpace swordConfig, Context context, Deposit deposit, DSpaceObject dso)
            throws SwordError, DSpaceSwordException
    {
        // determine if this is an acceptable file format
        if (!swordConfig.isAcceptableContentType(context, deposit.getMimeType(), dso))
        {
            log.error("Unacceptable content type detected: " + deposit.getMimeType() + " for object " + dso.getID());
            throw new SwordError(UriRegistry.ERROR_CONTENT,
                    "Unacceptable content type in deposit request: " + deposit.getMimeType());
        }

        // determine if this is an acceptable packaging type for the deposit
        // if not, we throw a 415 HTTP error (Unsupported Media Type, ERROR_CONTENT)
        if (!swordConfig.isAcceptedPackaging(deposit.getPackaging(), dso))
        {
            log.error("Unacceptable packaging type detected: " + deposit.getPackaging() + " for object " + dso.getID());
            throw new SwordError(UriRegistry.ERROR_CONTENT,
                    "Unacceptable packaging type in deposit request: " + deposit.getPackaging());
        }
    }

    public void storeOriginals(SwordConfigurationDSpace swordConfig, Context context, VerboseDescription verboseDescription, Deposit deposit, DepositResult result)
            throws DSpaceSwordException, SwordServerException
    {
        // if there's an item availalble, and we want to keep the original
        // then do that
        try
        {
            if (swordConfig.isKeepOriginal())
            {
                verboseDescription.append("DSpace will store an original copy of the deposit, " +
                        "as well as ingesting the item into the archive");

                // in order to be allowed to add the file back to the item, we need to ignore authorisations
                // for a moment
                boolean ignoreAuth = context.ignoreAuthorization();
                context.setIgnoreAuthorization(true);

                String bundleName = ConfigurationManager.getProperty("swordv2-server", "bundle.name");
                if (bundleName == null || "".equals(bundleName))
                {
                    bundleName = "SWORD";
                }
                Item item = result.getItem();
                Bundle[] bundles = item.getBundles(bundleName);
                Bundle swordBundle = null;
                if (bundles.length > 0)
                {
                    swordBundle = bundles[0];
                }
                if (swordBundle == null)
                {
                    swordBundle = item.createBundle(bundleName);
                }

                if (deposit.isMultipart() || deposit.isEntryOnly())
                {
                    String entry = deposit.getSwordEntry().toString();
                    ByteArrayInputStream bais = new ByteArrayInputStream(entry.getBytes());
                    Bitstream entryBitstream = swordBundle.createBitstream(bais);

                    String fn = this.createEntryFilename(context, deposit, true);
                    entryBitstream.setName(fn);
                    entryBitstream.setDescription("SWORD entry document");

                    BitstreamFormat bf = BitstreamFormat.findByMIMEType(context, "application/xml");
                    if (bf != null)
                    {
                        entryBitstream.setFormat(bf);
                    }

                    entryBitstream.update();

                    verboseDescription.append("Original entry stored as " + fn + ", in item bundle " + swordBundle);
                }

                if (deposit.isMultipart() || deposit.isBinaryOnly())
                {
                    String fn = this.createFilename(context, deposit, true);

                    Bitstream bitstream;
                    InputStream fis = null;
                    try
                    {
                        fis = deposit.getInputStream();
                        bitstream = swordBundle.createBitstream(fis);
                    }
                    finally
                    {
                        if (fis != null)
                        {
                            try
                            {
                                fis.close();
                            }
                            catch (IOException e)
                            {
                                // problem closing input stream; leave it to the garbage collector
                            }
                        }
                    }

                    bitstream.setName(fn);
                    bitstream.setDescription("SWORD deposit package");

                    BitstreamFormat bf = BitstreamFormat.findByMIMEType(context, deposit.getMimeType());
                    if (bf != null)
                    {
                        bitstream.setFormat(bf);
                    }

                    bitstream.update();
                    if (result.getOriginalDeposit() == null)
                    {
                        // it may be that the original deposit is already set, in which case we
                        // shouldn't mess with it
                        result.setOriginalDeposit(bitstream);
                    }
                    verboseDescription.append("Original package stored as " + fn + ", in item bundle " + swordBundle);
                }

                swordBundle.update();
                item.update();

                // now reset the context ignore authorisation
                context.setIgnoreAuthorization(ignoreAuth);
            }
        }
        catch (SQLException e)
        {
            log.error("caught exception: ", e);
            throw new DSpaceSwordException(e);
        }
        catch (AuthorizeException e)
        {
            log.error("caught exception: ", e);
            throw new DSpaceSwordException(e);
        }
        catch (FileNotFoundException e)
        {
            log.error("caught exception: ", e);
            throw new DSpaceSwordException(e);
        }
        catch (IOException e)
        {
            log.error("caught exception: ", e);
            throw new DSpaceSwordException(e);
        }
    }

    /**
     * Construct the most appropriate filename for the incoming deposit.
     *
     * @param context
     * @param deposit
     * @param original
     * @throws DSpaceSwordException
     */
    public String createFilename(Context context, Deposit deposit, boolean original)
            throws DSpaceSwordException
    {
        try
        {
            BitstreamFormat bf = BitstreamFormat.findByMIMEType(context, deposit.getMimeType());
            String[] exts = null;
            if (bf != null)
            {
                exts = bf.getExtensions();
            }

            String fn = deposit.getFilename();
            if (fn == null || "".equals(fn))
            {
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
                fn = "sword-" + sdf.format(new Date());
                if (original)
                {
                    fn = fn + ".original";
                }
                if (exts != null)
                {
                    fn = fn + "." + exts[0];
                }
            }

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

    public String createEntryFilename(Context context, Deposit deposit, boolean original)
            throws DSpaceSwordException
    {

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
        String fn = "sword-" + sdf.format(new Date());
        if (original)
        {
            fn = fn + ".original";
        }

        return fn + ".xml";
    }

	/**
	 *   Store original package on disk and companion file containing SWORD headers as found in the deposit object
	 *   Also write companion file with header info from the deposit object.
	 *
	 * @param deposit
	 */
	protected void storePackageAsFile(Deposit deposit, AuthCredentials auth, SwordConfigurationDSpace config) throws IOException
	{
		String path = config.getFailedPackageDir();

		File dir = new File(path);
		if (!dir.exists() || !dir.isDirectory())
		{
			throw new IOException("Directory does not exist for writing packages on ingest error.");
		}

		String filenameBase =  "sword-" + auth.getUsername() + "-" + (new Date()).getTime();

		File packageFile = new File(path, filenameBase);
		File headersFile = new File(path, filenameBase + "-headers");

		InputStream is = new BufferedInputStream(new FileInputStream(deposit.getFile()));
		OutputStream fos = new BufferedOutputStream(new FileOutputStream(packageFile));
		Utils.copy(is, fos);
		fos.close();
		is.close();

		//write companion file with headers
		PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(headersFile)));

		pw.println("Filename=" + deposit.getFilename());
		pw.println("Content-Type=" + deposit.getMimeType());
		pw.println("Packaging=" + deposit.getPackaging());
		pw.println("On Behalf of=" + auth.getOnBehalfOf());
		pw.println("Slug=" + deposit.getSlug());
		pw.println("User name=" + auth.getUsername());
		pw.close();
	}

	/**
	 *   Store original package on disk and companion file containing SWORD headers as found in the deposit object
	 *   Also write companion file with header info from the deposit object.
	 *
	 * @param deposit
	 */
	protected void storeEntryAsFile(Deposit deposit, AuthCredentials auth, SwordConfigurationDSpace config) throws IOException
	{
		String path = config.getFailedPackageDir();

		File dir = new File(path);
		if (!dir.exists() || !dir.isDirectory())
		{
			throw new IOException("Directory does not exist for writing packages on ingest error.");
		}

		String filenameBase =  "sword-" + auth.getUsername() + "-" + (new Date()).getTime();

		File packageFile = new File(path, filenameBase);
		File headersFile = new File(path, filenameBase + "-headers");

		String entry = deposit.getSwordEntry().toString();
        ByteArrayInputStream is = new ByteArrayInputStream(entry.getBytes());
		OutputStream fos = new BufferedOutputStream(new FileOutputStream(packageFile));
		Utils.copy(is, fos);
		fos.close();
		is.close();

		//write companion file with headers
		PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(headersFile)));

		pw.println("Filename=" + deposit.getFilename());
		pw.println("Content-Type=" + deposit.getMimeType());
		pw.println("Packaging=" + deposit.getPackaging());
		pw.println("On Behalf of=" + auth.getOnBehalfOf());
		pw.println("Slug=" + deposit.getSlug());
		pw.println("User name=" + auth.getUsername());
		pw.close();
	}
}
