/*
 * 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);
        }
    }

}
