/* See LICENSE for licensing and NOTICE for copyright. */
package org.cryptacular.io;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import org.cryptacular.CryptUtil;
import org.cryptacular.codec.Base64Encoder;
import org.cryptacular.codec.Encoder;
import org.cryptacular.codec.HexEncoder;

/**
 * Filters written bytes through an {@link Encoder} such that encoded data is written to the underlying output stream.
 *
 * @author  Middleware Services
 */
public class EncodingOutputStream extends FilterOutputStream
{

  /** Performs decoding. */
  private final Encoder encoder;

  /** Wraps the output stream to convert characters to bytes. */
  private final OutputStreamWriter writer;

  /** Receives encoding result. */
  private CharBuffer output;


  /**
   * Creates a new instance that wraps the given stream and performs encoding using the given encoder component.
   *
   * @param  out  Output stream to wrap.
   * @param  e  Encoder that provides on-the-fly encoding.
   */
  public EncodingOutputStream(final OutputStream out, final Encoder e)
  {
    super(CryptUtil.assertNotNullArg(out, "Output stream cannot be null"));
    encoder = CryptUtil.assertNotNullArg(e, "Encoder cannot be null");
    writer = new OutputStreamWriter(out);
  }


  @Override
  public void write(final int b)
    throws IOException
  {
    write(new byte[] {(byte) b});
  }


  @Override
  public void write(final byte[] b)
    throws IOException
  {
    CryptUtil.assertNotNullArg(b, "Byte array cannot be null");
    write(b, 0, b.length);
  }


  @Override
  public void write(final byte[] b, final int off, final int len)
    throws IOException
  {
    CryptUtil.assertNotNullArg(b, "Byte array cannot be null");
    final ByteBuffer input = ByteBuffer.wrap(b, off, len);
    final int required = encoder.outputSize(len - off);
    if (output == null || output.capacity() < required) {
      output = CharBuffer.allocate(required);
    } else {
      output.clear();
    }
    encoder.encode(input, output);
    output.flip();
    writer.write(output.toString());
    writer.flush();
  }


  @Override
  public void flush()
    throws IOException
  {
    writer.flush();
  }


  @Override
  public void close()
    throws IOException
  {
    if (output == null) {
      output = CharBuffer.allocate(8);
    } else {
      output.clear();
    }
    encoder.finalize(output);
    output.flip();
    writer.write(output.toString());
    writer.flush();
    writer.close();
  }


  /**
   * Creates a new instance that produces base64 output in the given stream.
   *
   * <p><strong>NOTE:</strong> there are no line breaks in the output with this version.</p>
   *
   * @param  out  Wrapped output stream.
   *
   * @return  Encoding output stream that produces base64 output.
   */
  public static EncodingOutputStream base64(final OutputStream out)
  {
    return base64(out, -1);
  }


  /**
   * Creates a new instance that produces base64 output in the given stream.
   *
   * <p><strong>NOTE:</strong> this version supports output with configurable line breaks.</p>
   *
   * @param  out  Wrapped output stream.
   * @param  lineLength  Length of each base64-encoded line in output. A zero or negative value disables line breaks.
   *
   * @return  Encoding output stream that produces base64 output.
   */
  public static EncodingOutputStream base64(final OutputStream out, final int lineLength)
  {
    return new EncodingOutputStream(out, new Base64Encoder(lineLength));
  }


  /**
   * Creates a new instance that produces hexadecimal output in the given stream.
   *
   * <p><strong>NOTE:</strong> there are no line breaks in the output.</p>
   *
   * @param  out  Wrapped output stream.
   *
   * @return  Encoding output stream that produces hexadecimal output.
   */
  public static EncodingOutputStream hex(final OutputStream out)
  {
    return new EncodingOutputStream(out, new HexEncoder());
  }
}
