/*
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * 
 *      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.eclipse.webdav.http.client;

import java.io.*;
import org.eclipse.webdav.client.Policy;
import org.eclipse.webdav.internal.kernel.utils.Assert;

/**
 * A resettable <code>InputStream</code>.
 * <p>
 * <b>Note:</b> This class/interface is part of an interim API that is still under 
 * development and expected to change significantly before reaching stability. 
 * It is being made available at this early stage to solicit feedback from pioneering 
 * adopters on the understanding that any code that uses this API will almost 
 * certainly be broken (repeatedly) as the API evolves.
 * </p>
 */
public class RequestInputStream extends InputStream {
    private long length = -1;
    private long totalBytesRead = 0;
    private InputStream is = null;
    private File file = null;
    private FileOutputStream fos = null;
    private boolean deleteFile = false;

    /**
     * Creates a <code>RequestInputStream</code> on the given byte array.
     *
     * @param b the underlying byte array
     */
    public RequestInputStream(byte[] b) {
        length = b.length;
        is = new ByteArrayInputStream(b);
    }

    /**
     * Creates a <code>RequestInputStream</code> on the given
     * <code>ByteArrayInputStream</code>.
     *
     * @param bais the underlying input stream
     */
    public RequestInputStream(ByteArrayInputStream bais) {
        Assert.isNotNull(bais);
        length = bais.available();
        is = bais;
    }

    /**
     * Creates a <code>RequestInputStream</code> on the given file.
     *
     * @param file the underlying file
     * @exception IOException if there is a problem opening the file
     */
    public RequestInputStream(File file) throws IOException {
        Assert.isNotNull(file);
        length = file.length();
        is = new FileInputStream(file);
        this.file = file;
    }

    /**
     * Creates a <code>RequestInputStream</code> on the given
     * <code>InputStream</code>. The length of the stream is set to be the
     * given length. If the length of the stream is unknown, the given length
     * must be -1.
     * <p>Note that to enable reset on streams created using this constructor,
     * the streams content is written to a temporary file while the stream
     * is read. This results in a loss of performance, so use this
     * constructor as a last resort.
     *
     * @param is the underlying input stream
     * @param length the length of the stream, or -1 if the length is unknown
     * @exception IOException if there is a problem creating or opening the
     * temporary file
     */
    public RequestInputStream(InputStream is, long length) throws IOException {
        Assert.isNotNull(is);
        Assert.isTrue(length >= -1);
        this.length = length;
        this.is = is;
        file = File.createTempFile("ris", ".tmp"); //$NON-NLS-1$ //$NON-NLS-2$
        fos = new FileOutputStream(file);
        deleteFile = true;
    }

    /**
     * @see InputStream#close()
     */
    public void close() throws IOException {
        is.close();
        if (fos != null)
            fos.close();
        if (deleteFile)
            file.delete();
    }

    /**
     * Returns the length of the stream, or -1 if the length of the stream is
     * unknown. Note the length of the stream is always known once the stream
     * has been reset.
     *
     * @return the length of the stream
     */
    public long length() {
        return length;
    }

    /**
     * @see InputStream#read()
     */
    public int read() throws IOException {
        int b = is.read();

        if (b == -1) {
            if (length != -1 && totalBytesRead < length) {
                throw new IOException(Policy.bind("exception.unexpectedEndStream")); //$NON-NLS-1$
            }
        } else {
            ++totalBytesRead;
            if (fos != null) {
                fos.write(b);
            }
        }

        return b;
    }

    /**
     * @see InputStream#read(byte[], int, int)
     */
    public int read(byte b[], int off, int len) throws IOException {
        int bytesRead = is.read(b, off, len);
        if (bytesRead == -1) {
            if (length != -1 && totalBytesRead < length) {
                throw new IOException(Policy.bind("exception.unexpectedEndStream")); //$NON-NLS-1$
            }
        } else {
            totalBytesRead += bytesRead;
            if (fos != null) {
                fos.write(b, off, len);
            }
        }
        return bytesRead;
    }

    /**
     * Resets the stream to its beginning so it can be read again.
     *
     * @exception IOException if there is an I/O error
     */
    public void reset() throws IOException {
        if (file == null) {
            ((ByteArrayInputStream) is).reset();
        } else {
            if (fos != null) {
                while (skip(4096) > 0);
                fos.close();
                fos = null;
                if (length == -1) {
                    length = totalBytesRead;
                }
            }
            is.close();
            is = new FileInputStream(file);
        }

        totalBytesRead = 0;
    }
}
