/*******************************************************************************
 * 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;
        }

    }
}
