/*******************************************************************************
 * Copyright (c) 1998 -2014 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the 
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 
 * which accompanies this distribution. 
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at 
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 ******************************************************************************/
package org.eclipse.persistence.internal.oxm.conversion;

import java.util.BitSet;

/**
 * INTERNAL: 
 * <p><b>Purpose</b>: Convert to/from XML base64Binary.</p>
 */

public class Base64 {
    private static final byte[] Base64EncMap;
    private static final byte[] Base64DecMap;

    private static final byte PADDING = 127;

    // Class Initializer
    static {
        // rfc-2045: Base64 Alphabet
        Base64EncMap = initEncodeMap();
        Base64DecMap = initDecodeMap();
    }

    private static byte[] initEncodeMap() {
        byte[] map = new byte[64];
        int i;
        for (i = 0; i < 26; i++) {
            map[i] = (byte) ('A' + i);
        }
        for (i = 26; i < 52; i++) {
            map[i] = (byte) ('a' + (i - 26));
        }
        for (i = 52; i < 62; i++) {
            map[i] = (byte) ('0' + (i - 52));
        }
        map[62] = '+';
        map[63] = '/';

        return map;
    }

    private static byte[] initDecodeMap() {
        byte[] map = new byte[128];
        int i;
        for (i = 0; i < 128; i++) {
            map[i] = -1;
        }

        for (i = 'A'; i <= 'Z'; i++) {
            map[i] = (byte) (i - 'A');
        }
        for (i = 'a'; i <= 'z'; i++) {
            map[i] = (byte) (i - 'a' + 26);
        }
        for (i = '0'; i <= '9'; i++) {
            map[i] = (byte) (i - '0' + 52);
        }
        map['+'] = 62;
        map['/'] = 63;
        map['='] = PADDING;

        return map;
    }

    /**
     * Base64 constructor comment.
     */
    private Base64() {
    }

    /**
     * computes the length of binary data speculatively.
     *
     * <p>
     * Our requirement is to create byte[] of the exact length to store the binary data.
     * If we do this in a straight-forward way, it takes two passes over the data.
     * Experiments show that this is a non-trivial overhead (35% or so is spent on
     * the first pass in calculating the length.)
     *
     * <p>
     * So the approach here is that we compute the length speculatively, without looking
     * at the whole contents. The obtained speculative value is never less than the
     * actual length of the binary data, but it may be bigger. So if the speculation
     * goes wrong, we'll pay the cost of reallocation and buffer copying.
     *
     * <p>
     * If the base64 text is tightly packed with no indentation nor illegal char
     * (like what most web services produce), then the speculation of this method
     * will be correct, so we get the performance benefit.
     */
    private static int guessLength(byte[] data) {
        final int len = data.length;

        // compute the tail '=' chars
        int j = len - 1;
        for (; j >= 0; j--) {
            byte code = Base64DecMap[data[j]];
            if (code == PADDING)
                continue;
            if (code == -1) // most likely this base64 data is indented. go with the upper bound
                return data.length / 4 * 3;
            break;
        }

        j++;    // data.charAt(j) is now at some base64 char, so +1 to make it the size
        int padSize = len - j;
        if (padSize > 2) // something is wrong with base64. be safe and go with the upper bound
            return data.length / 4 * 3;

        // so far this base64 looks like it's unindented tightly packed base64.
        // take a chance and create an array with the expected size
        return data.length / 4 * 3 - padSize;
    }

    /**
     * base64Binary data is likely to be long, and decoding requires
     * each character to be accessed twice (once for counting length, another
     * for decoding.)
     *
     * This method decodes the given byte[] using the base64-encoding
     * specified in RFC-2045 (Section 6.8).
     *
     * @param  data the base64-encoded data.
     * @return the decoded <var>data</var>.
     */
    public static byte[] base64Decode(byte[] data) {
        final int buflen = guessLength(data);
        final byte[] out = new byte[buflen];
        int o = 0;

        final int len = data.length;
        int i;

        final byte[] quadruplet = new byte[4];
        int q = 0;

        // convert each quadruplet to three bytes.
        for (i = 0; i < len; i++) {
            byte ch = data[i];
            byte v = Base64DecMap[ch];

            if (v != -1)
                quadruplet[q++] = v;

            if (q == 4) {
                // quadruplet is now filled.
                out[o++] = (byte) ((quadruplet[0] << 2) | (quadruplet[1] >> 4));
                if (quadruplet[2] != PADDING)
                    out[o++] = (byte) ((quadruplet[1] << 4) | (quadruplet[2] >> 2));
                if (quadruplet[3] != PADDING)
                    out[o++] = (byte) ((quadruplet[2] << 6) | (quadruplet[3]));
                q = 0;
            }
        }

        if (buflen == o) // speculation worked out to be OK
            return out;

        // we overestimated, so need to create a new buffer
        byte[] nb = new byte[o];
        System.arraycopy(out, 0, nb, 0, o);
        return nb;
    }

    /**
     * This method encodes the given byte[] using the base64-encoding
     * specified in RFC-2045 (Section 6.8).
     *
     * @param  data the data
     * @return the base64-encoded <var>data</var>
     */
    public static byte[] base64Encode(byte[] data) {
        if (data == null) {
            return null;
        }

        int sidx;
        int didx;
        byte[] dest = new byte[((data.length + 2) / 3) * 4];

        // 3-byte to 4-byte conversion + 0-63 to ascii printable conversion
        for (sidx = 0, didx = 0; sidx < (data.length - 2); sidx += 3) {
            dest[didx++] = Base64EncMap[(data[sidx] >>> 2) & 077];
            dest[didx++] = Base64EncMap[((data[sidx + 1] >>> 4) & 017) | ((data[sidx] << 4) & 077)];
            dest[didx++] = Base64EncMap[((data[sidx + 2] >>> 6) & 003) | ((data[sidx + 1] << 2) & 077)];
            dest[didx++] = Base64EncMap[data[sidx + 2] & 077];
        }
        if (sidx < data.length) {
            dest[didx++] = Base64EncMap[(data[sidx] >>> 2) & 077];
            if (sidx < (data.length - 1)) {
                dest[didx++] = Base64EncMap[((data[sidx + 1] >>> 4) & 017) | ((data[sidx] << 4) & 077)];
                dest[didx++] = Base64EncMap[(data[sidx + 1] << 2) & 077];
            } else {
                dest[didx++] = Base64EncMap[(data[sidx] << 4) & 077];
            }
        }

        // add padding
        for (; didx < dest.length; didx++) {
            dest[didx] = (byte)'=';
        }

        return dest;
    }
}
