/*
 * 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.
 */
/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.broad.igv.sam.reader;


import net.sf.samtools.SAMFileHeader;
import net.sf.samtools.util.CloseableIterator;
import org.apache.log4j.Logger;
import org.broad.igv.PreferenceManager;
import org.broad.igv.exceptions.DataLoadException;
import org.broad.igv.sam.Alignment;
import org.broad.igv.sam.EmptyAlignmentIterator;
import org.broad.igv.ui.IGVMainFrame;
import org.broad.igv.ui.util.MessageUtils;
import org.broad.igv.util.LRUCache;
import org.broad.igv.util.RuntimeUtils;

import javax.swing.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * A wrapper for an AlignmentQueryReader that caches query results
 *
 * @author jrobinso
 */
public class CachingQueryReader implements AlignmentQueryReader {

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

    private static final int LOW_MEMORY_THRESHOLD = 150000000;
    private static final int KB = 1000;
    private static final int MITOCHONDRIA_TILE_SIZE = 1000;
    private static int DEFAULT_TILE_SIZE = 1600;
    private static int MAX_TILE_COUNT = 100;

    String cachedChr = "";
    private int tileSize = DEFAULT_TILE_SIZE;
    AlignmentQueryReader reader;
    LRUCache<Integer, Tile> cache;
    private int maxReadCount = 500000;

    public CachingQueryReader(AlignmentQueryReader reader) {
        this.reader = reader;
        cache = new LRUCache(MAX_TILE_COUNT);
        tileSize = Math.min(DEFAULT_TILE_SIZE,
                (int) (PreferenceManager.getInstance().getSAMPreferences().getMaxVisibleRange() * KB));

    }

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

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

    public SAMFileHeader getHeader() {
        return reader.getHeader();
    }

    public CloseableIterator<Alignment> iterator() {
        return reader.iterator();
    }

    public boolean hasIndex() {
        return reader.hasIndex();
    }

    public CloseableIterator<Alignment> query(String sequence, int start, int end, boolean contained) {

        int startTile = (start + 1) / getTileSize(sequence);
        int endTile = end / getTileSize(sequence);    // <= inclusive
        List<Tile> tiles = getTiles(sequence, startTile, endTile);

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

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

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

    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);

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

            if (tile == null) {
                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);

        maxReadCount = PreferenceManager.getInstance().getSAMPreferences().getMaxReads();

        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<Alignment> iter = null;

        //log.debug("Loading : " + start + " - " + end);
        int alignmentCount = 0;
        try {
            iter = reader.query(seq, start, end, false);

            int tileSize = getTileSize(seq);
            while (iter != null && iter.hasNext()) {
                Alignment record = iter.next();

                // Range of tile indeces that this alignment contributes to.
                int aStart = record.getAlignmentStart();
                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);
                    }
                }

                alignmentCount++;
                if (alignmentCount % MITOCHONDRIA_TILE_SIZE == 0) {
                    IGVMainFrame.getInstance().setStatusBarMessage("Reads loaded: " + alignmentCount);
                    if (checkMemory() == false) {
                        return false;
                    }
                }

                if (alignmentCount > maxReadCount) {
                    String msg = "Maximum # of reads exceeded (" + maxReadCount + "). Continue?";
                    int v = JOptionPane.showConfirmDialog(null, msg, msg, JOptionPane.YES_NO_OPTION);
                    if (v == JOptionPane.YES_OPTION) {
                        maxReadCount = Math.min(Integer.MAX_VALUE, 2 * maxReadCount);
                    } else {
                        break;
                    }
                }
            }

            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() < LOW_MEMORY_THRESHOLD) {
            LRUCache.clearCaches();
            if (RuntimeUtils.getAvailableMemory() < LOW_MEMORY_THRESHOLD) {
                String msg = "Memory is low, reading terminating.";
                MessageUtils.showMessage(msg);
                return false;
            }
        }
        return true;
    }

    /**
     * @return the tileSize
     */
    public int getTileSize(String chr) {
        if (chr.equals("M") || chr.equals("chrM") || chr.equals("MT") || chr.equals("chrMT")) {
            return MITOCHONDRIA_TILE_SIZE;
        } else {
            return tileSize;
        }
    }

    static class Tile {

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

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

        public int getTileNumber() {
            return tileNumber;
        }


        public int getStart() {
            return start;
        }

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

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

        public void setContainedRecords(List<Alignment> containedRecords) {
            this.containedRecords = containedRecords;
        }

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

        public void setOverlappingRecords(List<Alignment> overlappingRecords) {
            this.overlappingRecords = overlappingRecords;
        }

        public boolean isLoaded() {
            return loaded;
        }

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

    }

    public class TiledIterator implements CloseableIterator<Alignment> {

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

        TiledIterator(int start, int end, List<Alignment> 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 Alignment next() {
            Alignment 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.getAlignmentStart() > end) {
                    nextRecord = null;
                }
            } else {
                nextRecord = null;
            }
        }
    }
}


