/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 */
package org.opends.server.protocols.asn1;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.locks.ReentrantLock;

import org.opends.server.types.ByteSequence;

/**
 * This class is for writing ASN.1 elements directly to an
 * NIO WritableByteChannel with an embedded ByteBuffer.
 * The NIO ByteBuffer will be flushed to the channel automatically
 * when full or when the flush() method is called.
 */
final class ASN1ByteChannelWriter implements ASN1Writer
{
  // The byte channel to write to.
  private final WritableByteChannel byteChannel;

  // The wrapped ASN.1 OutputStream writer.
  private final ASN1OutputStreamWriter writer;

  // The NIO ByteStringBuilder to write to.
  private final ByteBuffer byteBuffer;

  // The lock to ensure atomic message flush.
  private final ReentrantLock flushLock;

  /**
   * An adaptor class provides a streaming interface to write to a
   * NIO ByteBuffer. This class is also responsible for writing
   * the ByteBuffer out to the channel when full and clearing it.
   */
  private class ByteBufferOutputStream extends OutputStream
  {
    /**
     * {@inheritDoc}
     */
    @Override
    public void write(int i) throws IOException {
      if(!byteBuffer.hasRemaining())
      {
        // No more space left in the buffer, send out to the channel.
        ASN1ByteChannelWriter.this.lockAndFlush();
      }
      byteBuffer.put((byte)i);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void write(byte[] bytes) throws IOException {
      write(bytes, 0, bytes.length);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void write(byte[] bytes, int i, int i1) throws IOException {
      if(i < 0 || i1 < 0 || i + i1 > bytes.length)
        throw new IndexOutOfBoundsException();

      int bytesToWrite = i1;
      int len;
      while(bytesToWrite > 0)
      {
        len = byteBuffer.remaining();
        if(len < bytesToWrite)
        {
          byteBuffer.put(bytes, i + i1 - bytesToWrite, len);
          bytesToWrite -= len;
          // No more space left in the buffer, send out to the channel.
          ASN1ByteChannelWriter.this.lockAndFlush();
        }
        else
        {
          byteBuffer.put(bytes, i + i1 - bytesToWrite, bytesToWrite);
          bytesToWrite = 0;
        }
      }
    }
  }

  /**
   * Constructs a new ASN1ByteChannelWriter.
   *
   * @param byteChannel The WritableByteChannel to write to.
   * @param writeBufferSize The NIO ByteBuffer size.
   */
  ASN1ByteChannelWriter(WritableByteChannel byteChannel,
                               int writeBufferSize)
  {
    this.byteChannel = byteChannel;
    this.byteBuffer = ByteBuffer.allocate(writeBufferSize);
    this.flushLock = new ReentrantLock(false);

    ByteBufferOutputStream bufferStream = new ByteBufferOutputStream();
    this.writer = new ASN1OutputStreamWriter(bufferStream);
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeBoolean(boolean booleanValue) throws IOException {
    writer.writeBoolean(booleanValue);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeBoolean(byte type, boolean booleanValue)
      throws IOException {
    writer.writeBoolean(type, booleanValue);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeEndSet() throws IOException {
    writer.writeEndSet();
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeEndSequence() throws IOException {
    writer.writeEndSequence();
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeInteger(int intValue) throws IOException {
    writer.writeInteger(intValue);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeInteger(long longValue) throws IOException {
    writer.writeInteger(longValue);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeInteger(byte type, int intValue) throws IOException {
    writer.writeInteger(type, intValue);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeInteger(byte type, long longValue) throws IOException {
    writer.writeInteger(type, longValue);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeEnumerated(int intValue) throws IOException {
    writer.writeEnumerated(intValue);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeNull() throws IOException {
    writer.writeNull();
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeNull(byte type) throws IOException {
    writer.writeNull(type);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeOctetString(byte type, byte[] value,
                                     int offset, int length)
      throws IOException {
    writer.writeOctetString(type, value, offset, length);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeOctetString(byte type, ByteSequence value)
      throws IOException {
    writer.writeOctetString(type, value);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeOctetString(byte type, String value)
      throws IOException {
    writer.writeOctetString(type, value);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeOctetString(byte[] value, int offset, int length)
      throws IOException {
    writer.writeOctetString(value, offset, length);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeOctetString(ByteSequence value) throws IOException {
    writer.writeOctetString(value);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeOctetString(String value) throws IOException {
    writer.writeOctetString(value);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeStartSequence() throws IOException {
    writer.writeStartSequence();
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeStartSequence(byte type) throws IOException {
    writer.writeStartSequence(type);
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeStartSet() throws IOException {
    writer.writeStartSet();
    return this;
  }

  /**
   * {@inheritDoc}
   */
  public ASN1Writer writeStartSet(byte type) throws IOException {
    writer.writeStartSet(type);
    return this;
  }

  /**
   * Flush the entire contents of the NIO ByteBuffer out to the
   * channel.  Note that the caller should only invoke this
   * method when it has finished writing the entire message and
   * not earlier. Otherwise, calling this method prematurely
   * while sharing this writer among several threads can cause
   * messages to interleave. While it is possible to ensure the
   * safety by either single threaded usage or using external
   * locking its generally not advised to do so and the caller
   * should instead obey no flush until the entire message is
   * written rule when using this writer.
   *
   * @throws IOException If an error occurs while flushing.
   */
  public void flush() throws IOException
  {
    byteBuffer.flip();
    try
    {
      if (!flushLock.isHeldByCurrentThread())
      {
        flushLock.lock();
      }
      byteChannel.write(byteBuffer);
    }
    finally
    {
      flushLock.unlock();
    }
    byteBuffer.clear();
  }

  /**
   * Take the flush lock and flush the entire contents of
   * the NIO ByteBuffer out to the channel.
   *
   * @throws IOException If an error occurs while flushing.
   */
  private void lockAndFlush() throws IOException
  {
    byteBuffer.flip();
    if (!flushLock.isHeldByCurrentThread())
    {
      flushLock.lock();
    }
    while(byteBuffer.hasRemaining())
    {
      byteChannel.write(byteBuffer);
    }
    byteBuffer.clear();
  }

  /**
   * Closes this ASN.1 writer and the underlying channel.
   *
   * @throws IOException if an error occurs while closing the writer.
   */
  public void close() throws IOException {
    // Close the writer first to flush the writer to the NIO byte buffer.
    writer.close();
    flush();
    byteChannel.close();
  }
}
