/*
 * Copyright 2007 Werner Guttmann
 *
 * Licensed 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.castor.core.util;

import java.io.IOException;
import java.io.OutputStream;
import java.io.ByteArrayOutputStream;

/**
 * Hex encoder/decoder implementation (borrowed from BouncyCastle=.
 * 
 * @author Johan Lindquist
 * @since 1.1.1
 * @version $Revision$
 */
public final class HexDecoder {

  /**
   * Identifies the data type supported by this decoder.
   */
  public static final String DATA_TYPE = "hexBinary";

  /**
   * Initial size of the decoding table.
   */
  private static final int DECODING_TABLE_SIZE = 128;

  /**
   * Encoding table.
   */
  protected static final byte[] ENCODING_TABLE = {(byte) '0', (byte) '1', (byte) '2', (byte) '3',
      (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'A',
      (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F'};

  /**
   * Decoding table.
   */
  protected static final byte[] DECODING_TABLE = new byte[DECODING_TABLE_SIZE];

  /**
   * Initialize the decoding table.
   */
  protected static void initialiseDecodingTable() {
    for (int i = 0; i < ENCODING_TABLE.length; i++) {
      DECODING_TABLE[ENCODING_TABLE[i]] = (byte) i;
    }

    // deal with lower case letters as well
    DECODING_TABLE['a'] = DECODING_TABLE['A'];
    DECODING_TABLE['b'] = DECODING_TABLE['B'];
    DECODING_TABLE['c'] = DECODING_TABLE['C'];
    DECODING_TABLE['d'] = DECODING_TABLE['D'];
    DECODING_TABLE['e'] = DECODING_TABLE['E'];
    DECODING_TABLE['f'] = DECODING_TABLE['F'];
  }

  static {
    initialiseDecodingTable();
  }

  /**
   * Creates an instance of this class.
   */
  private HexDecoder() {
    // Nothing to do ...
  }

  /**
   * Encodes the input data producing a Hex output stream.
   * 
   * @param data The input data to be HEX encoded
   * @param off Initiak offset
   * @param length Initial length of the input data array
   * @param out The {@link OutputStream} instance holding the encoded input data.
   * @return the number of bytes produced.
   * @throws IOException If encoding fails.
   */
  public static int encode(final byte[] data, final int off, final int length,
      final OutputStream out) throws IOException {
    for (int i = off; i < (off + length); i++) {
      int v = data[i] & 0xff;

      out.write(ENCODING_TABLE[(v >>> 4)]);
      out.write(ENCODING_TABLE[v & 0xf]);
    }

    return length * 2;
  }

  /**
   * Indicates whether a given character should be ignored during en-/decoding.
   * 
   * @param c The character at question.
   * @return True if the given character should be ignored.
   */
  private static boolean ignore(final char c) {
    return (c == '\n' || c == '\r' || c == '\t' || c == ' ');
  }

  /**
   * Decodes the Hex encoded byte data writing it to the given output stream, whitespace characters
   * will be ignored.
   * 
   * @param data The data to be encoded
   * @param off Initial offset.
   * @param length Initial length
   * @param out The {@link OutputStream} instance
   * @return the number of bytes produced.
   * @throws IOException If encoding failed.
   */
  public static int decode(final byte[] data, final int off, final int length,
      final OutputStream out) throws IOException {
    byte b1, b2;
    int outLen = 0;

    int end = off + length;

    while (end > off) {
      if (!ignore((char) data[end - 1])) {
        break;
      }

      end--;
    }

    int i = off;
    while (i < end) {
      while (i < end && ignore((char) data[i])) {
        i++;
      }

      b1 = DECODING_TABLE[data[i++]];

      while (i < end && ignore((char) data[i])) {
        i++;
      }

      b2 = DECODING_TABLE[data[i++]];

      out.write((b1 << 4) | b2);

      outLen++;
    }

    return outLen;
  }

  /**
   * Decodes the Hex encoded String data writing it to the given output stream, whitespace
   * characters will be ignored.
   * 
   * @param data The data to be encoded
   * @param out The {@link OutputStream} instance
   * @return the number of bytes produced.
   * @throws IOException If encoding failed.
   */
  public static int decode(final String data, final OutputStream out) throws IOException {
    byte b1, b2;
    int length = 0;

    int end = data.length();

    while (end > 0) {
      if (!ignore(data.charAt(end - 1))) {
        break;
      }

      end--;
    }

    int i = 0;
    while (i < end) {
      while (i < end && ignore(data.charAt(i))) {
        i++;
      }

      b1 = DECODING_TABLE[data.charAt(i++)];

      while (i < end && ignore(data.charAt(i))) {
        i++;
      }

      b2 = DECODING_TABLE[data.charAt(i++)];

      out.write((b1 << 4) | b2);

      length++;
    }

    return length;
  }

  /**
   * Encodes the input data producing a Hex output stream.
   * 
   * @param data Input data to encode.
   * @return the number of bytes produced.
   */
  public static String encode(final byte[] data) {
    try {
      final ByteArrayOutputStream out = new ByteArrayOutputStream();
      encode(data, 0, data.length, out);
      out.close();
      return new String(out.toByteArray());
    } catch (IOException e) {
      e.printStackTrace();
      throw new RuntimeException(e.getMessage(), e);
    }
  }

  /**
   * Decodes the HEX input data producing a output stream.
   * 
   * @param data Input data to be decoded.
   * @return A byte array representing the decoded input data.
   */
  public static byte[] decode(final String data) {
    try {
      final ByteArrayOutputStream out = new ByteArrayOutputStream();
      decode(data, out);
      out.close();
      return out.toByteArray();
    } catch (IOException e) {
      e.printStackTrace();
      throw new RuntimeException(e.getMessage(), e);
    }
  }

}
