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 NON-INFRINGEMENT. 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.mapcolorui;
  
  import com.jidesoft.swing.JideButton;
  import juicebox.HiC;
  import juicebox.HiCGlobals;
  import juicebox.data.MatrixZoomData;
  import juicebox.gui.SuperAdapter;
  import juicebox.windowui.HiCZoom;
  import org.broad.igv.ui.FontManager;
  
  import javax.swing.*;
  import javax.swing.border.LineBorder;
  import javax.swing.event.ChangeEvent;
  import javax.swing.event.ChangeListener;
  import java.awt.*;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
  import java.awt.font.TextAttribute;
  import java.util.Dictionary;
  import java.util.Hashtable;
  import java.util.List;
  import java.util.Map;
  import java.util.concurrent.ConcurrentHashMap;
  
  public class ResolutionControl extends JPanel {
  
      private static final long serialVersionUID = 9000032;
      private final ImageIcon lockOpenIcon;
      private final ImageIcon lockIcon;
      private final HiC hic;
      private final HeatmapPanel heatmapPanel;
      private final JideButton lockButton;
      private final JLabel resolutionLabel;
      private final Map<Integer, HiCZoom> idxZoomMap = new ConcurrentHashMap<>();
      private final Map<Integer, String> bpLabelMap;
      private final HiCZoom pearsonZoom = new HiCZoom(HiC.Unit.BP, 500000);
      public HiC.Unit unit = HiC.Unit.BP;
      private boolean resolutionLocked = false;
      private final JSlider resolutionSlider;
      private int lastValue = 0;
  
      {
          // todo @nchernia shouldn't this use hic.getDataset().getBpZooms() ?
          bpLabelMap = new Hashtable<>();
          bpLabelMap.put(2500000, "2.5 MB");
          bpLabelMap.put(1000000, "1 MB");
          bpLabelMap.put(500000, "500 KB");
          bpLabelMap.put(250000, "250 KB");
          bpLabelMap.put(100000, "100 KB");
          bpLabelMap.put(50000, "50 KB");
          bpLabelMap.put(25000, "25 KB");
          bpLabelMap.put(10000, "10 KB");
          bpLabelMap.put(5000, "5 KB");
          bpLabelMap.put(1000, "1 KB");
  
      }
  
      public ResolutionControl(final SuperAdapter superAdapter) {
  
          this.hic = superAdapter.getHiC();
          this.heatmapPanel = superAdapter.getHeatmapPanel();
  
          this.setBorder(LineBorder.createGrayLineBorder());
          this.setLayout(new BorderLayout());
  
          resolutionLabel = new JLabel(getUnitLabel());
          resolutionLabel.setHorizontalAlignment(SwingConstants.CENTER);
          resolutionLabel.setBackground(HiCGlobals.backgroundColor);
  
          JPanel resolutionLabelPanel = new JPanel();
          resolutionLabelPanel.setBackground(HiCGlobals.backgroundColor);
          resolutionLabelPanel.setLayout(new BorderLayout());
          resolutionLabelPanel.add(resolutionLabel, BorderLayout.CENTER);
  
          // TODO not working
          // supposed to underline "resolution text" but why? is this an important gui issue?
          resolutionLabelPanel.addMouseListener(new MouseAdapter() {
              private Font original;
  
              @SuppressWarnings({"unchecked", "rawtypes"})
              @Override
              public void mouseEntered(MouseEvent e) {
                  original = e.getComponent().getFont();
                  if (resolutionSlider.isEnabled()) {
                      original = e.getComponent().getFont();
                      Map attributes = original.getAttributes();
                      attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
                      e.getComponent().setFont(original.deriveFont(attributes));
                  }
              }
  
              @Override
              public void mouseExited(MouseEvent e) {
                  e.getComponent().setFont(original);
              }
  
          });
  
  
          resolutionLabelPanel.addMouseListener(new MouseAdapter() {
  
              @Override
              public void mousePressed(MouseEvent e) {
                  if (e.isPopupTrigger() && resolutionSlider.isEnabled() && hic != null && hic.getDataset() != null) {
                      if (hic.getDataset().hasFrags()) {
                          processClick();
                      }
                  }
              }
  
              @Override
              public void mouseClicked(MouseEvent e) {
                  //No double click here...
                  if (e.getClickCount() == 1 && resolutionSlider.isEnabled() && hic != null && hic.getDataset() != null) {
                      if (hic.getDataset().hasFrags()) {
                          processClick();
                      }
                  }
              }
  
              private void processClick() {
                  unit = (unit == HiC.Unit.FRAG ? HiC.Unit.BP : HiC.Unit.FRAG);
                  resolutionLabel.setText(getUnitLabel());
                  Runnable runnable = new Runnable() {
                      public void run() {
                          reset();
                          superAdapter.refresh(); // necessary to correct BP/FRAG switching all red box
                      }
                  };
                  superAdapter.executeLongRunningTask(runnable, "Resolution switched");
              }
          });
  
  
          this.add(resolutionLabelPanel, BorderLayout.PAGE_START);
  
  
          JPanel resolutionButtonPanel = new JPanel();
          resolutionButtonPanel.setLayout(new BoxLayout(resolutionButtonPanel, BoxLayout.X_AXIS));
  
          //---- resolutionSlider ----
          JPanel sliderPanel = new JPanel();
          sliderPanel.setLayout(new BoxLayout(sliderPanel, BoxLayout.X_AXIS));
          resolutionSlider = new JSlider();
          sliderPanel.add(resolutionSlider);
  
          lockButton = new JideButton();
  
          lockIcon = new ImageIcon(getClass().getResource("/images/lock.png"));
          lockOpenIcon = new ImageIcon(getClass().getResource("/images/lock_open.png"));
          resolutionLocked = false;
          lockButton.setIcon(lockOpenIcon);
          lockButton.addActionListener(new ActionListener() {
              @Override
              public void actionPerformed(ActionEvent e) {
                  toggleLockButton();
              }
          });
          sliderPanel.add(lockButton);
  
  
          resolutionButtonPanel.add(sliderPanel);
          this.add(resolutionButtonPanel, BorderLayout.CENTER);
  
  
          // Setting the zoom should always be done by calling resolutionSlider.setValue() so work isn't done twice.
          resolutionSlider.addChangeListener(new ChangeListener() {
              // Change zoom level while staying centered on current location.
              // Centering is relative to the bounds of the data, which might not be the bounds of the window
  
              public void stateChanged(ChangeEvent e) {
                  final MatrixZoomData zd;
                  try {
                      zd = hic.getZd();
                  } catch (Exception ex) {
                      return;
                  }
                  if (hic == null || hic.getMatrix() == null || zd == null || resolutionSlider.getValueIsAdjusting())
                      return;
                  final ChangeEvent eF = e;
                  Runnable runnable = new Runnable() {
                      public void run() {
                          unsafeStateChanged(eF, zd);
                      }
                  };
                  superAdapter.executeLongRunningTask(runnable, "Resolution slider change");
              }
  
              private void unsafeStateChanged(ChangeEvent e, MatrixZoomData zd) {
  
                  int idx = resolutionSlider.getValue();
  
                  final HiCZoom zoom = idxZoomMap.get(idx);
                  if (zoom == null) return;
  
                  if (zoom.getBinSize() == hic.getXContext().getZoom().getBinSize() &&
                          zoom.getUnit() == hic.getXContext().getZoom().getUnit()) return;
  
                  if (hic.isInPearsonsMode() && hic.isPearsonsNotAvailableAtSpecificZoom(false, zoom)) {
                      JOptionPane.showMessageDialog(getParent(), "Pearson's matrix is not available at this resolution");
                      setZoom(pearsonZoom);
                      return;
                  }
  
                  if (hic.getXContext() != null) {
      
                      double scaledXWidth = heatmapPanel.getWidth() / hic.getScaleFactor();
                      double scaledYHeight = heatmapPanel.getHeight() / hic.getScaleFactor();
                      double centerBinX = hic.getXContext().getBinOrigin() + scaledXWidth / 2;
                      double centerBinY = hic.getYContext().getBinOrigin() + scaledYHeight / 2;
                      long xGenome = zd.getXGridAxis().getGenomicMid(centerBinX);
                      long yGenome = zd.getYGridAxis().getGenomicMid(centerBinY);
      
                      // this to center zooming when there is lots of whitespace in the margins
                      try {
                          if (scaledXWidth > hic.getZd().getXGridAxis().getBinCount()) {
                              xGenome = hic.getXContext().getChrLength() / 2;
                          }
                      } catch (Exception ignored) {
                      }
      
                      try {
                          if (scaledYHeight > hic.getZd().getYGridAxis().getBinCount()) {
                              yGenome = hic.getYContext().getChrLength() / 2;
                          }
                      } catch (Exception ignored) {
                      }
  
                      final String chrXName = hic.getXContext().getChromosome().toString();
                      final String chrYName = hic.getYContext().getChromosome().toString();
  
                      if (zd == null) {
                          hic.unsafeActuallySetZoomAndLocation(chrXName, chrYName, zoom, 0, 0, -1, true, HiC.ZoomCallType.STANDARD,
                                  true, isResolutionLocked() ? 1 : 0, true);
                      } else {
                          if (hic.unsafeActuallySetZoomAndLocation(chrXName, chrYName, zoom, xGenome, yGenome, -1, true,
                                  HiC.ZoomCallType.STANDARD, true, isResolutionLocked() ? 1 : 0, true)) {
                              lastValue = resolutionSlider.getValue();
                          } else {
                              resolutionSlider.setValue(lastValue);
                          }
                      }
                  }
              }
          });
          setEnabled(false);
      }
  
      private String getUnitLabel() {
          return unit == HiC.Unit.FRAG ? "Resolution (Frag)" : "Resolution (BP)";
      }
  
      public void setEnabled(boolean enabled) {
          resolutionSlider.setEnabled(enabled);
          lockButton.setEnabled(enabled);
      }
  
      /**
       * Called when a new dataset is loaded, or when units are switched bp<->frag
       */
      public void reset() {
          if (hic == null || hic.getDataset() == null) return;
  
          if (heatmapPanel != null) heatmapPanel.reset();
  
          int currentIdx = resolutionSlider.getValue();
  
          List<HiCZoom> binSizes = unit == HiC.Unit.BP ?
                  hic.getDataset().getBpZooms() : hic.getDataset().getFragZooms();
  
          idxZoomMap.clear();
          for (int i = 0; i < binSizes.size(); i++) {
              HiCZoom zoom = binSizes.get(i);
              idxZoomMap.put(i, zoom);
          }
  
          int maxIdx = binSizes.size() - 1;
          resolutionSlider.setMaximum(maxIdx);
          resolutionSlider.setMajorTickSpacing(1);
          resolutionSlider.setPaintTicks(true);
          resolutionSlider.setSnapToTicks(true);
          resolutionSlider.setPaintLabels(true);
          resolutionSlider.setMinorTickSpacing(1);
  
          // Create labels
          Dictionary<Integer, JLabel> resolutionLabels = new Hashtable<>();
          Font f = FontManager.getFont(8);
          int skip = maxIdx > 6 ? 2 : 1;   // Skip every other if more than 6 levels
          for (int i = 0; i <= maxIdx; i++) {
              if (i % skip == 0) {
                  String label = sizeToLabel(binSizes.get(i).getBinSize());
                  final JLabel tickLabel = new JLabel(label);
                  tickLabel.setFont(f);
                  resolutionLabels.put(i, tickLabel);
              }
              HiCZoom zoom = binSizes.get(i);
              idxZoomMap.put(i, zoom);
          }
  
          resolutionSlider.setLabelTable(resolutionLabels);
  
          // Really we should find the closest matching resolution
          int newIdx = Math.min(currentIdx, maxIdx);
          HiCZoom newZoom = idxZoomMap.get(newIdx);
          setZoom(newZoom);
      }
  
      private String sizeToLabel(int binSize) {
  
          if (unit == HiC.Unit.FRAG) {
              return binSize + " f";
          }
  
          // TODO delete the bpLabelMap? We dont need a hard-coded map if the labels change based on hicMapScale
  //        if (bpLabelMap.containsKey(binSize)) {
  //            return bpLabelMap.get(binSize);
  //        }
  
          String label;
          int adjustedBinSize = (int) (binSize * HiCGlobals.hicMapScale);
  
          if (adjustedBinSize >= 1000000) {
              label = ((double) adjustedBinSize / 1000000) + " MB";
          } else if (adjustedBinSize >= 1000) {
              label = ((double) adjustedBinSize / 1000) + " KB";
          } else {
              label = adjustedBinSize + " BP";
          }
          return removeTrailingZeros(label);
      }
  
      private String removeTrailingZeros(String label) {
          return label.split(" ")[0].replaceAll("\\.(0)+$", "") + " " + label.split(" ")[1];
      }
  
      public void setZoom(HiCZoom newZoom) {
          if (unit != newZoom.getUnit()) {
              unit = newZoom.getUnit();
              reset();
          }
          resolutionLabel.setText(getUnitLabel());
          resolutionLabel.setForeground(Color.BLUE);
          for (Map.Entry<Integer, HiCZoom> entry : idxZoomMap.entrySet()) {
              if (entry.getValue().equals(newZoom)) {
                  resolutionSlider.setValue(entry.getKey());
              }
          }
      }
  
      private void toggleLockButton() {
          setResolutionLocked(!resolutionLocked);
          updateLockButton();
      }
  
      private void updateLockButton() {
          lockButton.setIcon(resolutionLocked ? lockIcon : lockOpenIcon);
      }
  
      public boolean isResolutionLocked() {
          return resolutionLocked;
      }
  
      public void setResolutionLocked(boolean resolutionLocked) {
          this.resolutionLocked = resolutionLocked;
          updateLockButton();
      }
  }
  