/*
 * 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 legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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 legal-notices/CDDLv1_0.txt.
 * 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.
 *      Portions Copyright 2015 ForgeRock AS
 */
package org.opends.server.tools.makeldif;



import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import org.opends.server.types.LDIFExportConfig;
import org.opends.server.util.LDIFException;
import org.opends.server.util.LDIFWriter;



/**
 * This class creates an input stream that can be used to read entries generated
 * by MakeLDIF as if they were being read from another source like a file.  It
 * has a fixed-size queue that dictates how many entries may be held in memory
 * at any given time.
 */
public class MakeLDIFInputStream
       extends InputStream
       implements EntryWriter
{
  /** Indicates whether all of the entries have been generated. */
  private boolean allGenerated;

  /** Indicates whether this input stream has been closed. */
  private boolean closed;

  /**
   * The byte array output stream that will be used to convert entries to byte
   * arrays with their LDIF representations.
   */
  private ByteArrayOutputStream entryOutputStream;

  /**
   * The byte array that will hold the LDIF representation of the next entry to
   * be read.
   */
  private ByteBuffer entryBytes;

  /** The IOException that should be thrown the next time a read is requested. */
  private IOException ioException;

  /** The LDIF writer that will be used to write the entries to LDIF. */
  private LDIFWriter ldifWriter;

  /** The queue used to hold generated entries until they can be read. */
  private LinkedBlockingQueue<TemplateEntry> entryQueue;

  /** The background thread being used to actually generate the entries. */
  private MakeLDIFInputStreamThread generatorThread;

  /** The template file to use to generate the entries. */
  private TemplateFile templateFile;



  /**
   * Creates a new MakeLDIF input stream that will generate entries based on the
   * provided template file.
   *
   * @param  templateFile  The template file to use to generate the entries.
   */
  public MakeLDIFInputStream(TemplateFile templateFile)
  {
    this.templateFile = templateFile;

    allGenerated = false;
    closed       = false;
    entryQueue   = new LinkedBlockingQueue<>(10);
    ioException  = null;
    entryBytes   = null;

    entryOutputStream = new ByteArrayOutputStream(8192);
    LDIFExportConfig exportConfig = new LDIFExportConfig(entryOutputStream);

    try
    {
      ldifWriter = new LDIFWriter(exportConfig);
    }
    catch (IOException ioe)
    {
      // This should never happen.
      ioException = ioe;
    }

    generatorThread = new MakeLDIFInputStreamThread(this, templateFile);
    generatorThread.start();
  }



  /**
   * Closes this input stream so that no more data may be read from it.
   */
  public void close()
  {
    closed      = true;
    ioException = null;
  }



  /**
   * Reads a single byte of data from this input stream.
   *
   * @return  The byte read from the input stream, or -1 if the end of the
   *          stream has been reached.
   *
   * @throws  IOException  If a problem has occurred while generating data for
   *                       use by this input stream.
   */
  public int read()
         throws IOException
  {
    if (closed)
    {
      return -1;
    }
    else if (ioException != null)
    {
      throw ioException;
    }

    if ((entryBytes == null || !entryBytes.hasRemaining())
        && !getNextEntry())
    {
      closed = true;
      return -1;
    }

    return 0xFF & entryBytes.get();
  }



  /**
   * Reads data from this input stream.
   *
   * @param  b    The array into which the data should be read.
   * @param  off  The position in the array at which point the data read may be
   *              placed.
   * @param  len  The maximum number of bytes that may be read into the
   *              provided array.
   *
   * @return  The number of bytes read from the input stream into the provided
   *          array, or -1 if the end of the stream has been reached.
   *
   * @throws  IOException  If a problem has occurred while generating data for
   *                       use by this input stream.
   */
  public int read(byte[] b, int off, int len)
         throws IOException
  {
    if (closed)
    {
      return -1;
    }
    else if (ioException != null)
    {
      throw ioException;
    }

    if ((entryBytes == null || !entryBytes.hasRemaining())
        && !getNextEntry())
    {
      closed = true;
      return -1;
    }

    int bytesRead = Math.min(len, entryBytes.remaining());
    entryBytes.get(b, off, bytesRead);
    return bytesRead;
  }



  /** {@inheritDoc} */
  public boolean writeEntry(TemplateEntry entry)
         throws IOException, MakeLDIFException
  {
    while (! closed)
    {
      try
      {
        if (entryQueue.offer(entry, 500, TimeUnit.MILLISECONDS))
        {
          return true;
        }
      } catch (InterruptedException ie) {}
    }

    return false;
  }



  /** {@inheritDoc} */
  public void closeEntryWriter()
  {
    allGenerated = true;
  }



  /**
   * Sets the I/O exception that should be thrown on any subsequent calls to
   * <CODE>available</CODE> or <CODE>read</CODE>.
   *
   * @param  ioException   The I/O exception that should be thrown.
   */
  void setIOException(IOException ioException)
  {
    this.ioException = ioException;
  }



  /**
   * Retrieves the next entry and puts it in the entry byte buffer.
   *
   * @return  <CODE>true</CODE> if the next entry is available, or
   *          <CODE>false</CODE> if there are no more entries or if the input
   *          stream has been closed.
   */
  private boolean getNextEntry()
  {
    TemplateEntry entry = entryQueue.poll();
    while (entry == null)
    {
      if (closed)
      {
        return false;
      }
      else if (allGenerated)
      {
        entry = entryQueue.poll();
        if (entry == null)
        {
          return false;
        }
      }
      else
      {
        try
        {
          entry = entryQueue.poll(500, TimeUnit.MILLISECONDS);
        } catch (InterruptedException ie) {}
      }
    }

    try
    {
      entryOutputStream.reset();
      ldifWriter.writeTemplateEntry(entry);
      ldifWriter.flush();
      entryBytes = ByteBuffer.wrap(entryOutputStream.toByteArray());
    }
    catch (LDIFException le)
    {
      // This should never happen.
      ioException = new IOException(le.getMessage());
      return false;
    }
    catch (IOException ioe)
    {
      // Neither should this.
      ioException = ioe;
      return false;
    }

    return true;
  }
}

