/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2007-2015 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.broad.igv.ui.panel;

import org.broad.igv.feature.Locus;
import org.broad.igv.feature.genome.Genome;
import org.broad.igv.lists.GeneList;
import org.broad.igv.prefs.Constants;
import org.broad.igv.prefs.PreferencesManager;
import org.broad.igv.track.RegionScoreType;
import org.broad.igv.track.Track;
import org.broad.igv.ui.IGV;
import org.broad.igv.ui.action.SearchCommand;
import org.broad.igv.event.GenomeChangeEvent;
import org.broad.igv.event.IGVEventBus;
import org.broad.igv.event.IGVEventObserver;
import org.broad.igv.ui.util.MessageUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * @author jrobinso
 * @date Sep 10, 2010
 */
public class FrameManager implements IGVEventObserver {

    private static List<ReferenceFrame> frames = new ArrayList();
    private static ReferenceFrame defaultFrame;

    public static final String DEFAULT_FRAME_NAME = "genome";

    static {
        frames.add(getDefaultFrame());
    }

    public synchronized static ReferenceFrame getDefaultFrame() {
        if (defaultFrame == null) {
            defaultFrame = new ReferenceFrame(DEFAULT_FRAME_NAME);
        }
        return defaultFrame;
    }

    public static List<ReferenceFrame> getFrames() {
        return frames;
    }

    public static ReferenceFrame getFrame(String frameName) {
        for (ReferenceFrame frame : frames) {
            if (frame.getName().equals(frameName)) {
                return frame;
            }
        }
        return null;
    }

    public static void setFrames(List<ReferenceFrame> f) {
        frames = f;
        IGVEventBus.getInstance().post(new ChangeEvent(frames));
    }

    public static boolean isGeneListMode() {
        return frames.size() > 1;
    }


    public static void setToDefaultFrame(String searchString) {
        frames.clear();
        if (searchString != null) {
            Locus locus = getLocus(searchString, 0);
            if (locus != null) {
                getDefaultFrame().jumpTo(locus);
            }
        }
        frames.add(getDefaultFrame());
        getDefaultFrame().recordHistory();

        IGVEventBus.getInstance().post(new ChangeEvent(frames));
    }

    private static boolean addNewFrame(String searchString) {
        boolean locusAdded = false;
        Locus locus = getLocus(searchString);
        if (locus != null) {
            ReferenceFrame referenceFrame = new ReferenceFrame(searchString);
            referenceFrame.jumpTo(locus);
            locusAdded = frames.add(referenceFrame);
        }
        return locusAdded;
    }

    public static void resetFrames(GeneList gl) {
        frames.clear();

        if (gl == null) {
            frames.add(getDefaultFrame());
        } else {
            List<String> lociNotFound = new ArrayList();
            List<String> loci = gl.getLoci();
            if (loci.size() == 1) {
                Locus locus = getLocus(loci.get(0));
                if (locus == null) {
                    lociNotFound.add(loci.get(0));
                } else {
                    IGV.getInstance().getSession().setCurrentGeneList(null);
                    getDefaultFrame().jumpTo(locus.getChr(), locus.getStart(), locus.getEnd());
                    frames.add(getDefaultFrame());
                }
            } else {
                for (String searchString : gl.getLoci()) {
                    if (!addNewFrame(searchString)) {
                        lociNotFound.add(searchString);
                    }
                }
            }

            if (lociNotFound.size() > 1) {
                StringBuffer message = new StringBuffer();
                message.append("<html>The following loci could not be found in the currently loaded annotation sets: <br>");
                for (String s : lociNotFound) {
                    message.append(s + " ");
                }
                MessageUtils.showMessage(message.toString());

            }
        }

        IGVEventBus.getInstance().post(new ChangeEvent(frames));
    }

    /**
     * @return The minimum scale among all active frames
     * TODO -- track this with "rescale" events, rather than compute on the fly
     */
    public static double getMinimumScale() {
        double minScale = Double.MAX_VALUE;
        for (ReferenceFrame frame : frames) {
            minScale = Math.min(minScale, frame.getScale());
        }
        return minScale;
    }


    /**
     * Uses default flanking region with
     * {@link #getLocus(String, int)}
     *
     * @param searchString
     * @return
     */
    public static Locus getLocus(String searchString) {
        int flankingRegion = PreferencesManager.getPreferences().getAsInt(Constants.FLANKING_REGION);
        return getLocus(searchString, flankingRegion);
    }

    /**
     * Runs a search for the specified string, and returns a locus
     * of the given region with additional space on each side.
     * Note: We DO NOT add the flanking region if the {@code searchString}
     * is a locus (e.g. chr1:50-100), only if it's a gene or feature name (or something else)
     *
     * @param searchString
     * @param flankingRegion
     * @return The found locus, null if not found
     */
    public static Locus getLocus(String searchString, int flankingRegion) {
        SearchCommand cmd = new SearchCommand(getDefaultFrame(), searchString);
        List<SearchCommand.SearchResult> results = cmd.runSearch(searchString);
        Locus locus = null;
        for (SearchCommand.SearchResult result : results) {
            if (result.getType() != SearchCommand.ResultType.ERROR) {
                int delta = 0;

                if (result.getType() != SearchCommand.ResultType.LOCUS) {
                    if (flankingRegion < 0) {
                        delta = (-flankingRegion * (result.getEnd() - result.getStart())) / 100;
                    } else {
                        delta = flankingRegion;
                    }
                }

                int start = result.getStart() - delta;
                //Don't allow flanking region to extend past origin
                //There are some circumstances in which we render before origin (e.g. soft-clips)
                //so we are conservative
                if (start < 0 && result.getStart() >= -1) {
                    start = 0;
                }
                locus = new Locus(
                        result.getChr(),
                        start,
                        result.getEnd() + delta);
                //We just take the first result
                break;
            }
        }
        return locus;
    }

    public static void removeFrame(ReferenceFrame frame) {
        frames.remove(frame);
    }

    public static void sortFrames(final Track t) {

        Collections.sort(frames, new Comparator<ReferenceFrame>() {
            @Override
            public int compare(ReferenceFrame o1, ReferenceFrame o2) {
                float s1 = t.getRegionScore(o1.getChromosome().getName(), (int) o1.getOrigin(), (int) o1.getEnd(),
                        o1.getZoom(), RegionScoreType.SCORE, o1.getName());
                float s2 = t.getRegionScore(o2.getChromosome().getName(), (int) o2.getOrigin(), (int) o2.getEnd(),
                        o2.getZoom(), RegionScoreType.SCORE, o2.getName());
                return (s1 == s2 ? 0 : (s1 > s2) ? -1 : 1);
            }
        });

    }

    /**
     * Increment the zoom level of the visibile frame(s). Supports batch commands zoomIn and zoomOut
     *
     * @param zoom the zoom level increment, usually -1 or 1
     */
    public static void incrementZoom(int zoom) {

        if (isGeneListMode()) {
            for (ReferenceFrame frame : getFrames()) {
                frame.doZoomIncrement(zoom);
            }
        } else {
            getDefaultFrame().doZoomIncrement(zoom);
        }
    }

    @Override
    public void receiveEvent(Object event) {
        if (event instanceof GenomeChangeEvent) {
            Genome newGenome = ((GenomeChangeEvent) event).genome;
            boolean force = true;
            getDefaultFrame().setChromosomeName(newGenome.getHomeChromosome(), force);
        }
    }


    public static class ChangeEvent {
        List<ReferenceFrame> frames;

        public ChangeEvent(List<ReferenceFrame> frames) {
            this.frames = frames;
        }

        public List<ReferenceFrame> getFrames() {
            return frames;
        }
    }

}

