/*
 * 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.igv.track.tribble;

import net.sf.samtools.util.CloseableIterator;
import org.apache.log4j.Logger;
import org.broad.igv.exceptions.DataLoadException;
import org.broad.igv.ui.util.MessageUtils;
import org.broad.igv.util.LRUCache;
import org.broad.igv.util.RuntimeUtils;
import org.broad.tribble.Feature;
import org.broad.tribble.FeatureReader;
import org.broad.tribble.util.CloseableTribbleIterator;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.io.IOException;
import java.util.Set;


/**
 * Created by IntelliJ IDEA.
 * User: jrobinso
 * Date: Jun 24, 2010
 * Time: 10:45:32 PM
 * To change this template use File | Settings | File Templates.
 */
public class CachingFeatureReader implements FeatureReader {

    private static Logger log = Logger.getLogger(CachingFeatureReader.class);
    String cachedChr = "";
    static int maxTileCount = 10;
    private static int defaultTileSize = 1000000;
    private int tileSize = defaultTileSize;
    FeatureReader reader;
    LRUCache<Integer, Tile> cache;


    public CachingFeatureReader(FeatureReader reader) {
        this(reader, defaultTileSize);
    }



    public CachingFeatureReader(FeatureReader reader, int tileSize) {
        this.reader = reader;
        this.cache = new LRUCache(maxTileCount);
        this.tileSize = tileSize;
    }

    public void setTileSize(int tileSize) {
        cache.clear();
        this.tileSize = tileSize;

    }

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

    /**
     * Return an iterator over the entire file.  Nothing to cache,  just delegate to the wrapped reader
     *
     * @throws IOException
     */
    public CloseableTribbleIterator iterator() throws IOException {
        return reader.iterator();
    }

    public void close() throws IOException {
        cache.clear();
        reader.close();
    }

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

    /**
     * TODO -- implement contained support (specifically contained == true).  Not needed for IGV
     * @param chr
     * @param start
     * @param end
     * @param contained
     * @return
     * @throws IOException
     */
    public CloseableTribbleIterator query(String chr, int start, int end, boolean contained) throws IOException {

        int startTile = start / tileSize;
        int endTile = end / tileSize;    // <= inclusive
        List<Tile> tiles = getTiles(chr, startTile, endTile);

        if (tiles.size() == 0) {
            return null;
        }

        // Count total # of records
        int recordCount = tiles.get(0).getOverlappingRecords().size();
        for (Tile t : tiles) {
            recordCount += t.getContainedRecords().size();
        }

        List<Feature> alignments = new ArrayList(recordCount);
        alignments.addAll(tiles.get(0).getOverlappingRecords());
        for (Tile t : tiles) {
            alignments.addAll(t.getContainedRecords());
        }
        return new TiledIterator(start, end, alignments);
    }


    /**
     * Return loaded tiles that span the query interval.
     *
     * @param seq
     * @param startTile
     * @param endTile
     * @return
     */
    private List<Tile> getTiles(String seq, int startTile, int endTile) {

        if (!seq.equals(cachedChr)) {
            if (log.isDebugEnabled()) {
                log.debug("Creating cache for: " + seq);
            }
            cache.clear();
            cachedChr = seq;
        }


        List<Tile> tiles = new ArrayList(endTile - startTile + 1);
        List<Tile> tilesToLoad = new ArrayList(endTile - startTile + 1);

        for (int t = startTile; t <= endTile; t++) {
            Tile tile = cache.get(t);

            if (tile == null) {
                if (log.isDebugEnabled()) {
                    log.debug("Tile cache miss: " + t);
                }
                int start = t * tileSize;
                int end = start + tileSize;
                tile = new Tile(t, start, end);
                cache.put(t, tile);
            }

            tiles.add(tile);

            // The current tile is loaded,  load any preceding tiles we have pending
            if (tile.isLoaded()) {
                if (tilesToLoad.size() > 0) {
                    if (!loadTiles(seq, tilesToLoad)) {
                        return tiles;
                    }
                }
                tilesToLoad.clear();
            } else {
                tilesToLoad.add(tile);
            }
        }

        if (tilesToLoad.size() > 0) {
            loadTiles(seq, tilesToLoad);
        }

        return tiles;
    }

    private boolean loadTiles(String seq, List<Tile> tiles) {

        assert(tiles.size() > 0);

         if (log.isDebugEnabled()) {
            int first = tiles.get(0).getTileNumber();
            int end = tiles.get(tiles.size() - 1).getTileNumber();
            log.debug("Loading tiles: " + first + "-" + end);
        }

        int start = tiles.get(0).start;
        int end = tiles.get(tiles.size() - 1).end;
        CloseableIterator<Feature> iter = null;

        //log.debug("Loading : " + start + " - " + end);
        int featureCount = 0;
        long t0 = System.currentTimeMillis();
        try {
            iter = reader.query(seq, start, end);

            while (iter != null && iter.hasNext()) {
                Feature record = iter.next();

                // Range of tile indeces that this alignment contributes to.
                int aStart = record.getStart();
                int aEnd = record.getEnd();
                int idx0 = Math.max(0, (aStart - start) / tileSize);
                int idx1 = Math.min(tiles.size() - 1, (aEnd - start) / tileSize);

                // Loop over tiles this read overlaps
                for (int i = idx0; i <= idx1; i++) {
                    Tile t = tiles.get(i);

                    if ((aStart >= t.start) && (aStart < t.end)) {
                        t.containedRecords.add(record);
                    } else if ((aEnd >= t.start) && (aStart < t.start)) {
                        t.overlappingRecords.add(record);
                    }
                }
            }

            for (Tile t : tiles) {
                t.setLoaded(true);
            }

            return true;

        } catch (Exception e) {
            log.error("Error loading alignment data", e);
            throw new DataLoadException("", "Error: " + e.toString());
        }

        finally {
            if (iter != null) {
                iter.close();
            }
            //IGVMainFrame.getInstance().resetStatusMessage();
        }
    }

    private boolean checkMemory() {
        if (RuntimeUtils.getAvailableMemory() < 50000000) {
            LRUCache.clearCaches();
            if (RuntimeUtils.getAvailableMemory() < 50000000) {
                String msg = "Memory is low, reading terminating.";
                MessageUtils.showMessage(msg);
                return false;
            }
        }
        return true;
    }



    static class Tile {

        private boolean loaded = false;
        private int start;
        private int end;
        private int tileNumber;
        private List<Feature> containedRecords;
        private List<Feature> overlappingRecords;

        Tile(int tileNumber, int start, int end) {
            this.tileNumber = tileNumber;
            this.start = start;
            this.end = end;
            containedRecords = new ArrayList(1000);
            overlappingRecords = new ArrayList(100);
        }

        public int getTileNumber() {
            return tileNumber;
        }


        public int getStart() {
            return start;
        }

        public void setStart(int start) {
            this.start = start;
        }

        public List<Feature> getContainedRecords() {
            return containedRecords;
        }

        public List<Feature> getOverlappingRecords() {
            return overlappingRecords;
        }

        public boolean isLoaded() {
            return loaded;
        }

        public void setLoaded(boolean loaded) {
            this.loaded = loaded;
        }

    }

    /**
     * TODO -- this is a pointeless class.  It would make sense if it actually took tiles, instead of the collection
     * TODO -- of alignments.
     */
    public class TiledIterator implements CloseableTribbleIterator {

        Iterator<Feature> currentSamIterator;
        int end;
        Feature nextRecord;
        int start;
        List<Feature> alignments;

        TiledIterator(int start, int end, List<Feature> alignments) {
            this.alignments = alignments;
            this.start = start;
            this.end = end;
            currentSamIterator = alignments.iterator();
            advanceToFirstRecord();
        }

        public void close() {
            // No-op
        }

        public boolean hasNext() {
            return nextRecord != null;
        }

        public Feature next() {
            Feature ret = nextRecord;

            advanceToNextRecord();

            return ret;
        }

        public void remove() {
            // ignored
        }

        private void advanceToFirstRecord() {
            advanceToNextRecord();
        }

        private void advanceToNextRecord() {
            advance();

            while ((nextRecord != null) && (nextRecord.getEnd() < start)) {
                advance();
            }
        }

        private void advance() {
            if (currentSamIterator.hasNext()) {
                nextRecord = currentSamIterator.next();
                if (nextRecord.getStart() > end) {
                    nextRecord = null;
                }
            } else {
                nextRecord = null;
            }
        }

        public Iterator iterator() {
            return this;
        }
    }
}

