/*******************************************************************************
 * Copyright (c) 2006, 2008 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.equinox.internal.transforms;

import java.io.*;

/**
 * This class facilitates the moving of data from one input stream to another.  
 * Subclasses may customize the behavior of this move by overriding the {@link #pipeInput(InputStream, OutputStream)} method.
 */
public class Pipe {

	protected InputStream input;
	private PipedInputStream pipedInputStream;
	protected PipedOutputStream pipedOutputStream;

	/**
	 * Create a new Pipe based on the provided input stream.
	 * @param original the original stream.
	 * @throws IOException thrown if there is an issue establishing the pipe.
	 */
	public Pipe(InputStream original) throws IOException {
		this.input = original;

		// The following streams do the majority of the work.  
		// The first operation on the input stream will provoke a new thread to start.
		// This thread will invoke pipeInput and push the data from the original input stream to the output stream.
		// The output stream is tied to this input stream via PipedI/OStream properties so the data is available to callers on the input stream.
		// Any IOException thrown from within the thread will be caught and rethrown to callers of methods on this stream.
		this.pipedInputStream = new PipedInputStream() {
			protected IOException failure;
			private boolean started = false;
			protected Object lock = this;

			private void start() throws IOException {
				synchronized (lock) {
					if (failure != null) {
						IOException e = new IOException("Problem piping the stream."); //$NON-NLS-1$
						e.fillInStackTrace();
						e.initCause(failure);
						throw e;
					}
					if (!started) {
						started = true;
						Thread pipeThread = new Thread(new Runnable() {
							@Override
							public void run() {
								try {
									pipeInput(input, pipedOutputStream);
									pipedOutputStream.close();
								} catch (IOException e) {
									synchronized (lock) {
										failure = e;
									}
								}
							}
						});
						pipeThread.start();
					}
				}
			}

			@Override
			public synchronized int available() throws IOException {
				start();
				return super.available();
			}

			@Override
			public synchronized int read() throws IOException {
				start();
				int c = super.read();
				return c;
			}

			@Override
			public int read(byte[] b) throws IOException {
				start();
				return super.read(b);
			}

			@Override
			public synchronized int read(byte[] b, int off, int len) throws IOException {
				start();
				return super.read(b, off, len);
			}

			@Override
			public synchronized void reset() throws IOException {
				started = false;
				failure = null;
				input.reset();
				super.reset();
			}
		};
		this.pipedOutputStream = new PipedOutputStream(pipedInputStream);

	}

	/**
	 * Get the stream that has resulted from piping the original input stream through {@link #pipeInput(InputStream, OutputStream)}.
	 * @return the new stream.
	 */
	public InputStream getPipedInputStream() {
		return pipedInputStream;
	}

	/**
	 * Pipe the input stream to the output stream.
	 * The default implementation of this method does a simple copy operations.  
	 * Subclasses may elaborate on this behavior.
	 * @param original the original stream
	 * @param result the result stream
	 * @throws IOException thrown if there is an issue reading from the input stream or writing to the output stream.
	 */
	protected void pipeInput(InputStream original, OutputStream result) throws IOException {
		byte[] buffer = new byte[2048];
		int len = 0;
		while ((len = original.read(buffer)) != 0) {
			result.write(buffer, 0, len);
		}
	}
}
