/*
 * 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.brooklyn.util.stream;

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;

import org.apache.commons.io.output.TeeOutputStream;

public class ThreadLocalPrintStream extends DelegatingPrintStream {

    protected PrintStream defaultPrintStream;
    protected final ThreadLocal<PrintStream> customStream = new ThreadLocal<PrintStream>();
    
    public ThreadLocalPrintStream(PrintStream defaultPrintStream) {
        this.defaultPrintStream = defaultPrintStream;
    }
    
    @Override
    public PrintStream getDelegate() {
        PrintStream delegate = customStream.get();
        if (delegate!=null) return delegate;
        return defaultPrintStream;
    }

    /** sets the PrintStream that callers from this thread should see;
     * returns any previously custom PrintStream for this thread */
    public PrintStream setThreadLocalPrintStream(OutputStream stream) {
        PrintStream old = customStream.get();
        if (!(stream instanceof PrintStream))
            stream = new PrintStream(stream);
        customStream.set((PrintStream)stream);
        return old;
    }

    public PrintStream clearThreadLocalPrintStream() {
        PrintStream old = customStream.get();
        customStream.remove();
        if (old!=null) old.flush();
        return old;
    }
    
    /** creates a capturing context which eats the output to this stream, blocking the original target */
    public OutputCapturingContext capture() {
        return new OutputCapturingContext(this);
    }
    
    /** creates a capturing context which sees the output to this stream, without interrupting the original target */
    public OutputCapturingContext captureTee() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PrintStream toRestore = setThreadLocalPrintStream(new TeeOutputStream(getDelegate(), out));
        return new OutputCapturingContext(this, out, toRestore);
    }
    
    public static class OutputCapturingContext {
        protected final ThreadLocalPrintStream stream;
        protected final ByteArrayOutputStream out;
        protected final OutputStream streamToRestore;
        protected boolean finished = false;
        /** constructor which installs a ByteArrayOutputStream to this stream */
        public OutputCapturingContext(ThreadLocalPrintStream stream) {
            this.stream = stream;
            this.out = new ByteArrayOutputStream();
            this.streamToRestore = stream.setThreadLocalPrintStream(out);
        }
        /** constructor for a capturing context which is already installed */
        public OutputCapturingContext(ThreadLocalPrintStream stream, ByteArrayOutputStream capturingStream, OutputStream optionalStreamToRestore) {
            this.stream = stream;
            this.out = capturingStream;
            this.streamToRestore = optionalStreamToRestore;
        }
        public String getOutputSoFar() {
            return out.toString();
        }
        public String end() {
            if (streamToRestore!=null)
                stream.setThreadLocalPrintStream(streamToRestore);
            else
                stream.clearThreadLocalPrintStream();
            finished = true;
            return out.toString();
        }
        public boolean isActive() {
            return !finished;
        }
        public ByteArrayOutputStream getOutputStream() {
            return out;
        }
        @Override
        public String toString() {
            return getOutputSoFar();
        }
        public boolean isEmpty() {
            return out.size()==0;
        }
    }
    
    /** installs a thread local print stream to System.out if one is not already set;
     * caller may then #capture and #captureTee on it.
     * @return the ThreadLocalPrintStream which System.out is using */
    public synchronized static ThreadLocalPrintStream stdout() {
        PrintStream oldOut = System.out;
        if (oldOut instanceof ThreadLocalPrintStream) return (ThreadLocalPrintStream)oldOut;
        ThreadLocalPrintStream newOut = new ThreadLocalPrintStream(System.out);
        System.setOut(newOut);
        return newOut;
    }

    /** installs a thread local print stream to System.err if one is not already set;
     * caller may then #capture and #captureTee on it.
     * @return the ThreadLocalPrintStream which System.err is using */
    public synchronized static ThreadLocalPrintStream stderr() {
        PrintStream oldErr = System.err;
        if (oldErr instanceof ThreadLocalPrintStream) return (ThreadLocalPrintStream)oldErr;
        ThreadLocalPrintStream newErr = new ThreadLocalPrintStream(System.err);
        System.setErr(newErr);
        return newErr;
    }

}
