/**
 * 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 java.io.File;
import java.sql.SQLException;
import java.util.Date;
import java.util.StringTokenizer;

import org.apache.log4j.Logger;

import org.dspace.content.*;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.packager.PackageIngester;
import org.dspace.content.packager.PackageParameters;
import org.dspace.content.service.ItemService;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Context;
import org.dspace.core.factory.CoreServiceFactory;

import org.dspace.handle.factory.HandleServiceFactory;
import org.dspace.handle.service.HandleService;
import org.purl.sword.base.Deposit;
import org.purl.sword.base.SWORDErrorException;

public class SWORDMETSIngester implements SWORDIngester
{
    private SWORDService swordService;

    protected ItemService itemService = ContentServiceFactory.getInstance()
            .getItemService();

    /** Log4j logger */
    public static final Logger log = Logger.getLogger(SWORDMETSIngester.class);

    /* (non-Javadoc)
     * @see org.dspace.sword.SWORDIngester#ingest(org.dspace.core.Context, org.purl.sword.base.Deposit)
     */
    public DepositResult ingest(SWORDService service, Deposit deposit,
            DSpaceObject dso)
            throws DSpaceSWORDException, SWORDErrorException
    {
        try
        {
            // first, make sure this is the right kind of ingester, and set the collection
            if (!(dso instanceof Collection))
            {
                throw new DSpaceSWORDException(
                        "Tried to run an ingester on wrong target type");
            }
            Collection collection = (Collection) dso;

            // now set the sword service
            swordService = service;

            // get the things out of the service that we need
            Context context = swordService.getContext();

            // get deposited file as InputStream
            File depositFile = deposit.getFile();

            // load the plugin manager for the required configuration
            String cfg = ConfigurationManager.getProperty("sword-server",
                    "mets-ingester.package-ingester");
            if (cfg == null || "".equals(cfg))
            {
                cfg = "METS";  // default to METS
            }
            swordService.message("Using package manifest format: " + cfg);

            PackageIngester pi = (PackageIngester) CoreServiceFactory.getInstance().getPluginService()
                    .getNamedPlugin(PackageIngester.class, cfg);
            swordService.message(
                    "Loaded package ingester: " + pi.getClass().getName());

            // the licence is either in the zip or the mets manifest.  Either way
            // it's none of our business here
            String licence = null;

            // Initialize parameters to packager
            PackageParameters params = new PackageParameters();

            // Force package ingester to respect Collection workflows
            params.setWorkflowEnabled(true);

            // Should restore mode be enabled, i.e. keep existing handle?
            if (ConfigurationManager
                    .getBooleanProperty("sword-server", "restore-mode.enable",
                            false))
            {
                params.setRestoreModeEnabled(true);
            }

            // Whether or not to use the collection template
            params.setUseCollectionTemplate(ConfigurationManager
                    .getBooleanProperty(
                            "mets.default.ingest.useCollectionTemplate",
                            false));

            // ingest the item from the temp file
            DSpaceObject ingestedObject = pi
                    .ingest(context, collection, depositFile, params, licence);
            if (ingestedObject == null)
            {
                swordService.message(
                        "Failed to ingest the package; throwing exception");
                throw new SWORDErrorException(
                        DSpaceSWORDErrorCodes.UNPACKAGE_FAIL,
                        "METS package ingester failed to unpack package");
            }

            //Verify we have an Item as a result -- SWORD can only ingest Items
            if (!(ingestedObject instanceof Item))
            {
                throw new DSpaceSWORDException(
                        "DSpace Ingester returned wrong object type -- not an Item result.");
            }
            else
            {
                //otherwise, we have an item, and a workflow should have already been started for it.
                swordService.message("Workflow process started");
            }

            // get reference to item so that we can report on it
            Item installedItem = (Item) ingestedObject;

            // update the item metadata to inclue the current time as
            // the updated date
            this.setUpdatedDate(context, installedItem);

            // DSpace ignores the slug value as suggested identifier, but
            // it does store it in the metadata
            this.setSlug(installedItem, deposit.getSlug());

            // in order to write these changes, we need to bypass the
            // authorisation briefly, because although the user may be
            // able to add stuff to the repository, they may not have
            // WRITE permissions on the archive.
            context.turnOffAuthorisationSystem();
            itemService.update(context, installedItem);
            context.restoreAuthSystemState();

            // for some reason, DSpace will not give you the handle automatically,
            // so we have to look it up
            HandleService handleService = HandleServiceFactory.getInstance()
                    .getHandleService();
            String handle = handleService.findHandle(context, installedItem);

            swordService.message("Ingest successful");
            swordService.message("Item created with internal identifier: " +
                    installedItem.getID());
            if (handle != null)
            {
                swordService.message(
                        "Item created with external identifier: " + handle);
            }
            else
            {
                swordService.message(
                        "No external identifier available at this stage (item in workflow)");
            }

            DepositResult dr = new DepositResult();
            dr.setItem(installedItem);
            dr.setHandle(handle);
            dr.setTreatment(this.getTreatment());

            return dr;
        }
        catch (RuntimeException re)
        {
            log.error("caught exception: ", re);
            throw re;
        }
        catch (Exception e)
        {
            log.error("caught exception: ", e);
            throw new DSpaceSWORDException(e);
        }
    }

    /**
     * Add the current date to the item metadata.  This looks up
     * the field in which to store this metadata in the configuration
     * sword.updated.field
     *
     *
     * @param context
     * @param item
     * @throws DSpaceSWORDException
     */
    private void setUpdatedDate(Context context, Item item)
            throws DSpaceSWORDException
    {
        String field = ConfigurationManager
                .getProperty("sword-server", "updated.field");
        if (field == null || "".equals(field))
        {
            throw new DSpaceSWORDException(
                    "No configuration, or configuration is invalid for: sword.updated.field");
        }

        MetadataFieldInfo dc = this.configToDC(field, null);
        try
        {
            itemService.clearMetadata(context, item, dc.schema, dc.element,
                    dc.qualifier, Item.ANY);
            DCDate date = new DCDate(new Date());
            itemService.addMetadata(context, item, dc.schema, dc.element,
                    dc.qualifier, null, date.toString());
        }
        catch (SQLException e)
        {
            log.error("Caught exception: ", e);
            throw new DSpaceSWORDException(e);
        }

        swordService.message(
                "Updated date added to response from item metadata where available");
    }

    /**
     * Store the given slug value (which is used for suggested identifiers,
     * and which DSpace ignores) in the item metadata.  This looks up the
     * field in which to store this metadata in the configuration
     * sword.slug.field
     *
     * @param item
     * @param slugVal
     * @throws DSpaceSWORDException
     */
    private void setSlug(Item item, String slugVal)
            throws DSpaceSWORDException
    {
        // if there isn't a slug value, don't set it
        if (slugVal == null)
        {
            return;
        }

        String field = ConfigurationManager
                .getProperty("sword-server", "slug.field");
        if (field == null || "".equals(field))
        {
            throw new DSpaceSWORDException(
                    "No configuration, or configuration is invalid for: sword.slug.field");
        }

        MetadataFieldInfo mfi = this.configToDC(field, null);
        try
        {
            itemService
                    .clearMetadata(swordService.getContext(), item, mfi.schema,
                            mfi.element, mfi.qualifier, Item.ANY);
            itemService.addMetadata(swordService.getContext(), item, mfi.schema,
                    mfi.element, mfi.qualifier, null, slugVal);
        }
        catch (SQLException e)
        {
            log.error("Caught exception: ", e);
            throw new DSpaceSWORDException(e);
        }

        swordService.message("Slug value set in response where available");
    }

    /**
     * utility method to turn given metadata fields of the form
     schema.element.qualifier into Metadatum objects which can be
     used to access metadata in items.
     *
     * The def parameter should be null, * or "" depending on how
     you intend to use the Metadatum object
     *
     * @param config
     * @param def
     * @return
     */
    private MetadataFieldInfo configToDC(String config, String def)
    {
        MetadataFieldInfo mfi = new MetadataFieldInfo();
        mfi.schema = def;
        mfi.element = def;
        mfi.qualifier = def;

        StringTokenizer stz = new StringTokenizer(config, ".");
        mfi.schema = stz.nextToken();
        mfi.element = stz.nextToken();
        if (stz.hasMoreTokens())
        {
            mfi.qualifier = stz.nextToken();
        }

        return mfi;
    }

    /**
     * The human readable description of the treatment this ingester has
     * put the deposit through
     *
     * @return
     * @throws DSpaceSWORDException
     */
    private String getTreatment() throws DSpaceSWORDException
    {
        return "The package has been deposited into DSpace.  Each file has been unpacked " +
                "and provided with a unique identifier.  The metadata in the manifest has been " +
                "extracted and attached to the DSpace item, which has been provided with " +
                "an identifier leading to an HTML splash page.";
    }

    private class MetadataFieldInfo
    {
        private String schema;

        private String element;

        private String qualifier;
    }
}
