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

/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
 */
package org.broad.igv.track;

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

import org.broad.igv.logging.*;
import org.broad.igv.prefs.Constants;
import org.broad.igv.prefs.PreferencesManager;
import org.broad.igv.renderer.GraphicUtils;
import org.broad.igv.ui.FontManager;
import org.broad.igv.ui.IGV;

import java.awt.*;
import java.util.*;
import java.util.List;

/**
 * Container for a group of tracks.  Behaves as a single unit when sorting
 * by region score.
 *
 * @author jrobinso
 */
public class TrackGroup {

    private static Logger log = LogManager.getLogger(TrackGroup.class);

    /**
     * Key used to group tracks (e.g. SAMPLE_ID).
     */
    private String name;

    private boolean drawBorder = true;

    private List<Track> tracks;

    private boolean selected;

    private boolean autoScale = false;


    public TrackGroup() {
        this("");
    }


    public TrackGroup(String name) {
        this.name = name;
        tracks = Collections.synchronizedList(new ArrayList<Track>());
    }


    public boolean contains(Track track) {
        return tracks.contains(track);
    }

    public List<Track> getTracks() {
        return tracks;
    }

    public List<Track> getVisibleTracks() {
        List<Track> visibleTracks = new ArrayList<Track>();
        for(Track t : tracks) {
            if(t.isVisible()) visibleTracks.add(t);
        }
        return visibleTracks;
    }

    public boolean isAutoScale() {
        return autoScale;
    }

    public void setAutoScale(boolean autoScale) {
        this.autoScale = autoScale;
    }

    public int indexOf(Track track) {
        return tracks.indexOf(track);
    }


    public int size() {
        return tracks.size();
    }


    public void add(Track track) {
        if (track == null) {
            log.warn("Attempt to add null track");
        } else {
            tracks.add(track);
        }
    }

    public void add(int pos, Track track) {
        if (track == null) {
            log.warn("Attempt to add null track");
        } else {
            tracks.add(pos, track);
        }
    }

    public void addAll(Collection<Track> trackList) {
        tracks.addAll(trackList);
    }

    public void addAll(int index, Collection<Track> trackList) {
        tracks.addAll(index, trackList);
    }

    public void remove(Track track) {
        tracks.remove(track);
    }

    /**
     * Return a composite score for the entire group.  For now use the maximum track
     * score.   Note that scores for tracks not appropriate to the score type will
     * return -Float.MAX, so they are effectively ignored.
     *
     * @param chr
     * @param start
     * @param end
     * @param zoom
     * @param type
     * @param frameName
     * @return
     */
    public float getRegionScore(String chr, int start, int end, int zoom, RegionScoreType type, String frameName) {
        float score = -Float.MAX_VALUE;
        for (Track track : tracks) {
            if (track.isVisible()) {
                score = Math.max(score, track.getRegionScore(chr, start, end, zoom, type, frameName));

            }
        }
        return score;
    }


    public String getName() {
        return name;
    }

    public boolean isDrawBorder() {
        return drawBorder;
    }


    public void setDrawBorder(boolean drawBorder) {
        this.drawBorder = drawBorder;
    }

    public boolean isVisible() {
       return tracks.stream().anyMatch(t -> t != null && t.isVisible());
    }

    public int getHeight() {

        int h = 0;
        for (Track track : tracks) {
            if (track != null && track.isVisible()) {
                h += track.getHeight();
            }
        }
        return h;
    }

    public void renderName(Graphics2D g2D, Rectangle rect) {

        // Calculate fontsize
        int fontSize = PreferencesManager.getPreferences().getAsInt(Constants.DEFAULT_FONT_SIZE);
        Font font = FontManager.getFont(Font.BOLD, fontSize);
        g2D.setFont(font);
        GraphicUtils.drawWrappedText(getName(), rect, g2D, true);
    }

    /**
     * Sort tracks by the array of attribute names.
     *
     * @param attributeNames
     * @param ascending
     */

    public void sortByAttributes(final String[] attributeNames,
                                 final boolean[] ascending) {


        if ((tracks != null) && !tracks.isEmpty()) {
            List<Track> allTracks = new ArrayList<Track>(tracks);
            try {
                Comparator<Track> comparator = new TrackAttributeComparator(attributeNames, ascending);

                // Step 1, remove non-sortable tracks and remember position
                List<Track> nonsortableTracks = new ArrayList<Track>();
                Map<Track, Integer> trackIndices = new HashMap<Track, Integer>();
                for (int i = tracks.size() - 1; i >= 0; i--) {
                    if (!tracks.get(i).isSortable()) {
                        Track t = tracks.remove(i);
                        nonsortableTracks.add(t);
                        trackIndices.put(t, i);

                    }
                }

                // Step 2, sort "sortable" tracks
                Collections.sort(tracks, comparator);

                // Step 2.5, internal sort by sample attributes for variant tracks.  This is ugly but neccessary as
                // variant tracks are implemented as monoliths, with sample rows internal to the track.
                for (Track t : allTracks) {
                    if (t instanceof org.broad.igv.variant.VariantTrack) {
                        ((org.broad.igv.variant.VariantTrack) t).sortSamples(new SampleAttributeComparator(attributeNames, ascending));
                    }
                }

                // Step 3, put non-sortable tracks back in original order
                if (nonsortableTracks.size() > 0) {
                    for (int i = nonsortableTracks.size() - 1; i >= 0; i--) {
                        Track t = nonsortableTracks.get(i);
                        int index = trackIndices.get(t);
                        tracks.add(index, t);
                    }
                }
            } catch (Exception e) {
                log.error("Error sorting tracks by attribute", e);
                tracks = allTracks;
            }
        }

    }


    /**
     * Sorts the entire group, including tracks for the given score type as well as other tracks, by
     * the specified sample order
     */
    public void sortGroup(final RegionScoreType type,
                          List<String> sortedSamples) {

        // Step 1,  remove non-sortable tracks and remember position
        List<Track> unsortableTracks = new ArrayList();
        Map<Track, Integer> trackIndeces = new HashMap();
        for (int i = tracks.size() - 1; i >= 0; i--) {
            if (!tracks.get(i).isSortable()) {
                Track t = tracks.remove(i);
                unsortableTracks.add(t);
                trackIndeces.put(t, i);

            }
        }

        List<Track> tracksWithScore = new ArrayList(getVisibleTracks().size());
        List<Track> otherTracks = new ArrayList(getVisibleTracks().size());
        for (Track t : getVisibleTracks()) {
            if (t.isRegionScoreType(type)) {
                tracksWithScore.add(t);
            } else {
                otherTracks.add(t);
            }
        }

        sortBySampleOrder(tracksWithScore, sortedSamples);
        sortBySampleOrder(otherTracks, sortedSamples);

        tracks.clear();
        tracks.addAll(tracksWithScore);
        tracks.addAll(otherTracks);

        // Step 3, put unortable tracks back in original order
        if (unsortableTracks.size() > 0) {
            for (int i = unsortableTracks.size() - 1; i >= 0; i--) {
                Track t = unsortableTracks.get(i);
                int index = trackIndeces.get(t);
                tracks.add(index, t);
            }
        }

    }

    /**
     * @param sortedSamples
     */
    private void sortBySampleOrder(List<Track> tracks,
                                   List<String> sortedSamples) {
        if ((tracks != null) && (sortedSamples != null) && !tracks.isEmpty()) {

            // Create a rank hash.  Loop backwards so that the lowest index for an attribute
            final HashMap<String, Integer> rankMap = new HashMap(sortedSamples.size() * 2);
            for (int i = sortedSamples.size() - 1; i >= 0; i--) {
                rankMap.put(sortedSamples.get(i), i);
            }
            // Comparator for sorting in ascending order
            Comparator<Track> c = new Comparator<Track>() {

                public int compare(Track t1, Track t2) {
                    String a1 = t1.getSample(); //t1.getAttributeValue(attributeId);
                    String a2 = t2.getSample(); //t2.getAttributeValue(attributeId);
                    Integer r1 = ((a1 == null) ? null : rankMap.get(a1));
                    Integer r2 = ((a2 == null) ? null : rankMap.get(a2));
                    if ((r1 == null) && (r2 == null)) {
                        return 0;
                    } else if (r1 == null) {
                        return 1;
                    } else if (r2 == null) {
                        return -1;
                    } else {
                        return r1.intValue() - r2.intValue();
                    }

                }
            };

            Collections.sort(tracks, c);

        }

    }

    /**
     * @param trackIds
     */
    public void sortByList(List<String> trackIds) {

        final Map<String, Integer> trackPositions = new HashMap();
        for (int i = 0; i < trackIds.size(); i++) {
            trackPositions.put(trackIds.get(i), i);
        }
        Comparator c = new Comparator<Track>() {
            public int compare(Track t1, Track t2) {
                String id1 = t1.getId();
                int p1 = trackPositions.containsKey(id1) ? trackPositions.get(id1) : Integer.MAX_VALUE;
                String id2 = t2.getId();
                int p2 = trackPositions.containsKey(id2) ? trackPositions.get(id2) : Integer.MAX_VALUE;
                return p1 - p2;
            }
        };
        Collections.sort(tracks, c);
    }

    public void removeTracks(Collection<? extends Track> tracksToRemove) {
        tracks.removeAll(tracksToRemove);
    }


    /**
     * If this group contains the targetTrack, insert the selectedTracks collection either before or after
     * the target and return true.   Otherwise return false.
     *
     * @param selectedTracks
     * @param targetTrack
     * @param before
     */
    public boolean moveSelectedTracksTo(Collection<? extends Track> selectedTracks,
                                        Track targetTrack,
                                        boolean before) {

        int index = (targetTrack == null ? tracks.size() : tracks.indexOf(targetTrack));
        if (index < 0) {
            return false;
        }

        if (!before) {
            index = index + 1;
        }

        // 1. Divdide the target list up into 2 parts, one before the index and one after
        List<Track> beforeList = new ArrayList(tracks.subList(0, index));
        List<Track> afterList = new ArrayList(tracks.subList(index, tracks.size()));

        // 2.  Remove the selected tracks from anywhere they occur
        beforeList.removeAll(selectedTracks);
        afterList.removeAll(selectedTracks);

        // 3. Now insert the selected tracks
        tracks.clear();
        tracks.addAll(beforeList);
        tracks.addAll(selectedTracks);
        tracks.addAll(afterList);

        return true;

    }

    public boolean isSelected() {
        return selected;
    }

    public void setSelected(boolean selected) {
        this.selected = selected;
        IGV igv = IGV.getInstance();
        igv.clearSelections();
        igv.setTrackSelections(new HashSet(tracks));

    }

    private abstract static class AttributeComparator<T> implements Comparator<T> {

        private final String[] attributeNames;
        private final boolean[] ascending;

        AttributeComparator(String[] attributeNames, boolean[] ascending) {
            assert attributeNames.length == ascending.length;
            this.attributeNames = attributeNames;
            this.ascending = ascending;
        }

        protected abstract String getAttributeValue(T track, String attName);

        public int compare(T t1, T t2) {
            // Loop through the attributes in order (primary, secondary, tertiary, ...).  The
            // first attribute to yield a non-zero comparison wins
            for (int i = 0; i < attributeNames.length; i++) {
                String attName = attributeNames[i];

                if (attName != null) {
                    String value1 = getAttributeValue(t1, attName);
                    String value2 = getAttributeValue(t2, attName);

                    boolean isNumeric = AttributeManager.getInstance().isNumeric(attName);

                    int c = 0;
                    if (isNumeric) {
                        double d1;
                        try {
                            d1 = Double.parseDouble(value1);
                        } catch (NumberFormatException e) {
                            d1 = Double.MIN_VALUE;
                        }
                        double d2;
                        try {
                            d2 = Double.parseDouble(value2);
                        } catch (NumberFormatException e) {
                            d2 = Double.MIN_VALUE;
                        }
                        c = Double.compare(d1, d2);
                    } else {
                        c = value1.compareTo(value2);
                    }

                    if (c != 0) {
                        return ascending[i] ? c : -c;
                    }

                }
            }

            // All compares are equal
            return 0;
        }
    }

    private static class TrackAttributeComparator extends AttributeComparator<Track> {

        public TrackAttributeComparator(String[] attributeNames, boolean[] ascending) {
            super(attributeNames, ascending);
        }

        protected String getAttributeValue(Track track, String attName) {
            String value = track.getAttributeValue(attName);

            if (value == null) {
                value = "";
            }

            return value.toLowerCase();
        }
    }


    /**
     * Sort samples by attribute value; logic copied wholesale from
     * AttributeComparator, probably some refactoring could be done
     * to minimize the duplicated code
     */
    private static class SampleAttributeComparator extends AttributeComparator<String> {

        public SampleAttributeComparator(String[] attributeNames, boolean[] ascending) {
            super(attributeNames, ascending);
        }

        protected String getAttributeValue(String sample, String attName) {
            String value = AttributeManager.getInstance().getAttribute(sample, attName);

            if (value == null) {
                value = "";
            }

            return value.toLowerCase();
        }

    }


}