/*
 * Copyright (c) 2007-2012 The Broad Institute, Inc.
 * SOFTWARE COPYRIGHT NOTICE
 * This software and its documentation are the copyright of the Broad Institute, Inc. All rights are reserved.
 *
 * This software is supplied without any warranty or guaranteed support whatsoever. The Broad Institute is not responsible for its use, misuse, or functionality.
 *
 * 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.
 */

package org.broad.igv.feature.exome;

import org.apache.log4j.Logger;
import org.broad.igv.feature.Chromosome;
import org.broad.igv.feature.Locus;
import org.broad.igv.feature.NamedFeature;
import org.broad.igv.feature.genome.Genome;
import org.broad.igv.feature.genome.GenomeManager;
import org.broad.igv.track.FeatureTrack;
import org.broad.igv.ui.IGV;
import org.broad.igv.ui.panel.ReferenceFrame;
import org.broad.tribble.Feature;

import java.util.*;

/**
 * @author Jim Robinson
 * @date 5/30/12
 */
public class ExomeReferenceFrame extends ReferenceFrame {

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

    Map<String, ExomeData> exomeBlockData = new HashMap();

    int firstBlockIdx;

    int exomeOrigin;

    private int blockGap;

    public ExomeReferenceFrame(ReferenceFrame otherFrame, FeatureTrack referenceTrack) {
        super(otherFrame);
        init(referenceTrack);
        this.calculateMaxZoom();
    }


    public ExomeReferenceFrame(ReferenceFrame otherFrame, Map<String, List<Feature>> featureMap) {
        super(otherFrame);
        init(featureMap);
        this.calculateMaxZoom();
    }


    private void init(FeatureTrack geneTrack) {
        Genome genome = GenomeManager.getInstance().getCurrentGenome();
        for (Chromosome chromosome : genome.getChromosomes()) {
            String chr = chromosome.getName();
            List<Feature> features = geneTrack.getFeatures(chr, 0, chromosome.getLength());
            if (features != null && features.size() > 0) {
                createExomeBlockData(chr, features);
            }

        }
    }

    private void init(Map<String, List<Feature>> featureMap) {
        for (String chr : featureMap.keySet()) {
            List<Feature> features = featureMap.get(chr);
            if (features.size() > 0) {
                createExomeBlockData(chr, features);
            }

        }
    }

    private void createExomeBlockData(String chr, List<Feature> features) {
        List<ExomeBlock> blocks = ExomeUtils.collapseTranscripts(features);
        List<Gene> genes = ExomeUtils.collapseToGenes(features);
        ExomeData exomeData = new ExomeData(blocks, genes);
        exomeBlockData.put(chr, exomeData);
    }

    /**
     * Move by the specified pixel amount.
     *
     * @param delta
     */
    @Override
    public void shiftOriginPixels(int delta) {

        if (exomeOrigin == 0 && delta < 0) return;

        double shiftBP = delta * getScale();
        exomeOrigin += shiftBP;
        if (exomeOrigin < 0) exomeOrigin = 0;

        // Find exome blocks that contains the new position.  We're assuming is very close to the current blocks.
        List<ExomeBlock> blocks = getBlocks(getChrName());
        ExomeBlock b = blocks.get(firstBlockIdx);
        int comp = b.compareExomePosition(exomeOrigin);
        if (comp > 0) {
            while (firstBlockIdx < blocks.size() - 1) {
                firstBlockIdx++;
                b = blocks.get(firstBlockIdx);
                if (b.compareExomePosition(exomeOrigin) <= 0) break;
            }
        } else if (comp < 0) {
            while (firstBlockIdx > 0) {
                firstBlockIdx--;
                b = blocks.get(firstBlockIdx);
                if (b.compareExomePosition(exomeOrigin) >= 0) break;
            }
        }

        // Find genomePosition
        double genomePosition = b.getGenomeStart() + (exomeOrigin - b.getExomeStart());
        setOrigin(genomePosition);
    }

    /**
     * Recalculate exomeOrigin and firstBlockIdx
     * based on current origin
     */
    private void calcExomeOrigin() {
        List<ExomeBlock> blocks = getBlocks(chrName);
        firstBlockIdx = getIndexForGenomePosition(blocks, origin);
        ExomeBlock firstBlock = blocks.get(firstBlockIdx);
        origin = Math.max(origin, firstBlock.getGenomeStart());

        exomeOrigin = origin > firstBlock.getGenomeEnd() ? firstBlock.getExomeEnd() :
                firstBlock.getExomeStart() + (int) (origin - firstBlock.getGenomeStart());
    }

    @Override
    protected void beforeScaleZoom(Locus locus){
        super.calculateMaxZoom();
        calcExomeOrigin();
    }

    @Override
    public void setOrigin(double genomePosition) {
        super.setOrigin(genomePosition);
        calcExomeOrigin();
    }

    public int getMaxCoordinate(){
        return super.getChromosomeLength();
    }

    /**
     * Chromosome length, in exome coordinates
     * Intended to be used for scaling.
     * @see #getMaxCoordinate()
     */
    @Override
    public int getChromosomeLength() {
        return genomeToExomePosition(super.getChromosomeLength());
    }

    @Override
    protected synchronized void computeLocationScale() {
        if(!this.initialLocus.getChr().equals(getChrName())){
            this.initialLocus = null;
        }
        if(this.initialLocus != null){
            int genomeEnd = this.initialLocus.getEnd();
            int exomeEnd = Math.max(exomeOrigin + minBP, genomeToExomePosition(genomeEnd));
            int bp = exomeEnd - exomeOrigin;
            int pw = widthInPixels <= 0 ? 1000 : widthInPixels;
            this.locationScale = (((double) bp) / pw);
        }else{
            double visibleRange = minBP*Math.pow(2, maxZoom - zoom);
            locationScale = (visibleRange) / widthInPixels;
        }

    }

    @Override
    protected void computeZoom(){
        int newZoom = calculateZoom(exomeOrigin, genomeToExomePosition((int) getEnd()));
        if(newZoom != this.zoom){
            setZoomWithinLimits(newZoom);
        }
    }
//
//    /**
//     * Jump to a specific locus (in genome coordinates).
//     *
//     * @param locus
//     */
//    @Override
//    public void jumpTo(Locus locus) {
//        this.initialLocus = locus;
//        this.chrName = locus.getChr();
//        this.origin = locus.getStart();    // Genome locus
//        int genomeEnd = locus.getEnd();
//
//        calcExomeOrigin();
//
//        int exomeEnd = Math.max(exomeOrigin + 40, genomeToExomePosition(genomeEnd));
//        int bp = exomeEnd - exomeOrigin;
//        int pw = widthInPixels <= 0 ? 1000 : widthInPixels;
//        this.locationScale = (((double) bp) / pw);
//
//        int newZoom = calculateZoom(exomeOrigin, exomeEnd);
//        if(newZoom != this.zoom){
//            setZoomWithinLimits(newZoom);
//        }
//    }

    @Override
    public synchronized void doSetZoomCenter(int newZoom, double newCenter) {

        newZoom = Math.max(0, Math.min(newZoom, maxZoom));
        double zoomFactor = Math.pow(2, newZoom - zoom);

        int currentBPLength = (int) (locationScale * widthInPixels);
        int delta = (int) (currentBPLength / (2 * zoomFactor));

        int exomeCenter = genomeToExomePosition((int) newCenter);
        exomeOrigin = Math.max(0, exomeCenter - delta);
        origin = exomeToGenomePosition(exomeOrigin);
        zoom = newZoom;

        if(zoom == 0){
            locationScale = getChromosomeLength() / widthInPixels;
        }else {
            double visibleRange = minBP*Math.pow(2, maxZoom - zoom);
            locationScale = (visibleRange) / widthInPixels;
        }
        List<ExomeBlock> blocks = getBlocks(chrName);
        firstBlockIdx = getIndexForGenomePosition(blocks, origin);

        IGV.repaintPanelsHeadlessSafe();
    }

    /**
     * Return the chromosome (genomic) position corresponding to the screen pixel position.
     *
     * @param screenPosition
     * @return
     */
    @Override
    public double getChromosomePosition(int screenPosition) {
        double exomePosition = exomeOrigin + getScale() * screenPosition;
        return exomeToGenomePosition((int) exomePosition);
    }

    @Override
    protected double getGenomeCenterPosition() {
        double centerExomePosition = exomeOrigin + getScale() * widthInPixels / 2;
        return exomeToGenomePosition((int) centerExomePosition);
    }

    @Override
    public double getEnd() {
        int exomeEnd = exomeOrigin + (int) (locationScale * widthInPixels);
        int genomeEnd = exomeToGenomePosition(exomeEnd);
        return genomeEnd;
    }

    public int genomeToExomePosition(int genomePosition) {

        ExomeData ed = exomeBlockData.get(chrName);
        if (ed == null) return -1;

        List<ExomeBlock> blocks = ed.getBlocks();
        int idx = getIndexForGenomePosition(blocks, genomePosition);
        ExomeBlock b = blocks.get(idx);

        if (genomePosition < b.getGenomeStart()) {
            return b.getExomeStart();
        } else {
            return genomePosition < b.getGenomeEnd() ?
                    b.getExomeStart() + (genomePosition - b.getGenomeStart()) :
                    b.getExomeEnd();
        }
    }

    public int exomeToGenomePosition(int exomePosition) {


        ExomeData ed = exomeBlockData.get(chrName);
        if (ed == null) return -1;

        List<ExomeBlock> blocks = ed.getBlocks();
        ExomeBlock b = getBlockAtExomePosition(blocks, exomePosition);
        if (b != null) {
            return b.getGenomeStart() + (exomePosition - b.getExomeStart());
        } else {
            // ?? Should be impossible if "exomePosition" is in bounds
            b = blocks.get(blocks.size() - 1);
            return b.getGenomeEnd();
        }
    }

    public List<ExomeBlock> getBlocks(String chr) {
        ExomeData exomeData = exomeBlockData.get(chr);
        return exomeData == null ? null : exomeData.blocks;
    }

    public List<Gene> getGenes(String chr) {
        ExomeData exomeData = exomeBlockData.get(chr);
        return exomeData == null ? null : exomeData.genes;

    }

    private ExomeBlock getBlockAtExomePosition(List<ExomeBlock> blocks, int exomePosition) {
        ExomeBlock key = new ExomeBlock(-1, -1, exomePosition, 1);
        int r = Collections.binarySearch(blocks, key, EXOME_POSITION_COMPARATOR);
        if (r >= 0) {
            return blocks.get(r);
        } else {
            return null;
        }
    }

    /**
     * Return the index to the last feature in the list with a start < the given position,
     * or 0 if the position is less than the start of the first block
     *
     * @param position
     * @param blocks
     * @return
     */
    public static int getIndexForGenomePosition(List<ExomeBlock> blocks, double position) {


        int startIdx = 0;
        int endIdx = blocks.size() - 1;

        while (startIdx != endIdx) {
            int idx = (startIdx + endIdx) / 2;
            double distance = blocks.get(idx).getGenomeStart() - position;
            if (distance <= 0) {
                startIdx = idx;
            } else {
                endIdx = idx;
            }
            if (endIdx - startIdx < 10) {
                break;
            }
        }

        if (blocks.get(endIdx).getGenomeStart() >= position) {
            for (int idx = endIdx; idx >= 0; idx--) {
                if (blocks.get(idx).getGenomeStart() <= position) {
                    return idx;
                }
            }
            return 0;
        } else {
            for (int idx = endIdx + 1; idx < blocks.size(); idx++) {
                if (blocks.get(idx).getGenomeStart() >= position) {
                    return idx - 1;
                }

            }
            return blocks.size() - 1;
        }
    }


    public List<ExomeBlock> getBlocks() {
        return getBlocks(getChrName());
    }

    public int getFirstBlockIdx() {
        return firstBlockIdx;
    }

    public boolean isExomeMode() {
        return true;
    }

    public int getExomeOrigin() {
        return exomeOrigin;
    }

    public int getBlockGap() {
        return 0; //blockGap;
    }


    public static class Gene implements NamedFeature {

        String name;
        String chr;
        int start;
        int end;

        Gene(NamedFeature f) {
            name = f.getName();
            chr = f.getChr();
            start = f.getStart();
            end = f.getEnd();
        }

        @Override
        public String getChr() {
            return chr;  //To change body of implemented methods use File | Settings | File Templates.
        }

        @Override
        public int getStart() {
            return start;  //To change body of implemented methods use File | Settings | File Templates.
        }

        @Override
        public int getEnd() {
            return end;  //To change body of implemented methods use File | Settings | File Templates.
        }


        @Override
        public String getName() {
            return name;
        }

        public void expand(Feature f) {
            //if(!chr.equals(f.getChr())) throw error
            start = Math.min(start, f.getStart());
            end = Math.max(end, f.getEnd());
        }

    }

    public static class ExomeData {
        private List<ExomeBlock> blocks;
        private List<Gene> genes;

        ExomeData(List<ExomeBlock> block, List<Gene> genes) {
            this.blocks = block;
            this.genes = genes;
        }

        public List<ExomeBlock> getBlocks() {
            return blocks;
        }

        public List<Gene> getGenes() {
            return genes;
        }
    }


    private static final Comparator<ExomeBlock> GENOME_POSITION_COMPARATOR = new Comparator<ExomeBlock>() {
        public int compare(ExomeBlock o1, ExomeBlock o2) {
            int genomeStart2 = o2.getGenomeStart();
            int genomeStart1 = o1.getGenomeStart();
            if (genomeStart2 >= genomeStart1 && o2.getGenomeEnd() <= o1.getGenomeEnd()) {
                return 0;
            } else {
                return genomeStart1 - genomeStart2;
            }
        }
    };

    private static final Comparator<ExomeBlock> EXOME_POSITION_COMPARATOR = new Comparator<ExomeBlock>() {
        public int compare(ExomeBlock o1, ExomeBlock o2) {
            int exomeStart2 = o2.getExomeStart();
            int exomeStart1 = o1.getExomeStart();
            if (exomeStart2 >= exomeStart1 && o2.getExomeEnd() <= o1.getExomeEnd()) {
                return 0;
            } else {
                return exomeStart1 - exomeStart2;
            }
        }
    };

}
