/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

/* $Id$ */

package org.apache.xmlgraphics.util.io;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;

// CSOFF: ConstantName
// CSOFF: InnerAssignment
// CSOFF: MultipleVariableDeclarations
// CSOFF: NeedBraces
// CSOFF: OneStatementPerLine
// CSOFF: WhitespaceAfter
// CSOFF: WhitespaceAround

/**
 * This class implements a Base64 Character encoder as specified in RFC1113.
 * Unlike some other encoding schemes there is nothing in this encoding
 * that indicates where a buffer starts or ends.
 *
 * This means that the encoded text will simply start with the first line
 * of encoded text and end with the last line of encoded text.
 *
 * @version $Id$
 *
 * Originally authored by Thomas DeWeese, Vincent Hardy, and Chuck McManis.
 */
public class Base64EncodeStream extends OutputStream {

    /** This array maps the 6 bit values to their characters */
    private static final byte[] PEM_ARRAY = {
    //   0   1   2   3   4   5   6   7
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0
        'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 1
        'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 2
        'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 3
        'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 4
        'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 5
        'w', 'x', 'y', 'z', '0', '1', '2', '3', // 6
        '4', '5', '6', '7', '8', '9', '+', '/'  // 7
    };

    byte [] atom = new byte[3];
    int     atomLen;
    byte [] encodeBuf = new byte[4];
    int     lineLen;

    PrintStream  out;
    boolean closeOutOnClose;

    public Base64EncodeStream(OutputStream out) {
        try {
            this.out = new PrintStream(out, false, StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        closeOutOnClose = true;
    }

    public Base64EncodeStream(OutputStream out, boolean closeOutOnClose) {
        try {
            this.out = new PrintStream(out, false, StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        this.closeOutOnClose = closeOutOnClose;
    }

    public void close() throws IOException {
        if (out != null) {
            encodeAtom();
            out.flush();
            if (closeOutOnClose) {
                out.close();
            }
            out = null;
        }
    }

    /**
     * This can't really flush out output since that may generate
     * '=' chars which would indicate the end of the stream.
     * Instead we flush out.  You can only be sure all output is
     * writen by closing this stream.
     */
    public void flush() throws IOException {
        out.flush();
    }

    public void write(int b) throws IOException {
        atom[atomLen++] = (byte)b;
        if (atomLen == 3) {
            encodeAtom();
        }
    }

    public void write(byte []data) throws IOException {
        encodeFromArray(data, 0, data.length);
    }

    public void write(byte [] data, int off, int len) throws IOException {
        encodeFromArray(data, off, len);
    }

    /**
     * enocodeAtom - Take three bytes of input and encode it as 4
     * printable characters. Note that if the length in len is less
     * than three is encodes either one or two '=' signs to indicate
     * padding characters.
     */
    void encodeAtom() throws IOException {
        byte a;
        byte b;
        byte c;

        switch (atomLen) {
        case 0: return;
        case 1:
            a = atom[0];
            encodeBuf[0] = PEM_ARRAY[((a >>> 2) & 0x3F)];
            encodeBuf[1] = PEM_ARRAY[((a <<  4) & 0x30)];
            encodeBuf[2] = encodeBuf[3] = '=';
            break;
        case 2:
            a = atom[0];
            b = atom[1];
            encodeBuf[0] = PEM_ARRAY[((a >>> 2) & 0x3F)];
            encodeBuf[1] = PEM_ARRAY[(((a << 4) & 0x30) | ((b >>> 4) & 0x0F))];
            encodeBuf[2] = PEM_ARRAY[((b  << 2) & 0x3C)];
            encodeBuf[3] = '=';
            break;
        default:
            a = atom[0];
            b = atom[1];
            c = atom[2];
            encodeBuf[0] = PEM_ARRAY[((a >>> 2) & 0x3F)];
            encodeBuf[1] = PEM_ARRAY[(((a << 4) & 0x30) | ((b >>> 4) & 0x0F))];
            encodeBuf[2] = PEM_ARRAY[(((b << 2) & 0x3C) | ((c >>> 6) & 0x03))];
            encodeBuf[3] = PEM_ARRAY[c & 0x3F];
        }
        if (lineLen == 64) {
            out.println();
            lineLen = 0;
        }
        out.write(encodeBuf);

        lineLen += 4;
        atomLen = 0;
    }

    /**
     * enocodeAtom - Take three bytes of input and encode it as 4
     * printable characters. Note that if the length in len is less
     * than three is encodes either one or two '=' signs to indicate
     * padding characters.
     */
    void encodeFromArray(byte[] data, int offset, int len)
        throws IOException {
        byte a;
        byte b;
        byte c;
        if (len == 0) {
            return;
        }

        // System.out.println("atomLen: " + atomLen +
        //                    " len: " + len +
        //                    " offset:  " + offset);

        if (atomLen != 0) {
            switch(atomLen) {
            case 1:
                atom[1] = data[offset++];
                len--;
                atomLen++;
                if (len == 0) {
                    return;
                }
                atom[2] = data[offset++];
                len--;
                atomLen++;
                break;
            case 2:
                atom[2] = data[offset++];
                len--;
                atomLen++;
                break;
            default:
            }
            encodeAtom();
        }

        while (len >= 3) {
            a = data[offset++];
            b = data[offset++];
            c = data[offset++];

            encodeBuf[0] = PEM_ARRAY[((a >>> 2) & 0x3F)];
            encodeBuf[1] = PEM_ARRAY[(((a << 4) & 0x30) | ((b >>> 4) & 0x0F))];
            encodeBuf[2] = PEM_ARRAY[(((b << 2) & 0x3C) | ((c >>> 6) & 0x03))];
            encodeBuf[3] = PEM_ARRAY[c & 0x3F];
            out.write(encodeBuf);

            lineLen += 4;
            if (lineLen == 64) {
                out.println();
                lineLen = 0;
            }

            len -= 3;
        }

        switch (len) {
        case 1:
            atom[0] = data[offset];
            break;
        case 2:
            atom[0] = data[offset];
            atom[1] = data[offset + 1];
            break;
        default:
        }
        atomLen = len;
    }



}
