/*
 * The MIT License
 *
 * Copyright (c) 2010 The Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package htsjdk.samtools;

import htsjdk.samtools.util.BinaryCodec;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;

/**
 * Class for writing binary BAM index files
 */
class BinaryBAMIndexWriter implements BAMIndexWriter {

    protected final int nRef;
    private final BinaryCodec codec;
    private int count = 0;

    /**
     * constructor
     *
     * @param nRef    Number of reference sequences
     * @param output  BAM Index output file
     */
    public BinaryBAMIndexWriter(final int nRef, final File output) {

        this.nRef = nRef;

        try {
            codec = new BinaryCodec(output, true);
            writeHeader();
        } catch (final Exception e) {
            throw new SAMException("Exception opening output file " + output, e);
        }
    }

    /**
     *
     * @param nRef Number of reference sequences.
     * @param output BAM index output stream.  This stream will be closed when BinaryBAMIndexWriter.close() is called.
     */
    public BinaryBAMIndexWriter(final int nRef, final OutputStream output) {

        this.nRef = nRef;

        try {
            codec = new BinaryCodec(output);
            writeHeader();
        } catch (final Exception e) {
            throw new SAMException("Exception opening output stream", e);
        }
    }

    /**
     * Write this content as binary output
     */
    public void writeReference(final BAMIndexContent content) {

        if (content == null) {
            writeNullContent();
            count++;
            return;
        }

        if (content.getReferenceSequence() != count){
            throw new SAMException("Unexpectedly writing reference " + content.getReferenceSequence() +
                ", expecting reference " + count);
        }
        count ++;

        // write bins

        final BAMIndexContent.BinList bins = content.getBins();
        final int size = bins == null ? 0 : content.getNumberOfNonNullBins();

        if (size == 0) {
            writeNullContent();
            return;
        }

        //final List<Chunk> chunks = content.getMetaData() == null ? null
        //        : content.getMetaData().getMetaDataChunks();
        final BAMIndexMetaData metaData = content.getMetaData();

        codec.writeInt(size + ((metaData != null)? 1 : 0 ));
        // codec.writeInt(size);
        for (final Bin bin : bins) {   // note, bins will always be sorted
            if (bin.getBinNumber() == GenomicIndexUtil.MAX_BINS)
                continue;
            writeBin(bin);
        }

        // write metadata "bin" and chunks        
        if (metaData != null)
            writeChunkMetaData(metaData);

        // write linear index

        final LinearIndex linearIndex = content.getLinearIndex();
        final long[] entries = linearIndex == null ? null : linearIndex.getIndexEntries();
        final int indexStart = linearIndex == null ? 0 : linearIndex.getIndexStart();
        final int n_intv = entries == null ? indexStart : entries.length + indexStart;
        codec.writeInt(n_intv);
        if (entries == null) {
            return;
        }
        // since indexStart is usually 0, this is usually a no-op
        for (int i = 0; i < indexStart; i++) {
            codec.writeLong(0);
        }
        for (int k = 0; k < entries.length; k++) {
            codec.writeLong(entries[k]);
        }
        try {
            codec.getOutputStream().flush();
        } catch (final IOException e) {
            throw new SAMException("IOException in BinaryBAMIndexWriter reference " + content.getReferenceSequence(), e);
        }
    }

    /**
     * Writes out the count of records without coordinates
     *
     * @param count
     */
    public void writeNoCoordinateRecordCount(final Long count) {
        codec.writeLong(count == null ? 0 : count);
    }

    /**
     * Any necessary processing at the end of the file
     */
    public void close() {
        codec.close();
    }

    private void writeBin(final Bin bin) {
        final int binNumber = bin.getBinNumber();
        if (binNumber >= GenomicIndexUtil.MAX_BINS){
            throw new SAMException("Unexpected bin number when writing bam index " + binNumber);
        }
        
        codec.writeInt(binNumber);
        if (bin.getChunkList() == null){
            codec.writeInt(0);
            return;
        }
        final List<Chunk> chunkList = bin.getChunkList();
        final int n_chunk = chunkList.size();
        codec.writeInt(n_chunk);
        for (final Chunk c : chunkList) {
            codec.writeLong(c.getChunkStart());
            codec.writeLong(c.getChunkEnd());
        }
    }

    /**
     * Write the meta data represented by the chunkLists associated with bin MAX_BINS 37450
     *
     * @param metaData information describing numAligned records, numUnAligned, etc
     */
    private void writeChunkMetaData(final BAMIndexMetaData metaData) {
        codec.writeInt(GenomicIndexUtil.MAX_BINS);
        final int nChunk = 2;
        codec.writeInt(nChunk);
        codec.writeLong(metaData.getFirstOffset());
        codec.writeLong(metaData.getLastOffset());
        codec.writeLong(metaData.getAlignedRecordCount());
        codec.writeLong(metaData.getUnalignedRecordCount());

    }

    private void writeHeader() {
        // magic string
        final byte[] magic = BAMFileConstants.BAM_INDEX_MAGIC;
        codec.writeBytes(magic);
        codec.writeInt(nRef);
    }

    private void writeNullContent() {
        codec.writeLong(0);  // 0 bins , 0 intv
    }
}
