/**
 * 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.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.Logger;
import org.dspace.content.BitstreamFormat;
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.core.Context;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.purl.sword.base.SWORDErrorException;

/**
 * @author Richard Jones
 *
 * Class to represent the principal configurations of the SWORD
 * service being offered.  Not all configuration is available through
 * this class, but the most useful common options, and those with
 * default values are available
 *
 * Note that changes to values via the API will not be persisted
 * between SWORD requests.
 *
 * For detailed descriptions of configuration values, see the sword
 * configuration documentation
 */
public class SWORDConfiguration {

    /**
     * logger
     */
    public static final Logger log = org.apache.logging.log4j.LogManager.getLogger(SWORDConfiguration.class);

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

    protected ConfigurationService configurationService = DSpaceServicesFactory
        .getInstance().getConfigurationService();

    /**
     * whether we can support noOp
     */
    private boolean noOp = true;

    /**
     * whether we can be verbose
     */
    private boolean verbose = true;

    /**
     * what our default max upload size is
     */
    private int maxUploadSize = -1;

    /**
     * do we support mediation
     */
    private boolean mediated = false;

    /**
     * should we keep the original package as bitstream
     */
    private boolean keepOriginal = false;

    /**
     * item bundle in which sword deposits are stored
     */
    private String swordBundle = "SWORD";

    /**
     * should we keep the original package as a file on ingest error
     */
    private boolean keepPackageOnFailedIngest = false;

    /**
     * location of directory to store packages on ingest error
     */
    private String failedPackageDir = null;

    /**
     * Accepted formats
     */
    private List<String> swordaccepts;

    /**
     * Initialise the sword configuration.  It is at this stage that the
     * object will interrogate the DSpace Configuration for details
     */
    public SWORDConfiguration() {
        // set the max upload size
        int mus = configurationService.getIntProperty(
            "sword-server.max-upload-size");
        if (mus > 0) {
            this.maxUploadSize = mus;
        }

        // set the mediation value
        this.mediated = configurationService.getBooleanProperty(
            "sword-server.on-behalf-of.enable");

        // find out if we keep the original as bitstream
        this.keepOriginal = configurationService.getBooleanProperty(
            "sword-server.keep-original-package");

        // get the sword bundle
        String bundle = configurationService.getProperty(
            "sword-server.bundle.name");
        if (bundle != null && "".equals(bundle)) {
            this.swordBundle = bundle;
        }

        // find out if we keep the package as a file in specified directory
        this.keepPackageOnFailedIngest = configurationService
            .getBooleanProperty("sword-server.keep-package-on-fail", false);

        // get directory path and name
        this.failedPackageDir = configurationService.getProperty(
            "sword-server.failed-package.dir");

        // Get the accepted formats
        String[] acceptsFormats = configurationService
            .getArrayProperty("sword-server.accepts");
        swordaccepts = new ArrayList<String>();
        if (acceptsFormats == null) {
            acceptsFormats = new String[] {"application/zip"};
        }
        for (String element : acceptsFormats) {
            swordaccepts.add(element.trim());
        }
    }

    /**
     * Get the bundle name that SWORD will store its original deposit
     * packages in, when storing them inside an item.
     *
     * @return bundle name where to store the original deposit packages
     */
    public String getSwordBundle() {
        return swordBundle;
    }

    /**
     * Set the bundle name that SWORD will store its original deposit
     * packages in, when storing them inside an item.
     *
     * @param swordBundle set bundle name where to store the original
     *                    deposit packages
     */
    public void setSwordBundle(String swordBundle) {
        this.swordBundle = swordBundle;
    }

    /**
     * Is this a no-op deposit?
     *
     * @return true if this is a no-op deposit
     */
    public boolean isNoOp() {
        return noOp;
    }

    /**
     * Set whether this is a no-op deposit.
     *
     * @param noOp sets whether this is a no-op deposit
     */
    public void setNoOp(boolean noOp) {
        this.noOp = noOp;
    }

    /**
     * Is this a verbose deposit?
     *
     * @return true if is verbose deposit
     */
    public boolean isVerbose() {
        return verbose;
    }

    /**
     * Set whether this is a verbose deposit.
     *
     * @param verbose is verbose deposit?
     */
    public void setVerbose(boolean verbose) {
        this.verbose = verbose;
    }

    /**
     * What is the max upload size (in bytes) for the SWORD interface?
     *
     * @return maximum upload size
     */
    public int getMaxUploadSize() {
        return maxUploadSize;
    }

    /**
     * set the max upload size (in bytes) for the SWORD interface
     *
     * @param maxUploadSize maximum upload size to set
     */
    public void setMaxUploadSize(int maxUploadSize) {
        this.maxUploadSize = maxUploadSize;
    }

    /**
     * Does the server support mediated deposit (aka on-behalf-of)?
     *
     * @return true if server supports mediated deposit
     */
    public boolean isMediated() {
        return mediated;
    }

    /**
     * Set whether the server supports mediated deposit (aka on-behalf-of).
     *
     * @param mediated set whether server supports mediated deposit
     */
    public void setMediated(boolean mediated) {
        this.mediated = mediated;
    }

    /**
     * Should the repository keep the original package?
     *
     * @return true if repository keeps copies of original package
     */
    public boolean isKeepOriginal() {
        return keepOriginal;
    }

    /**
     * Set whether the repository should keep copies of the original package.
     *
     * @param keepOriginal set whether to keep copies of original package
     */
    public void setKeepOriginal(boolean keepOriginal) {
        this.keepOriginal = keepOriginal;
    }

    /**
     * set whether the repository should write file of the original package if ingest fails
     *
     * @param keepOriginalOnFail set whether to keep copies of original
     *                           package if ingest fails
     */
    public void setKeepPackageOnFailedIngest(boolean keepOriginalOnFail) {
        keepPackageOnFailedIngest = keepOriginalOnFail;
    }

    /**
     * should the repository write file of the original package if ingest fails
     *
     * @return true if repository keeps copies of original package if ingest
     * fails
     */
    public boolean isKeepPackageOnFailedIngest() {
        return keepPackageOnFailedIngest;
    }

    /**
     * set the directory to write file of the original package
     *
     * @param dir directory where original package is kept
     */
    public void setFailedPackageDir(String dir) {
        failedPackageDir = dir;
    }

    /**
     * directory location of the files with original packages
     * for failed ingests
     *
     * @return dir directory where original package is kept
     */
    public String getFailedPackageDir() {
        return failedPackageDir;
    }

    /**
     * Get the list of MIME types that the given DSpace object will
     * accept as packages.
     *
     * @param context The relevant DSpace Context.
     * @param dso     target DSpace object
     * @return the list of MIME types that the given DSpace object will
     * accept as packages.
     * @throws DSpaceSWORDException can be thrown by the internals of the DSpace SWORD implementation
     */
    public List<String> getAccepts(Context context, DSpaceObject dso)
        throws DSpaceSWORDException {
        try {
            List<String> accepts = new ArrayList<String>();
            if (dso instanceof Collection) {
                for (String format : swordaccepts) {
                    accepts.add(format);
                }
            } else if (dso instanceof Item) {
                List<BitstreamFormat> bfs = bitstreamFormatService
                    .findNonInternal(context);
                for (BitstreamFormat bf : bfs) {
                    accepts.add(bf.getMIMEType());
                }
            }

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

    /**
     * Get the list of MIME types that a Collection will accept as packages
     *
     * @return the list of MIME types
     * @throws DSpaceSWORDException can be thrown by the internals of the DSpace SWORD implementation
     */
    public List<String> getCollectionAccepts() throws DSpaceSWORDException {
        List<String> accepts = new ArrayList<String>();
        for (String format : swordaccepts) {
            accepts.add(format);
        }
        return accepts;
    }

    /**
     * Get a map of packaging URIs to Q values for the packaging types which
     * the given collection will accept.
     *
     * The URI should be a unique identifier for the packaging type,
     * such as:
     *
     * http://purl.org/net/sword-types/METSDSpaceSIP
     *
     * and the Q value is a floating point between 0 and 1 which defines
     * how much  the server "likes" this packaging type
     *
     * @param col target collection
     * @return map of packaging URIs to Q values for the packaging types which
     * the given collection will accept.
     */
    public Map<String, Float> getAcceptPackaging(Collection col) {
        Map<String, String> identifiers = new HashMap<String, String>();
        Map<String, String> qs = new HashMap<String, String>();
        String handle = col.getHandle();

        // build the holding maps of identifiers and q values
        String acceptPackagingPrefix = "sword-server.accept-packaging";
        List<String> keys = configurationService.getPropertyKeys(acceptPackagingPrefix);
        for (String key : keys) {
            // extract the configuration into the holding Maps
            String suffix = key.substring(acceptPackagingPrefix.length() + 1);

            String[] bits = suffix.split("\\.");
            if (bits.length == 2) {
                // global settings
                String value = configurationService.getProperty(key);
                if (bits[1].equals("identifier")) {
                    identifiers.put(bits[0], value);
                } else if (bits[1].equals("q")) {
                    qs.put(bits[0], value);
                }
            }

            // collection settings
            if (bits.length == 3 && bits[0].equals(handle)) {
                // this is configuration for our collection
                String value = configurationService.getProperty(key);
                if (bits[2].equals("identifier")) {
                    identifiers.put(bits[1], value);
                } else if (bits[2].equals("q")) {
                    qs.put(bits[1], value);
                }
            }
        }

        // merge the holding maps into the Accept Packaging settings
        Map<String, Float> ap = new HashMap<String, Float>();
        for (String ik : identifiers.keySet()) {
            String id = identifiers.get(ik);
            String qv = qs.get(ik);
            Float qf = Float.parseFloat(qv);
            ap.put(id, qf);
        }

        return ap;
    }

    /**
     * Is the given packaging/media type supported by the given DSpace
     * object?
     *
     * @param mediaType packaging/media type to check
     * @param dso       target DSpace object
     * @return true if the given packaging/media type is supported by the
     * given DSpace object
     * @throws DSpaceSWORDException can be thrown by the internals of the DSpace SWORD implementation
     * @throws SWORDErrorException  on generic SWORD exception
     */
    public boolean isSupportedMediaType(String mediaType, DSpaceObject dso)
        throws DSpaceSWORDException, SWORDErrorException {
        if (mediaType == null || "".equals(mediaType)) {
            return true;
        }

        if (dso instanceof Collection) {
            Map<String, Float> accepts = this
                .getAcceptPackaging((Collection) dso);
            for (String accept : accepts.keySet()) {
                if (accept.equals(mediaType)) {
                    return true;
                }
            }
        } else if (dso instanceof Item) {
            // items don't unpackage, so they don't care what the media type is
            return true;
        }
        return false;
    }

    /**
     * Is the given content MIME type acceptable to the given DSpace object?
     *
     * @param context The relevant DSpace Context.
     * @param type    MIME type to check
     * @param dso     target DSpace object
     * @return true if the given content MIME type is acceptable
     * to the given DSpace object
     * @throws DSpaceSWORDException can be thrown by the internals of the DSpace SWORD implementation
     */
    public boolean isAcceptableContentType(Context context, String type,
                                           DSpaceObject dso)
        throws DSpaceSWORDException {
        List<String> accepts = this.getAccepts(context, dso);
        return accepts.contains(type);
    }

    /**
     * Get the temp directory for storing files during deposit.
     *
     * @return temp directory for storing files during deposit
     * @throws DSpaceSWORDException can be thrown by the internals of the DSpace SWORD implementation
     */
    public String getTempDir()
        throws DSpaceSWORDException {
        return (configurationService.getProperty("upload.temp.dir") != null)
            ? configurationService.getProperty("upload.temp.dir")
            : System.getProperty("java.io.tmpdir");
    }
}
