/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.axiom.om.util;

import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMDocument;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMException;
import org.apache.axiom.om.OMNamedInformationItem;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.OMProcessingInstruction;
import org.apache.axiom.om.OMText;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Helper class to provide the functionality of the digest value generation. This is an
 * implementation of the DOMHASH algorithm on OM.
 */
public class DigestGenerator {

    /**
     * This method is an overloaded method for the digest generation for OMDocument
     *
     * @param document
     * @param digestAlgorithm
     * @return Returns a byte array representing the calculated digest
     */
    public byte[] getDigest(OMDocument document, String digestAlgorithm) throws OMException {
        byte[] digest = new byte[0];
        try {
            MessageDigest md = MessageDigest.getInstance(digestAlgorithm);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            DataOutputStream dos = new DataOutputStream(baos);
            dos.writeInt(9);
            Collection<OMNode> childNodes = getValidElements(document);
            dos.writeInt(childNodes.size());
            Iterator<OMNode> itr = childNodes.iterator();
            while (itr.hasNext()) {
                OMNode node = itr.next();
                if (node.getType() == OMNode.PI_NODE)
                    dos.write(getDigest((OMProcessingInstruction) node, digestAlgorithm));
                else if (
                        node.getType() == OMNode.ELEMENT_NODE)
                    dos.write(getDigest((OMElement) node, digestAlgorithm));
            }
            dos.close();
            md.update(baos.toByteArray());
            digest = md.digest();
        } catch (NoSuchAlgorithmException e) {
            throw new OMException(e);
        } catch (IOException e) {
            throw new OMException(e);
        }
        return digest;
    }

    /**
     * This method is an overloaded method for the digest generation for OMNode
     *
     * @param node
     * @param digestAlgorithm
     * @return Returns a byte array representing the calculated digest value
     */
    public byte[] getDigest(OMNode node, String digestAlgorithm) {
        if (node.getType() == OMNode.ELEMENT_NODE)
            return getDigest((OMElement) node, digestAlgorithm);
        else if (
                node.getType() == OMNode.TEXT_NODE)
            return getDigest((OMText) node, digestAlgorithm);
        else if (node.getType() == OMNode.PI_NODE)
            return getDigest((OMProcessingInstruction) node, digestAlgorithm);
        else return new byte[0];
    }

    /**
     * This method is an overloaded method for the digest generation for OMElement
     *
     * @param element
     * @param digestAlgorithm
     * @return Returns a byte array representing the calculated digest value
     */
    public byte[] getDigest(OMElement element, String digestAlgorithm) throws OMException {
        byte[] digest = new byte[0];
        try {
            MessageDigest md = MessageDigest.getInstance(digestAlgorithm);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            DataOutputStream dos = new DataOutputStream(baos);
            dos.writeInt(1);
            dos.write(getExpandedName(element).getBytes("UnicodeBigUnmarked"));
            dos.write((byte) 0);
            dos.write((byte) 0);
            Collection<OMAttribute> attrs = getAttributesWithoutNS(element);
            dos.writeInt(attrs.size());
            for (Iterator<OMAttribute> itr = attrs.iterator(); itr.hasNext(); ) {
                dos.write(getDigest(itr.next(), digestAlgorithm));
            }
            OMNode node = element.getFirstOMChild();
            // adjoining Texts are merged,
            // there is  no 0-length Text, and
            // comment nodes are removed.
            int length = 0;
            for (Iterator<OMNode> itr = element.getChildren(); itr.hasNext(); ) {
                OMNode child = itr.next();
                if (child instanceof OMElement || child instanceof OMText || child instanceof OMProcessingInstruction) {
                    length++;
                }
            }
            dos.writeInt(length);
            while (node != null) {
                dos.write(getDigest(node, digestAlgorithm));
                node = node.getNextOMSibling();
            }
            dos.close();
            md.update(baos.toByteArray());
            digest = md.digest();
        } catch (NoSuchAlgorithmException e) {
            throw new OMException(e);
        } catch (IOException e) {
            throw new OMException(e);
        }
        return digest;
    }

    /**
     * This method is an overloaded method for the digest generation for OMProcessingInstruction
     *
     * @param pi
     * @param digestAlgorithm
     * @return Returns a byte array representing the calculated digest value
     */
    public byte[] getDigest(OMProcessingInstruction pi, String digestAlgorithm) throws OMException {
        byte[] digest = new byte[0];
        try {
            MessageDigest md = MessageDigest.getInstance(digestAlgorithm);
            md.update((byte) 0);
            md.update((byte) 0);
            md.update((byte) 0);
            md.update((byte) 7);
            md.update(pi.getTarget().getBytes("UnicodeBigUnmarked"));
            md.update((byte) 0);
            md.update((byte) 0);
            md.update(pi.getValue().getBytes("UnicodeBigUnmarked"));
            digest = md.digest();
        } catch (NoSuchAlgorithmException e) {
            throw new OMException(e);
        } catch (UnsupportedEncodingException e) {
            throw new OMException(e);
        }
        return digest;
    }

    /**
     * This method is an overloaded method for the digest generation for OMAttribute
     *
     * @param attribute
     * @param digestAlgorithm
     * @return Returns a byte array representing the calculated digest value
     */
    public byte[] getDigest(OMAttribute attribute, String digestAlgorithm) throws OMException {
        byte[] digest = new byte[0];
        if (!(attribute.getLocalName().equals("xmlns") ||
                attribute.getLocalName().startsWith("xmlns:"))) try {
            MessageDigest md = MessageDigest.getInstance(digestAlgorithm);
            md.update((byte) 0);
            md.update((byte) 0);
            md.update((byte) 0);
            md.update((byte) 2);
            md.update(getExpandedName(attribute).getBytes("UnicodeBigUnmarked"));
            md.update((byte) 0);
            md.update((byte) 0);
            md.update(attribute.getAttributeValue().getBytes("UnicodeBigUnmarked"));
            digest = md.digest();
        } catch (NoSuchAlgorithmException e) {
            throw new OMException(e);
        } catch (UnsupportedEncodingException e) {
            throw new OMException(e);
        }
        return digest;
    }

    /**
     * This method is an overloaded method for the digest generation for OMText
     *
     * @param text
     * @param digestAlgorithm
     * @return Returns a byte array representing the calculated digest value
     */
    public byte[] getDigest(OMText text, String digestAlgorithm) throws OMException {
        byte[] digest = new byte[0];
        try {
            MessageDigest md = MessageDigest.getInstance(digestAlgorithm);
            md.update((byte) 0);
            md.update((byte) 0);
            md.update((byte) 0);
            md.update((byte) 3);
            md.update(text.getText().getBytes("UnicodeBigUnmarked"));
            digest = md.digest();
        } catch (NoSuchAlgorithmException e) {
            throw new OMException(e);
        } catch (UnsupportedEncodingException e) {
            throw new OMException(e);
        }
        return digest;
    }

    /**
     * This method is an overloaded method for getting the expanded name namespaceURI followed by
     * the local name for OMElement
     *
     * @param element
     * @return Returns the expanded name of OMElement
     */
    public String getExpandedName(OMElement element) {
        return internalGetExpandedName(element);
    }

    /**
     * This method is an overloaded method for getting the expanded name namespaceURI followed by
     * the local name for OMAttribute
     *
     * @param attribute
     * @return Returns the expanded name of the OMAttribute
     */
    public String getExpandedName(OMAttribute attribute) {
        return internalGetExpandedName(attribute);
    }
    
    private String internalGetExpandedName(OMNamedInformationItem informationItem) {
        String uri = informationItem.getNamespaceURI();
        return uri == null ? informationItem.getLocalName() : uri + ":" + informationItem.getLocalName();
    }

    /**
     * Gets the collection of attributes which are none namespace declarations for an OMElement
     *
     * @param element
     * @return Returns the collection of attributes which are none namespace declarations
     */
    public Collection<OMAttribute> getAttributesWithoutNS(OMElement element) {
        SortedMap<String,OMAttribute> map = new TreeMap<String,OMAttribute>();
        Iterator<OMAttribute> itr = element.getAllAttributes();
        while (itr.hasNext()) {
            OMAttribute attribute = itr.next();
            if (!(attribute.getLocalName().equals("xmlns") ||
                    attribute.getLocalName().startsWith("xmlns:")))
                map.put(getExpandedName(attribute), attribute);
        }
        return map.values();
    }

    /**
     * Gets the valid element collection of an OMDocument. OMElement and OMProcessingInstruction
     * only
     *
     * @param document
     * @return Returns a collection of OMProcessingInstructions and OMElements
     */
    public Collection<OMNode> getValidElements(OMDocument document) {
        ArrayList<OMNode> list = new ArrayList<OMNode>();
        Iterator<OMNode> itr = document.getChildren();
        while (itr.hasNext()) {
            OMNode node = itr.next();
            if (node.getType() == OMNode.ELEMENT_NODE || node.getType() == OMNode.PI_NODE)
                list.add(node);
        }
        return list;
    }

    /**
     * Gets the String representation of the byte array
     *
     * @param array
     * @return Returns the String of the byte
     */
    public String getStringRepresentation(byte[] array) {
        String str = "";
        for (int i = 0; i < array.length; i++) str += array[i];
        return str;
    }

    /**
     * Compares two OMNodes for the XML equality
     *
     * @param node
     * @param comparingNode
     * @param digestAlgorithm
     * @return Returns true if the OMNode XML contents are equal
     */
    public boolean compareOMNode(OMNode node, OMNode comparingNode, String digestAlgorithm) {
        return Arrays.equals(getDigest(node, digestAlgorithm),
                             getDigest(comparingNode, digestAlgorithm));
    }

    /**
     * Compares two OMDocuments for the XML equality
     *
     * @param document
     * @param comparingDocument
     * @param digestAlgorithm
     * @return Returns true if the OMDocument XML content are equal
     */
    public boolean compareOMDocument(OMDocument document, OMDocument comparingDocument,
                                     String digestAlgorithm) {
        return Arrays.equals(getDigest(document, digestAlgorithm),
                             getDigest(comparingDocument, digestAlgorithm));
    }

    /**
     * Compares two OMAttributes for the XML equality
     *
     * @param attribute
     * @param comparingAttribute
     * @param digestAlgorithm
     * @return Returns true if the OMDocument XML content are equal
     */
    public boolean compareOMAttribute(OMAttribute attribute, OMAttribute comparingAttribute,
                                      String digestAlgorithm) {
        return Arrays.equals(getDigest(attribute, digestAlgorithm),
                             getDigest(comparingAttribute, digestAlgorithm));
    }

    /** String representing the MD5 digest algorithm */
    public static final String md5DigestAlgorithm = "MD5";

    /** String representing the SHA digest algorithm */
    public static final String shaDigestAlgorithm = "SHA";

    /** String representing the SHA1 digest algorithm */
    public static final String sha1DigestAlgorithm = "SHA1";
}
