/*
 * The MIT License
 *
 * Copyright (c) 2009 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.ProgressLoggerInterface;
import htsjdk.samtools.util.SortingCollection;

import java.io.File;
import java.io.StringWriter;
import java.util.function.Supplier;

/**
 * Base class for implementing SAM writer with any underlying format.
 * Mostly this manages accumulation & sorting of SAMRecords when appropriate,
 * and produces the text version of the header, since that seems to be a popular item
 * in both text and binary file formats.
 */
public abstract class SAMFileWriterImpl implements SAMFileWriter
{
    private static int DEAFULT_MAX_RECORDS_IN_RAM = 500000;      
    private int maxRecordsInRam = DEAFULT_MAX_RECORDS_IN_RAM;
    private SAMFileHeader.SortOrder sortOrder;
    private SAMFileHeader header;
    private SortingCollection<SAMRecord> alignmentSorter;
    private File tmpDir = new File(System.getProperty("java.io.tmpdir"));
    private ProgressLoggerInterface progressLogger = null;
    private boolean isClosed = false;

    // If true, records passed to addAlignment are already in the order specified by sortOrder
    private boolean presorted;

    // For validating presorted records.
    private SAMSortOrderChecker sortOrderChecker;

    /**
     * When writing records that are not presorted, specify the number of records stored in RAM
     * before spilling to disk.  This method sets the default value for all SamFileWriterImpl
     * instances. Must be called before the constructor is called.
     * @param maxRecordsInRam
     */
    public static void setDefaultMaxRecordsInRam(final int maxRecordsInRam) {
        DEAFULT_MAX_RECORDS_IN_RAM = maxRecordsInRam;    
    }
    
    /**
     * When writing records that are not presorted, this number determines the 
     * number of records stored in RAM before spilling to disk.
     * @return DEAFULT_MAX_RECORDS_IN_RAM 
     */
    public static int getDefaultMaxRecordsInRam() {
        return DEAFULT_MAX_RECORDS_IN_RAM;    
    }

    /**
     * Sets the progress logger used by this implementation. Setting this lets this writer emit log
     * messages as SAM records in a SortingCollection are being written to disk.
     */
    @Override
    public void setProgressLogger(final ProgressLoggerInterface progress) {
        this.progressLogger = progress;
    }

    /**
     * Must be called before calling setHeader().  SortOrder value in the header passed
     * to setHeader() is ignored.  If setSortOrder is not called, default is SortOrder.unsorted.
     */
    public void setSortOrder(final SAMFileHeader.SortOrder sortOrder, final boolean presorted) {
        if (header != null) {
            throw new IllegalStateException("Cannot call SAMFileWriterImpl.setSortOrder after setHeader for " +
                    getFilename());
        }
        this.sortOrder = sortOrder;
        this.presorted = presorted;
    }

    /**
     * Must be called after calling setHeader().
     */
    protected SAMFileHeader.SortOrder getSortOrder() {
        return this.sortOrder;
    }

    /**
     * When writing records that are not presorted, specify the number of records stored in RAM
     * before spilling to disk.  Must be called before setHeader().
     * @param maxRecordsInRam
     */
    protected void setMaxRecordsInRam(final int maxRecordsInRam) {
        if (this.header != null) {
            throw new IllegalStateException("setMaxRecordsInRam must be called before setHeader()");
        }
        this.maxRecordsInRam = maxRecordsInRam;
    }

    protected int getMaxRecordsInRam() {
        return maxRecordsInRam;
    }

    /**
     * When writing records that are not presorted, specify the path of the temporary directory 
     * for spilling to disk.  Must be called before setHeader().
     * @param tmpDir path to the temporary directory
     */
    protected void setTempDirectory(final File tmpDir) {
        if (tmpDir!=null) {
            this.tmpDir = tmpDir;
        }
    }

    protected File getTempDirectory() {
        return tmpDir;
    }

    /**
     * Must be called before addAlignment. Header cannot be null.
     */
    public void setHeader(final SAMFileHeader header)
    {
        if (null == header) {
            throw new IllegalArgumentException("A non-null SAMFileHeader is required for a writer");
        }
        this.header = header;
        if (sortOrder == null) {
             sortOrder = SAMFileHeader.SortOrder.unsorted;
        }
        header.setSortOrder(sortOrder);

        writeHeader(header);

        if (presorted) {
            if (sortOrder.equals(SAMFileHeader.SortOrder.unsorted)) {
                presorted = false;
            } else {
                sortOrderChecker = new SAMSortOrderChecker(sortOrder);
            }
        } else if (!sortOrder.equals(SAMFileHeader.SortOrder.unsorted)) {
            alignmentSorter = SortingCollection.newInstance(SAMRecord.class,
                    new BAMRecordCodec(header), sortOrder.getComparatorInstance(), maxRecordsInRam, tmpDir);
        }
    }

    @Override
    public SAMFileHeader getFileHeader() {
        return header;
    }

    /**
     * Add an alignment record to be emitted by the writer.
     *
     * @param alignment Must not be null. The record will be updated to use the header used by this writer, which will
     *                  in turn cause any unresolved reference and mate reference indices to be resolved against the
     *                  header's sequence dictionary.
     * @throws IllegalArgumentException if the record's reference or mate reference indices cannot be
     * resolved against the writer's header using the current reference and mate reference names
     */
    @Override
    public void addAlignment(final SAMRecord alignment)
    {
        alignment.setHeaderStrict(header); // re-establish the record header and resolve reference indices
        if (sortOrder.equals(SAMFileHeader.SortOrder.unsorted)) {
            writeAlignment(alignment);
        } else if (presorted) {
            assertPresorted(alignment);
            writeAlignment(alignment);
        } else {
            alignmentSorter.add(alignment);
        }
    }

    private void assertPresorted(final SAMRecord alignment) {
        final SAMRecord prev = sortOrderChecker.getPreviousRecord();
        if (!sortOrderChecker.isSorted(alignment)) {
            throw new IllegalArgumentException("Alignments added out of order in SAMFileWriterImpl.addAlignment for " +
                    getFilename() + ". Sort order is " + this.sortOrder + ". Offending records are at ["
                    + sortOrderChecker.getSortKey(prev) + "] and ["
                    + sortOrderChecker.getSortKey(alignment) + "]");
        }
    }

    /**
     * Must be called or else file will likely be defective.
     */
    @Override
    public final void close()
    {
        try {
            if (!isClosed) {
                if (alignmentSorter != null) {
                    try {
                        for (final SAMRecord alignment : alignmentSorter) {
                            writeAlignment(alignment);
                            if (progressLogger != null)
                                progressLogger.record(alignment);
                        }
                    } finally {
                        alignmentSorter.cleanup();
                    }
                }
                finish();
            }
        } finally {
            isClosed = true;
        }
    }

    /**
     * Writes the record to disk.  Sort order has been taken care of by the time
     * this method is called. The record must hava a non-null SAMFileHeader.
     * @param alignment
     */
    abstract protected void writeAlignment(SAMRecord alignment);

    /**
     * Write the header to disk.  Header object is available via getHeader().
     * @param textHeader for convenience if the implementation needs it.
     * @deprecated since 06/2018. {@link #writeHeader(SAMFileHeader)} is preferred for avoid String construction if not need it.
     */
    @Deprecated
    abstract protected void writeHeader(String textHeader);

    /**
     * Write the header to disk. Header object is available via getHeader().
     *
     * <p>IMPORTANT: this method will be abstract once {@link #writeHeader(String)} is removed.
     *
     * <p>Note: default implementation uses {@link SAMTextHeaderCodec#encode} and calls
     * {@link #writeHeader(String)}.
     *
     * @param header object to write.
     */
    protected void writeHeader(final SAMFileHeader header) {
        final StringWriter headerTextBuffer = new StringWriter();
        new SAMTextHeaderCodec().encode(headerTextBuffer, header);
        writeHeader(headerTextBuffer.toString());
    }

    /**
     * Do any required flushing here.
     */
    abstract protected void finish();

    /**
     * For producing error messages.
     * @return Output filename, or null if there isn't one.
     */
    abstract protected String getFilename();
}
