/*
 * Copyright (c) 2010-2015 Pivotal Software, Inc. All rights reserved.
 *
 * Licensed 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. See accompanying
 * LICENSE file.
 */
package com.gemstone.gemfire.internal.io;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * Delegates all operations to a collection of OutputStreams.
 * 
 * @author Kirk Lund
 * @since 7.0
 */
public class CompositeOutputStream extends OutputStream implements Iterable<OutputStream> {

  protected final Object lock = new Object();
  
  private volatile Set<OutputStream> streams = Collections.<OutputStream>emptySet();
  
  /**
   * Constructs a new instance of CompositeOutputStream with zero or more OutputStreams.
   * 
   * @param out zero or more OutputStreams to add to the new instance of CompositeOutputStream
   */
  public CompositeOutputStream(OutputStream... out) {
    final Set<OutputStream> newSet = new HashSet<OutputStream>();
    for (OutputStream stream : out) {
      newSet.add(stream);
    }
    this.streams = newSet;
  }

  /**
   * @return <tt>true</tt> if this CompositeOutputStream did not already contain the specified OutputStream
   */
  public boolean addOutputStream(OutputStream out) {
    synchronized(this.lock) {
      final Set<OutputStream> oldSet = this.streams;
      if (oldSet.contains(out)) {
        return false;
      } else {
        final Set<OutputStream> newSet = new HashSet<OutputStream>(oldSet);
        final boolean added = newSet.add(out);
        this.streams = newSet;
        return added;
      }
    }
  }
  
  /**
   * @return <tt>true</tt> if this CompositeOutputStream contained the specified OutputStream
   */
  public boolean removeOutputStream(OutputStream out) {
    synchronized(this.lock) {
      final Set<OutputStream> oldSet = this.streams;
      if (!oldSet.contains(out)) {
        return false;
      } else if (oldSet.size() == 1) {
        this.streams = Collections.<OutputStream>emptySet();
        return true;
      } else {
        final Set<OutputStream> newSet = new HashSet<OutputStream>(oldSet);
        final boolean removed = newSet.remove(out);
        this.streams = newSet;
        return removed;
      }
    }
  }
  
  /**
   * Returns <tt>true</tt> if this CompositeOutputStream contains no OutputStreams.
   *
   * @return <tt>true</tt> if this CompositeOutputStream contains no OutputStreams
   */
  public boolean isEmpty() {
    return this.streams.isEmpty();
  }
  
  /**
   * Returns the number of OutputStreams in this CompositeOutputStream (its cardinality).
   *
   * @return the number of OutputStreams in this CompositeOutputStream (its cardinality)
   */
  public int size() {
    return this.streams.size();
  }
  
  public Iterator<OutputStream> iterator() {
    return this.streams.iterator();
  }
  
  /**
   * Writes the specified <code>byte</code> to this output stream. 
   * <p>
   * The <code>write</code> method of <code>FilterOutputStream</code> 
   * calls the <code>write</code> method of its underlying output stream, 
   * that is, it performs <tt>out.write(b)</tt>.
   * <p>
   * Implements the abstract <tt>write</tt> method of <tt>OutputStream</tt>. 
   *
   * @param      b   the <code>byte</code>.
   * @exception  IOException  if an I/O error occurs.
   */
  public void write(int b) throws IOException {
    Set<OutputStream> outputStreams = this.streams;
    for (OutputStream out : outputStreams) {
      out.write(b);
    }
  }

  /**
   * Flushes this output stream and forces any buffered output bytes 
   * to be written out to the stream. 
   * <p>
   * The <code>flush</code> method of <code>FilterOutputStream</code> 
   * calls the <code>flush</code> method of its underlying output stream. 
   *
   * @exception  IOException  if an I/O error occurs.
   * @see        java.io.FilterOutputStream#out
   */
  public void flush() throws IOException {
    Set<OutputStream> outputStreams = this.streams;
    for (OutputStream out : outputStreams) {
      out.flush();
    }
  }

  /**
   * Closes this output stream and releases any system resources 
   * associated with the stream. 
   * <p>
   * The <code>close</code> method of <code>FilterOutputStream</code> 
   * calls its <code>flush</code> method, and then calls the 
   * <code>close</code> method of its underlying output stream. 
   *
   * @exception  IOException  if an I/O error occurs.
   * @see        java.io.FilterOutputStream#flush()
   * @see        java.io.FilterOutputStream#out
   */
  public void close() throws IOException {
    Set<OutputStream> outputStreams = this.streams;
    for (OutputStream out : outputStreams) {
      try {
        out.flush();
      } catch (IOException ignored) {
      }
      out.close();
    }
  }
  
  @Override
  public String toString() {
    final StringBuilder sb = new StringBuilder(getClass().getSimpleName());
    sb.append("@").append(System.identityHashCode(this)).append("{");
    sb.append("size=").append(this.streams.size());
    return sb.append("}").toString();
  }
}
