/*
 * 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.geode.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.
 *
 * @since GemFire 7.0
 */
public class CompositeOutputStream extends OutputStream implements Iterable<OutputStream> {

  protected final Object lock = new Object();

  private volatile Set<OutputStream> streams = Collections.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<>();
    for (OutputStream stream : out) {
      newSet.add(stream);
    }
    streams = newSet;
  }

  /**
   * @return <tt>true</tt> if this CompositeOutputStream did not already contain the specified
   *         OutputStream
   */
  public boolean addOutputStream(OutputStream out) {
    synchronized (lock) {
      final Set<OutputStream> oldSet = streams;
      if (oldSet.contains(out)) {
        return false;
      } else {
        final Set<OutputStream> newSet = new HashSet<>(oldSet);
        final boolean added = newSet.add(out);
        streams = newSet;
        return added;
      }
    }
  }

  /**
   * @return <tt>true</tt> if this CompositeOutputStream contained the specified OutputStream
   */
  public boolean removeOutputStream(OutputStream out) {
    synchronized (lock) {
      final Set<OutputStream> oldSet = streams;
      if (!oldSet.contains(out)) {
        return false;
      } else if (oldSet.size() == 1) {
        streams = Collections.emptySet();
        return true;
      } else {
        final Set<OutputStream> newSet = new HashSet<>(oldSet);
        final boolean removed = newSet.remove(out);
        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 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 streams.size();
  }

  @Override
  public Iterator<OutputStream> iterator() {
    return 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.
   */
  @Override
  public void write(int b) throws IOException {
    Set<OutputStream> outputStreams = streams;
    for (OutputStream out : outputStreams) {
      out.write(b);
    }
  }

  @Override
  public void write(byte[] b, int off, int len) throws IOException {
    Set<OutputStream> outputStreams = streams;
    for (OutputStream out : outputStreams) {
      out.write(b, off, len);
    }
  }

  /**
   * 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 OutputStream#flush()
   */
  @Override
  public void flush() throws IOException {
    Set<OutputStream> outputStreams = 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 OutputStream#flush()
   */
  @Override
  public void close() throws IOException {
    Set<OutputStream> outputStreams = streams;
    for (OutputStream out : outputStreams) {
      try {
        out.flush();
      } catch (IOException ignored) {
      }
      out.close();
    }
  }

  @Override
  public String toString() {
    return getClass().getSimpleName() + "@" + System.identityHashCode(this) + "{"
        + "size=" + streams.size()
        + "}";
  }
}
