/*
 * Copyright (c) 2007-2010 by The Broad Institute, Inc. and the Massachusetts Institute of Technology.
 * All Rights Reserved.
 *
 * This software is licensed under the terms of the GNU Lesser General Public License (LGPL), Version 2.1 which
 * is available at http://www.opensource.org/licenses/lgpl-2.1.php.
 *
 * THE SOFTWARE IS PROVIDED "AS IS." THE BROAD AND MIT MAKE NO REPRESENTATIONS OR WARRANTIES OF
 * ANY KIND CONCERNING THE SOFTWARE, EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT
 * OR OTHER DEFECTS, WHETHER OR NOT DISCOVERABLE.  IN NO EVENT SHALL THE BROAD OR MIT, OR THEIR
 * RESPECTIVE TRUSTEES, DIRECTORS, OFFICERS, EMPLOYEES, AND AFFILIATES BE LIABLE FOR ANY DAMAGES OF
 * ANY KIND, INCLUDING, WITHOUT LIMITATION, INCIDENTAL OR CONSEQUENTIAL DAMAGES, ECONOMIC
 * DAMAGES OR INJURY TO PROPERTY AND LOST PROFITS, REGARDLESS OF WHETHER THE BROAD OR MIT SHALL
 * BE ADVISED, SHALL HAVE OTHER REASON TO KNOW, OR IN FACT SHALL KNOW OF THE POSSIBILITY OF THE
 * FOREGOING.
 */

package org.broad.tribble.index;

import net.sf.samtools.SAMSequenceDictionary;
import net.sf.samtools.SAMSequenceRecord;
import org.apache.log4j.Logger;
import org.broad.tribble.util.LittleEndianInputStream;
import org.broad.tribble.util.LittleEndianOutputStream;

import java.io.*;
import java.util.*;

/**
 * Created by IntelliJ IDEA.
 * User: jrobinso
 * Date: Jul 9, 2010
 * Time: 8:37:26 AM
 * To change this template use File | Settings | File Templates.
 */
public abstract class AbstractIndex implements Index {
    private static Logger log = Logger.getLogger(AbstractIndex.class);

    private SAMSequenceDictionary sequenceDictionary;

    protected int version;
    String indexedFile;
    long indexedFileSize;
    long indexedFileTS;
    String indexedFileMD5 = "";
    protected Map<String, ChrIndex> chrIndeces;
    private static final int SEQUENCE_DICTIONARY_FLAG = 0x8000;


    public AbstractIndex() {
        chrIndeces = new LinkedHashMap();
    }

    public AbstractIndex(String featureFile) {
        chrIndeces = new LinkedHashMap();
        this.indexedFile = featureFile;
        this.indexedFileSize = featureFile.length();
        // TODO -- get time stamp
        //this.indexedFileTS = featureFile.lastModified();
    }

    public void setMD5(String md5) {
        this.indexedFileMD5 = md5;
    }

    public boolean containsChromosome(String chr) {
        return chrIndeces.containsKey(chr);
    }

    /**
     * get the sequence dictionary
     *
     * @return a sam sequence dictionary
     */
    public SAMSequenceDictionary getSequenceDictionary() {
        // if we don't have a sequence dictionary, but have a means to generate it, do so
        if (sequenceDictionary == null && (chrIndeces != null && !chrIndeces.isEmpty())) {
            sequenceDictionary = new SAMSequenceDictionary();
            for (String name : chrIndeces.keySet()) {
                SAMSequenceRecord seq = new SAMSequenceRecord(name, 0);
                sequenceDictionary.addSequence(seq);
            }
        }
        return sequenceDictionary;
    }

    /**
     * set the sequence dictionary
     *
     * @param dictionary the dictionary to set for this index
     */
    public void setSequenceDictionary(SAMSequenceDictionary dictionary) {
        // TODO: we should do validation here
        this.sequenceDictionary = dictionary;
    }

    protected void writeHeader(LittleEndianOutputStream dos) throws IOException {

        int magicNumber = 1480870228;   //  byte[]{'T', 'I', 'D', 'X'};

        dos.writeInt(magicNumber);
        dos.writeInt(getType());
        dos.writeInt(version);
        dos.writeString(indexedFile);
        dos.writeLong(indexedFileSize);
        dos.writeLong(indexedFileTS);
        dos.writeString(indexedFileMD5);
        dos.writeInt(SEQUENCE_DICTIONARY_FLAG);   // Reserved for future use
        writeSequenceDictionary(dos);
    }

    protected abstract int getType();

    /**
     * write the sequence dictionary out to the disk
     *
     * @param dos the output stream
     * @throws java.io.IOException thrown from the writing operations
     */
    private void writeSequenceDictionary(LittleEndianOutputStream dos) throws IOException {
        if (sequenceDictionary == null)
            writeDictFromIndexInfo(dos);
        else {
            dos.writeInt(sequenceDictionary.size());
            for (SAMSequenceRecord record : sequenceDictionary.getSequences()) {
                dos.writeString(record.getSequenceName());
                dos.writeInt(record.getSequenceLength());
            }
        }
    }

    private void writeDictFromIndexInfo(LittleEndianOutputStream dos) throws IOException {
        dos.writeInt(chrIndeces.size());
        for (Map.Entry<String, ChrIndex> record : chrIndeces.entrySet()) {
            dos.writeString(record.getKey());
            dos.writeInt(0); // we don't know the size of the index
        }
    }

    protected void readHeader(LittleEndianInputStream dis) throws IOException {

        version = dis.readInt();
        indexedFile = dis.readString();
        indexedFileSize = dis.readLong();
        indexedFileTS = dis.readLong();
        indexedFileMD5 = dis.readString();
        int flags = dis.readInt();
        if ((flags & SEQUENCE_DICTIONARY_FLAG) == SEQUENCE_DICTIONARY_FLAG)
            readSequenceDictionary(dis);

    }

    private void readSequenceDictionary(LittleEndianInputStream dis) throws IOException {
        int size = dis.readInt();
        if (size < 0) throw new IllegalStateException("Size of the sequence dictionary entries is negitive");
        SAMSequenceDictionary dictionary = new SAMSequenceDictionary();
        for (int x = 0; x < size; x++) {
            SAMSequenceRecord seq = new SAMSequenceRecord(dis.readString(), dis.readInt());
            dictionary.addSequence(seq);
        }
        this.sequenceDictionary = dictionary;
    }

    public Set<String> getSequenceNames() {
        return chrIndeces.keySet();
    }

    /**
     * @param chr
     * @param start
     * @param end
     * @return
     */
    public List<Block> getBlocks(String chr, int start, int end) {

        ChrIndex chrIdx = chrIndeces.get(chr);
        if (chrIdx == null) {

            // Todo -- throw an exception ?
            return null;
        } else {
            return chrIdx.getBlocks(start, end);
        }
    }

    /**
     * Store self to a file
     *
     * @param f
     * @throws java.io.IOException
     */
    public void write(File f) throws IOException {

        LittleEndianOutputStream dos = null;

        try {
            dos = new LittleEndianOutputStream(new BufferedOutputStream(new FileOutputStream(f)));
            writeHeader(dos);
            //# of chromosomes
            dos.writeInt(chrIndeces.size());
            for (ChrIndex chrIdx : chrIndeces.values()) {
                chrIdx.write(dos);
            }
        } finally {
            dos.close();
        }

    }

    public abstract Class getIndexClass();

    public void read(LittleEndianInputStream dis) throws IOException {

        try {
             readHeader(dis);

            int nChromosomes = dis.readInt();
            chrIndeces = new HashMap(nChromosomes * 2);

            while (nChromosomes-- > 0) {
                ChrIndex chrIdx = (ChrIndex) getIndexClass().newInstance();
                chrIdx.read(dis);
                chrIndeces.put(chrIdx.getName(), chrIdx);
            }

        } catch (InstantiationException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        } catch (IllegalAccessException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        } finally {
            dis.close();
        }

    }
}
