/*
 *  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.ivy.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public final class ChecksumHelper {

    private static final int BUFFER_SIZE = 2048;
    private static Map algorithms = new HashMap();
    static {
        algorithms.put("md5", "MD5");
        algorithms.put("sha1", "SHA-1");
    }

    /**
     * Checks the checksum of the given file against the given checksumFile, and throws an
     * IOException if the checksum is not compliant
     * 
     * @param dest
     *            the file to test
     * @param checksumFile
     *            the file containing the expected checksum
     * @param algorithm
     *            the checksum algorithm to use
     * @throws IOException
     *             if an IO problem occur whle reading files or if the checksum is not compliant
     */
    public static void check(File dest, File checksumFile, String algorithm) throws IOException {
        String csFileContent = FileUtil.readEntirely(
            new BufferedReader(new FileReader(checksumFile))).trim().toLowerCase(Locale.US);
        String expected;
        if (csFileContent.indexOf(' ') > -1
                && (csFileContent.startsWith("md") || csFileContent.startsWith("sha"))) {
            int lastSpaceIndex = csFileContent.lastIndexOf(' ');
            expected = csFileContent.substring(lastSpaceIndex + 1);
        } else {
            int spaceIndex = csFileContent.indexOf(' ');
            if (spaceIndex != -1) {
                expected = csFileContent.substring(0, spaceIndex);
                
                // IVY-1155: support some strange formats like this one:
                // http://repo2.maven.org/maven2/org/apache/pdfbox/fontbox/0.8.0-incubator/fontbox-0.8.0-incubator.jar.md5
                if (expected.endsWith(":")) {
                    StringBuffer result = new StringBuffer();
                    char[] chars = csFileContent.substring(spaceIndex + 1).toCharArray();
                    for (int i = 0; i < chars.length; i++) {
                        if (!Character.isWhitespace(chars[i])) {
                            result.append(chars[i]);
                        }
                    }
                    expected = result.toString();
                }
            } else {
                expected = csFileContent;
            }
        }
        
        String computed = computeAsString(dest, algorithm).trim().toLowerCase(Locale.US);
        if (!expected.equals(computed)) {
            throw new IOException("invalid " + algorithm + ": expected=" + expected + " computed="
                    + computed);
        }
    }

    public static String computeAsString(File f, String algorithm) throws IOException {
        return byteArrayToHexString(compute(f, algorithm));
    }

    private static byte[] compute(File f, String algorithm) throws IOException {
        InputStream is = new FileInputStream(f);

        try {
            MessageDigest md = getMessageDigest(algorithm);
            md.reset();

            byte[] buf = new byte[BUFFER_SIZE];
            int len = 0;
            while ((len = is.read(buf)) != -1) {
                md.update(buf, 0, len);
            }
            return md.digest();
        } finally {
            is.close();
        }
    }

    public static boolean isKnownAlgorithm(String algorithm) {
        return algorithms.containsKey(algorithm);
    }
    
    private static MessageDigest getMessageDigest(String algorithm) {
        String mdAlgorithm = (String) algorithms.get(algorithm);
        if (mdAlgorithm == null) {
            throw new IllegalArgumentException("unknown algorithm " + algorithm);
        }
        try {
            return MessageDigest.getInstance(mdAlgorithm);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("unknown algorithm " + algorithm);
        }
    }

    // byte to hex string converter
    private static final char[] CHARS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 
            'a', 'b', 'c', 'd', 'e', 'f'};

    /**
     * Convert a byte[] array to readable string format. This makes the "hex" readable!
     * 
     * @return result String buffer in String format
     * @param in
     *            byte[] buffer to convert to string format
     */
    public static String byteArrayToHexString(byte[] in) {
        byte ch = 0x00;

        if (in == null || in.length <= 0) {
            return null;
        }

        StringBuffer out = new StringBuffer(in.length * 2);

        //CheckStyle:MagicNumber OFF
        for (int i = 0; i < in.length; i++) {
            ch = (byte) (in[i] & 0xF0); // Strip off high nibble
            ch = (byte) (ch >>> 4); // shift the bits down
            ch = (byte) (ch & 0x0F); // must do this is high order bit is on!

            out.append(CHARS[(int) ch]); // convert the nibble to a String Character
            ch = (byte) (in[i] & 0x0F); // Strip off low nibble
            out.append(CHARS[(int) ch]); // convert the nibble to a String Character
        }
        //CheckStyle:MagicNumber ON

        return out.toString();
    }

    private ChecksumHelper() {
    }
}
