/*
 *  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.harmony.luni.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;

/**
 * The class contains static {@link java.io.InputStream} utilities.
 */
public class InputStreamHelper {

    /**
     * Provides access to a protected underlying buffer of
     * <code>ByteArrayInputStream</code>.
     */
    private static final Field BAIS_BUF;

    /**
     * Provides access to a protected position in the underlying buffer of
     * <code>ByteArrayInputStream</code>.
     */
    private static final Field BAIS_POS;

    static {
        final Field[] f = new Field[2];
        AccessController.doPrivileged(new PrivilegedAction<Object>() {
            public Object run() {
                try {
                    f[0] = ByteArrayInputStream.class.getDeclaredField("buf");
                    f[0].setAccessible(true);
                    f[1] = ByteArrayInputStream.class.getDeclaredField("pos");
                    f[1].setAccessible(true);
                } catch (NoSuchFieldException nsfe) {
                    throw new InternalError(nsfe.getLocalizedMessage());
                }
                return null;
            }
        });
        BAIS_BUF = f[0];
        BAIS_POS = f[1];
    }

    /**
     * The extension of <code>ByteArrayInputStream</code> which exposes an
     * underlying buffer.
     */
    static class ExposedByteArrayInputStream extends ByteArrayInputStream {
        public ExposedByteArrayInputStream(byte[] buf) {
            super(buf);
        }

        public ExposedByteArrayInputStream(byte[] buf, int offset, int length) {
            super(buf, offset, length);
        }

        /**
         * Reads the whole stream and returns the stream snapshot.
         */
        public synchronized byte[] expose() {
            if (pos == 0 && count == buf.length) {
                skip(count);
                return buf;
            }

            final int available = available();
            final byte[] buffer = new byte[available];
            System.arraycopy(buf, pos, buffer, 0, available);
            skip(available);
            return buffer;
        }
    }

    /**
     * Reads all bytes from {@link java.io.ByteArrayInputStream} using its
     * underlying buffer directly.
     *
     * @return an underlying buffer, if a current position is at the buffer
     *         beginning, and an end position is at the buffer end, or a copy of
     *         the underlying buffer part.
     */
    private static byte[] expose(ByteArrayInputStream bais) {
        byte[] buffer, buf;
        int pos;
        synchronized (bais) {
            int available = bais.available();
            try {
                buf = (byte[]) BAIS_BUF.get(bais);
                pos = BAIS_POS.getInt(bais);
            } catch (IllegalAccessException iae) {
                throw new InternalError(iae.getLocalizedMessage());
            }
            if (pos == 0 && available == buf.length) {
                buffer = buf;
            } else {
                buffer = new byte[available];
                System.arraycopy(buf, pos, buffer, 0, available);
            }
            bais.skip(available);
        }
        return buffer;
    }

    /**
     * The utility method for reading the whole input stream into a snapshot
     * buffer. To speed up the access it works with an underlying buffer for a
     * given {@link java.io.ByteArrayInputStream}.
     *
     * @param is
     *            the stream to be read.
     * @return the snapshot wrapping the buffer where the bytes are read to.
     * @throws UnsupportedOperationException
     *             if the input stream data cannot be exposed
     */
    public static byte[] expose(InputStream is) throws IOException,
            UnsupportedOperationException {
        if (is instanceof ExposedByteArrayInputStream) {
            return ((ExposedByteArrayInputStream) is).expose();
        }

        if (is.getClass().equals(ByteArrayInputStream.class)) {
            return expose((ByteArrayInputStream) is);
        }

        // We don't know how to do this
        throw new UnsupportedOperationException();
    }

    /**
     * Reads all the bytes from the given input stream.
     *
     * Calls read multiple times on the given input stream until it receives an
     * end of file marker. Returns the combined results as a byte array. Note
     * that this method may block if the underlying stream read blocks.
     *
     * @param is
     *            the input stream to be read.
     * @return the content of the stream as a byte array.
     * @throws IOException
     *             if a read error occurs.
     */
    public static byte[] readFullyAndClose(InputStream is) throws IOException {

        try {
            // Initial read
            byte[] buffer = new byte[1024];
            int count = is.read(buffer);
            int nextByte = is.read();

            // Did we get it all in one read?
            if (nextByte == -1) {
                return Arrays.copyOf(buffer, count);
            }

            // Requires additional reads
            ByteArrayOutputStream baos = new ByteArrayOutputStream(count * 2);
            baos.write(buffer, 0, count);
            baos.write(nextByte);
            while (true) {
                count = is.read(buffer);
                if (count == -1) {
                    return baos.toByteArray();
                }
                baos.write(buffer, 0, count);
            }
        } finally {
            is.close();
        }
    }
}
