// ============================================================================
//
// Copyright (C) 2006-2015 Talend Inc. - www.talend.com
//
// This source code is available under agreement available at
// %InstallDIR%\features\org.talend.rcp.branding.%PRODUCTNAME%\%PRODUCTNAME%license.txt
//
// You should have received a copy of the agreement
// along with this program; if not, write to Talend SA
// 9 rue Pages 92150 Suresnes, France
//
// ============================================================================
package routines;

import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
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.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

/*
 * user specification: the function's comment should contain keys as follows: 1. write about the function's comment.but
 * it must be before the "{talendTypes}" key.
 * 
 * 2. {talendTypes} 's value must be talend Type, it is required . its value should be one of: String, char | Character,
 * long | Long, int | Integer, boolean | Boolean, byte | Byte, Date, double | Double, float | Float, Object, short |
 * Short
 * 
 * 3. {Category} define a category for the Function. it is required. its value is user-defined .
 * 
 * 4. {param} 's format is: {param} <type>[(<default value or closed list values>)] <name>[ : <comment>]
 * 
 * <type> 's value should be one of: string, int, list, double, object, boolean, long, char, date. <name>'s value is the
 * Function's parameter name. the {param} is optional. so if you the Function without the parameters. the {param} don't
 * added. you can have many parameters for the Function.
 * 
 * 5. {example} gives a example for the Function. it is optional.
 */
public class MDM {

    /**
     * getFK: Return one of the FK component by position in a mangled FK (FKs are mangled in MDM to accommodate for
     * compound keys)
     * 
     * 
     * {talendTypes} String
     * 
     * {Category} MDM
     * 
     * {param} string(FKs) mangledFK: original mangled FK.
     * 
     * {param} int(0) pos: key position (starts at 0)
     * 
     * {example} getFK(FKs,0) # 12345
     */
    public static String getFK(String mangledFK, int pos) {
        if (mangledFK == null) {
            return null;
        }
        Pattern p = Pattern.compile("(\\[[^\\[\\]]*\\])"); //$NON-NLS-1$
        Matcher m = p.matcher(mangledFK.trim());
        int i = 0;
        while (m.find()) {
            if (i == pos) {
                String targetValue = m.group(0);
                return targetValue.substring(1, targetValue.length() - 1);
            }
            i++;
        }
        return null;
    }

    /**
     * createFK: Returns the mangled FK string of a given key (FKs are mangled in MDM to accommodate for compound keys).
     * Returns null if key is null.
     * 
     * 
     * {talendTypes} String
     * 
     * {Category} MDM
     * 
     * {param} string singleKey: original key.
     * 
     * 
     * {example} createFK("0") # return "[0]"
     */
    public static String createFK(String singleKey) {
        if (singleKey != null) {
            return "[" + singleKey + "]"; //$NON-NLS-1$ //$NON-NLS-2$
        } else {
            return null;
        }
    }

    /**
     * createFK: Returns the mangled FK string of a given array of keys (FKs are mangled in MDM to accommodate for
     * compound keys). Returns null if one of the keys is null.
     * 
     * 
     * {talendTypes} String
     * 
     * {Category} MDM
     * 
     * {param} string singleKey: original key array.
     * 
     * 
     * {example} createFK({"0","1"}) # return "[0][1]"
     */
    public static String createFK(String[] keys) {
        StringBuffer sb = new StringBuffer();
        for (String key : keys) {
            if (key == null) {
                return null;
            }
            sb.append("[").append(key).append("]"); //$NON-NLS-1$ //$NON-NLS-2$
        }
        return sb.toString();
    }

    /**
     * get repeating element in xmlString according to the xpath & position
     * 
     * {talendTypes} String
     * 
     * {Category} MDM
     * 
     * {param} string(xml) xml: xml
     * 
     * {param} string(xpath) xpath: xpath.
     * 
     * {param} int(0) position: position.
     */
    public static String getRepeatingElement(String xml, String xpath, int position) throws Exception {

        Node node = parse(xml);
        NodeList list = getNodeList(node, xpath, false);
        for (int i = 0; i < list.getLength(); i++) {
            if (i == position) {
                Node n = list.item(i);
                return n.getNodeValue();
            }
        }
        return null;
    }

    /**
     * check repeating elements in xmlString according to xpath & text
     * 
     * {talendTypes} Boolean
     * 
     * {Category} MDM
     * 
     * {param} string(xml) xml: xml.
     * 
     * {param} string(xpath) xpath: xpath.
     * 
     * {param} String(text) text: text.
     */
    public static boolean hasRepeatingElement(String xml, String xpath, String text) throws Exception {
        Node node = parse(xml);
        NodeList list = getNodeList(node, xpath, false);
        for (int i = 0; i < list.getLength(); i++) {
            Node n = list.item(i);
            if (n.getNodeValue().equals(text)) {
                return true;
            }
        }
        return false;
    }

    /**
     * list repeating elements in xmlString according to xpath & delimiter
     * 
     * {talendTypes} String
     * 
     * {Category} MDM
     * 
     * {param} string(xml) xml: xml.
     * 
     * {param} string(xpath) xpath: xpath.
     * 
     * {param} char(delimiter) delimiter: delimiter.
     */
    public static String listRepeatingElement(String xml, String xpath, char delimiter) throws Exception {
        Node node = parse(xml);

        NodeList list = getNodeList(node, xpath, false);
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < list.getLength(); i++) {
            Node n = list.item(i);
            sb.append(n.getNodeValue());
            if (i >= 0 && i < list.getLength() - 1) {
                sb.append(delimiter);
            }
        }
        return sb.toString();
    }

    /**
     * add repeating elements in xmlString according to xpath & text
     * 
     * {talendTypes} String
     * 
     * {Category} MDM
     * 
     * {param} string(xml) xml: xml
     * 
     * {param} string(xpath) xpath: xpath
     * 
     * {param} String(text) text: text
     */
    public static String addRepeatingElement(String xml, String xpath, String text) throws Exception {
        Node node = parse(xml);

        int pos = xpath.lastIndexOf('/');
        String name = xpath.substring(pos + 1);
        String parentPath = xpath.substring(0, pos);
        NodeList plist = getNodeList(node, parentPath, true);
        if (plist.getLength() > 0) {
            Element el = node.getOwnerDocument().createElement(name);
            el.setTextContent(text);
            plist.item(0).appendChild(el);
        }

        return nodeToString(node, true);
    }

    /**
     * @deprecated Generate an <error code="X">msg</error> fragment
     * 
     * 
     * {talendTypes} String
     * 
     * {Category} MDM
     * 
     * {param} string msg: error message.
     * 
     * {param} int(0) code: error code, (1:ERROR, 0:NORMAL)
     * 
     * {example} genErrMsg("test message",0) # return <error code="0">test message</error>
     */
    @Deprecated
    public static String createReturnMessage(String msg, int code) {
        return "<error code=\"" + code + "\">" + msg + "</error>"; //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
    }

    /**
     * Generate an <report><message type="X">msg</message><report> fragment
     * 
     * 
     * {talendTypes} String
     * 
     * {Category} MDM
     * 
     * {param} string msg: error message.
     * 
     * {param} String(0) type: error code, (info|error)
     * 
     * {example} genErrMsg("test message",0) # return <error code="0">test message</error>
     */
    public static String createReturnMessage(String msg, String type) {
        return "<report><message type=\"" + type + "\">" + msg + "</message></report>"; //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
    }

    /**
     * Add or update an ISO variant to the multi-lingual text value
     * 
     * 
     * {talendTypes} String
     * 
     * {Category} MDM
     * 
     * {param} string(iso) iso: iso
     * 
     * {param} string(value) value: value
     * 
     * {param} string(rawValue) rawValue: rawValue
     * 
     * {example} setLanguageVariant("EN","abc","[EN:ab][FR:ab_fr]") # return [EN:abc][FR:ab_fr]
     */
    public static String setLanguageVariant(String iso, String value, String rawValue) {

        return setLanguageVariant(iso, value, rawValue, "EN", true); //$NON-NLS-1$

    }

    /**
     * Add or update an ISO variant to the multi-lingual text value with defaultIso and sort option (For the legacy
     * value which do not follow the multi-lingual field syntax, it will be adapted to defaultIso)
     * 
     * {talendTypes} String
     * 
     * {Category} MDM
     * 
     * {param} string(iso) iso: iso
     * 
     * {param} string(value) value: value
     * 
     * {param} string(rawValue) rawValue: rawValue
     * 
     * {param} string(defaultIso) defaultIso: defaultIso
     * 
     * {param} string(sort) sort: sort
     * 
     * {example} setLanguageVariant("FR","ab_fr","ab","EN", true) # return [EN:ab][FR:ab_fr]
     */
    public static String setLanguageVariant(String iso, String value, String rawValue, String defaultIso, boolean sort) {

        if (iso == null || value == null) {
            throw new IllegalArgumentException();
        }

        iso = iso.toUpperCase();

        Map<String, String> isoValues = new LinkedHashMap<String, String>();

        if (rawValue == null || rawValue.trim().length() == 0) {
            isoValues.put(iso, value);
        } else {

            Pattern p = Pattern.compile("\\[(\\w+)\\:([^\\[\\]]*?)\\]{1,}"); //$NON-NLS-1$
            Matcher m = p.matcher(rawValue);
            while (m.find()) {
                isoValues.put(m.group(1).toUpperCase(), m.group(2));
            }

            // illegal/legacy raw value
            if (isoValues.size() == 0) {
                // throw new IllegalArgumentException();
                if (defaultIso != null && defaultIso.trim().length() > 0) {
                    isoValues.put(defaultIso.toUpperCase(), rawValue);
                } else {
                    isoValues.put("EN", rawValue); //$NON-NLS-1$
                }
            }

            isoValues.put(iso, value);
        }

        StringBuilder result = new StringBuilder();
        if (isoValues.size() > 0) {

            List<String> isoList = new ArrayList<String>(isoValues.keySet());
            // sort
            if (sort) {
                Collections.sort(isoList);
            }

            for (String string : isoList) {
                String isoKey = string;
                String isoValue = isoValues.get(isoKey);
                result.append("[").append(isoKey).append(":").append(isoValue).append("]"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            }
        }

        return result.toString();

    }

    /**
     * Give an ISO value from a multi-lingual text value, with default iso fallback
     * 
     * {talendTypes} String
     * 
     * {Category} MDM
     * 
     * {param} string(iso) iso: iso
     * 
     * {param} string(defaultIso) defaultIso: Default iso used in case of fallback
     * 
     * {param} string(rawValue) rawValue: rawValue
     * 
     * {example} getLanguageVariant("DE","EN","[EN:ab][FR:ab_fr]") # return [EN:ab]
     */
    public static String getLanguageVariant(String iso, String defaultIso, String rawValue) {
        String requestedLanguageVariant = getLanguageVariant(iso, rawValue);
        if (requestedLanguageVariant == null || "".equals(requestedLanguageVariant)) { //$NON-NLS-1$
            // fallback to the default variant
            return getLanguageVariant(defaultIso, rawValue);
        } else {
            return requestedLanguageVariant;
        }
    }

    /**
     * Give an ISO value from a multi-lingual text value
     * 
     * 
     * {talendTypes} String
     * 
     * {Category} MDM
     * 
     * {param} string(iso) iso: iso
     * 
     * {param} string(rawValue) rawValue: rawValue
     * 
     * {example} getLanguageVariant("FR","[EN:ab][FR:ab_fr]") # return ab_fr
     */
    public static String getLanguageVariant(String iso, String rawValue) {

        if (iso == null || rawValue == null) {
            throw new IllegalArgumentException();
        }

        iso = iso.toUpperCase();

        Map<String, String> isoValues = new HashMap<String, String>();
        Pattern p = Pattern.compile("\\[(\\w+)\\:([^\\[\\]]*?)\\]{1,}"); //$NON-NLS-1$
        Matcher m = p.matcher(rawValue);
        while (m.find()) {
            isoValues.put(m.group(1).toUpperCase(), m.group(2));
        }

        return isoValues.get(iso);
    }

    // Utility methods
    /**
     * Get a nodelist from an xPath
     * 
     * @throws Exception
     */
    private static NodeList getNodeList(Node contextNode, String xPath, boolean isParent) throws Exception {
        if (!xPath.matches(".*@[^/\\]]+")) { //$NON-NLS-1$
            if (!xPath.endsWith(")") && !isParent) { //$NON-NLS-1$
                xPath += "/text()"; //$NON-NLS-1$
            }
        }
        XPathFactory factory = XPathFactory.newInstance();
        XPath xpath = factory.newXPath();
        XPathExpression expr = xpath.compile(xPath);

        Object result = expr.evaluate(contextNode, XPathConstants.NODESET);
        NodeList nodes = (NodeList) result;
        return nodes;
    }

    /**
     * parse the xml
     * 
     * @param xml
     * @return
     * @throws Exception
     */
    private static Node parse(String xml) throws Exception {
        DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
        domFactory.setNamespaceAware(true); // never forget this!
        DocumentBuilder builder = domFactory.newDocumentBuilder();
        Document doc = builder.parse(new InputSource(new StringReader(xml)));
        return doc.getDocumentElement();
    }

    /**
     * Generates an xml string from a node with or without the xml declaration (not pretty formatted)
     * 
     * @param n the node
     * @return the xml string
     * @throws TransformerException
     */
    private static String nodeToString(Node n, boolean omitXMLDeclaration) throws TransformerException {
        StringWriter sw = new StringWriter();
        Transformer transformer = TransformerFactory.newInstance().newTransformer();
        if (omitXMLDeclaration) {
            transformer.setOutputProperty("omit-xml-declaration", "yes"); //$NON-NLS-1$ //$NON-NLS-2$
        } else {
            transformer.setOutputProperty("omit-xml-declaration", "no"); //$NON-NLS-1$ //$NON-NLS-2$
        }
        transformer.setOutputProperty("indent", "yes"); //$NON-NLS-1$ //$NON-NLS-2$
        transformer.transform(new DOMSource(n), new StreamResult(sw));
        return sw.toString().replaceAll("\r\n", "\n"); //$NON-NLS-1$ //$NON-NLS-2$
    }
}
