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

import org.broad.igv.Globals;
import org.broad.igv.data.AbstractDataSource;
import org.broad.igv.data.DataTile;
import org.broad.igv.feature.BasicFeature;
import org.broad.igv.feature.FeatureUtils;
import org.broad.igv.feature.IGVFeature;
import org.broad.igv.feature.LocusScore;
import org.broad.igv.feature.genome.Genome;
import org.broad.igv.ui.panel.ReferenceFrame;
import org.broad.igv.util.collections.CollUtils;
import htsjdk.tribble.Feature;

import java.util.*;

/**
 * Implementation of FeatureSource that wraps a list or map of features for the
 * entire genome.  Instances are typically created by parsing a bed or gff file.  This
 * is a legacy implementation, and does not scale to large feature tracks.
 * <p/>
 * User: jrobinso
 * Date: Jan 31, 2010
 */
public class FeatureCollectionSource implements FeatureSource {

    private TrackType type;

    private Map<String, List<htsjdk.tribble.Feature>> featureMap;

    CoverageDataSource coverageData;

    Genome genome;

    Object header;  // usually null

    public FeatureCollectionSource(Iterable<? extends Feature> allFeatures, Genome genome) {
        this.genome = genome;
        initFeatures(allFeatures);
        coverageData = new CoverageDataSource(genome);
        coverageData.computeGenomeCoverage();
        sampleGenomeFeatures();
    }

    public List<LocusScore> getCoverageScores(String chr, int startLocation, int endLocation, int zoom) {
        return coverageData == null ? Collections.<LocusScore>emptyList() :
                coverageData.getSummaryScoresForRange(chr, startLocation, endLocation, zoom);
    }

    /**
     * Return features over the interval as an iterator
     * @param chr
     * @param start
     * @param end
     * @return
     */
    public Iterator<Feature> getFeatures(String chr, int start, int end) {
        return getFeatureList(chr, start, end).iterator();
    }

    public List<Feature> getFeatureList(String chr, int start, int end) {

        List<Feature> features = featureMap.get(chr);
        if (features == null) {
            return Collections.<Feature>emptyList();
        }
        List<Feature> filteredFeatures = CollUtils.filter(features, FeatureUtils.getOverlapPredicate(chr, start, end));
        return filteredFeatures;
    }

    @Override
    public boolean isLoaded(ReferenceFrame frame) {
        return true;  // All features are loaded by definition
    }

    public List<Feature> getFeatures(String chr) {
        return featureMap.get(chr);
    }

    private void initFeatures(Iterable<? extends Feature> allFeatures) {
        // Separate features by chromosome

            featureMap = new HashMap();
            for (Feature f : allFeatures) {

                String chr = genome != null ? genome.getCanonicalChrName(f.getChr()) : f.getChr();

                List<Feature> fList = featureMap.get(chr);
                if (fList == null) {
                    fList = new ArrayList();
                    featureMap.put(chr, fList);
                }
                fList.add(f);
            }

            for (List<Feature> featureList : featureMap.values()) {
                FeatureUtils.sortFeatureList(featureList);
            }

            if (featureMap.size() < 100) {
                sampleGenomeFeatures();
            }
    }


    private void setFeatures(String chr, List<Feature> features) {
        FeatureUtils.sortFeatureList(features);
        featureMap.put(chr, features);
    }

    public TrackType getType() {
        return type;
    }

    public void setType(TrackType type) {
        this.type = type;
    }

    @Override
    public Object getHeader() {
        return header;
    }

    public void setHeader(Object header) {
        this.header = header;
    }

    protected void sampleGenomeFeatures() {

        if(genome == null) return;

        List<Feature> chrAllFeatures = new ArrayList(1000);
        int sampleLength = (int) ((double) genome.getWGLength() / (1000 * 700));
        int lastFeaturePosition = -1;
        for (String chr : genome.getLongChromosomeNames()) {
            List<Feature> features = getFeatures(chr);
            if (features != null) {
                long offset = genome.getCumulativeOffset(chr);
                for (Feature feature : features) {
                    if (feature instanceof IGVFeature) {
                        IGVFeature f = (IGVFeature) feature;
                        int genStart = (int) ((offset + f.getStart()) / 1000);
                        int genEnd = (int) ((offset + f.getEnd()) / 1000);
                        if (genEnd > lastFeaturePosition + sampleLength) {
                            BasicFeature f2 = new BasicFeature(Globals.CHR_ALL, genStart, genEnd);
                            if (f instanceof BasicFeature) {
                                BasicFeature bf = (BasicFeature) f;
                                f2.setThickEnd((int) ((offset + bf.getThickEnd()) / 1000));
                                f2.setThickStart((int) ((offset + bf.getThickStart()) / 1000));
                                f2.setName(f.getName());
                            }
                            chrAllFeatures.add(f2);

                            lastFeaturePosition = genEnd;
                        }
                    }
                }
            }
        }

        setFeatures(Globals.CHR_ALL, chrAllFeatures);
    }



    class CoverageDataSource extends AbstractDataSource {

        int windowSize = 1000;
        double dataMin = 0;
        double dataMax = 0;

        Map<String, DataTile> dataCache = new HashMap();

        CoverageDataSource(Genome genome) {
            super(genome);
        }

        protected DataTile getRawData(String chr, int startLocation, int endLocation) {


            DataTile coverageData = dataCache.get(chr);
            if (coverageData == null) {
                coverageData = computeCoverage(chr, startLocation, endLocation);
                dataCache.put(chr, coverageData);
            }
            return coverageData;

        }

        @Override
        protected List<LocusScore> getPrecomputedSummaryScores(String chr, int startLocation, int endLocation, int zoom) {
            return null;  //To change body of implemented methods use File | Settings | File Templates.
        }

        @Override
        public int getLongestFeature(String chr) {
            return windowSize;
        }

        public double getDataMax() {
            return dataMax;
        }

        public double getDataMin() {
            return dataMin;
        }

        public TrackType getTrackType() {
            return TrackType.OTHER;  //To change body of implemented methods use File | Settings | File Templates.
        }

        // This won't work for large track!

        private DataTile computeCoverage(String chr, int start, int end) {

            int nBins = (end - start) / windowSize + 1;
            int[] starts = new int[nBins];
            int[] ends = new int[nBins];
            for (int i = 0; i < nBins; i++) {
                starts[i] = start + i * windowSize;
                ends[i] = starts[i] + windowSize;
            }
            float[] values = new float[nBins];
            List<Feature> features = featureMap.get(chr);
            if (features != null) {
                for (Feature f : features) {
                    int startBin = f.getStart() / windowSize;
                    int endBin = Math.min(f.getEnd(), end) / windowSize;
                    for (int i = startBin; i < endBin; i++) {
                        values[i] = values[i] + 1;
                        dataMax = Math.max(dataMax, values[i]);
                    }
                }
            }
            return new DataTile(starts, ends, values, null);

        }

        protected void computeGenomeCoverage() {
            int nBins = 1000;
            int[] starts = new int[nBins];
            int[] ends = new int[nBins];
            float[] values = new float[nBins];
            Arrays.fill(values, 0);


            double step = ((double) genome.getWGLength() / 1000) / nBins;
            for (int i = 0; i < nBins; i++) {
                starts[i] = (int) (i * step);
                ends[i] = (int) ((i + 1) * step);
            }


            for (String chr : genome.getLongChromosomeNames()) {
                List<Feature> features = featureMap.get(chr);
                if (features != null) {
                    long offset = genome.getCumulativeOffset(chr);
                    for (Feature f : features) {
                        int genStart = (int) ((offset + f.getStart()) / 1000);
                        int genEnd = (int) ((offset + f.getEnd()) / 1000);
                        int binStart = Math.min(values.length - 1, (int) (genStart / step));
                        int binEnd = Math.min(values.length - 1, (int) (genEnd / step));
                        for (int i = binStart; i <= binEnd; i++) {
                            values[i] = values[i] + 1;
                            dataMax = Math.max(dataMax, values[i]);
                        }

                    }
                }
            }

            dataCache.put(Globals.CHR_ALL, new DataTile(starts, ends, values, null));
        }


        public String getValueString(String chr, double position, ReferenceFrame frame) {

            int zoom = Math.max(0, frame.getZoom());
            List<LocusScore> scores = getSummaryScoresForRange(chr, (int) position - 10, (int) position + 10, zoom);

            // give a 2 pixel window, otherwise very narrow features will be missed.
            double bpPerPixel = frame.getScale();
            int minWidth = (int) (2 * bpPerPixel);    /* * */

            if (scores == null) {
                return "";
            } else {
                LocusScore score = (LocusScore) FeatureUtils.getFeatureAt(position, minWidth, scores);
                return score == null ? "" : "Mean count: " + score.getScore();
            }
        }


    }


}
