/*
* Copyright 2012-2015 Broad Institute, Inc.
* 
* 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 org.broadinstitute.gatk.utils;

import com.google.java.contract.Ensures;
import com.google.java.contract.Requires;
import htsjdk.samtools.SAMSequenceDictionary;
import htsjdk.samtools.SAMSequenceRecord;
import org.broadinstitute.gatk.utils.exceptions.ReviewedGATKException;

/**
 * A wrapper class that provides efficient most recently used caching for the global
 * SAMSequenceDictionary underlying all of the GATK engine capabilities.  It is essential
 * that these class be as efficient as possible.  It doesn't need to be thread-safe, as
 * GenomeLocParser uses a thread-local variable to ensure that each thread gets its own MRU
 * cache.
 *
 * The MRU elements are the SAMSequenceRecord, the lastContig, and the lastIndex.  The
 * cached value is the actual SAMSequenceRecord of the most recently accessed value from
 * getSequence, along with local variables for the contig index and contig string.
 */
final class MRUCachingSAMSequenceDictionary {
    /**
     * Our sequence dictionary
     */
    private final SAMSequenceDictionary dict;

    SAMSequenceRecord lastSSR = null;
    String lastContig = "";
    int lastIndex = -1;

    /**
     * Create a new MRUCachingSAMSequenceDictionary that provides information about sequences in dict
     * @param dict a non-null, non-empty sequencing dictionary
     */
    @Ensures("lastSSR == null")
    public MRUCachingSAMSequenceDictionary(final SAMSequenceDictionary dict) {
        if ( dict == null ) throw new IllegalArgumentException("Dictionary cannot be null");
        if ( dict.size() == 0 ) throw new IllegalArgumentException("Dictionary cannot have size zero");

        this.dict = dict;
    }

    /**
     * Get our sequence dictionary
     * @return a non-null SAMSequenceDictionary
     */
    @Ensures("result != null")
    public SAMSequenceDictionary getDictionary() {
        return dict;
    }

    /**
     * Is contig present in the dictionary?  Efficiently caching.
     * @param contig a non-null contig we want to test
     * @return true if contig is in dictionary, false otherwise
     */
    @Requires("contig != null")
    public final boolean hasContig(final String contig) {
        return contig.equals(lastContig) || dict.getSequence(contig) != null;
    }

    /**
     * Is contig index present in the dictionary?  Efficiently caching.
     * @param contigIndex an integer offset that might map to a contig in this dictionary
     * @return true if contigIndex is in dictionary, false otherwise
     */
    @Requires("contigIndex >= 0")
    public final boolean hasContigIndex(final int contigIndex) {
        return lastIndex == contigIndex || dict.getSequence(contigIndex) != null;
    }

    /**
     * Same as SAMSequenceDictionary.getSequence but uses a MRU cache for efficiency
     *
     * @param contig the contig name we want to get the sequence record of
     * @throws ReviewedGATKException if contig isn't present in the dictionary
     * @return the sequence record for contig
     */
    @Requires("contig != null")
    @Ensures("result != null")
    public final SAMSequenceRecord getSequence(final String contig) {
        if ( isCached(contig) )
            return lastSSR;
        else
            return updateCache(contig, -1);
    }

    /**
     * Same as SAMSequenceDictionary.getSequence but uses a MRU cache for efficiency
     *
     * @param index the contig index we want to get the sequence record of
     * @throws ReviewedGATKException if contig isn't present in the dictionary
     * @return the sequence record for contig
     */
    @Requires("index >= 0")
    @Ensures("result != null")
    public final SAMSequenceRecord getSequence(final int index) {
        if ( isCached(index) )
            return lastSSR;
        else
            return updateCache(null, index);
    }

    /**
     * Same as SAMSequenceDictionary.getSequenceIndex but uses a MRU cache for efficiency
     *
     * @param contig the contig we want to get the sequence record of
     * @throws ReviewedGATKException if index isn't present in the dictionary
     * @return the sequence record index for contig
     */
    @Requires("contig != null")
    @Ensures("result >= 0")
    public final int getSequenceIndex(final String contig) {
        if ( ! isCached(contig) ) {
            updateCache(contig, -1);
        }

        return lastIndex;
    }

    /**
     * Is contig the MRU cached contig?
     * @param contig the contig to test
     * @return true if contig is the currently cached contig, false otherwise
     */
    @Requires({"contig != null"})
    protected boolean isCached(final String contig) {
        return contig.equals(lastContig);
    }

    /**
     * Is the contig index index the MRU cached index?
     * @param index the contig index to test
     * @return true if contig index is the currently cached contig index, false otherwise
     */
    protected boolean isCached(final int index) {
        return lastIndex == index;
    }

    /**
     * The key algorithm.  Given a new record, update the last used record, contig
     * name, and index.
     *
     * @param contig the contig we want to look up.  If null, index is used instead
     * @param index the contig index we want to look up.  Only used if contig is null
     * @throws ReviewedGATKException if index isn't present in the dictionary
     * @return the SAMSequenceRecord for contig / index
     */
    @Requires("contig != null || index >= 0")
    @Ensures("result != null")
    private SAMSequenceRecord updateCache(final String contig, int index ) {
        SAMSequenceRecord rec = contig == null ? dict.getSequence(index) : dict.getSequence(contig);
        if ( rec == null ) {
            throw new ReviewedGATKException("BUG: requested unknown contig=" + contig + " index=" + index);
        } else {
            lastSSR = rec;
            lastContig = rec.getSequenceName();
            lastIndex = rec.getSequenceIndex();
            return rec;
        }
    }
}
