/*
 * 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.readers;

import org.apache.log4j.Logger;
import org.broad.tribble.Feature;
import org.broad.tribble.FeatureCodec;
import org.broad.tribble.FeatureReader;
import org.broad.tribble.index.Index;
import org.broad.tribble.util.AsciiLineReader;
import org.broad.tribble.util.CloseableTribbleIterator;
import org.broad.tribble.util.LineReader;
import org.broad.tribble.util.ParsingUtils;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * jrobinso
 * <p/>
 * the feature reader class, which uses indices and codecs to read in Tribble file formats.
 */
public class BasicFeatureReader<T extends Feature> implements FeatureReader {

    private static Logger log = Logger.getLogger(BasicFeatureReader.class);


    String path;

    //
    private final QueryReader querySource;
    private final FeatureCodec codec;
    private final Object header;

    /**
     * Constructor for unknown file type,  could be ascii or could be tabix, or something else.
     *
     * @param featureFile
     * @param codec
     * @throws FileNotFoundException
     */

    public BasicFeatureReader(String featureFile, FeatureCodec codec) throws IOException {

        this.path = featureFile;

        // Crude test for now
        if (featureFile.endsWith(".gz") && TabixReader.isTabix(featureFile)) {
            querySource = new TabixReader(featureFile);
        } else {
            String indexFile = featureFile + ".idx";
            querySource = new AsciiQueryReader(featureFile, indexFile);
        }
        this.codec = codec;
        header = readHeader();
    }

    /**
     * Constructor for ascii indexed files
     *
     * @param featureFile
     * @param indexInstance
     * @param codec
     * @throws FileNotFoundException
     */
    public BasicFeatureReader(String featureFile, Index indexInstance, FeatureCodec codec) throws IOException {
        this.path = featureFile;
        this.codec = codec;
        querySource = new AsciiQueryReader(featureFile, indexInstance);
        header = readHeader();
    }

    /**
     * Constructor for ascii indexed files
     *
     * @param featureFile
     * @param indexFile
     * @param codec
     * @throws FileNotFoundException
     */
    public BasicFeatureReader(String featureFile, String indexFile, FeatureCodec codec) throws IOException {
        this.path = featureFile;
        querySource = new AsciiQueryReader(featureFile, indexFile);
        this.codec = codec;
        header = readHeader();
    }


    private Object readHeader() throws IOException {
        AsciiLineReader reader = null;
        try {
            reader = ParsingUtils.openAsciiReader(path);
            return codec.readHeader(reader);
        }
        finally {
            if (reader != null) {
                reader.close();
            }
        }
    }

    public Object getHeader() {
        return header;
    }


    public void close() throws IOException {
        if (querySource != null) {
            querySource.close();
        }
    }

    public CloseableTribbleIterator<T> query(final String chr, final int start, final int end) throws IOException {
        return query(chr, start, end, false);
    }

    public CloseableTribbleIterator<T> iterator() throws IOException {
        return new IteratorImpl<T>(this);
    }

    public CloseableTribbleIterator<T> query(final String chr, final int start, final int end, final boolean contained) throws IOException {

        return new IteratorImpl<T>(this, chr, start, end, contained);
    }


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

    /**
     * the basic feature iterator for indexed files
     */
    public static class IteratorImpl<T extends Feature> implements CloseableTribbleIterator {

        private static Logger log = Logger.getLogger("IteratorImpl");

        String chr;
        int start;
        int end;
        boolean contained;
        T currentRecord;

        final LineReader reader;

        BasicFeatureReader<T> BasicFeatureReader;

        IteratorImpl(BasicFeatureReader<T> BasicFeatureReader) throws IOException {
            this.BasicFeatureReader = BasicFeatureReader;
            reader = BasicFeatureReader.querySource.iterate();
            // we have to read the header off in this case (since the reader seeked to position 0)
            readNextRecord();
        }

        IteratorImpl(BasicFeatureReader<T> BasicFeatureReader,
                     String sequence,
                     int start,
                     int end,
                     boolean contained) throws IOException {

            this.BasicFeatureReader = BasicFeatureReader;
            this.chr = sequence;
            this.start = start;
            this.end = end;
            this.contained = contained;
            reader = BasicFeatureReader.querySource.query(chr, start, end);

            advanceToFirstRecord();
        }

        private T readNextRecord() throws IOException {

            currentRecord = null;
            String nextLine;
            while (currentRecord == null && reader != null && (nextLine = reader.readLine()) != null) {

                Feature f = BasicFeatureReader.codec.decode(nextLine);
                if (f == null)
                    continue;
                else if (f.getStart() > end) {
                    break;
                } else if ((contained && f.getStart() >= start) ||
                        (!contained && f.getEnd() >= start)) {
                    // todo -- any way to avoid this cast?
                    currentRecord = (T) f;
                }
            }
            return currentRecord;
        }

        private void advanceToFirstRecord() throws IOException {
            T record = readNextRecord();
            while (record != null) {
                if (!currentRecord.getChr().equals(chr)) {
                    break;
                } else if ((contained && currentRecord.getStart() >= start) ||
                        (!contained && currentRecord.getEnd() >= start)) {
                    break;
                } else if (currentRecord.getStart() > end) {
                    currentRecord = null;
                    break;
                }
                record = readNextRecord();
            }
        }

        public boolean hasNext() {

            // chr == null => iterator, not query.  Fix this
            if (chr == null) {
                return currentRecord != null;
            }

            return !(currentRecord == null ||
                    !chr.equals(currentRecord.getChr())) && (contained ? currentRecord.getEnd() <= end : currentRecord.getStart() <= end);
        }

        public T next() {
            T ret = currentRecord;
            try {
                readNextRecord();
            } catch (IOException e) {
                throw new RuntimeException("Unable to read the next record, the last record was at " + ret.getChr() + ":" + ret.getStart() + "-" + ret.getEnd(), e);
            }
            return ret;

        }

        public void remove() {
            throw new UnsupportedOperationException("Remove is not supported in Iterators");
        }


        public void close() {
            // we don't have anything open, so don't sweat it
        }

        public Iterator<Feature> iterator() {
            return this;
        }
    }
}