/**
 * 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.FileInputStream;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.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.content.factory.ContentServiceFactory;
import org.dspace.content.service.BitstreamFormatService;
import org.dspace.content.service.BitstreamService;
import org.dspace.content.service.BundleService;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.purl.sword.base.Deposit;
import org.purl.sword.base.ErrorCodes;
import org.purl.sword.base.SWORDErrorException;

/**
 * @author Richard Jones
 *
 * A depositor which can deposit content into a DSpace Collection
 */
public class CollectionDepositor extends Depositor {
    /**
     * logger
     */
    private static final Logger log = LogManager.getLogger(CollectionDepositor.class);

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

    protected BundleService bundleService =
        ContentServiceFactory.getInstance().getBundleService();

    protected BitstreamService bitstreamService =
        ContentServiceFactory.getInstance().getBitstreamService();

    protected BitstreamFormatService bitstreamFormatService =
        ContentServiceFactory.getInstance().getBitstreamFormatService();

    private final ConfigurationService configurationService
            = DSpaceServicesFactory.getInstance().getConfigurationService();
    /**
     * The DSpace Collection we are depositing into
     */
    private Collection collection;

    /**
     * Construct a depositor for the given service instance on the
     * given DSpaceObject.  If the DSpaceObject is not an instance of Collection
     * this constructor will throw an Exception
     *
     * @param swordService SWORD service
     * @param dso          target DSpace object
     * @throws DSpaceSWORDException can be thrown by the internals of the DSpace SWORD implementation
     */
    public CollectionDepositor(SWORDService swordService, DSpaceObject dso)
        throws DSpaceSWORDException {
        super(swordService, dso);

        if (!(dso instanceof Collection)) {
            throw new DSpaceSWORDException(
                "You tried to initialise the collection depositor with something" +
                    " other than a collection object");
        }

        this.collection = (Collection) dso;

        log.debug("Created instance of CollectionDepositor");
    }

    /**
     * Perform a deposit, using the supplied SWORD Deposit object.
     *
     * @param deposit deposit request
     * @throws SWORDErrorException  on generic SWORD exception
     * @throws DSpaceSWORDException can be thrown by the internals of the DSpace SWORD implementation
     */
    @Override
    public DepositResult doDeposit(Deposit deposit)
        throws SWORDErrorException, DSpaceSWORDException {
        // get the things out of the service that we need
        Context context = swordService.getContext();
        SWORDConfiguration swordConfig = swordService.getSwordConfig();
        SWORDUrlManager urlManager = swordService.getUrlManager();

        // FIXME: the spec is unclear what to do in this situation.  I'm going
        // the throw a 415 (ERROR_CONTENT) until further notice
        //
        // determine if this is an acceptable file format
        if (!swordConfig
            .isAcceptableContentType(context, deposit.getContentType(),
                                     collection)) {
            log.error("Unacceptable content type detected: " +
                          deposit.getContentType() + " for collection " +
                          collection.getID());
            throw new SWORDErrorException(ErrorCodes.ERROR_CONTENT,
                                          "Unacceptable content type in deposit request: " +
                                              deposit.getContentType());
        }

        // 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.isSupportedMediaType(
            deposit.getPackaging(), this.collection)) {
            log.error("Unacceptable packaging type detected: " +
                          deposit.getPackaging() + "for collection" +
                          collection.getID());
            throw new SWORDErrorException(ErrorCodes.ERROR_CONTENT,
                                          "Unacceptable packaging type in deposit request: " +
                                              deposit.getPackaging());
        }

        // Obtain the relevant ingester from the factory
        SWORDIngester si = SWORDIngesterFactory.getInstance(
            context, deposit, collection);
        swordService.message("Loaded ingester: " + si.getClass().getName());

        // do the deposit
        DepositResult result = si.ingest(swordService, deposit, collection);
        swordService.message("Archive ingest completed successfully");

        // if there's an item availalble, and we want to keep the original
        // then do that
        try {
            if (swordConfig.isKeepOriginal()) {
                swordService.message(
                    "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
                context.turnOffAuthorisationSystem();

                String bundleName = configurationService.getProperty("sword-server.bundle.name", "SWORD");
                Item item = result.getItem();
                List<Bundle> bundles = item.getBundles();
                Bundle swordBundle = null;
                for (Bundle bundle : bundles) {
                    if (bundleName.equals(bundle.getName())) {
                        // we found one
                        swordBundle = bundle;
                        break;
                    }
                }
                if (swordBundle == null) {
                    swordBundle = bundleService.create(
                        context, item, bundleName);
                }

                String fn = swordService.getFilename(context, deposit, true);

                Bitstream bitstream;
                FileInputStream fis = null;
                try {
                    fis = new FileInputStream(deposit.getFile());
                    bitstream = bitstreamService.create(
                        context, swordBundle, fis);
                } finally {
                    if (fis != null) {
                        fis.close();
                    }
                }

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

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

                bitstreamService.update(context, bitstream);
                bundleService.update(context, swordBundle);
                itemService.update(context, item);

                swordService.message("Original package stored as " + fn +
                                         ", in item bundle " + swordBundle);

                // now reset the context ignore authorisation
                context.restoreAuthSystemState();

                // set the media link for the created item
                result.setMediaLink(urlManager.getMediaLink(bitstream));
            } else {
                // set the vanilla media link, which doesn't resolve to anything
                result.setMediaLink(urlManager.getBaseMediaLinkUrl());
            }
        } catch (SQLException | AuthorizeException | IOException e) {
            log.error("caught exception: ", e);
            throw new DSpaceSWORDException(e);
        }

        return result;
    }

    /**
     * Reverse any changes which may have resulted as the consequence of a deposit.
     *
     * This is intended for use during no-op deposits, and should be called at the
     * end of such a deposit process in order to remove any temporary files and
     * to abort the database connection, so no changes are written.
     *
     * @param result deposit result to undo
     * @throws DSpaceSWORDException can be thrown by the internals of the DSpace SWORD implementation
     */
    @Override
    public void undoDeposit(DepositResult result) throws DSpaceSWORDException {
        SWORDContext sc = swordService.getSwordContext();

        // abort the context, so no database changes are written
        // uploaded files will be deleted by the cleanup script
        sc.abort();
        swordService.message("Database changes aborted");
    }
}
