/**
 * 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.
 */

package org.apache.cxf.attachment;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.apache.cxf.common.util.Base64Exception;
import org.apache.cxf.common.util.Base64Utility;

/**
 * An implementation of a FilterInputStream that decodes the
 * stream data in BASE64 encoding format.  This version does the
 * decoding "on the fly" rather than decoding a single block of
 * data.  Since this version is intended for use by the MimeUtilty class,
 * it also handles line breaks in the encoded data.
 */
public class Base64DecoderStream extends FilterInputStream {

    static final String MAIL_BASE64_IGNOREERRORS = "mail.mime.base64.ignoreerrors";

    // number of decodeable units we'll try to process at one time.  We'll attempt to read that much
    // data from the input stream and decode in blocks.
    static final int BUFFERED_UNITS = 2000;

    // can be overridden by a system property.
    protected boolean ignoreErrors;

    // buffer for reading in chars for decoding (which can support larger bulk reads)
    protected char[] encodedChars = new char[BUFFERED_UNITS * 4];
    // a buffer for one decoding unit's worth of data (3 bytes).
    protected byte[] decodedChars;
    // count of characters in the buffer
    protected int decodedCount;
    // index of the next decoded character
    protected int decodedIndex;


    public Base64DecoderStream(InputStream in) {
        super(in);
    }

    /**
     * Test for the existance of decoded characters in our buffer
     * of decoded data.
     *
     * @return True if we currently have buffered characters.
     */
    private boolean dataAvailable() {
        return decodedCount != 0;
    }

    /**
     * Decode a requested number of bytes of data into a buffer.
     *
     * @return true if we were able to obtain more data, false otherwise.
     */
    private boolean decodeStreamData() throws IOException {
        decodedIndex = 0;

        // fill up a data buffer with input data
        int readCharacters = fillEncodedBuffer();

        if (readCharacters > 0) {
            try {
                decodedChars = Base64Utility.decodeChunk(encodedChars, 0, readCharacters);
            } catch (Base64Exception e) {
                throw new IOException(e);
            }
            decodedCount = decodedChars.length;
            return true;
        }
        return false;
    }


    /**
     * Retrieve a single byte from the decoded characters buffer.
     *
     * @return The decoded character or -1 if there was an EOF condition.
     */
    private int getByte() throws IOException {
        if (!dataAvailable() && !decodeStreamData()) {
            return -1;
        }
        decodedCount--;
        // we need to ensure this doesn't get sign extended
        return decodedChars[decodedIndex++] & 0xff;
    }

    private int getBytes(byte[] data, int offset, int length) throws IOException {

        int readCharacters = 0;
        while (length > 0) {
            // need data?  Try to get some
            if (!dataAvailable() && !decodeStreamData()) {
                // if we can't get this, return a count of how much we did get (which may be -1).
                return readCharacters > 0 ? readCharacters : -1;
            }

            // now copy some of the data from the decoded buffer to the target buffer
            int copyCount = Math.min(decodedCount, length);
            System.arraycopy(decodedChars, decodedIndex, data, offset, copyCount);
            decodedIndex += copyCount;
            decodedCount -= copyCount;
            offset += copyCount;
            length -= copyCount;
            readCharacters += copyCount;
        }
        return readCharacters;
    }


    /**
     * Fill our buffer of input characters for decoding from the
     * stream.  This will attempt read a full buffer, but will
     * terminate on an EOF or read error.  This will filter out
     * non-Base64 encoding chars and will only return a valid
     * multiple of 4 number of bytes.
     *
     * @return The count of characters read.
     */
    private int fillEncodedBuffer() throws IOException {
        int readCharacters = 0;

        while (true) {
            // get the next character from the stream
            int ch = in.read();
            // did we hit an EOF condition?
            if (ch == -1) {
                // now check to see if this is normal, or potentially an error
                // if we didn't get characters as a multiple of 4, we may need to complain about this.
                if ((readCharacters % 4) != 0) {
                    throw new IOException("Base64 encoding error, data truncated: " + readCharacters + " "
                                          + new String(encodedChars, 0, readCharacters));
                }
                // return the count.
                return readCharacters;
            } else if (Base64Utility.isValidBase64(ch)) {
                // if this character is valid in a Base64 stream, copy it to the buffer.
                encodedChars[readCharacters++] = (char)ch;
                // if we've filled up the buffer, time to quit.
                if (readCharacters >= encodedChars.length) {
                    return readCharacters;
                }
            }

            // we're filtering out whitespace and CRLF characters, so just ignore these
        }
    }


    // in order to function as a filter, these streams need to override the different
    // read() signature.

    @Override
    public int read() throws IOException {
        return getByte();
    }


    @Override
    public int read(byte [] buffer, int offset, int length) throws IOException {
        return getBytes(buffer, offset, length);
    }

    
    @Override
    public boolean markSupported() {
        return false;
    }


    @Override
    public int available() throws IOException {
        return Math.addExact((in.available() / 4) * 3, decodedCount);
    }
}
