/* Stax2 extension for basic Stax API (JSR-173).
 *
 * Copyright (c) 2005- Tatu Saloranta, tatu.saloranta@iki.fi
 *
 * Licensed under the License specified in file LICENSE, included with
 * the source code.
 * You may not use this file except in compliance with the License.
 *
 * 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.codehaus.stax2.typed;

import java.util.Arrays;

/**
 * This abstract base class is used to define specific details of which
 * variant of Base64 encoding/decoding is to be used. Although there is
 * somewhat standard basic version (so-called "MIME Base64"), other variants
 * exists, see <a href="http://en.wikipedia.org/wiki/Base64">Base64 Wikipedia entry</a> for details.
 *<p>
 * Implementation notes:
 *<ul>
 * <li>The main complication here is trying to limit access to the underlying
efficient encoding/decoding tables -- they are needed for fast operation,
but it is potentially risky to expose raw arrays since they can not be
protected against modification. The approach here is to try to limit access
essentially to the main base64 codec classes; but this leads to bit awkward
class structure</li>
 *</ul>
 * 
 * @author Tatu Saloranta
 *
 * @since 3.0.0
 */
public final class Base64Variant
{
    /**
     * Placeholder used by "no padding" variant, to be used when a character
     * value is needed.
     */
    final static char PADDING_CHAR_NONE = '\0';

    /**
     * Marker used to denote ascii characters that do not correspond
     * to a 6-bit value (in this variant), and is not used as a padding
     * character.
     */
    public final static int BASE64_VALUE_INVALID = -1;

    /**
     * Marker used to denote ascii character (in decoding table) that
     * is the padding character using this variant (if any).
     */
    public final static int BASE64_VALUE_PADDING = -2;

    /*
    ////////////////////////////////////////////////////
    // Encoding/decoding tables
    ////////////////////////////////////////////////////
     */

    /**
     * Decoding table used for base 64 decoding.
     */
    private final int[] _asciiToBase64 = new int[128];

    /**
     * Encoding table used for base 64 decoding when output is done
     * as characters.
     */
    private final char[] _base64ToAsciiC = new char[64];

    /**
     * Alternative encoding table used for base 64 decoding when output is done
     * as ascii bytes.
     */
    private final byte[] _base64ToAsciiB = new byte[64];

    /*
    ////////////////////////////////////////////////////
    // Other configuration
    ////////////////////////////////////////////////////
     */

    /**
     * Symbolic name of variant; used for diagnostics/debugging.
     */
    final String _name;

    /**
     * Whether this variant uses padding or not.
     */
    final boolean _usesPadding;

    /**
     * Characted used for padding, if any ({@link #PADDING_CHAR_NONE} if not).
     */
    final char _paddingChar;
    
    /**
     * Maximum number of encoded base64 characters to output during encoding
     * before adding a linefeed, if line length is to be limited
     * ({@link java.lang.Integer#MAX_VALUE} if not limited).
     *<p>
     * Note: for some output modes (when writing attributes) linefeeds may
     * need to be avoided, and this value ignored.
     */
    final int _maxLineLength;

    /*
    ////////////////////////////////////////////////////
    // Life-cycle
    ////////////////////////////////////////////////////
     */

    public Base64Variant(String name, String base64Alphabet, boolean usesPadding, char paddingChar, int maxLineLength)
    {
        _name = name;
        _usesPadding = usesPadding;
        _paddingChar = paddingChar;
        _maxLineLength = maxLineLength;

        // Ok and then we need to create codec tables.

        // First the main encoding table:
        int alphaLen = base64Alphabet.length();
        if (alphaLen != 64) {
            throw new IllegalArgumentException("Base64Alphabet length must be exactly 64 (was "+alphaLen+")");
        }

        // And then secondary encoding table and decoding table:
        base64Alphabet.getChars(0, alphaLen, _base64ToAsciiC, 0);
        Arrays.fill(_asciiToBase64, BASE64_VALUE_INVALID);
        for (int i = 0; i < alphaLen; ++i) {
            char alpha = _base64ToAsciiC[i];
            _base64ToAsciiB[i] = (byte) alpha;
            _asciiToBase64[alpha] = i;
        }

        // Plus if we use padding, add that in too
        if (usesPadding) {
            _asciiToBase64[(int) paddingChar] = BASE64_VALUE_PADDING;
        }
    }

    /**
     * "Copy constructor" that can be used when the base alphabet is identical
     * to one used by another variant except for the maximum line length
     * (and obviously, name).
     */
    public Base64Variant(Base64Variant base, String name, int maxLineLength)
    {
        this(base, name, base._usesPadding, base._paddingChar, maxLineLength);
    }

    /**
     * "Copy constructor" that can be used when the base alphabet is identical
     * to one used by another variant, but other details (padding, maximum
     * line length) differ
     */
    public Base64Variant(Base64Variant base, String name, boolean usesPadding, char paddingChar, int maxLineLength)
    {
        _name = name;
        byte[] srcB = base._base64ToAsciiB;
        System.arraycopy(srcB, 0, this._base64ToAsciiB, 0, srcB.length);
        char[] srcC = base._base64ToAsciiC;
        System.arraycopy(srcC, 0, this._base64ToAsciiC, 0, srcC.length);
        int[] srcV = base._asciiToBase64;
        System.arraycopy(srcV, 0, this._asciiToBase64, 0, srcV.length);

        _usesPadding = usesPadding;
        _paddingChar = paddingChar;
        _maxLineLength = maxLineLength;
    }

    /*
    ////////////////////////////////////////////////////
    // Public accessors
    ////////////////////////////////////////////////////
     */

    public String getName() { return _name; }

    public boolean usesPadding() { return _usesPadding; }
    public boolean usesPaddingChar(char c) { return c == _paddingChar; }
    public char getPaddingChar() { return _paddingChar; }
    public byte getPaddingByte() { return (byte)_paddingChar; }

    public int getMaxLineLength() { return _maxLineLength; }

    /*
    ////////////////////////////////////////////////////
    // Decoding support
    ////////////////////////////////////////////////////
     */

    /**
     * @return 6-bit decoded value, if valid character; 
     */
    public int decodeBase64Char(char c)
    {
        int ch = (int) c;
        return (ch <= 127) ? _asciiToBase64[ch] : BASE64_VALUE_INVALID;
    }

    public int decodeBase64Byte(byte b)
    {
        int ch = (int) b;
        return (ch <= 127) ? _asciiToBase64[ch] : BASE64_VALUE_INVALID;
    }

    /*
    ////////////////////////////////////////////////////
    // Encoding support
    ////////////////////////////////////////////////////
     */

    public char encodeBase64BitsAsChar(int value)
    {
        /* Let's assume caller has done necessary checks; this
         * method must be fast and inlinable
         */
        return _base64ToAsciiC[value];
    }

    /**
     * Method that encodes given right-aligned (LSB) 24-bit value
     * into 4 base64 characters, stored in given result buffer.
     */
    public int encodeBase64Chunk(int b24, char[] buffer, int ptr)
    {
        buffer[ptr++] = _base64ToAsciiC[(b24 >> 18) & 0x3F];
        buffer[ptr++] = _base64ToAsciiC[(b24 >> 12) & 0x3F];
        buffer[ptr++] = _base64ToAsciiC[(b24 >> 6) & 0x3F];
        buffer[ptr++] = _base64ToAsciiC[b24 & 0x3F];
        return ptr;
    }

    /**
     * Method that outputs partial chunk (which only encodes one
     * or two bytes of data). Data given is still aligned same as if
     * it as full data; that is, missing data is at the "right end"
     * (LSB) of int.
     *
     * @param outputBytes Number of encoded bytes included (either 1 or 2)
     */
    public int encodeBase64Partial(int bits, int outputBytes, char[] buffer, int outPtr)
    {
        buffer[outPtr++] = _base64ToAsciiC[(bits >> 18) & 0x3F];
        buffer[outPtr++] = _base64ToAsciiC[(bits >> 12) & 0x3F];
        if (_usesPadding) {
            buffer[outPtr++] = (outputBytes == 2) ?
                _base64ToAsciiC[(bits >> 6) & 0x3F] : _paddingChar;
            buffer[outPtr++] = _paddingChar;
        } else {
            if (outputBytes == 2) {
                buffer[outPtr++] = _base64ToAsciiC[(bits >> 6) & 0x3F];
            }
        }
        return outPtr;
    }

    public byte encodeBase64BitsAsByte(int value)
    {
        // As with above, assuming it is 6-bit value
        return _base64ToAsciiB[value];
    }

    /**
     * Method that encodes given right-aligned (LSB) 24-bit value
     * into 4 base64 bytes (ascii), stored in given result buffer.
     */
    public int encodeBase64Chunk(int b24, byte[] buffer, int ptr)
    {
        buffer[ptr++] = _base64ToAsciiB[(b24 >> 18) & 0x3F];
        buffer[ptr++] = _base64ToAsciiB[(b24 >> 12) & 0x3F];
        buffer[ptr++] = _base64ToAsciiB[(b24 >> 6) & 0x3F];
        buffer[ptr++] = _base64ToAsciiB[b24 & 0x3F];
        return ptr;
    }

    /**
     * Method that outputs partial chunk (which only encodes one
     * or two bytes of data). Data given is still aligned same as if
     * it as full data; that is, missing data is at the "right end"
     * (LSB) of int.
     *
     * @param outputBytes Number of encoded bytes included (either 1 or 2)
     */
    public int encodeBase64Partial(int bits, int outputBytes, byte[] buffer, int outPtr)
    {
        buffer[outPtr++] = _base64ToAsciiB[(bits >> 18) & 0x3F];
        buffer[outPtr++] = _base64ToAsciiB[(bits >> 12) & 0x3F];
        if (_usesPadding) {
            byte pb = (byte) _paddingChar;
            buffer[outPtr++] = (outputBytes == 2) ?
                _base64ToAsciiB[(bits >> 6) & 0x3F] : pb;
            buffer[outPtr++] = pb;
        } else {
            if (outputBytes == 2) {
                buffer[outPtr++] = _base64ToAsciiB[(bits >> 6) & 0x3F];
            }
        }
        return outPtr;
    }

    /*
    ////////////////////////////////////////////////////
    // other methods
    ////////////////////////////////////////////////////
     */

    @Override
    public String toString() { return _name; }
}

