/*******************************************************************************
 * Copyright (c) 2000, 2006 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.mylyn.commons.core.io;

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

import org.eclipse.core.runtime.IProgressMonitor;

/**
 * Updates a progress monitor as bytes are read from the input stream. Also starts a background thread to provide
 * responsive cancellation on read(). Supports resuming partially completed operations after an InterruptedIOException
 * if the underlying stream does. Check the bytesTransferred field to determine how much of the operation completed;
 * conversely, at what point to resume.
 * 
 * @since 3.7
 */
public abstract class ProgressMonitorInputStream extends FilterInputStream {

	private final IProgressMonitor monitor;

	private final int updateIncrement;

	private final long bytesTotal;

	private long bytesRead = 0;

	private long lastUpdate = -1;

	private long nextUpdate = 0;

	/**
	 * Creates a progress monitoring input stream.
	 * 
	 * @param in
	 *            the underlying input stream
	 * @param bytesTotal
	 *            the number of bytes to read in total (passed to updateMonitor())
	 * @param updateIncrement
	 *            the number of bytes read between updates
	 * @param monitor
	 *            the progress monitor
	 */
	public ProgressMonitorInputStream(InputStream in, long bytesTotal, int updateIncrement, IProgressMonitor monitor) {
		super(in);
		this.bytesTotal = bytesTotal;
		this.updateIncrement = updateIncrement;
		this.monitor = monitor;
		update(true);
	}

	protected abstract void updateMonitor(long bytesRead, long size, IProgressMonitor monitor);

	/**
	 * Wraps the underlying stream's method. Updates the progress monitor to the final number of bytes read.
	 * 
	 * @throws IOException
	 *             if an i/o error occurs
	 */
	@Override
	public void close() throws IOException {
		try {
			in.close();
		} finally {
			update(true);
		}
	}

	/**
	 * Wraps the underlying stream's method. Updates the progress monitor if the next update increment has been reached.
	 * 
	 * @throws InterruptedIOException
	 *             if the operation was interrupted before all of the bytes specified have been skipped,
	 *             bytesTransferred will be zero
	 * @throws IOException
	 *             if an i/o error occurs
	 */
	@Override
	public int read() throws IOException {
		int b = in.read();
		if (b != -1) {
			bytesRead += 1;
			update(false);
		}
		return b;
	}

	/**
	 * Wraps the underlying stream's method. Updates the progress monitor if the next update increment has been reached.
	 * 
	 * @throws InterruptedIOException
	 *             if the operation was interrupted before all of the bytes specified have been skipped,
	 *             bytesTransferred may be non-zero
	 * @throws IOException
	 *             if an i/o error occurs
	 */
	@Override
	public int read(byte[] buffer, int offset, int length) throws IOException {
		try {
			int count = in.read(buffer, offset, length);
			if (count != -1) {
				bytesRead += count;
				update(false);
			}
			return count;
		} catch (InterruptedIOException e) {
			bytesRead += e.bytesTransferred;
			update(false);
			throw e;
		}
	}

	/**
	 * Wraps the underlying stream's method. Updates the progress monitor if the next update increment has been reached.
	 * 
	 * @throws InterruptedIOException
	 *             if the operation was interrupted before all of the bytes specified have been skipped,
	 *             bytesTransferred may be non-zero
	 * @throws IOException
	 *             if an i/o error occurs
	 */
	@Override
	public long skip(long amount) throws IOException {
		try {
			long count = in.skip(amount);
			bytesRead += count;
			update(false);
			return count;
		} catch (InterruptedIOException e) {
			bytesRead += e.bytesTransferred;
			update(false);
			throw e;
		}
	}

	/**
	 * Mark is not supported by the wrapper even if the underlying stream does, returns false.
	 */
	@Override
	public boolean markSupported() {
		return false;
	}

	private void update(boolean now) {
		if (bytesRead >= nextUpdate || now) {
			nextUpdate = bytesRead - (bytesRead % updateIncrement);
			if (nextUpdate != lastUpdate) {
				updateMonitor(nextUpdate, bytesTotal, monitor);
			}
			lastUpdate = nextUpdate;
			nextUpdate += updateIncrement;
		}
	}
}
