/*
 * $Id$
 *
 * Copyright 2006, The jCoderZ.org Project. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above
 *      copyright notice, this list of conditions and the following
 *      disclaimer in the documentation and/or other materials
 *      provided with the distribution.
 *    * Neither the name of the jCoderZ.org Project nor the names of
 *      its contributors may be used to endorse or promote products
 *      derived from this software without specific prior written
 *      permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.jcoderz.commons.util;

import java.util.Arrays;

/**
 * This class converts byte array data from and to hex and also provides
 * a hexdump method.
 *
 * @author Albrecht Messner
 */
public final class HexUtil
{
   private static final int BYTE_UNSIGNED_MAX = 255;
   private static final int HALF_BYTE = 4;
   private static final byte LOW_HALF_MASK = (byte) 0x0F;
   private static final byte HIGH_HALF_MASK = (byte) 0xF0;
   private static final int BYTE_MASK = 0xFF;
   private static final int CHARS_PER_BYTE = 2;
   private static final int DECIMAL_OFFSET = 10;


   private static final char[] HEX_CHARS = {
      '0', '1', '2', '3', '4', '5', '6', '7',
      '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
   };

   private static final String[] BYTE_AS_HEX
         = new String[BYTE_UNSIGNED_MAX + 1];

   private static final int[] CHAR_TO_NIBBLE_LOW
         = new int[BYTE_UNSIGNED_MAX + 1];
   private static final int[] CHAR_TO_NIBBLE_HIGH
         = new int[BYTE_UNSIGNED_MAX + 1];

   private static final int DUMP_BYTES_PER_LINE = 16;
   private static final int DUMP_BYTES_PER_COLUMN = 8;
   private static final int DUMP_ADDR_LEN = 8;
   private static final int DUMP_BUFFER_SIZE = DUMP_ADDR_LEN // address
         + DUMP_BYTES_PER_LINE * CHARS_PER_BYTE // bytes
         + DUMP_BYTES_PER_LINE // whitespaces between bytes
         + 1 + 1; // extra whitespace after address and to separate columns

   static
   {
      // set up tables/arrays for byte to hex conversion
      final StringBuffer sb = new StringBuffer();
      for (int i = 0; i < BYTE_AS_HEX.length; i++)
      {
         sb.setLength(0);
         final int lowerFourBits = i & LOW_HALF_MASK;
         final int highFourBits = (i & HIGH_HALF_MASK) >> HALF_BYTE;
         sb.append(HEX_CHARS[highFourBits]);
         sb.append(HEX_CHARS[lowerFourBits]);
         BYTE_AS_HEX[i] = sb.toString();
      }

      // set up tables/arrays for hex to byte conversion
      Arrays.fill(CHAR_TO_NIBBLE_LOW, -1);
      Arrays.fill(CHAR_TO_NIBBLE_HIGH, -1);
      for (int i = '0'; i <= '9'; i++)
      {
         CHAR_TO_NIBBLE_LOW[i] = i - '0';
         CHAR_TO_NIBBLE_HIGH[i] = (i - '0') << HALF_BYTE;
      }
      for (int i = 'a'; i <= 'f'; i++)
      {
         CHAR_TO_NIBBLE_LOW[i] = i - 'a' + DECIMAL_OFFSET;
         CHAR_TO_NIBBLE_HIGH[i] = (i - 'a' + DECIMAL_OFFSET) << HALF_BYTE;
      }
      for (int i = 'A'; i <= 'F'; i++)
      {
         CHAR_TO_NIBBLE_LOW[i] = i - 'A' + DECIMAL_OFFSET;
         CHAR_TO_NIBBLE_HIGH[i] = (i - 'A' + DECIMAL_OFFSET) << HALF_BYTE;
      }
   }

   private HexUtil ()
   {
       // utility class that provides only static methods
   }

   /**
    * Converts a byte array into an upper-case hex string, starting at the
    * given offset and converting the given number of bytes.
    * @param data the byte data to convert to hex
    * @param offset the start offset in the byte array
    * @param length the number of bytes to convert
    * @return null if data was null, an empty string if data.length == 0,
    *       and the hex representation of the byte array otherwise
    * @throws IndexOutOfBoundsException if offset + length > data.lenght
    */
   public static String bytesToHex (
         final byte[] data, final int offset, final int length)
         throws IndexOutOfBoundsException
   {
      final String result;
      if (data == null)
      {
         result = null;
      }
      else
      {
         final StringBuffer sbuf = new StringBuffer();
         for (int i = offset; i < offset + length; i++)
         {
            sbuf.append(BYTE_AS_HEX[data[i] & BYTE_MASK]);
         }
         result = sbuf.toString();
      }
      return result;
   }

   /**
    * Converts a byte array into an upper-case hex string, starting at the
    * first byte and converting the whole array.
    * @param data the byte data to convert to hex
    * @return null if data was null, an empty string if data.length == 0,
    *       and the hex representation of the byte array otherwise
    */
   public static String bytesToHex (final byte[] data)
   {
      final String result;
      if (data != null)
      {
         result = bytesToHex(data, 0, data.length);
      }
      else
      {
         result = null;
      }
      return result;
   }

   /**
    * Convert the given hex string to a byte array.
    * @param s the string to convert
    * @return null if the string is null, an empty byte array if s.length == 0
    *       and a byte array representing the hex string otherwise
    * @throws IllegalArgumentException if the string is not a multiple of 2
    *       characters long, or if the string contains an invalid hex char
    */
   public static byte[] stringToBytes (final String s)
         throws IllegalArgumentException
   {
      final byte[] result;
      if (s == null)
      {
         result = null;
      }
      else if (s.length() == 0)
      {
         result = new byte[0];
      }
      else
      {
         // string must be a multiple of 2 chars
         if (s.length() % CHARS_PER_BYTE != 0)
         {
            throw new IllegalArgumentException(
                  "The length of a hex string must be a multiple of 2 (was "
                  + s.length() + ")");
         }
         int count = 0;
         result = new byte[s.length() / CHARS_PER_BYTE];
         try
         {
            for (int i = 0; i < s.length(); i++)
            {
               final char c1 = s.charAt(i);
               final char c2 = s.charAt(++i);
               final int b = CHAR_TO_NIBBLE_HIGH[c1] | CHAR_TO_NIBBLE_LOW[c2];
               if (b == -1)
               {
                  throw new IllegalArgumentException(
                        "'" + c1 + c2
                        + "' is not a valid hex representation of a byte");
               }
               result[count] = (byte) b;
               ++count;
            }
         }
         catch (IndexOutOfBoundsException ex)
         {
            final char c1 = s.charAt(count * CHARS_PER_BYTE);
            final char c2 = s.charAt(count * CHARS_PER_BYTE + 1);
            final IllegalArgumentException e = new IllegalArgumentException(
                  "'" + c1 + c2
                  + "' is not a valid hex representation of a byte");
            e.initCause(ex);
            throw e;
         }
      }
      return result;
   }

   /**
    * Produces a hexdump of the given byte array with a formatting
    * as in "hexdump -C" (canonical hex + ASCII display). This formatting
    * contains an address column, sixteen bytes of hex separated by spaces,
    * with an extra space after eight bytes, and an ascii print-out of the
    * bytes enclosed in pipe symbols. Non-ASCII characters are replaced by
    * dots.
    *
    * @param data the byte data to dump
    * @return a string containing the hexdump, or null if data was null
    */
   public static String dump (final byte[] data)
   {
      final String result;
      if (data == null)
      {
         result = null;
      }
      else
      {
         int offset = 0;
         final StringBuffer dumpBuffer = new StringBuffer();
         final StringBuffer lineBuffer = new StringBuffer();
         final StringBuffer charBuffer = new StringBuffer();
         while (offset < data.length)
         {
            lineBuffer.setLength(0);
            charBuffer.setLength(0);
            charBuffer.append('|');
            lineBuffer.append(offsetToHex(offset));
            lineBuffer.append(' ');
            final int end = (offset + DUMP_BYTES_PER_LINE < data.length
                  ? offset + DUMP_BYTES_PER_LINE
                  : data.length);
            for (int i = offset; i < end; i++)
            {
               final byte b = data[i];
               lineBuffer.append(bytesToHex(new byte[] {b}));
               lineBuffer.append(' ');

               if (i - offset == DUMP_BYTES_PER_COLUMN - 1)
               {
                  lineBuffer.append(' ');
               }
               final char c = (char) b;
               if ((! Character.isISOControl(c))
                     && StringUtil.isAscii(c))
               {
                  charBuffer.append(c);
               }
               else
               {
                  charBuffer.append('.');
               }
            }
            padBuffer(lineBuffer);
            charBuffer.append('|');
            dumpBuffer.append(lineBuffer);
            dumpBuffer.append(charBuffer);
            dumpBuffer.append(Constants.LINE_SEPARATOR);
            offset += DUMP_BYTES_PER_LINE;
         }
         result = dumpBuffer.toString();
      }
      return result;
   }

   private static void padBuffer (final StringBuffer sbuf)
   {
      while (sbuf.length() < DUMP_BUFFER_SIZE)
      {
         sbuf.append(' ');
      }
   }

   private static String offsetToHex (int offset)
   {
      String s = Integer.toHexString(offset);
      while (s.length() < DUMP_ADDR_LEN)
      {
         s = "0" + s;
      }
      return s;
   }
}

