content: /*
   * The MIT License (MIT)
   *
   * Copyright (c) 2011-2021 Broad Institute, Aiden Lab, Rice University, Baylor College of Medicine
   *
   * 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 juicebox.track;
  
  import juicebox.HiC;
  import juicebox.data.basics.Chromosome;
  import org.broad.igv.feature.LocusScore;
  import org.broad.igv.track.WindowFunction;
  
  import java.util.List;
  
  /**
   * An adapter class to serve as a bridge between an IGV data source and a HiC track.  HiC tracks differ from IGV tracks
   * in that the coordinate system is based on "bins", each of which can correspond to a variable genomic length.
   *
   * @author jrobinso Date: 9/10/12
   */
  public abstract class HiCDataAdapter implements HiCDataSource {
  
    private final double log2 = Math.log(2);
    private final HiC hic;
  
    private LoadedDataInterval loadedDataInterval;
  
    HiCDataAdapter(HiC hic) {
      this.hic = hic;
    }
  
  
    @Override
    public boolean isLog() {
      return getDataRange() != null && getDataRange().isLog();
    }
  
    @Override
    public HiCDataPoint[] getData(
        Chromosome chr,
        int startBin,
        int endBin,
        HiCGridAxis gridAxis,
        double scaleFactor,
        WindowFunction windowFunction) {
  
      String zoom;
      try {
        zoom = hic.getZd().getZoom().getKey();
      } catch (Exception e) {
        zoom = "null";
      }
      String axisType = gridAxis.getClass().getName();
  
      if (loadedDataInterval != null && loadedDataInterval.contains(zoom, (int) scaleFactor, axisType, windowFunction,
          chr.getName(), startBin, endBin)) { // another method is setting loadedDataInterval
        return loadedDataInterval.getData();
  
      } else {
  
  //    Expand starBin and endBin by 50% to facilitate panning //TODO: pass in expanded scaffolds instead of expanding here?
        int f = (endBin - startBin) / 2;
        startBin = Math.max(0, startBin - f);
        endBin = endBin + f;
  
        int igvZoom = gridAxis.getIGVZoom();
        int subCount = (int) scaleFactor;
    
        // Increase zoom level for "super-zoom" (=> get higher resolution data
        if (subCount > 1) {
          int z = (int) (Math.log(scaleFactor) / log2);
          igvZoom += (z + 1);
        }
    
        DataAccumulator[] tmp = new DataAccumulator[(endBin - startBin + 1) * subCount];
    
        long gStart = gridAxis.getGenomicStart(startBin);
        long gEnd = gridAxis.getGenomicEnd(endBin);
    
        List<LocusScore> scores = getLocusScores(chr.getName(), gStart, gEnd, igvZoom, windowFunction);
        if (scores == null) {
          return null;
        }
    
        int nPts = 0;
        for (LocusScore locusScore : scores) {
      
          int bs = Math.max(startBin, gridAxis.getBinNumberForGenomicPosition(locusScore.getStart()));
          int be = Math.min(endBin, gridAxis.getBinNumberForGenomicPosition(locusScore.getEnd() - 1));
  
  
          for (int b = bs; b <= be; b++) {
    
            long bStart = gridAxis.getGenomicStart(b);
            long bEnd = gridAxis.getGenomicEnd(b);
            double delta = ((double) (bEnd - bStart)) / subCount;
    
            int subBin0 = b == bs ? (int) ((locusScore.getStart() - bStart) / delta) : 0;
            int subBin1 = b == be ? (int) ((locusScore.getEnd() - bStart) / delta) : subCount - 1;
    
            for (int subBin = subBin0; subBin <= subBin1; subBin++) {
              final double subBinWidth = 1.0 / subCount;
      
              int idx = (b - startBin) * subCount + subBin;
  
              if (idx < 0 || idx >= tmp.length) {
                continue;
              }
  
              DataAccumulator dataBin = tmp[idx];
              if (dataBin == null) {
                double bPrime = b + ((double) subBin) / subCount;
  
                int g0 = (int) (bStart + subBin * delta);
                int g1 = (int) (bStart + (subBin + 1) * delta);
                dataBin = new DataAccumulator(bPrime, subBinWidth, g0, g1); // bStart, bEnd);
                tmp[idx] = dataBin;
                nPts++;
              }
              dataBin.addScore(locusScore);
            }
          }
        }
  
        // Copy data, leaving out null values
        DataAccumulator[] data;
        if (nPts == tmp.length) {
          data = tmp;
        } else {
          data = new DataAccumulator[nPts];
          int idx = 0;
          for (DataAccumulator sum : tmp) {
            if (sum != null) {
              data[idx++] = sum;
            }
          }
        }
  
        loadedDataInterval = new LoadedDataInterval(zoom, (int) scaleFactor, axisType, windowFunction,
            chr.getName(), startBin, endBin, data);
        return data; // should only return number of points in aggregate scaffold
      }
    }
  
    @Override
    public void setName(String text) {
  
    }
  
    protected abstract List<LocusScore> getLocusScores(String chr,
                                                       long gStart,
                                                       long gEnd,
                                                       int zoom,
                                                       WindowFunction windowFunction);
    
    public static class DataAccumulator implements HiCDataPoint {
      
      final double binNumber;
      public double width = 1;
      public int nPts = 0; //set after
      public double weightedSum = 0; //set after
      public double max = 0; //set after
      public long genomicStart;
      public long genomicEnd;
      
      
      public DataAccumulator(double binNumber) {
        this.binNumber = binNumber;
      }
      
      public DataAccumulator(double binNumber, double delta, long genomicStart, long genomicEnd) {
        this.binNumber = binNumber;
        this.width = delta;
        this.genomicStart = genomicStart;
        this.genomicEnd = genomicEnd;
      }
  
      @Override
      public double getBinNumber() {
        return binNumber;
      }
  
      @Override
      public double getWithInBins() {
        return width;
      }
      
      @Override
      public long getGenomicEnd() {
        return genomicEnd;
      }
      
      @Override
      public long getGenomicStart() {
        return genomicStart;
      }
  
      @Override
      public double getValue(WindowFunction windowFunction) {
        return windowFunction == WindowFunction.max ? max :
            (nPts == 0 ? 0 : (float) (weightedSum / nPts));
      }
  
      void addScore(LocusScore ls) {
  //            if (ls.getStart() >= genomicEnd || ls.getEnd() < genomicStart) return;
  //            double weight = ((double) (Math.min(genomicEnd, ls.getEnd()) - Math.max(genomicStart, ls.getStart()))) /
  //                    (genomicEnd - genomicStart);
        double weight = 1;
        final float score = ls.getScore();
        weightedSum += weight * score;
        nPts++;
  
        max = score > max ? score : max;
      }
  
  
    }
  
  
    class LoadedDataInterval {
  
      final String zoom;
      final int scaleFactor;
      final String axisType;
      final WindowFunction windowFunction;
      final String chr;
      final int startBin;
      final int endBin;
      final DataAccumulator[] data;
  
      LoadedDataInterval(String zoom, int scaleFactor, String axisType, WindowFunction windowFunction,
                         String chr, int startBin, int endBin, DataAccumulator[] data) {
        this.zoom = zoom;
        this.scaleFactor = scaleFactor;
        this.axisType = axisType;
        this.windowFunction = windowFunction;
        this.chr = chr;
        this.startBin = startBin;
        this.endBin = endBin;
        this.data = data;
      }
  
      boolean contains(String zoom, int scaleFactor, String axisType, WindowFunction windowFunction,
                       String chr, int startBin, int endBin) {
        return zoom.equals(this.zoom) &&
            scaleFactor == this.scaleFactor &&
            axisType.equals(this.axisType) &&
            windowFunction == this.windowFunction &&
            chr.equals(this.chr) &&
            startBin >= this.startBin &&
            endBin <= this.endBin;
      }
  
      DataAccumulator[] getData() {
        return data;
      }
    }
  }
  