/**
 * 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.app.itemupdate;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.lang3.StringUtils;
import org.dspace.content.Item;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataSchema;
import org.dspace.content.MetadataSchemaEnum;
import org.dspace.content.MetadataValue;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


/**
 * Miscellaneous methods for metadata handling that build on the API
 * which might have general utility outside of the specific use
 * in context in ItemUpdate.
 *
 * The XML methods were based on those in ItemImport
 */
public class MetadataUtilities {

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

    /**
     * Default constructor
     */
    private MetadataUtilities() { }

    /**
     * Working around Item API to delete a value-specific Metadatum
     * For a given element/qualifier/lang:
     * get all DCValues
     * clear (i.e. delete) all of these DCValues
     * add them back, minus the one to actually delete
     *
     * @param context          DSpace Context
     * @param item             Item Object
     * @param dtom             metadata field
     * @param isLanguageStrict whether strict or not
     * @return true if metadata field is found with matching value and was deleted
     * @throws SQLException if database error
     */
    public static boolean deleteMetadataByValue(Context context, Item item, DtoMetadata dtom, boolean isLanguageStrict)
        throws SQLException {
        List<MetadataValue> ar = null;

        if (isLanguageStrict) {   // get all for given type
            ar = itemService.getMetadata(item, dtom.schema, dtom.element, dtom.qualifier, dtom.language);
        } else {
            ar = itemService.getMetadata(item, dtom.schema, dtom.element, dtom.qualifier, Item.ANY);
        }

        boolean found = false;

        //build new set minus the one to delete
        List<String> vals = new ArrayList<String>();
        for (MetadataValue dcv : ar) {
            if (dcv.getValue().equals(dtom.value)) {
                found = true;
            } else {
                vals.add(dcv.getValue());
            }
        }

        if (found) {  //remove all for given type  ??synchronize this block??
            if (isLanguageStrict) {
                itemService.clearMetadata(context, item, dtom.schema, dtom.element, dtom.qualifier, dtom.language);
            } else {
                itemService.clearMetadata(context, item, dtom.schema, dtom.element, dtom.qualifier, Item.ANY);
            }

            itemService.addMetadata(context, item, dtom.schema, dtom.element, dtom.qualifier, dtom.language, vals);
        }
        return found;
    }

    /**
     * Append text to value metadata field to item
     *
     * @param context          DSpace Context
     * @param item             DSpace Item
     * @param dtom             metadata field
     * @param isLanguageStrict if strict
     * @param textToAppend     text to append
     * @throws IllegalArgumentException - When target metadata field is not found
     * @throws SQLException             if database error
     */
    public static void appendMetadata(Context context, Item item, DtoMetadata dtom, boolean isLanguageStrict,
                                      String textToAppend)
        throws IllegalArgumentException, SQLException {
        List<MetadataValue> ar = null;

        // get all values for given element/qualifier
        if (isLanguageStrict) {  // get all for given element/qualifier
            ar = itemService.getMetadata(item, dtom.schema, dtom.element, dtom.qualifier, dtom.language);
        } else {
            ar = itemService.getMetadata(item, dtom.schema, dtom.element, dtom.qualifier, Item.ANY);
        }

        if (ar.size() == 0) {
            throw new IllegalArgumentException("Metadata to append to not found");
        }

        int idx = 0;  //index of field to change
        if (ar.size() > 1) { //need to pick one, can't be sure it's the last one
            // TODO maybe get highest id ?
        }

        //build new set minus the one to delete
        List<String> vals = new ArrayList<String>();
        for (int i = 0; i < ar.size(); i++) {
            if (i == idx) {
                vals.add(ar.get(i).getValue() + textToAppend);
            } else {
                vals.add(ar.get(i).getValue());
            }
        }

        if (isLanguageStrict) {
            itemService.clearMetadata(context, item, dtom.schema, dtom.element, dtom.qualifier, dtom.language);
        } else {
            itemService.clearMetadata(context, item, dtom.schema, dtom.element, dtom.qualifier, Item.ANY);
        }

        itemService.addMetadata(context, item, dtom.schema, dtom.element, dtom.qualifier, dtom.language, vals);
    }

    /**
     * Modification of method from ItemImporter.loadDublinCore
     * as a Factory method
     *
     * @param docBuilder DocumentBuilder
     * @param is         - InputStream of dublin_core.xml
     * @return list of DtoMetadata representing the metadata fields relating to an Item
     * @throws IOException                  if IO error
     * @throws ParserConfigurationException if parser config error
     * @throws SAXException                 if XML error
     */
    public static List<DtoMetadata> loadDublinCore(DocumentBuilder docBuilder, InputStream is)
        throws IOException, XPathExpressionException, SAXException {
        Document document = docBuilder.parse(is);

        List<DtoMetadata> dtomList = new ArrayList<DtoMetadata>();

        // Get the schema, for backward compatibility we will default to the
        // dublin core schema if the schema name is not available in the import file
        String schema;
        XPath xPath = XPathFactory.newInstance().newXPath();
        NodeList metadata = (NodeList) xPath.compile("/dublin_core").evaluate(document, XPathConstants.NODESET);
        Node schemaAttr = metadata.item(0).getAttributes().getNamedItem("schema");
        if (schemaAttr == null) {
            schema = MetadataSchemaEnum.DC.getName();
        } else {
            schema = schemaAttr.getNodeValue();
        }

        // Get the nodes corresponding to formats
        NodeList dcNodes = (NodeList) xPath.compile("/dublin_core/dcvalue").evaluate(document, XPathConstants.NODESET);

        for (int i = 0; i < dcNodes.getLength(); i++) {
            Node n = dcNodes.item(i);
            String value = getStringValue(n).trim();
            // compensate for empty value getting read as "null", which won't display
            if (value == null) {
                value = "";
            }
            String element = getAttributeValue(n, "element");
            if (element != null) {
                element = element.trim();
            }
            String qualifier = getAttributeValue(n, "qualifier");
            if (qualifier != null) {
                qualifier = qualifier.trim();
            }
            String language = getAttributeValue(n, "language");
            if (language != null) {
                language = language.trim();
            }

            if ("none".equals(qualifier) || "".equals(qualifier)) {
                qualifier = null;
            }

            // a goofy default, but consistent with DSpace treatment elsewhere
            if (language == null) {
                language = "en";
            } else if ("".equals(language)) {
                language = DSpaceServicesFactory.getInstance()
                        .getConfigurationService()
                        .getProperty("default.language");
            }

            DtoMetadata dtom = DtoMetadata.create(schema, element, qualifier, language, value);
            ItemUpdate.pr(dtom.toString());
            dtomList.add(dtom);
        }
        return dtomList;
    }

    /**
     * Write dublin_core.xml
     *
     * @param docBuilder DocumentBuilder
     * @param dtomList   List of metadata fields
     * @return xml document
     * @throws ParserConfigurationException      if parser config error
     * @throws TransformerConfigurationException if transformer config error
     * @throws TransformerException              if transformer error
     */
    public static Document writeDublinCore(DocumentBuilder docBuilder, List<DtoMetadata> dtomList)
        throws ParserConfigurationException, TransformerConfigurationException, TransformerException {
        Document doc = docBuilder.newDocument();
        Element root = doc.createElement("dublin_core");
        doc.appendChild(root);

        for (DtoMetadata dtom : dtomList) {
            Element mel = doc.createElement("dcvalue");
            mel.setAttribute("element", dtom.element);
            if (dtom.qualifier == null) {
                mel.setAttribute("qualifier", "none");
            } else {
                mel.setAttribute("qualifier", dtom.qualifier);
            }

            if (StringUtils.isEmpty(dtom.language)) {
                mel.setAttribute("language", "en");
            } else {
                mel.setAttribute("language", dtom.language);
            }
            mel.setTextContent(dtom.value);
            root.appendChild(mel);
        }

        return doc;
    }

    /**
     * write xml document to output stream
     *
     * @param doc         XML Document
     * @param transformer XML Transformer
     * @param out         OutputStream
     * @throws IOException          if IO Error
     * @throws TransformerException if Transformer error
     */
    public static void writeDocument(Document doc, Transformer transformer, OutputStream out)
        throws IOException, TransformerException {
        Source src = new DOMSource(doc);
        Result dest = new StreamResult(out);
        transformer.transform(src, dest);
    }


    // XML utility methods

    /**
     * Lookup an attribute from a DOM node.
     *
     * @param n    Node
     * @param name name
     * @return attribute value
     */
    private static String getAttributeValue(Node n, String name) {
        NamedNodeMap nm = n.getAttributes();

        for (int i = 0; i < nm.getLength(); i++) {
            Node node = nm.item(i);

            if (name.equals(node.getNodeName())) {
                return node.getNodeValue();
            }
        }

        return "";
    }

    /**
     * Return the String value of a Node.
     *
     * @param node node
     * @return string value
     */
    private static String getStringValue(Node node) {
        String value = node.getNodeValue();

        if (node.hasChildNodes()) {
            Node first = node.getFirstChild();

            if (first.getNodeType() == Node.TEXT_NODE) {
                return first.getNodeValue();
            }
        }

        return value;
    }

    /**
     * Rewrite of ItemImport's functionality
     * but just the parsing of the file, not the processing of its elements.
     *
     * @param f file
     * @return list of ContentsEntry
     * @throws FileNotFoundException if file doesn't exist
     * @throws IOException           if IO error
     * @throws ParseException        if parse error
     */
    public static List<ContentsEntry> readContentsFile(File f)
        throws FileNotFoundException, IOException, ParseException {
        List<ContentsEntry> list = new ArrayList<ContentsEntry>();

        BufferedReader in = null;

        try {
            in = new BufferedReader(new FileReader(f));
            String line = null;

            while ((line = in.readLine()) != null) {
                line = line.trim();
                if ("".equals(line)) {
                    continue;
                }
                ItemUpdate.pr("Contents entry: " + line);
                list.add(ContentsEntry.parse(line));
            }
        } finally {
            try {
                in.close();
            } catch (IOException e) {
                //skip
            }
        }

        return list;
    }

    /**
     * @param f file
     * @return list of lines as strings
     * @throws FileNotFoundException if file doesn't exist
     * @throws IOException           if IO Error
     */
    public static List<String> readDeleteContentsFile(File f)
        throws FileNotFoundException, IOException {
        List<String> list = new ArrayList<>();

        BufferedReader in = null;

        try {
            in = new BufferedReader(new FileReader(f));
            String line = null;

            while ((line = in.readLine()) != null) {
                line = line.trim();
                if ("".equals(line)) {
                    continue;
                }

                list.add(line);
            }
        } finally {
            try {
                in.close();
            } catch (IOException e) {
                //skip
            }
        }

        return list;
    }

    /**
     * Get display of Metadatum
     *
     * @param dcv MetadataValue
     * @return string displaying elements of the Metadatum
     */
    public static String getDCValueString(MetadataValue dcv) {
        MetadataField metadataField = dcv.getMetadataField();
        MetadataSchema metadataSchema = metadataField.getMetadataSchema();
        return "schema: " + metadataSchema.getName() + "; element: " + metadataField
            .getElement() + "; qualifier: " + metadataField.getQualifier() +
            "; language: " + dcv.getLanguage() + "; value: " + dcv.getValue();
    }

    /**
     * Return compound form of a metadata field (i.e. schema.element.qualifier)
     *
     * @param schema    schema
     * @param element   element
     * @param qualifier qualifier
     * @return a String representation of the two- or three-part form of a metadata element
     * e.g. dc.identifier.uri
     */
    public static String getCompoundForm(String schema, String element, String qualifier) {
        StringBuilder sb = new StringBuilder();
        sb.append(schema).append(".").append(element);

        if (qualifier != null) {
            sb.append(".").append(qualifier);
        }
        return sb.toString();
    }

    /**
     * Parses metadata field given in the form {@code <schema>.<element>[.<qualifier>|.*]}
     * checks for correct number of elements (2 or 3) and for empty strings
     *
     * @param compoundForm compound form of metadata field
     * @return String Array
     * @throws ParseException if validity checks fail
     */
    public static String[] parseCompoundForm(String compoundForm)
        throws ParseException {
        String[] ar = compoundForm.split("\\s*\\.\\s*");  //trim ends

        if ("".equals(ar[0])) {
            throw new ParseException("schema is empty string: " + compoundForm, 0);
        }

        if ((ar.length < 2) || (ar.length > 3) || "".equals(ar[1])) {
            throw new ParseException("element is malformed or empty string: " + compoundForm, 0);
        }

        return ar;
    }

}
