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

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Utility functions for hex encoding.
 *
 * @author John Hewson
 */
public final class Hex
{
    private static final Log LOG = LogFactory.getLog(Hex.class);

    /**
     * for hex conversion.
     * 
     * https://stackoverflow.com/questions/2817752/java-code-to-convert-byte-to-hexadecimal
     *
     */
    private static final byte[] HEX_BYTES = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
    private static final char[] HEX_CHARS = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};

    private Hex() {}

    /**
     * Returns a hex string of the given byte.
     */
    public static String getString(byte b)
    {
        char[] chars = new char[]{HEX_CHARS[getHighNibble(b)], HEX_CHARS[getLowNibble(b)]};
        return new String(chars);
    }

    /**
     * Returns a hex string of the given byte array.
     */
    public static String getString(byte[] bytes)
    {
        StringBuilder string = new StringBuilder(bytes.length * 2);
        for (byte b : bytes)
        {
            string.append(HEX_CHARS[getHighNibble(b)]).append(HEX_CHARS[getLowNibble(b)]);
        }
        return string.toString();
    }

    /**
     * Returns the bytes corresponding to the ASCII hex encoding of the given byte.
     */
    public static byte[] getBytes(byte b)
    {
        return new byte[]{HEX_BYTES[getHighNibble(b)], HEX_BYTES[getLowNibble(b)]};
    }
    
    /**
     * Returns the bytes corresponding to the ASCII hex encoding of the given bytes.
     */
    public static byte[] getBytes(byte[] bytes)
    {
        byte[] asciiBytes = new byte[bytes.length*2];
        for(int i=0; i< bytes.length; i++)
        {
            asciiBytes[i*2] = HEX_BYTES[getHighNibble(bytes[i])];
            asciiBytes[i*2+1] = HEX_BYTES[getLowNibble(bytes[i])];
        }
        return asciiBytes;
    }

    /** 
     * Returns the characters corresponding to the ASCII hex encoding of the given short.
     */
    public static char[] getChars(short num)
    {
        char[] hex = new char[4];
        hex[0] = HEX_CHARS[(num >> 12) & 0x0F];
        hex[1] = HEX_CHARS[(num >> 8) & 0x0F];
        hex[2] = HEX_CHARS[(num >> 4) & 0x0F];
        hex[3] = HEX_CHARS[num & 0x0F];
        return hex;
    }

    /**
     * Takes the characters in the given string, convert it to bytes in UTF16-BE format
     * and build a char array that corresponds to the ASCII hex encoding of the resulting
     * bytes.
     *
     * Example:
     * <pre>
     *   getCharsUTF16BE("ab") == new char[]{'0','0','6','1','0','0','6','2'}
     * </pre>
     *
     * @param text The string to convert
     * @return The string converted to hex
     */
    public static char[] getCharsUTF16BE(String text)
    {
        // Note that the internal representation of string in Java is already UTF-16. Therefore
        // we do not need to use an encoder to convert the string to its byte representation.
        char[] hex = new char[text.length()*4];

        for (int stringIdx = 0, charIdx = 0; stringIdx < text.length(); stringIdx++)
        {
            char c = text.charAt(stringIdx);
            hex[charIdx++] = HEX_CHARS[(c >> 12) & 0x0F];
            hex[charIdx++] = HEX_CHARS[(c >> 8) & 0x0F];
            hex[charIdx++] = HEX_CHARS[(c >> 4) & 0x0F];
            hex[charIdx++] = HEX_CHARS[c & 0x0F];
        }

        return hex;
    }

    /**
     * Writes the given byte as hex value to the given output stream.
     * @param b the byte to be written
     * @param output the output stream to be written to
     * @throws IOException exception if anything went wrong
     */
    public static void writeHexByte(byte b, OutputStream output) throws IOException
    {
        output.write(HEX_BYTES[getHighNibble(b)]);
        output.write(HEX_BYTES[getLowNibble(b)]);
    }

    /** 
     * Writes the given byte array as hex value to the given output stream.
     * @param bytes the byte array to be written
     * @param output the output stream to be written to
     * @throws IOException exception if anything went wrong
     */
    public static void writeHexBytes(byte[] bytes, OutputStream output) throws IOException
    {
        for (byte b : bytes)
        {
            writeHexByte(b, output);
        }
    }
    
    /**
     * Get the high nibble of the given byte.
     * 
     * @param b the given byte
     * @return the high nibble
     */
    private static int getHighNibble(byte b)
    {
        return (b & 0xF0) >> 4;
    }

    /**
     * Get the low nibble of the given byte.
     * 
     * @param b the given byte
     * @return the low nibble
     */
    private static int getLowNibble(byte b)
    {
        return b & 0x0F;
    }

    /**
     * Decode a base64 String.
     *
     * @param base64Value a base64 encoded String.
     *
     * @return the decoded String as a byte array.
     *
     * @throws IllegalArgumentException if this isn't a base64 encoded string.
     */
    public static byte[] decodeBase64(String base64Value)
    {
        // https://stackoverflow.com/questions/469695/decode-base64-data-in-java
        try
        {
            // jdk8 and higher? java.util.Base64.getDecoder().decode()
            Class<?> b64Class = Class.forName("java.util.Base64");
            Method getDecoderMethod = b64Class.getMethod("getDecoder");
            Object base64Decoder = getDecoderMethod.invoke(b64Class);
            Method decodeMethod = base64Decoder.getClass().getMethod("decode", String.class);
            return (byte[]) decodeMethod.invoke(base64Decoder, base64Value.replaceAll("\\s", ""));
        }
        catch (ClassNotFoundException ex)
        {
            LOG.debug(ex);
        }
        catch (IllegalAccessException ex)
        {
            LOG.debug(ex);
        }
        catch (IllegalArgumentException ex)
        {
            LOG.debug(ex);
        }
        catch (NoSuchMethodException ex)
        {
            LOG.debug(ex);
        }
        catch (SecurityException ex)
        {
            LOG.debug(ex);
        }
        catch (InvocationTargetException ex)
        {
            LOG.debug(ex);
        }
        try
        {
            // up to java7? javax.xml.bind.DatatypeConverter.parseBase64Binary()
            Class<?> datatypeConverterClass = Class.forName("javax.xml.bind.DatatypeConverter");
            Method parseBase64BinaryMethod = datatypeConverterClass.getMethod("parseBase64Binary", String.class);
            return (byte[]) parseBase64BinaryMethod.invoke(null, base64Value);
        }
        catch (ClassNotFoundException ex)
        {
            LOG.debug(ex);
        }
        catch (IllegalAccessException ex)
        {
            LOG.debug(ex);
        }
        catch (IllegalArgumentException ex)
        {
            LOG.debug(ex);
        }
        catch (NoSuchMethodException ex)
        {
            LOG.debug(ex);
        }
        catch (SecurityException ex)
        {
            LOG.debug(ex);
        }
        catch (InvocationTargetException ex)
        {
            LOG.debug(ex);
        }
        LOG.error("Can't decode base64 value, try adding javax.xml.bind:jaxb-api to your build");
        return new byte[0];
    }

    /**
     * Decodes a hex String into a byte array.
     *
     * @param s A String with ASCII hex.
     * @return decoded byte array.
     * @throws IOException
     */
    public static byte[] decodeHex(String s) throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int i = 0;
        while (i < s.length() - 1)
        {
            if (s.charAt(i) == '\n' || s.charAt(i) == '\r')
            {
                ++i;
            }
            else
            {
                String hexByte = s.substring(i, i + 2);
                try
                {
                    baos.write(Integer.parseInt(hexByte, 16)); // Byte.parseByte won't work with "9C"
                }
                catch (NumberFormatException ex)
                {
                    LOG.error("Can't parse " + hexByte + ", aborting decode", ex);
                    break;
                }
                i += 2;
            }
        }
        return baos.toByteArray();
    }
}
