package picard.util;

import htsjdk.samtools.util.Log;
import htsjdk.samtools.util.RuntimeIOException;
import picard.PicardException;
import picard.cmdline.CommandLineProgram;
import picard.cmdline.CommandLineProgramProperties;
import picard.cmdline.Option;
import picard.cmdline.programgroups.None;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.text.DecimalFormat;
import java.text.NumberFormat;

/**
 * A program that is designed to act as a large memory buffer between processes that are
 * connected with unix pipes where one or more processes either produce or consume their
 * input or output in bursts.  By inserting a large memory buffer between such processes
 * each process can run at full speed and the bursts can be smoothed out.
 *
 * For example:
 *   java -jar SamToFastq.jar F=my.fastq INTERLEAVE=true | java -jar FifoBuffer | bwa mem -t 8 ...
 *
 * @author Tim Fennell
 */
@CommandLineProgramProperties(
        usage = "Provides a large, configurable, FIFO buffer that can be used to buffer input and output " +
                "streams between programs with a buffer size that is larger than that offered by native unix FIFOs (usually 64k).",
        usageShort = "FIFO buffer used to buffer input and output streams with a customizable buffer size ",
        programGroup = None.class
)
public class FifoBuffer extends CommandLineProgram {
    @Option(doc="The size of the memory buffer in bytes.")
    public int BUFFER_SIZE = 512 * 1024 * 1024;

    @Option(doc="The size, in bytes, to read/write atomically to the input and output streams.")
    public int IO_SIZE = 64 * 1024; // 64k == most common unix pipe buffer size

    @Option(doc="How frequently, in seconds, to report debugging statistics. Set to zero for never.")
    public int DEBUG_FREQUENCY = 0;

    @Option(doc="Name to use for Fifo in debugging statements.", optional=true)
    public String NAME;

    // Standard log object
    private final Log log = Log.getInstance(FifoBuffer.class);

    /** The input stream that bytes will be read from and deposited into the byte buffer. */
    private final InputStream inputStream;

    /** The output [Print] stream which bytes read from the buffer will be emitted into. */
    private final PrintStream outputStream;

    /** Small custom exception handler for threads. */
    class LoggingExceptionHandler implements Thread.UncaughtExceptionHandler {
        public Throwable throwable;

        @Override public void uncaughtException(final Thread t, final Throwable e) {
            this.throwable = e;
            log.error(e, "Exception caught on thread ", t.getName());
        }
    }

    /**
     * Constructor that defaults to QUIET since Fifos don't do anything beyond buffering having their
     * start/end information logged is often undesirable.
     */
    public FifoBuffer(final InputStream in, final PrintStream out) {
        this.inputStream = in;
        this.outputStream = out;
        this.QUIET=true;
    }

    /**
     * Constructor that defaults to QUIET since Fifos don't do anything beyond buffering having their
     * start/end information logged is often undesirable.
     */
    public FifoBuffer() {
        this(System.in, System.out);
    }

    // Stock main method
    public static void main(final String[] args) {
        new FifoBuffer().instanceMainWithExit(args);
    }

    @Override
    protected int doWork() {
        final CircularByteBuffer fifo = new CircularByteBuffer(BUFFER_SIZE);

        // Input thread that reads from inputStream until it is closed and writes the contents
        // into the circular byte buffer.
        final Thread input = new Thread(new Runnable() {
            @Override public void run() {
                try {
                    final byte[] buffer = new byte[IO_SIZE];
                    int read = 0;
                    while ((read = inputStream.read(buffer)) > -1) {
                        int start = 0;
                        while (start < read) {
                            start += fifo.write(buffer, start, read-start);
                        }
                    }
                }
                catch (final IOException ioe) {
                    throw new RuntimeIOException(ioe);
                }
                finally {
                    fifo.close();
                }
            }
        });

        // Output thread that reads from the circular byte buffer until it is closed and writes
        // the results to the outputStream
        final Thread output = new Thread(new Runnable() {
            @Override public void run() {
                final byte[] buffer = new byte[IO_SIZE];
                int read = 0;
                while ((read = fifo.read(buffer, 0, buffer.length)) > 0 || !fifo.isClosed()) {
                    outputStream.write(buffer, 0, read);
                }
            }
        });

        try {
            // If debugging is turned on then start another thread that will report on the utilization of the
            // circular byte buffer every N seconds.
            if (DEBUG_FREQUENCY > 0) {
                final Thread debug = new Thread(new Runnable() {
                    @Override public void run() {
                        final NumberFormat pFmt = NumberFormat.getPercentInstance();
                        final NumberFormat iFmt = new DecimalFormat("#,##0");
                        while (true) {
                            final int capacity = fifo.getCapacity();
                            final int used     = fifo.getBytesAvailableToRead();
                            final double pct   = used / (double) capacity;
                            final String name = NAME == null ? "" : NAME + " ";
                            log.info("Fifo buffer ", name, "used ", iFmt.format(used), " / ", iFmt.format(capacity), " (", pFmt.format(pct), ").");
                            try { Thread.sleep(DEBUG_FREQUENCY * 1000); } catch (final InterruptedException ie) { /* do nothing */ }
                        }
                    }
                });

                debug.setName("BufferDebugThread");
                debug.setDaemon(true);
                debug.start();
            }

            // Start the input and output threads.
            final LoggingExceptionHandler inputExceptionHandler = new LoggingExceptionHandler();
            input.setUncaughtExceptionHandler(inputExceptionHandler);
            input.setName("Fifo Input Thread");
            input.start();

            final LoggingExceptionHandler outputExceptionHandler = new LoggingExceptionHandler();
            output.setUncaughtExceptionHandler(new LoggingExceptionHandler());
            output.setName("Fifo Output Thread");
            output.start();

            // Join on both the input and output threads to make sure that we've fully flushed the buffer
            input.join();
            output.join();

            // Double check that neither thread exited with an exception; If so propagate the exception
            if (inputExceptionHandler.throwable  != null) throw new PicardException("Exception on input thread.",  inputExceptionHandler.throwable);
            if (outputExceptionHandler.throwable != null) throw new PicardException("Exception on output thread.", outputExceptionHandler.throwable);
        }
        catch (final InterruptedException ie) {
            throw new PicardException("Interrupted!", ie);
        }

        return 0;
    }
}
