package htsjdk.samtools;


import htsjdk.samtools.util.CloserUtil;
import htsjdk.samtools.util.RuntimeIOException;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.nio.file.Path;

/**
 * OutputStream implementation that writes output to an underlying output stream while also copying the
 * same bytes to a PipedOutputStream that routes the data back into an Indexer to generate a BAMIndex
 * by inflating and decoding the stream and feeding the SAMRecords to a BAMIndexer.
 */
class StreamInflatingIndexingOutputStream extends OutputStream {
    private final OutputStream s1;
    private final PipedOutputStream s2;
    private final Thread thread;

    public StreamInflatingIndexingOutputStream(final OutputStream s1, final File indexFile) {
        this(s1, indexFile.toPath());
    }

    public StreamInflatingIndexingOutputStream(final OutputStream s1, final Path indexPath) {
        try {
            this.s1 = s1;
            this.s2 = new PipedOutputStream();
            final PipedInputStream pin = new PipedInputStream(this.s2, Defaults.NON_ZERO_BUFFER_SIZE);
            this.thread = new Thread(new Indexer(indexPath, pin), "BamIndexingThread");
            this.thread.start();
        } catch (final IOException ioe) {
            throw new RuntimeIOException(ioe);
        }
    }

    @Override
    public final void write(final int b) throws IOException {
        this.s1.write(b);
        this.s2.write(b);
    }

    @Override
    public final void write(final byte[] b) throws IOException {
        this.s1.write(b);
        this.s2.write(b);
    }

    @Override
    public final void write(final byte[] b, final int off, final int len) throws IOException {
        this.s1.write(b, off, len);
        this.s2.write(b, off, len);
    }

    @Override
    public final void flush() throws IOException {
        this.s1.flush();
        this.s2.flush();
    }

    @Override
    public final void close() throws IOException {
        this.s1.close();
        this.s2.close();

        try {
            this.thread.join();
        } catch (final InterruptedException ie) {
            throw new RuntimeException(ie);
        }
    }
}

/**
 * A little class that takes an InputStream from which it reads a BAM file, generates
 * a BAMIndex and then writes the index to the Path provided.  All operations are designed
 * to be carried out in a separate thread.
 */
class Indexer implements Runnable {
    private final Path index;
    private final InputStream stream;

    /** Constructs an indexer that reads from the stream provided and writes an index to the Path provided. */
    Indexer(final Path index, final InputStream stream) {
        this.index = index;
        this.stream = stream;
    }

    /** Runnable implementation that reads the entire stream and writes the index. */
    @Override
    public void run() {
        final SamReader in = SamReaderFactory.makeDefault().validationStringency(ValidationStringency.SILENT)
                .enable(SamReaderFactory.Option.INCLUDE_SOURCE_IN_RECORDS).disable(SamReaderFactory.Option.VALIDATE_CRC_CHECKSUMS)
                .open(SamInputResource.of(this.stream));

        final BAMIndexer indexer = new BAMIndexer(this.index, in.getFileHeader());
        for (final SAMRecord rec : in) {
            indexer.processAlignment(rec);
        }

        indexer.finish();
        CloserUtil.close(in);
    }
}
