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

//~--- non-JDK imports --------------------------------------------------------

import org.apache.log4j.Logger;
import org.broad.igv.Globals;
import org.broad.igv.PreferenceManager;
import org.broad.igv.feature.Chromosome;
import org.broad.igv.feature.Genome;
import org.broad.igv.feature.GenomeDescriptor;
import org.broad.igv.feature.GenomeManager;
import org.broad.igv.sam.AlignmentTrack;
import org.broad.igv.track.Track;
import org.broad.igv.track.TrackGroup;
import org.broad.igv.ui.IGVMainFrame;
import org.broad.igv.ui.UIConstants;
import org.broad.igv.ui.panel.DataPanel;
import org.broad.igv.ui.panel.DragEventManager;
import org.broad.igv.ui.panel.TrackPanelScrollPane;
import org.broad.igv.ui.util.MessageUtils;

import javax.swing.*;
import javax.swing.text.NumberFormatter;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.*;
import java.util.List;

/**
 * @author jrobinso
 */
public class ViewContext {

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

    private static ViewContext instance = new ViewContext();

    private static NumberFormat numberFormat = NumberFormat.getInstance(Locale.US);

    private String genomeId;

    Genome genome;

    /**
     * The nominal viewport width in pixels.
     */
    public int binsPerTile = 700;

    int dataPanelWidth = 700;

    /**
     * The chromosome currently in view
     */
    private String chrName = "chrAll";

    /**
     * The minimum zoom level for the current screen size + chromosome combination.
     */
    private int minZoom = 0;

    /**
     * The current zoom level.  Zoom level -1 corresponds to the whole
     * genome view (chromosome "all")
     */
    private int zoom = minZoom;
    /**
     * The maximum zoom level.  Set to prevent integer overflow.  This is a function
     * of chromosom length.
     */
    public static int maxZoom = 23;
    /**
     * The number of tiles for this zoom level, = 2^zoom
     */
    private int nTiles = 1;
    /**
     * The maximum virtual pixel value.
     */
    private double maxPixel;
    /**
     * The origin in bp
     */
    private double origin = 0;

    /**
     * The location (x axis) locationScale in base pairs / virtual pixel
     */
    private double locationScale;

    private boolean locationScaleValid = false;
    /**
     * Indicates if the data is stretched to fill the window.  This happens at low
     * resolution views when drawing at the resolution of the zoom level would
     * only partially fill the screen.
     */
    private boolean stretched = false;

    public History history;


    public static ViewContext getInstance() {
        return instance;
    }

    /**
     * Constructs ...
     */
    private ViewContext() {
        String lastChromosomeViewed = PreferenceManager.getInstance().getLastChromosomeViewed();

        if ((lastChromosomeViewed == null) || lastChromosomeViewed.trim().equals("")) {
            this.chrName = getHomeChr();
        } else {
            this.chrName = lastChromosomeViewed;

        }

        history = new History(100);
    }

    /**
     * Return the "home chromosome" for the current genome.  This should probably be a method
     * on the Genome class.
     *
     * @return
     */
    public String getHomeChr() {
        return genome == null ? Globals.CHR_ALL : genome.getHomeChromosome();
    }

    /**
     * Compute the maximum zoom level, which is a function of chromosome length.
     */
    private void computeMaxZoom() {

        // Compute max zoom.  Assume window size @ max zoom of ~ 50 bp
        if (genome != null && chrName != null && genome.getChromosome(chrName) != null) {
            if (chrName.equals(Globals.CHR_ALL)) {
                maxZoom = 0;
            } else {
                int chrLength = genome.getChromosome(chrName).getLength();

                maxZoom = (int) (Math.log(chrLength / 50.0) / log2) + 1;
            }

            if (zoom > maxZoom) {
                setZoom(maxZoom);
            }
        }


        // TODO -- fire chromosome changed event rather than this
    }

    private void setZoom(int newZoom) {
        zoom = Math.min(maxZoom, newZoom);
        nTiles = (int) Math.pow(2, Math.max(minZoom, zoom));
        maxPixel = getTilesTimesBinsPerTile();
        invalidateLocationScale();

        // TODO -- do this with events,
        if (IGVMainFrame.hasInstance()) {
            IGVMainFrame.getInstance().repaintStatusAndZoomSlider();
        }

    }

    /**
     * Method description
     *
     * @param increment
     */
    public void incrementZoom(int increment) {
        zoomAndCenter((int) zoom + increment);
    }

    /**
     * Method description
     *
     * @param newZoom
     */
    public void zoomAndCenterAdjusted(int newZoom) {
        zoomAndCenter(minZoom + newZoom);
    }

    /**
     * Method description
     *
     * @param newZoom
     */
    public void zoomAndCenter(int newZoom) {

        // Zoom but remain centered about current center
        double currentCenter = origin + ((dataPanelWidth / 2) * getScale());

        zoomTo(newZoom, currentCenter);
    }

    /**
     * Method description
     *
     * @param newZoom
     * @param newCenter
     */
    public void zoomTo(final int newZoom, final double newCenter) {

        if (chrName.equals(Globals.CHR_ALL)) {
            chrName = getHomeChr();
        }

        if (chrName.equals(Globals.CHR_ALL)) {

            // DISABLE ZOOMING FOR GENOME VIEW
            // Translate the location to chromosome number
            jumpToChromosomeForGenomeLocation(newCenter);
            IGVMainFrame.getInstance().chromosomeChangeEvent();
        } else {
            if (zoom != newZoom) {

                setZoom(newZoom);
                computeLocationScaleImmediately();

                double newLocationScale = getScale();

                // Adjust origin so newCenter is centered
                double newOrigin = Math.round(
                        newCenter - ((dataPanelWidth / 2) * newLocationScale));

                setOrigin(newOrigin);

            }
        }

        recordHistory();

        // This is a hack,  this is not a drag event but is a "jump"
        DragEventManager.getInstance().dragStopped();

    }

    public void recordHistory() {
        history.push(getFormattedLocusString());
    }

    private void jumpToChromosomeForGenomeLocation(double locationMB) {
        double startMB = 0;

        for (String chr : getGenome().getChromosomeNames()) {
            double endMB = startMB + getGenome().getChromosome(chr).getLength() / 1000.0;

            if ((locationMB > startMB) && (locationMB <= endMB)) {

                // this.jumpTo(chr, -1, -1);
                this.setChromosomeName(chr);
                break;
            }

            startMB = endMB;
        }
    }

    /**
     * Method description
     *
     * @param delta
     */
    public void shiftOriginPixels(double delta) {
        double shiftBP = delta * getScale();
        setOrigin(origin + shiftBP);
    }

    public void snapToGrid() {
        setOrigin(Math.round(origin));
    }

    /**
     * Method description
     *
     * @param chrLocation
     */
    public void centerOnLocation(String chr, double chrLocation) {
        if (!chrName.equals(chr)) {
            chrName = chr;
            computeMaxZoom();
            if (zoom > maxZoom) {
                setZoom(maxZoom);
            }
        }
        double windowWidth = (dataPanelWidth * getScale()) / 2;
        setOrigin(Math.round(chrLocation - windowWidth));
    }

    /**
     * Method description
     *
     * @param chrLocation
     */
    public void centerOnLocation(double chrLocation) {
        double windowWidth = (dataPanelWidth * getScale()) / 2;
        setOrigin(Math.round(chrLocation - windowWidth));
        recordHistory();
    }

    public boolean windowAtEnd() {
        double windowLengthBP = dataPanelWidth * getScale();
        return origin + windowLengthBP + 1 > getChromosomeLength();

    }


    /* Keep origin within data range */

    /**
     * Method description
     *
     * @param newOrigin
     */
    public void setOrigin(double newOrigin) {

        int windowLengthBP = (int) (dataPanelWidth * getScale());
        if (PreferenceManager.getInstance().getSAMPreferences().isShowSoftClipped()) {
            origin = Math.max(-1000, Math.min(newOrigin, getChromosomeLength() + 1000 - windowLengthBP));
        } else {
            origin = Math.max(0, Math.min(newOrigin, getChromosomeLength() - windowLengthBP));
        }

        for (TrackPanelScrollPane sp : IGVMainFrame.getInstance().getTrackManager().getTrackPanelScrollPanes()) {
            preloadTrackData(sp.getDataPanel());
        }

        // If zoomed in sufficiently track the center position
        if (locationScale < 10) {
            IGVMainFrame.getInstance().setStatusBarMessage(chrName + ":" + ((int) getCenter() + 1));
        }

        // Repaint
        IGVMainFrame.getInstance().repaintDataAndHeaderPanels();
        IGVMainFrame.getInstance().repaintStatusAndZoomSlider();
    }

    static double log2 = Math.log(2);

    public void jumpTo(String chr, int start, int end) {

        if (chr != null) {
            if (genome.getChromosome(chr) == null && !chr.contains(Globals.CHR_ALL)) {
                JOptionPane.showMessageDialog(IGVMainFrame.getInstance(),
                        chr + " is not a valid chromosome.");
                return;
            }
            setChromosomeName(chr);
        }

        if (start >= 0) {

            int z = (int) (Math.log(getChromosomeLength() / (end - start)) / log2) + 1;
            if (z != this.zoom) {
                zoom = Math.min(maxZoom, Math.max(minZoom, z));
                nTiles = (int) Math.pow(2, zoom);
                maxPixel = getTilesTimesBinsPerTile();
            }

            int w = dataPanelWidth;
            setLocationScale(((double) (end - start)) / w);
            origin = start;
        }


        for (TrackPanelScrollPane sp : IGVMainFrame.getInstance().getTrackManager().getTrackPanelScrollPanes()) {
            preloadTrackData(sp.getDataPanel());
        }

        if (log.isDebugEnabled()) {
            log.debug("Data panel width = " + dataPanelWidth);
            log.debug("New start = " + (int) getOrigin());
            log.debug("New end = " + (int) getEnd());
            log.debug("New center = " + (int) getCenter());
        }


        // Repaint
        IGVMainFrame.getInstance().repaintDataAndHeaderPanels();
        IGVMainFrame.getInstance().repaintStatusAndZoomSlider();

    }

    /**
     * Method description
     *
     * @return
     */
    public double getOrigin() {
        return origin;
    }

    public double getCenter() {
        return origin + getScale() * dataPanelWidth / 2;
    }

    public double getEnd() {
        return origin + getScale() * dataPanelWidth;
    }

    /**
     * Method description
     *
     * @return
     */
    public int getZoom() {
        return zoom;
    }

    /**
     * Return the maximum zoom level
     *
     * @return
     */
    public int getMaxZoom() {
        return maxZoom;
    }

    /**
     * Method description
     *
     * @return
     */
    public int getAdjustedZoom() {
        return zoom - minZoom;
    }

    /**
     * Method description
     *
     * @return
     */
    public int getNTiles() {
        return nTiles;
    }

    /**
     * Method description
     *
     * @return
     */
    public double getMaxPixel() {
        return maxPixel;
    }

    /**
     * Method description
     *
     * @param panel
     */
    public void setDataPanel(DataPanel panel) {

        if (panel != null) {

            panel.addComponentListener(new ComponentAdapter() {

                @Override
                public void componentResized(ComponentEvent e) {
                    int w = e.getComponent().getWidth();
                    if (w != dataPanelWidth) {
                        dataPanelWidth = w;
                        invalidateLocationScale();
                    }
                }
            });
        }
    }

    /**
     * Method description
     *
     * @param name
     */
    public void setChrName(String name) {
        this.setChromosomeName(name);
    }

    /**
     * @param name
     * @param force
     * @ deprecated, replace with calls to setChrName();
     */
    public void setChromosomeName(String name, boolean force) {

        if ((chrName == null) || !name.equals(chrName) || force) {
            chrName = name;
            origin = 0;
            setZoom(0);
            computeMaxZoom();
        }
    }

    /**
     * @param name
     * @ deprecated, replace with calls to setChrName();
     */
    public void setChromosomeName(String name) {
        setChromosomeName(name, false);
    }

    /**
     * Method description
     *
     * @return
     */
    public String getChrName() {
        return chrName;
    }

    public String getNextChrName(String chr) {
        List<String> chrList = genome.getChromosomeNames();
        for (int i = 0; i < chrList.size() - 1; i++) {
            if (chrList.get(i).equals(chr)) {
                return chrList.get(i + 1);
            }
        }
        return null;
    }

    public String getPrevChrName(String chr) {
        List<String> chrList = genome.getChromosomeNames();
        for (int i = chrList.size() - 1; i > 0; i--) {
            if (chrList.get(i).equals(chr)) {
                return chrList.get(i - 1);
            }
        }
        return null;
    }

    // TODO -- this parameter shouldn't be stored here.  Maybe in a specialized
    // layout manager?

    /**
     * Method description
     *
     * @return
     */
    public int getDataPanelWidth() {
        return dataPanelWidth;
    }

    /**
     * Return the current locationScale in base pairs / pixel
     *
     * @return
     */
    public double getScale() {
        if ((locationScale == 0) || !locationScaleValid) {
            computeLocationScaleImmediately();
        }

        if (locationScale < 0) {
            System.err.println("Negative scale");
        }

        return locationScale;
    }

    /**
     * Method description
     */
    public void invalidateLocationScale() {
        //Thread.dumpStack();
        locationScaleValid = false;
    }

    public void computeLocationScaleImmediately() {

        if (genome != null) {

            computeMinZoom();

            double virtualPixelSize = getTilesTimesBinsPerTile();

            stretched = virtualPixelSize < dataPanelWidth;

            double nPixel = Math.max(virtualPixelSize, dataPanelWidth);

            setLocationScale(((double) getChromosomeLength()) / nPixel);
        }
    }

    /**
     * Compute the minimum zoom level for the data panel width.  This is defined as the maximum
     * zoom level for which all data bins will fit in the window without loss of
     * data,  i.e. the maximum zoom level for which nBins < nPixels.  The number
     * of bins is defined as
     * nBins =  2^z
     * so minZoom is the value z such that nBins < dataPanelWidth
     */
    private void computeMinZoom() {
        if (this.chrName.equals(Globals.CHR_ALL)) {
            minZoom = 0;
        } else {
            minZoom = Math.max(0, (int) (Math.log((dataPanelWidth / binsPerTile)) / log2));

            if (zoom < minZoom) {
                zoom = minZoom;
                nTiles = (int) Math.pow(2, zoom);
                maxPixel = getTilesTimesBinsPerTile();
            }
        }

    }

    /**
     * Return the chromosome position corresponding to the pixel index.  The
     * pixel index is the pixel "position" translated by -origin.
     *
     * @param pixelIndex
     * @return
     */
    public double getChromosomePosition(int pixelIndex) {
        return origin + (getScale() * pixelIndex);
    }

    /**
     * Return the pixel position corresponding to the chromosomal position.
     *
     * @param chromosomePosition
     * @return
     */
    public int getPixelPosition(double chromosomePosition) {
        return (int) ((chromosomePosition - origin) / getScale());
    }

    /**
     * Method description
     *
     * @return
     */
    public Chromosome getChromosome() {
        if (genome == null) {
            log.error("Genome not loaded!");
            return null;
        }

        return genome.getChromosome(chrName);
    }

    /**
     * Method description
     *
     * @return
     */
    public int getChromosomeLength() {

        if (genome == null) {
            return 1;
        }

        if (chrName.equals("All")) {

            // TODO -- remove the hardcoded unit divider ("1000")
            return (int) (genome.getLength() / 1000);

            // return genome.getLength();
        } else {
            if (getChromosome() == null) {
                System.out.println("Null chromosome: " + chrName);
                if (genome == null) {
                    return 1;
                } else {
                    return genome.getChromosomes().iterator().next().getLength();
                }
            }

            return getChromosome().getLength();
        }
    }

    // /////////////////////////////////////////////////////////////////////////
    // Genome methods /////////////////////////////////////////////////////////

    /**
     * Attempt to switch genomes to newGenome.  Return the actual genome, which
     * might differ if the load of newGenome fails.
     *
     * @param newGenome
     */
    public String setGenomeId(String newGenome) {

        if (genomeId != null && !genomeId.equals(newGenome)) {
            history.clear();
        }

        if (log.isDebugEnabled()) {
            log.debug("Setting genome id: " + newGenome);
        }

        boolean startUp = (genomeId == null);
        boolean loadFailed = false;

        genomeId = newGenome;
        if (!GenomeManager.getInstance().isGenomeLoaded(genomeId)) {
            try {
                if (log.isDebugEnabled()) {
                    log.debug("findGenomeAndLoad: " + genomeId);
                }
                GenomeManager.getInstance().findGenomeAndLoad(genomeId);
            } catch (IOException e) {
                log.error("Error loading genome: " + genomeId, e);
                loadFailed = true;
                MessageUtils.showMessage("Load of genome: " + genomeId + " failed.");
            }
        }
        genome = GenomeManager.getInstance().getGenome(genomeId);

        if (genome == null || loadFailed) {
            GenomeManager.GenomeListItem defaultDesc = GenomeManager.getInstance().getTopGenomeListItem();
            String msg = "Could not locate genome: " + genomeId + ".  Loading " + defaultDesc.getDisplayableName();
            MessageUtils.showMessage(msg);
            log.error("Could not locate genome: " + genomeId + ".  Loading " + defaultDesc.getDisplayableName());

            // The previously used genome is unavailable.   Load hg18, we are assuming that
            // this is always available.  // TODO -- permit IGV starting with no selected genome
            genomeId = defaultDesc.getId();
            try {
                if (log.isDebugEnabled()) {
                    log.debug("findGenomeAndLoad: " + genomeId);
                }
                GenomeManager.getInstance().findGenomeAndLoad(genomeId);
            } catch (IOException e) {
                log.error("Error loading genome: " + genomeId, e);
                MessageUtils.showMessage("<html>Load of genome: " + genomeId + " failed." +
                        "<br>IGV is in an ustable state and will be closed." +
                        "<br>Please report this error to igv-help@broadinstitute.org");
                System.exit(-1);
            }

            if (log.isDebugEnabled()) {
                log.debug("Get genome id");
            }
            genome = GenomeManager.getInstance().getGenome(genomeId);

            PreferenceManager.getInstance().setDefaultGenome(genomeId);
            this.setChrName(getHomeChr());


        } else if (!startUp) {
            // We don't know what chromosomes the new genome has, set to "all" (whole genome)
            this.setChrName(getHomeChr());
        }
        computeMaxZoom();
        invalidateLocationScale();
        if (!Globals.isHeadless()) {
            IGVMainFrame.getInstance().chromosomeChangeEvent();
        }
        return genomeId;
    }

    /**
     * Version for command line tools.
     *
     * @param newGenome
     */
    public void setGenomeIdHeadless(String newGenome) {


        genomeId = newGenome;
        genome = GenomeManager.getInstance().getGenome(genomeId);


    }

    /**
     * Method description
     *
     * @return
     */
    public String getGenomeId() {
        return genomeId;
    }

    /**
     * Method description
     *
     * @return
     */
    public Genome getGenome() {
        return genome;
    }

    /**
     * Return the collection of chromosome names for the currently loaded
     * genome
     *
     * @return
     */
    public Collection<String> getChromosomeNames() {
        return genome.getChromosomeNames();
    }

    /**
     * Method description
     *
     * @return
     */
    public boolean isStretched() {
        return stretched;
    }

    /**
     * Method description
     *
     * @return
     */
    public double getTilesTimesBinsPerTile() {
        return (double) nTiles * (double) binsPerTile;
    }

    /**
     * Get the UCSC style locus string corresponding to the current view.  THe UCSC
     * conventions are followed for coordinates,  specifically the internal representation
     * is "zero" based (first base is numbered 0) but the display representation is
     * "one" based (first base is numbered 1).   Consequently 1 is added to the
     * computed positions.
     *
     * @return
     */
    public String getFormattedLocusString() {

        if (zoom == 0) {
            return getChrName();
        } else {
            Range range = getCurrentRange();
            String startStr = numberFormat.format(range.getStart());
            String endStr = numberFormat.format(range.getEnd());
            String position = range.getChr() + ":" + startStr + "-" + endStr;
            return position;
        }

    }

    public Range getCurrentRange() {
        int start = 0;
        int end = getDataPanelWidth();
        int startLoc = (int) getChromosomePosition(start) + 1;
        int endLoc = (int) getChromosomePosition(end);
        Range range = new Range(getChrName(), startLoc, endLoc);
        return range;
    }

    private void setLocationScale(double locationScale) {
        this.locationScale = locationScale;
        locationScaleValid = true;

        //if (log.isDebugEnabled()) {
        //    log.debug("locationScale = " + locationScale);
        //}
    }

    /**
     *
     */
    private void preloadTrackData(DataPanel dp) {

        Rectangle visibleRect = dp.getVisibleRect();
        int start = (int) origin;
        int end = (int) (start + dp.getWidth() * locationScale);
        int trackY = 0;
        Collection<TrackGroup> groups = dp.getTrackGroups();

        for (Iterator<TrackGroup> groupIter = groups.iterator(); groupIter.hasNext();) {
            TrackGroup group = groupIter.next();
            if (group.isVisible()) {
                if (groups.size() > 1) {
                    trackY += UIConstants.groupGap;
                }
                for (Track track : group.getTracks()) {
                    int trackHeight = track.getHeight();

                    if (!(track instanceof AlignmentTrack) && visibleRect != null) {
                        if (trackY > visibleRect.y + visibleRect.height) {
                            break;
                        } else if (trackY + trackHeight < visibleRect.y) {
                            trackY += trackHeight;
                            continue;
                        }
                    }


                    if (track.isVisible()) {
                        //    track.preloadData(chrName, start, end, zoom);
                        //
                        trackY += track.getHeight();
                    }
                }
            }

        }
    }

    public List<String> getAllHistory() {
        return history.getAllHistory();
    }

    public static class Range {
        private String chr;
        private int start;
        private int end;

        public Range(String chr, int start, int end) {
            this.chr = chr;
            this.start = start;
            this.end = end;
        }

        public String getChr() {
            return chr;
        }

        public int getStart() {
            return start;
        }

        public int getEnd() {
            return end;
        }
    }

    public static class History {

        int maxEntries = 100;
        int currPos = 0;

        LinkedList<String> activeStack;
        List<String> allHistory;

        History(int maxEntries) {
            this.maxEntries = maxEntries;
            activeStack = new LinkedList();
            allHistory = new ArrayList();
        }


        public void push(String s) {

            log.debug("History: " + s);
            allHistory.add(s);

            while (currPos > 0) {
                activeStack.removeFirst();
                currPos--;
            }
            activeStack.addFirst(s);


        }


        public String back() {
            if (activeStack.size() == 0 || currPos >= (activeStack.size() - 1)) {
                return null;
            }
            currPos++;
            return activeStack.get(currPos);
        }

        public String forward() {

            if (activeStack.size() == 0 || currPos == 0) {
                return null;
            }
            currPos--;
            printStack();
            return activeStack.get(currPos);
        }


        public String peekBack() {
            if (activeStack.size() == 0 || (currPos + 1) >= activeStack.size()) {
                return null;
            }
            return activeStack.get(currPos + 1);
        }

        public String peekForward() {
            if (activeStack.size() == 0 || (currPos - 1) < 0) {
                return null;
            }
            return activeStack.get(currPos - 1);
        }

        public void clear() {
            activeStack.clear();
            allHistory.clear();
            currPos = 0;
        }

        public void printStack() {
            System.out.println("curr pos=" + currPos);
            for (String s : activeStack) {
                System.out.println(s);
            }
            System.out.println();
        }

        public List<String> getAllHistory() {
            return allHistory;
        }

    }
}
