/*
 * FindBugs - Find Bugs in Java programs
 * Copyright (C) 2005, University of Maryland
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs.ba;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;

import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;

import edu.umd.cs.findbugs.util.Util;
import edu.umd.cs.findbugs.xml.XMLOutput;
import edu.umd.cs.findbugs.xml.XMLWriteable;

/**
 * Compute a hash of method names and signatures. This allows us to find out
 * when a class has been renamed, but not changed in any other obvious way.
 *
 * @author David Hovemeyer
 */
public class ClassHash implements XMLWriteable, Comparable<ClassHash> {
    /**
     * XML element name for a ClassHash.
     */
    public static final String CLASS_HASH_ELEMENT_NAME = "ClassHash";

    /**
     * XML element name for a MethodHash.
     */
    public static final String METHOD_HASH_ELEMENT_NAME = "MethodHash";

    // Fields
    private String className;

    private byte[] classHash;

    private final Map<XMethod, MethodHash> methodHashMap;

    /**
     * Constructor.
     */
    public ClassHash() {
        this.methodHashMap = new HashMap<XMethod, MethodHash>();
    }

    /**
     * Constructor.
     *
     * @param classHash
     *            pre-computed class hash
     */
    public ClassHash(String className, byte[] classHash) {
        this();
        this.className = className;
        this.classHash = new byte[classHash.length];
        System.arraycopy(classHash, 0, this.classHash, 0, classHash.length);
    }

    /**
     * Set method hash for given method.
     *
     * @param method
     *            the method
     * @param methodHash
     *            the method hash
     */
    public void setMethodHash(XMethod method, byte[] methodHash) {
        methodHashMap.put(method, new MethodHash(method.getName(), method.getSignature(), method.isStatic(), methodHash));
    }

    /**
     * @return Returns the className.
     */
    public String getClassName() {
        return className;
    }

    /**
     * Get class hash.
     *
     * @return the class hash
     */
    public byte[] getClassHash() {
        return classHash;
    }

    /**
     * Set class hash.
     *
     * @param classHash
     *            the class hash value to set
     */
    public void setClassHash(byte[] classHash) {
        this.classHash = new byte[classHash.length];
        System.arraycopy(classHash, 0, this.classHash, 0, classHash.length);
    }

    /**
     * Get method hash for given method.
     *
     * @param method
     *            the method
     * @return the MethodHash
     */
    public MethodHash getMethodHash(XMethod method) {
        return methodHashMap.get(method);
    }

    /**
     * Compute hash for given class and all of its methods.
     *
     * @param javaClass
     *            the class
     * @return this object
     */
    public ClassHash computeHash(JavaClass javaClass) {
        this.className = javaClass.getClassName();

        Method[] methodList = new Method[javaClass.getMethods().length];

        // Sort methods
        System.arraycopy(javaClass.getMethods(), 0, methodList, 0, javaClass.getMethods().length);
        Arrays.sort(methodList, new Comparator<Method>() {
            @Override
            public int compare(Method o1, Method o2) {
                // sort by name, then signature
                int cmp = o1.getName().compareTo(o2.getName());
                if (cmp != 0) {
                    return cmp;
                }
                return o1.getSignature().compareTo(o2.getSignature());

            }
        });

        Field[] fieldList = new Field[javaClass.getFields().length];

        // Sort fields
        System.arraycopy(javaClass.getFields(), 0, fieldList, 0, javaClass.getFields().length);
        Arrays.sort(fieldList, new Comparator<Field>() {
            /*
             * (non-Javadoc)
             *
             * @see java.util.Comparator#compare(T, T)
             */
            @Override
            public int compare(Field o1, Field o2) {
                int cmp = o1.getName().compareTo(o2.getName());
                if (cmp != 0) {
                    return cmp;
                }
                return o1.getSignature().compareTo(o2.getSignature());
            }
        });

        MessageDigest digest = Util.getMD5Digest();

        // Compute digest of method names and signatures, in order.
        // Also, compute method hashes.
        CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder();
        for (Method method : methodList) {
            work(digest, method.getName(), encoder);
            work(digest, method.getSignature(), encoder);

            MethodHash methodHash = new MethodHash().computeHash(method);
            methodHashMap.put(XFactory.createXMethod(javaClass, method), methodHash);
        }

        // Compute digest of field names and signatures.
        for (Field field : fieldList) {
            work(digest, field.getName(), encoder);
            work(digest, field.getSignature(), encoder);
        }

        classHash = digest.digest();

        return this;
    }

    private static void work(MessageDigest digest, String s, CharsetEncoder encoder) {
        try {
            CharBuffer cbuf = CharBuffer.allocate(s.length());
            cbuf.put(s);
            cbuf.flip();

            ByteBuffer buf = encoder.encode(cbuf);
            // System.out.println("pos="+buf.position() +",limit=" +
            // buf.limit());
            int nbytes = buf.limit();
            byte[] encodedBytes = new byte[nbytes];
            buf.get(encodedBytes);

            digest.update(encodedBytes);
        } catch (CharacterCodingException e) {
            // This should never happen, since we're encoding to UTF-8.
        }
    }

    @Override
    public void writeXML(XMLOutput xmlOutput) throws IOException {
        xmlOutput.startTag(CLASS_HASH_ELEMENT_NAME);
        xmlOutput.addAttribute("class", className);
        xmlOutput.addAttribute("value", hashToString(classHash));
        xmlOutput.stopTag(false);

        for (Map.Entry<XMethod, MethodHash> entry : methodHashMap.entrySet()) {
            xmlOutput.startTag(METHOD_HASH_ELEMENT_NAME);
            xmlOutput.addAttribute("name", entry.getKey().getName());
            xmlOutput.addAttribute("signature", entry.getKey().getSignature());
            xmlOutput.addAttribute("isStatic", String.valueOf(entry.getKey().isStatic()));
            xmlOutput.addAttribute("value", hashToString(entry.getValue().getMethodHash()));
            xmlOutput.stopTag(true);
        }

        xmlOutput.closeTag(CLASS_HASH_ELEMENT_NAME);
    }

    private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', };

    /**
     * Convert a hash to a string of hex digits.
     *
     * @param hash
     *            the hash
     * @return a String representation of the hash
     */
    public static String hashToString(byte[] hash) {
        StringBuilder buf = new StringBuilder();
        for (byte b : hash) {
            buf.append(HEX_CHARS[(b >> 4) & 0xF]);
            buf.append(HEX_CHARS[b & 0xF]);
        }
        return buf.toString();
    }

    private static int hexDigitValue(char c) {
        if (c >= '0' && c <= '9') {
            return c - '0';
        } else if (c >= 'a' && c <= 'f') {
            return 10 + (c - 'a');
        } else if (c >= 'A' && c <= 'F') {
            return 10 + (c - 'A');
        } else {
            throw new IllegalArgumentException("Illegal hex character: " + c);
        }
    }

    /**
     * Convert a string of hex digits to a hash.
     *
     * @param s
     *            string of hex digits
     * @return the hash value represented by the string
     */
    public static byte[] stringToHash(String s) {
        if (s.length() % 2 != 0) {
            throw new IllegalArgumentException("Invalid hash string: " + s);
        }
        byte[] hash = new byte[s.length() / 2];
        for (int i = 0; i < s.length(); i += 2) {
            byte b = (byte) ((hexDigitValue(s.charAt(i)) << 4) + hexDigitValue(s.charAt(i + 1)));
            hash[i / 2] = b;
        }
        return hash;
    }

    /**
     * Return whether or not this class hash has the same hash value as the one
     * given.
     *
     * @param other
     *            another ClassHash
     * @return true if the hash values are the same, false if not
     */
    public boolean isSameHash(ClassHash other) {
        return Arrays.equals(classHash, other.classHash);
    }

    @Override
    public int hashCode() {
        if (classHash == null) {
            return 0;
        }

        int result = 1;
        for (byte element : classHash) {
            result = 31 * result + element;
        }

        return result;

    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ClassHash)) {
            return false;
        }
        return isSameHash((ClassHash) o);
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Comparable#compareTo(T)
     */
    @Override
    public int compareTo(ClassHash other) {
        int cmp = MethodHash.compareHashes(this.classHash, other.classHash);
        // System.out.println(this + " <=> " + other + ": compareTo=" + cmp);
        return cmp;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#toString()
     */

    @Override
    public String toString() {
        return getClassName() + ":" + hashToString(this.classHash);
    }
}
