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

import java.io.CharArrayWriter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;

import org.apache.commons.lang3.ArrayUtils;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.Site;
import org.dspace.content.authority.Choices;
import org.dspace.content.dto.MetadataValueDTO;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.CollectionService;
import org.dspace.content.service.CommunityService;
import org.dspace.content.service.ItemService;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.core.factory.CoreServiceFactory;
import org.dspace.handle.factory.HandleServiceFactory;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.jdom2.Content;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.Verifier;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.jdom2.transform.JDOMResult;
import org.jdom2.transform.JDOMSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Configurable XSLT-driven dissemination Crosswalk
 * <p>
 * See the XSLTCrosswalk superclass for details on configuration.
 * </p>
 * <h3>Additional Configuration of Dissemination crosswalk:</h3>
 * The disseminator also needs to be configured with an XML Namespace
 * (including prefix and URI) and an XML Schema for output format.  This
 * is configured on additional properties in the DSpace Configuration, i.e.:
 * <pre>
 *   crosswalk.dissemination.<i>PluginName</i>.namespace.<i>Prefix</i> = <i>namespace-URI</i>
 *   crosswalk.dissemination.<i>PluginName</i>.schemaLocation = <i>schemaLocation value</i>
 *   crosswalk.dissemination.<i>PluginName</i>.preferList = <i>boolean</i> (default is false)
 * </pre>
 * For example:
 * <pre>
 *   crosswalk.dissemination.qdc.namespace.dc = http://purl.org/dc/elements/1.1/
 *   crosswalk.dissemination.qdc.namespace.dcterms = http://purl.org/dc/terms/
 *   crosswalk.dissemination.qdc.schemaLocation = \
 *      http://purl.org/dc/elements/1.1/ http://dublincore.org/schemas/xmls/qdc/2003/04/02/qualifieddc.xsd
 *   crosswalk.dissemination.qdc.preferList = true
 * </pre>
 *
 * @author Larry Stone
 * @author Scott Phillips
 * @author Pascal-Nicolas Becker
 * @see XSLTCrosswalk
 */
public class XSLTDisseminationCrosswalk
    extends XSLTCrosswalk
    implements ParameterizedDisseminationCrosswalk {
    /**
     * log4j category
     */
    private static final Logger LOG = LoggerFactory.getLogger(XSLTDisseminationCrosswalk.class);

    /**
     * DSpace context, will be created if XSLTDisseminationCrosswalk had been started by command-line.
     */
    private static Context context;

    private static final String DIRECTION = "dissemination";

    protected static final CommunityService communityService
            = ContentServiceFactory.getInstance().getCommunityService();
    protected static final CollectionService collectionService
            = ContentServiceFactory.getInstance().getCollectionService();
    protected static final ItemService itemService
            = ContentServiceFactory.getInstance().getItemService();
    protected static final ConfigurationService configurationService
            = DSpaceServicesFactory.getInstance().getConfigurationService();

    private static final String aliases[] = makeAliases(DIRECTION);

    public static String[] getPluginNames() {
        return (String[]) ArrayUtils.clone(aliases);
    }

    // namespace and schema; don't worry about initializing these
    // until there's an instance, so do it in constructor.
    private String schemaLocation = null;

    private Namespace namespaces[] = null;

    private boolean preferList = false;

    // load the namespace and schema from config
    private void init()
        throws CrosswalkInternalException {
        if (namespaces != null || schemaLocation != null) {
            return;
        }
        String myAlias = getPluginInstanceName();
        if (myAlias == null) {
            LOG.error("Must use PluginService to instantiate XSLTDisseminationCrosswalk so the class knows its name.");
            throw new CrosswalkInternalException(
                "Must use PluginService to instantiate XSLTDisseminationCrosswalk so the class knows its name.");
        }

        // all configs for this plugin instance start with this:
        String prefix = CONFIG_PREFIX + DIRECTION + "." + myAlias + ".";

        // get the schema location string, should already be in the
        // right format for value of "schemaLocation" attribute.
        schemaLocation = configurationService.getProperty(prefix + "schemaLocation");
        if (schemaLocation == null) {
            LOG.warn("No schemaLocation for crosswalk=" + myAlias + ", key=" + prefix + "schemaLocation");
        } else if (schemaLocation.length() > 0 && schemaLocation.indexOf(' ') < 0) {
            // sanity check: schemaLocation should have space.
            LOG.warn("Possible INVALID schemaLocation (no space found) for crosswalk=" +
                         myAlias + ", key=" + prefix + "schemaLocation" +
                         "\n\tCorrect format is \"{namespace} {schema-URL}\"");
        }

        // grovel for namespaces of the form:
        //  crosswalk.diss.{PLUGIN_NAME}.namespace.{PREFIX} = {URI}
        String nsPrefix = prefix + "namespace.";
        List<String> configKeys = configurationService.getPropertyKeys(nsPrefix);
        List<Namespace> nsList = new ArrayList<>();
        for (String key : configKeys) {
            nsList.add(Namespace.getNamespace(key.substring(nsPrefix.length()),
                                              configurationService.getProperty(key)));
        }
        namespaces = nsList.toArray(new Namespace[nsList.size()]);

        preferList = configurationService.getBooleanProperty(prefix + "preferList", false);
    }

    /**
     * Return the namespace used by this crosswalk.
     *
     * @see DisseminationCrosswalk
     */
    @Override
    public Namespace[] getNamespaces() {
        try {
            init();
        } catch (CrosswalkInternalException e) {
            LOG.error(e.toString());
        }
        return (Namespace[]) ArrayUtils.clone(namespaces);
    }

    /**
     * Return the schema location used by this crosswalk.
     *
     * @see DisseminationCrosswalk
     */
    @Override
    public String getSchemaLocation() {
        try {
            init();
        } catch (CrosswalkInternalException e) {
            LOG.error(e.toString());
        }
        return schemaLocation;
    }

    @Override
    public Element disseminateElement(Context context, DSpaceObject dso)
        throws CrosswalkException, IOException, SQLException, AuthorizeException {
        return disseminateElement(context, dso, new HashMap());
    }

    @Override
    public Element disseminateElement(Context context, DSpaceObject dso,
                                      Map<String, String> parameters)
        throws CrosswalkException,
        IOException, SQLException, AuthorizeException {
        int type = dso.getType();
        if (!(type == Constants.ITEM ||
            type == Constants.COLLECTION ||
            type == Constants.COMMUNITY)) {
            throw new CrosswalkObjectNotSupported(
                "XSLTDisseminationCrosswalk can only crosswalk items, collections, and communities.");
        }

        init();

        Transformer xform = getTransformer(DIRECTION);
        if (xform == null) {
            throw new CrosswalkInternalException(
                "Failed to initialize transformer, probably error loading stylesheet.");
        }

        for (Map.Entry<String, String> parameter : parameters.entrySet()) {
            LOG.debug("Setting parameter {} to {}", parameter.getKey(), parameter.getValue());
            xform.setParameter(parameter.getKey(), parameter.getValue());
        }

        try {
            Document ddim = new Document(createDIM(dso));
            JDOMResult result = new JDOMResult();
            xform.transform(new JDOMSource(ddim), result);
            Element root = result.getDocument().getRootElement();
            root.detach();
            return root;
        } catch (TransformerException e) {
            LOG.error("Got error: " + e.toString());
            throw new CrosswalkInternalException("XSL translation failed: " + e.toString(), e);
        }
    }

    /**
     * Disseminate the DSpace item, collection, or community.
     *
     * @param context context
     * @throws CrosswalkException crosswalk error
     * @throws IOException        if IO error
     * @throws SQLException       if database error
     * @throws AuthorizeException if authorization error
     * @see DisseminationCrosswalk
     * @return List of Elements
     */
    @Override
    public List<Element> disseminateList(Context context, DSpaceObject dso)
        throws CrosswalkException,
        IOException, SQLException, AuthorizeException {
        int type = dso.getType();
        if (!(type == Constants.ITEM ||
            type == Constants.COLLECTION ||
            type == Constants.COMMUNITY)) {
            throw new CrosswalkObjectNotSupported(
                "XSLTDisseminationCrosswalk can only crosswalk a items, collections, and communities.");
        }

        init();

        Transformer xform = getTransformer(DIRECTION);
        if (xform == null) {
            throw new CrosswalkInternalException(
                "Failed to initialize transformer, probably error loading stylesheet.");
        }

        try {
            JDOMResult result = new JDOMResult();
            xform.transform(new JDOMSource(createDIM(dso).getChildren()), result);
            List<Content> contentList = result.getResult();
            // Transform List<Content> into List<Element>
            List<Element> elementList = contentList.stream()
                                                   .filter(obj -> obj instanceof Element)
                                                   .map(Element.class::cast).collect(Collectors.toList());
            return elementList;
        } catch (TransformerException e) {
            LOG.error("Got error: " + e.toString());
            throw new CrosswalkInternalException("XSL translation failed: " + e.toString(), e);
        }
    }

    /**
     * Determine is this crosswalk can dessiminate the given object.
     *
     * @see DisseminationCrosswalk
     */
    @Override
    public boolean canDisseminate(DSpaceObject dso) {
        return dso.getType() == Constants.ITEM;
    }

    /**
     * return true if this crosswalk prefers the list form over an single
     * element, otherwise false.
     *
     * @see DisseminationCrosswalk
     */
    @Override
    public boolean preferList() {
        try {
            init();
        } catch (CrosswalkInternalException e) {
            LOG.error(e.toString());
        }
        return preferList;
    }

    /**
     * Generate an intermediate representation of a DSpace object.
     *
     * @param dso  The dspace object to build a representation of.
     * @param dcvs list of metadata
     * @return element
     */
    public static Element createDIM(DSpaceObject dso, List<MetadataValueDTO> dcvs) {
        Element dim = new Element("dim", DIM_NS);
        String type = Constants.typeText[dso.getType()];
        dim.setAttribute("dspaceType", type);

        for (int i = 0; i < dcvs.size(); i++) {
            MetadataValueDTO dcv = dcvs.get(i);
            Element field =
                createField(dcv.getSchema(), dcv.getElement(), dcv.getQualifier(),
                            dcv.getLanguage(), dcv.getValue(), dcv.getAuthority(), dcv.getConfidence());
            dim.addContent(field);
        }
        return dim;
    }

    /**
     * Generate an intermediate representation of a DSpace object.
     *
     * @param dso The dspace object to build a representation of.
     * @return element
     */
    public static Element createDIM(DSpaceObject dso) {
        if (dso.getType() == Constants.ITEM) {
            Item item = (Item) dso;
            return createDIM(dso, item2Metadata(item));
        } else {
            Element dim = new Element("dim", DIM_NS);
            String type = Constants.typeText[dso.getType()];
            dim.setAttribute("dspaceType", type);

            if (dso.getType() == Constants.COLLECTION) {
                Collection collection = (Collection) dso;

                String description = collectionService.getMetadataFirstValue(collection,
                        CollectionService.MD_INTRODUCTORY_TEXT, Item.ANY);
                String description_abstract = collectionService.getMetadataFirstValue(collection,
                        CollectionService.MD_SHORT_DESCRIPTION, Item.ANY);
                String description_table = collectionService.getMetadataFirstValue(collection,
                        CollectionService.MD_SIDEBAR_TEXT, Item.ANY);
                String identifier_uri = "hdl:" + collection.getHandle();
                String provenance = collectionService.getMetadataFirstValue(collection,
                        CollectionService.MD_PROVENANCE_DESCRIPTION, Item.ANY);
                String rights = collectionService.getMetadataFirstValue(collection,
                        CollectionService.MD_COPYRIGHT_TEXT, Item.ANY);
                String rights_license = collectionService.getMetadataFirstValue(collection,
                        CollectionService.MD_LICENSE, Item.ANY);
                String title = collectionService.getMetadataFirstValue(collection,
                        CollectionService.MD_NAME, Item.ANY);

                dim.addContent(createField("dc", "description", null, null, description));
                dim.addContent(createField("dc", "description", "abstract", null, description_abstract));
                dim.addContent(createField("dc", "description", "tableofcontents", null, description_table));
                dim.addContent(createField("dc", "identifier", "uri", null, identifier_uri));
                dim.addContent(createField("dc", "provenance", null, null, provenance));
                dim.addContent(createField("dc", "rights", null, null, rights));
                dim.addContent(createField("dc", "rights", "license", null, rights_license));
                dim.addContent(createField("dc", "title", null, null, title));
            } else if (dso.getType() == Constants.COMMUNITY) {
                Community community = (Community) dso;

                String description = communityService.getMetadataFirstValue(community,
                        CommunityService.MD_INTRODUCTORY_TEXT, Item.ANY);
                String description_abstract = communityService.getMetadataFirstValue(community,
                        CommunityService.MD_SHORT_DESCRIPTION, Item.ANY);
                String description_table = communityService.getMetadataFirstValue(community,
                        CommunityService.MD_SIDEBAR_TEXT, Item.ANY);
                String identifier_uri = "hdl:" + community.getHandle();
                String rights = communityService.getMetadataFirstValue(community,
                        CommunityService.MD_COPYRIGHT_TEXT, Item.ANY);
                String title = communityService.getMetadataFirstValue(community,
                        CommunityService.MD_NAME, Item.ANY);

                dim.addContent(createField("dc", "description", null, null, description));
                dim.addContent(createField("dc", "description", "abstract", null, description_abstract));
                dim.addContent(createField("dc", "description", "tableofcontents", null, description_table));
                dim.addContent(createField("dc", "identifier", "uri", null, identifier_uri));
                dim.addContent(createField("dc", "rights", null, null, rights));
                dim.addContent(createField("dc", "title", null, null, title));
            } else if (dso.getType() == Constants.SITE) {
                Site site = (Site) dso;

                String identifier_uri = "hdl:" + site.getHandle();
                String title = site.getName();
                String url = site.getURL();

                //FIXME: adding two URIs for now (site handle and URL), in case site isn't using handles
                dim.addContent(createField("dc", "identifier", "uri", null, identifier_uri));
                dim.addContent(createField("dc", "identifier", "uri", null, url));
                dim.addContent(createField("dc", "title", null, null, title));
            }
            // XXX FIXME: Nothing to crosswalk for bitstream?
            return dim;
        }
    }

    protected static List<MetadataValueDTO> item2Metadata(Item item) {
        List<MetadataValue> dcvs = itemService.getMetadata(item, Item.ANY, Item.ANY, Item.ANY,
                                                           Item.ANY);
        List<MetadataValueDTO> result = new ArrayList<>();
        for (MetadataValue metadataValue : dcvs) {
            result.add(new MetadataValueDTO(metadataValue));
        }

        return result;
    }


    /**
     * Create a new DIM field element with the given attributes.
     *
     * @param schema    The schema the DIM field belongs to.
     * @param element   The element the DIM field belongs to.
     * @param qualifier The qualifier the DIM field belongs to.
     * @param language  The language the DIM field belongs to.
     * @param value     The value of the DIM field.
     * @return A new DIM field element
     */
    private static Element createField(String schema, String element, String qualifier, String language, String value) {
        return createField(schema, element, qualifier, language, value, null, -1);
    }

    /**
     * Create a new DIM field element with the given attributes.
     *
     * @param schema     The schema the DIM field belongs to.
     * @param element    The element the DIM field belongs to.
     * @param qualifier  The qualifier the DIM field belongs to.
     * @param language   The language the DIM field belongs to.
     * @param value      The value of the DIM field.
     * @param authority  The authority
     * @param confidence confidence in the authority
     * @return A new DIM field element
     */
    private static Element createField(String schema, String element, String qualifier, String language, String value,
                                       String authority, int confidence) {
        Element field = new Element("field", DIM_NS);
        field.setAttribute("mdschema", schema);
        field.setAttribute("element", element);
        if (qualifier != null) {
            field.setAttribute("qualifier", qualifier);
        }
        if (language != null) {
            field.setAttribute("lang", language);
        }

        field.setText(checkedString(value));

        if (authority != null) {
            field.setAttribute("authority", authority);
            field.setAttribute("confidence", Choices.getConfidenceText(confidence));
        }

        return field;
    }

    // Return string with non-XML characters (i.e. low control chars) excised.
    private static String checkedString(String value) {
        if (value == null) {
            return null;
        }
        String reason = Verifier.checkCharacterData(value);
        if (reason == null) {
            return value;
        } else {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Filtering out non-XML characters in string, reason=" + reason);
            }
            StringBuilder result = new StringBuilder(value.length());
            for (int i = 0; i < value.length(); ++i) {
                char c = value.charAt(i);
                if (Verifier.isXMLCharacter((int) c)) {
                    result.append(c);
                }
            }
            return result.toString();
        }
    }

    /**
     * Simple command-line rig for testing the DIM output of a stylesheet.
     * Usage:  {@code java XSLTDisseminationCrosswalk  <crosswalk-name> <handle> [output-file]}
     *
     * @param argv the command line arguments given
     * @throws Exception if error
     */
    public static void main(String[] argv) throws Exception {
        LOG.error("started.");
        if (argv.length < 2 || argv.length > 3) {
            System.err.println("Usage:  java XSLTDisseminationCrosswalk <crosswalk-name> <handle> [output-file]");
            LOG.error("You started Dissemination Crosswalk Test/Export with a wrong number of parameters.");
            System.exit(1);
        }

        String xwalkname = argv[0];
        String handle = argv[1];
        OutputStream out = System.out;
        if (argv.length > 2) {
            try {
                out = new FileOutputStream(argv[2]);
            } catch (FileNotFoundException e) {
                System.err.format("Can't write to the specified file: %s%n",
                                  e.getMessage());
                System.err.println("Will write output to stdout.");
            }
        }

        DisseminationCrosswalk xwalk
            = (DisseminationCrosswalk) CoreServiceFactory.getInstance()
                                                         .getPluginService()
                                                         .getNamedPlugin(DisseminationCrosswalk.class, xwalkname);
        if (xwalk == null) {
            System.err.format("Error: Cannot find a DisseminationCrosswalk plugin for: \"%s\"%n", xwalkname);
            LOG.error("Cannot find the Dissemination Crosswalk plugin.");
            System.exit(1);
        }

        context = new Context();
        context.turnOffAuthorisationSystem();

        DSpaceObject dso = null;
        try {
            dso = HandleServiceFactory.getInstance().getHandleService().resolveToObject(context, handle);
        } catch (SQLException e) {
            System.err
                .println("Error: A problem with the database connection occurred, check logs for further information.");
            System.exit(1);
        }

        if (null == dso) {
            System.err.format("Can't find a DSpaceObject with the handle \"%s\"%n", handle);
            System.exit(1);
        }

        if (!xwalk.canDisseminate(dso)) {
            System.err.println("Dissemination Crosswalk can't disseminate this DSpaceObject.");
            LOG.error("Dissemination Crosswalk can't disseminate this DSpaceObject.");
            System.exit(1);
        }

        Element root = null;
        try {
            root = xwalk.disseminateElement(context, dso);
        } catch (CrosswalkException | IOException | SQLException | AuthorizeException e) {
            // as this script is for testing dissemination crosswalks, we want
            // verbose information in case of an exception.
            System.err.println("An error occurred while processing the dissemination crosswalk.");
            System.err.println("=== Error Message ===");
            System.err.println(e.getMessage());
            System.err.println("===  Stack Trace  ===");
            e.printStackTrace(System.err);
            System.err.println("=====================");
            LOG.error("Caught: {}.", e.toString());
            LOG.error(e.getMessage());
            CharArrayWriter traceWriter = new CharArrayWriter(2048);
            e.printStackTrace(new PrintWriter(traceWriter));
            LOG.error(traceWriter.toString());
            System.exit(1);
        }

        try {
            XMLOutputter xmlout = new XMLOutputter(Format.getPrettyFormat());
            xmlout.output(new Document(root), out);
        } catch (IOException e) {
            // as this script is for testing dissemination crosswalks, we want
            // verbose information in case of an exception.
            System.err.println("An error occurred after processing the dissemination crosswalk.");
            System.err.println("The error occurred while trying to print the generated XML.");
            System.err.println("=== Error Message ===");
            System.err.println(e.getMessage());
            System.err.println("===  Stack Trace  ===");
            e.printStackTrace(System.err);
            System.err.println("=====================");
            LOG.error("Caught: {}.", e.toString());
            LOG.error(e.getMessage());
            CharArrayWriter traceWriter = new CharArrayWriter(2048);
            e.printStackTrace(new PrintWriter(traceWriter));
            LOG.error(traceWriter.toString());
            System.exit(1);
        }

        context.complete();
        if (out instanceof FileOutputStream) {
            out.close();
        }
    }
}
