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

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

import java.io.*;
import java.nio.charset.*;

import org.apache.juneau.*;
import org.apache.juneau.internal.*;

/**
 * A wrapper around an object that a serializer sends its output to.
 *
 * <p>
 * For character-based serializers, the output object can be any of the following:
 * <ul>
 * 	<li>{@link Writer}
 * 	<li>{@link OutputStream} - Output will be written as UTF-8 encoded stream.
 * 	<li>{@link File} - Output will be written as system-default encoded stream.
 * 	<li>{@link StringBuilder}
 * </ul>
 *
 * <p>
 * For stream-based serializers, the output object can be any of the following:
 * <ul>
 * 	<li>{@link OutputStream}
 * 	<li>{@link File}
 * </ul>
 */
public final class SerializerPipe implements Closeable {

	private final Object output;
	private final boolean autoClose;

	private OutputStream outputStream;
	private Writer writer;
	private Charset charset;

	/**
	 * Writer-based constructor.
	 *
	 * @param output The object to pipe the serializer output to.
	 */
	SerializerPipe(Object output, Charset streamCharset, Charset fileCharset) {
		boolean isFile = (output instanceof File);
		this.output = output;
		this.autoClose = isFile;
		Charset cs = isFile ? fileCharset : streamCharset;
		if (cs == null)
			cs = isFile ? Charset.defaultCharset() : UTF8;
		this.charset = cs;
	}

	/**
	 * Stream-based constructor.
	 *
	 * @param output The object to pipe the serializer output to.
	 */
	SerializerPipe(Object output) {
		this.output = output;
		this.autoClose = false;
		this.charset = null;
	}

	/**
	 * Wraps the specified output object inside an output stream.
	 *
	 * <p>
	 * Subclasses can override this method to implement their own specialized output streams.
	 *
	 * <p>
	 * This method can be used if the output object is any of the following class types:
	 * <ul>
	 * 	<li>{@link OutputStream}
	 * 	<li>{@link File}
	 * </ul>
	 *
	 * @return
	 * 	The output object wrapped in an output stream.
	 * 	Calling {@link OutputStream#close()} on the returned object simply flushes the response and does not close
	 * 	the underlying stream.
	 * @throws IOException If object could not be converted to an output stream.
	 */
	public OutputStream getOutputStream() throws IOException {
		if (output == null)
			throw new IOException("Output cannot be null.");

		if (output instanceof OutputStream)
			outputStream = (OutputStream)output;
		else if (output instanceof File)
			outputStream = new BufferedOutputStream(new FileOutputStream((File)output));
		else
			throw new IOException("Cannot convert object of type "+output.getClass().getName()+" to an OutputStream.");

		return new NoCloseOutputStream(outputStream);
	}


	/**
	 * Wraps the specified output object inside a writer.
	 *
	 * <p>
	 * Subclasses can override this method to implement their own specialized writers.
	 *
	 * <p>
	 * This method can be used if the output object is any of the following class types:
	 * <ul>
	 * 	<li>{@link Writer}
	 * 	<li>{@link OutputStream} - Output will be written as UTF-8 encoded stream.
	 * 	<li>{@link File} - Output will be written as system-default encoded stream.
	 * </ul>
	 *
	 * @return
	 * 	The output object wrapped in a writer.
	 * 	Calling {@link Writer#close()} on the returned object simply flushes the response and does not close
	 * 	the underlying writer.
	 * @throws IOException If object could not be converted to a writer.
	 */
	public Writer getWriter() throws IOException {
		if (output == null)
			throw new IOException("Output cannot be null.");

		if (output instanceof Writer)
			writer = (Writer)output;
		else if (output instanceof OutputStream)
			writer = new OutputStreamWriter((OutputStream)output, charset);
		else if (output instanceof File)
			writer = new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream((File)output)));
		else if (output instanceof StringBuilder)
			writer = new StringBuilderWriter((StringBuilder)output);
		else
			throw new IOException("Cannot convert object of type "+output.getClass().getName()+" to a Writer.");

		return new NoCloseWriter(writer);
	}

	/**
	 * Overwrites the writer in this pipe.
	 *
	 * <p>
	 * Used when wrapping the writer returned by {@link #getWriter()} so that the wrapped writer will be flushed
	 * and closed when {@link #close()} is called.
	 *
	 * @param writer The wrapped writer.
	 */
	public void setWriter(Writer writer) {
		this.writer = writer;
	}

	/**
	 * Overwrites the output stream in this pipe.
	 *
	 * <p>
	 * Used when wrapping the stream returned by {@link #getOutputStream()} so that the wrapped stream will be flushed
	 * when {@link #close()} is called.
	 *
	 * @param outputStream The wrapped stream.
	 */
	public void setOutputStream(OutputStream outputStream) {
		this.outputStream = outputStream;
	}

	/**
	 * Returns the raw output object passed into this session.
	 *
	 * @return The raw output object passed into this session.
	 */
	public Object getRawOutput() {
		return output;
	}

	/**
	 * Closes the output pipe.
	 */
	@Override /* Closeable */
	public void close() {
		try {
			IOUtils.flush(writer, outputStream);
			if (autoClose)
				IOUtils.close(writer, outputStream);
		} catch (IOException e) {
			throw new BeanRuntimeException(e);
		}
	}
}
