/*******************************************************************************
 * Copyright (c) 2010, 2017 SAP AG 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:
 *     Lazar Kirchev, SAP AG - initial API and implementation  
 *******************************************************************************/

package org.eclipse.equinox.console.common;

import java.io.IOException;
import java.io.OutputStream;

/**
 * This class wraps the actual output stream (e.g., a socket output stream) and is responsible for
 * buffering and flushing the characters to the actual output stream.
 */
public class ConsoleOutputStream extends OutputStream {

	/**
	 * A size of the used buffer.
	 */
	public static final int BUFFER_SIZE = 2048;
	public final static byte CR = (byte) '\r';
	public final static byte LF = (byte) '\n';

	OutputStream out;
	OutputStream oldOut;

	private boolean isEcho = true;
	private boolean queueing = false;
	private byte prevByte;
	private byte[] buffer;
	private int pos;

	/**
	 * Initiates with instance of the output stream to which it will send data. Here it writes to
	 * a socket output stream.
	 *
	 * @param out OutputStream for console output
	 */
	public ConsoleOutputStream(OutputStream out) {
		this.out = out;
		buffer = new byte[BUFFER_SIZE];
		pos = 0;
	}

	/**
	 * An implementation of the corresponding abstract method in OutputStream.
	 */
	@Override
	public synchronized void write(int i) throws IOException {

		if (!queueing) {
			if (isEcho) {
				if (i == '\r' || i == '\0') {
					queueing = true;
					prevByte = (byte) i;
				} else if (i == '\n') {
					add(CR);
					add(LF);
				} else {
					add(i);
				}
			}
		} else { // awaiting '\n' AFTER '\r', and '\b' AFTER '\0'
			if (prevByte == '\0' && i == '\b') {
				isEcho = !isEcho;
			} else if (isEcho) {
				if (prevByte == '\r' && i == '\n') {
					add(CR);
					add(LF);
				} else {
					add(CR);
					add(LF);
					add(i);
				}
			}

			queueing = false;
			flush();
		}

	}

	/**
	 * Empties the buffer and sends data to the socket output stream.
	 *
	 * @throws IOException
	 */
	@Override
	public synchronized void flush() throws IOException {
		if (pos > 0) {
			try {
				out.write(buffer, 0, pos);
				out.flush();
			} finally {
				pos = 0;
			}
			
		}
	}

	/**
	 * Adds a variable of type integer to the buffer.
	 *
	 * @param i integer to add
	 * @throws java.io.IOException if there are problems adding the integer
	 */
	private void add(int i) throws IOException {
		buffer[pos] = (byte) i;
		pos++;

		if (pos == buffer.length) {
			flush();
		}
	}

	/**
	 * Closes this OutputStream.
	 *
	 * @throws IOException
	 */
	@Override
	public void close() throws IOException {
		out.close();
	}

	/**
	 * Substitutes the output stream. The old one is stored so that it can be restored later.
	 *
	 * @param newOut new output stream to use.
	 */
	public void setOutput(OutputStream newOut) {
		if (newOut != null) {
			oldOut = out;
			out = newOut;
		} else {
			out = oldOut;
		}

	}
}
