// ***************************************************************************************************************************
// * 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.juneau.utils;

import static org.apache.juneau.internal.IOUtils.*;
import static org.apache.juneau.internal.ThrowableUtils.*;

import java.io.*;
import java.util.*;

import org.apache.juneau.internal.*;

/**
 * A utility class for piping input streams and readers to output streams and writers.
 *
 * <p>
 * A typical usage is as follows...
 * <p class='bcode w800'>
 * 	InputStream in = getInputStream();
 * 	Writer out = getWriter();
 * 	IOPipe.create(in, out).closeOut().run();
 * </p>
 *
 * <p>
 * By default, the input stream is closed and the output stream is not.
 * This can be changed by calling {@link #closeOut()} and {@link #close(boolean, boolean)}.
 */
public class IOPipe {

	private Object input, output;
	private boolean byLines;
	private boolean closeIn = true, closeOut;
	private int buffSize = 1024;
	private LineProcessor lineProcessor;

	private IOPipe(Object input, Object output) {
		assertFieldNotNull(input, "input");
		assertFieldNotNull(output, "output");

		if (input instanceof InputStream || input instanceof Reader || input instanceof File || input instanceof byte[] || input instanceof CharSequence || input == null)
			this.input = input;
		else
			illegalArg("Invalid input class type.  Must be one of the following:  InputStream, Reader, CharSequence, byte[], File");

		if (output instanceof OutputStream || output instanceof Writer)
			this.output = output;
		else
			illegalArg("Invalid output class type.  Must be one of the following:  OutputStream, Writer");
	}

	/**
	 * Creates a new pipe with the specified input and output.
	 *
	 * @param input The input.  Must be one of the following types:  Reader, InputStream, CharSequence.
	 * @param output The output.  Must be one of the following types:  Writer, OutputStream.
	 * @return This object (for method chaining).
	 */
	public static IOPipe create(Object input, Object output) {
		return new IOPipe(input, output);
	}

	/**
	 * Close output after piping.
	 *
	 * @return This object (for method chaining).
	 */
	public IOPipe closeOut() {
		this.closeOut = true;
		return this;
	}

	/**
	 * Specifies whether to close the input and output after piping.
	 *
	 * @param in Close input stream.  Default is <jk>true</jk>.
	 * @param out Close output stream.  Default is <jk>false</jk>.
	 * @return This object (for method chaining).
	 */
	public IOPipe close(boolean in, boolean out) {
		this.closeIn = in;
		this.closeOut = out;
		return this;
	}

	/**
	 * Specifies the temporary buffer size.
	 *
	 * @param buffSize The buffer size.  Default is <c>1024</c>.
	 * @return This object (for method chaining).
	 */
	public IOPipe buffSize(int buffSize) {
		assertFieldPositive(buffSize, "buffSize");
		this.buffSize = buffSize;
		return this;
	}

	/**
	 * Specifies whether the content should be piped line-by-line.
	 *
	 * <p>
	 * This can be useful if you're trying to pipe console-based input.
	 *
	 * @param byLines Pipe content line-by-line.  Default is <jk>false</jk>.
	 * @return This object (for method chaining).
	 */
	public IOPipe byLines(boolean byLines) {
		this.byLines = byLines;
		return this;
	}

	/**
	 * Same as calling {@link #byLines()} with <jk>true</jk>.
	 *
	 * @return This object (for method chaining).
	 */
	public IOPipe byLines() {
		this.byLines = true;
		return this;
	}

	/**
	 * Specifies a line processor that can be used to process lines before they're piped to the output.
	 *
	 * @param lineProcessor The line processor.
	 * @return This object (for method chaining).
	 */
	public IOPipe lineProcessor(LineProcessor lineProcessor) {
		this.lineProcessor = lineProcessor;
		return this;
	}

	/**
	 * Interface to implement for the {@link #lineProcessor(LineProcessor)} method.
	 */
	public interface LineProcessor {
		/**
		 * Process the specified line.
		 *
		 * @param line The line to process.
		 * @return The processed line.
		 */
		public String process(String line);
	}

	/**
	 * Performs the piping of the input to the output.
	 *
	 * @return The number of bytes (if streams) or characters (if readers/writers) piped.
	 * @throws IOException Thrown by underlying stream.
	 */
	public int run() throws IOException {

		int c = 0;

		try {
			if (input == null)
				return 0;

			if ((input instanceof InputStream || input instanceof byte[]) && output instanceof OutputStream && lineProcessor == null) {
				OutputStream out = (OutputStream)output;
				if (input instanceof InputStream) {
					InputStream in = (InputStream)input;
					byte[] b = new byte[buffSize];
					int i;
					while ((i = in.read(b)) > 0) {
						c += i;
						out.write(b, 0, i);
					}
				} else {
					byte[] b = (byte[])input;
					out.write(b);
					c = b.length;
				}
				out.flush();
			} else {
				@SuppressWarnings("resource")
				Writer out = (output instanceof Writer ? (Writer)output : new OutputStreamWriter((OutputStream)output, UTF8));
				closeIn |= input instanceof File;
				if (byLines || lineProcessor != null) {
					Reader in = null;
					if (input instanceof Reader)
						in = (Reader)input;
					else if (input instanceof InputStream)
						in = new InputStreamReader((InputStream)input, UTF8);
					else if (input instanceof File)
						in = new FileReader((File)input);
					else if (input instanceof byte[])
						in = new StringReader(new String((byte[])input, "UTF8"));
					else if (input instanceof CharSequence)
						in = new StringReader(input.toString());
					try (Scanner s = new Scanner(in)) {
						while (s.hasNextLine()) {
							String l = s.nextLine();
							if (lineProcessor != null)
								l = lineProcessor.process(l);
							if (l != null) {
								out.write(l);
								out.write("\n");
								out.flush();
								c += l.length() + 1;
							}
						}
					}
				} else {
					if (input instanceof InputStream)
						input = new InputStreamReader((InputStream)input, UTF8);
					else if (input instanceof File)
						input = new FileReader((File)input);
					else if (input instanceof byte[])
						input = new String((byte[])input, UTF8);

					if (input instanceof Reader) {
						Reader in = (Reader)input;
						int i;
						char[] b = new char[buffSize];
						while ((i = in.read(b)) > 0) {
							c += i;
							out.write(b, 0, i);
						}
					} else {
						String s = input.toString();
						out.write(s);
						c = s.length();
					}
				}
				out.flush();
			}
		} finally {
			closeQuietly(input, output);
		}
		return c;
	}

	private void closeQuietly(Object input, Object output) {
		if (closeIn)
			IOUtils.closeQuietly(input);
		if (closeOut)
			IOUtils.closeQuietly(output);
	}
}
