/* See LICENSE for licensing and NOTICE for copyright. */
package org.cryptacular.codec;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.Arrays;
import org.cryptacular.EncodingException;

/**
 * Stateful hexadecimal character-to-byte decoder.
 *
 * @author  Middleware Services
 */
public class HexDecoder implements Decoder
{

  /** Hex character decoding table. */
  private static final byte[] DECODING_TABLE = new byte[128];

  /* Initializes the character decoding table. */
  static {
    Arrays.fill(DECODING_TABLE, (byte) -1);
    for (int i = 0; i < 10; i++) {
      DECODING_TABLE[i + 48] = (byte) i;
    }
    for (int i = 0; i < 6; i++) {
      DECODING_TABLE[i + 65] = (byte) (10 + i);
      DECODING_TABLE[i + 97] = (byte) (10 + i);
    }
  }

  /** Number of encoded characters processed. */
  private int count;


  @Override
  public void decode(final CharBuffer input, final ByteBuffer output) throws EncodingException
  {
    // Ignore leading 0x characters if present
    if (input.get(0) == '0' && input.get(1) == 'x') {
      input.position(input.position() + 2);
    }

    byte hi = 0;
    byte lo;
    char current;
    while (input.hasRemaining()) {
      current = input.get();
      if (current == ':' || Character.isWhitespace(current)) {
        continue;
      }
      if ((count++ & 0x01) == 0) {
        hi = lookup(current);
      } else {
        lo = lookup(current);
        output.put((byte) ((hi << 4) | lo));
      }
    }
  }


  @Override
  public void finalize(final ByteBuffer output) throws EncodingException
  {
    count = 0;
  }


  @Override
  public int outputSize(final int inputSize)
  {
    return inputSize / 2;
  }


  /**
   * Looks up the byte that corresponds to the given character.
   *
   * @param  c  Encoded character.
   *
   * @return  Decoded byte.
   */
  private static byte lookup(final char c)
  {
    final byte b = DECODING_TABLE[c & 0x7F];
    if (b < 0) {
      throw new EncodingException("Invalid hex character " + c);
    }
    return b;
  }
}
