/**
   * Gets an IndexColorModel by loading a hardcoded LUT file. This is just a temporary expedient,
   * really belongs elsewhere.
   *
   * @return null or color model
   */
  public static IndexColorModel getIndexColorModel() {
    IndexColorModel colorModel = null;

    // 'getDirectory("luts")' works in IJ but not in NetBeans development
    String lutPath = IJ.getDirectory("luts");
    if (null == lutPath) {
      // when you run from a shortcut in Linux 'getDirectory("startup")'
      // gives you the directory of the link!
      final String startupPath = IJ.getDirectory("startup");
      lutPath = addSeparator(startupPath) + "luts";
    }
    lutPath = addSeparator(lutPath) + LUT;
    try {
      colorModel = LutLoader.open(lutPath);
    } catch (final IOException e) {
      IJ.showMessage("Missing LUT", "Install lifetime.lut from LOCI FIJI update site.");
      IJ.log("Problem loading LUT " + lutPath);
      return null;
    }
    // IJ converts the FloatProcessor to 8-bits and then uses this palette
    // for display. Unfortunately values less than or greater than the LUT
    // range still get displayed with LUT colors. To work around this, use
    // only 254 of the LUT colors. The first and last colors will represent
    // values less than and greater than the LUT range respectively.

    colorModel = PaletteFix.fixIndexColorModel(colorModel, Color.BLACK, Color.BLACK);
    return colorModel;
  }
 /*
  * Converts image value to histogram onscreen horizontal pixel.
  */
 private int valueToPixel(final double value) {
   synchronized (_synchObject) {
     final double[] minMaxView = _histogramDataGroup.getMinMaxView();
     final double min = minMaxView[0];
     final double max = minMaxView[1];
     final int pixel = (int) (PaletteFix.getSize() * (value - min) / (max - min));
     return pixel;
   }
 }
 private double pixelToValueRelative(final int pixel) {
   synchronized (_synchObject) {
     final double[] minMaxView = _histogramDataGroup.getMinMaxView();
     final double min = minMaxView[0];
     final double max = minMaxView[1];
     final double valuePerPixel = (max - min) / PaletteFix.getSize();
     return pixel * valuePerPixel;
   }
 }
 /**
  * Listens to the HistogramPanel, gets minimum and maximum cursor bar positions in pixels.
  * Called during a drag operation.
  *
  * @param min
  * @param max
  */
 @Override
 public void dragMinMaxPixels(final int min, final int max) {
   if (min < 0 || max >= PaletteFix.getSize()) {
     // cursor is out of bounds, set up a periodic task to stretch
     // the bounds, if not already running
     if (min < 0) {
       _dragPixels = min;
     } else {
       _dragPixels = max - PaletteFix.getSize() + 1;
     }
     if (null == _timer) {
       _timer = new Timer();
       _timer.schedule(new PeriodicTask(), 0, TASK_PERIOD);
     }
   } else {
     // dragging within bounds now, kill the periodic task, if any
     killTimer();
     final double minLUT = pixelToValue(min);
     final double maxLUT = pixelToValue(max + 1);
     _uiPanel.dragMinMaxLUT(minLUT, maxLUT);
   }
 }
/**
 * This is the main class for the histogram tool. It handles layout and wiring of UI components and
 * the logic of updating the histogram.
 *
 * @author Aivar Grislis
 */
public class HistogramTool {

  private static final String WIDTH_KEY = "width";
  private static final String HEIGHT_KEY = "height";
  private static final int SET_WIDTH = 263;
  private static final int SET_HEIGHT = 302;
  private static final int MAX_HEIGHT = 500;
  private static final int MIN_HEIGHT = 200;
  private static final int WIDTH = PaletteFix.getSize();
  private static final int INSET = 5;
  private static final int HISTOGRAM_HEIGHT = 140;
  private static final int COLORBAR_HEIGHT = 20;
  private static final int TASK_PERIOD = 100;
  private static final String LUT = "lifetime.lut"; // use a specific LUT; most
  // IJ LUTs unsuitable
  private static HistogramTool INSTANCE = null;
  private final Object _synchObject = new Object();
  private JFrame _frame;
  private HistogramDataGroup _histogramDataGroup;
  private HistogramPanel _histogramPanel;
  private ColorBarPanel _colorBarPanel;
  private HistogramUIPanel _uiPanel;
  private boolean _excludePixels;

  /**
   * Class is a singleton for convenience.
   *
   * @return
   */
  public static synchronized HistogramTool getInstance() {
    if (null == INSTANCE) {
      INSTANCE = new HistogramTool();
    }
    return INSTANCE;
  }

  /**
   * Initializer, handles layout and wiring. Called when set of images is created.
   *
   * @param hasChannels
   */
  public void show(final boolean hasChannels) {
    if (null == _frame || !_frame.isShowing()) {
      // create the histogram and color bar display panels
      _histogramPanel = new HistogramPanel(WIDTH, INSET, HISTOGRAM_HEIGHT);
      _histogramPanel.setListener(new HistogramPanelListener());
      _colorBarPanel = new ColorBarPanel(WIDTH, INSET, COLORBAR_HEIGHT);
      _colorBarPanel.setLUT(getLUT());

      _uiPanel = new HistogramUIPanel(hasChannels);
      _uiPanel.setListener(new UIPanelListener());

      _frame = new JFrame("Histogram");
      // _frame.setResizable(false);
      _frame.setLayout(new BorderLayout());

      final JPanel panel = new JPanel();
      panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
      panel.add(_colorBarPanel);
      panel.add(_uiPanel);
      _frame.getContentPane().add(_histogramPanel, BorderLayout.CENTER);
      _frame.getContentPane().add(panel, BorderLayout.SOUTH);
      _frame.pack();
      _frame.setVisible(true);
      // IJ.log("initial size " + _frame.getSize());
      _frame.addComponentListener(
          new ComponentListener() {

            @Override
            public void componentHidden(final ComponentEvent e) {}

            @Override
            public void componentMoved(final ComponentEvent e) {}

            @Override
            public void componentResized(final ComponentEvent e) {
              // constrain maximum size
              boolean resize = false;
              final Dimension size = _frame.getSize();
              if (size.width != (SET_WIDTH)) {
                size.width = SET_WIDTH;
                resize = true;
              }
              if (size.height > MAX_HEIGHT) {
                size.height = MAX_HEIGHT;
                resize = true;
              }
              if (size.height < MIN_HEIGHT) {
                size.height = MIN_HEIGHT;
                resize = true;
              }
              if (resize) {
                _frame.setSize(size);
              }
              saveSizeInPreferences(size);
            }

            @Override
            public void componentShown(final ComponentEvent e) {}
          });
      _frame.setSize(getSizeFromPreferences());
    }
  }

  /**
   * Gets an IndexColorModel by loading a hardcoded LUT file. This is just a temporary expedient,
   * really belongs elsewhere.
   *
   * @return null or color model
   */
  public static IndexColorModel getIndexColorModel() {
    IndexColorModel colorModel = null;

    // 'getDirectory("luts")' works in IJ but not in NetBeans development
    String lutPath = IJ.getDirectory("luts");
    if (null == lutPath) {
      // when you run from a shortcut in Linux 'getDirectory("startup")'
      // gives you the directory of the link!
      final String startupPath = IJ.getDirectory("startup");
      lutPath = addSeparator(startupPath) + "luts";
    }
    lutPath = addSeparator(lutPath) + LUT;
    try {
      colorModel = LutLoader.open(lutPath);
    } catch (final IOException e) {
      IJ.showMessage("Missing LUT", "Install lifetime.lut from LOCI FIJI update site.");
      IJ.log("Problem loading LUT " + lutPath);
      return null;
    }
    // IJ converts the FloatProcessor to 8-bits and then uses this palette
    // for display. Unfortunately values less than or greater than the LUT
    // range still get displayed with LUT colors. To work around this, use
    // only 254 of the LUT colors. The first and last colors will represent
    // values less than and greater than the LUT range respectively.

    colorModel = PaletteFix.fixIndexColorModel(colorModel, Color.BLACK, Color.BLACK);
    return colorModel;
  }

  private static String addSeparator(String path) {
    if (!path.endsWith(File.separator)) {
      path += File.separatorChar;
    }
    return path;
  }

  /**
   * Gets a LUT. Temporary expedient, belongs elsewhere.
   *
   * @return
   */
  public static LUT getLUT() {
    final IndexColorModel colorModel = getIndexColorModel();
    final LUT lut = new LUT(colorModel, -Double.MAX_VALUE, Double.MAX_VALUE);
    return lut;
  }

  /**
   * This method should be called whenever a new set of histogram values is to be displayed (i.e.
   * when a different image gets focus).
   *
   * @param histogramData
   */
  public void setHistogramData(final HistogramDataGroup histogramData) {
    double[] minMaxView;
    double[] minMaxLUT;
    synchronized (_synchObject) {
      _histogramDataGroup = histogramData;
      _histogramDataGroup.setListener(new HistogramDataListener());
      minMaxView = _histogramDataGroup.getMinMaxView();
      minMaxLUT = _histogramDataGroup.getMinMaxLUT();
    }

    //		IJ.log("----");
    //		IJ.log("setHistogramData");
    //		IJ.log("view " + minMaxView[0] + " "  + minMaxView[1] + " lut " + minMaxLUT[0] + " " +
    // minMaxLUT[1]);
    //		IJ.log("----");

    if (null != _frame) {
      if (_frame.isVisible()) {
        _frame.setVisible(true);
      }
      _frame.setTitle(histogramData.getTitle());

      _histogramPanel.setStatistics(histogramData.getStatistics(WIDTH));

      final boolean autoRange = histogramData.getAutoRange();
      if (autoRange) {
        // turn cursors off
        _histogramPanel.setCursors(null, null);
      } else {
        // set cursors to edges
        _histogramPanel.setCursors(INSET, INSET + WIDTH - 1);
      }
      _uiPanel.setAutoRange(autoRange);
      _uiPanel.setExcludePixels(histogramData.getExcludePixels());
      _uiPanel.setCombineChannels(histogramData.getCombineChannels());
      _uiPanel.setDisplayChannels(histogramData.getDisplayChannels());
      _uiPanel.enableChannels(histogramData.hasChannels());
      _uiPanel.setMinMaxLUT(minMaxLUT[0], minMaxLUT[1]);

      _colorBarPanel.setMinMax(minMaxView[0], minMaxView[1], minMaxLUT[0], minMaxLUT[1]);
    }
  }

  /*
   * Converts histogram onscreen horizontal pixel amounts to image values.
   */
  private double pixelToValue(final int pixel) {
    synchronized (_synchObject) {
      final double[] minMaxView = _histogramDataGroup.getMinMaxView();
      final double min = minMaxView[0];
      return min + pixelToValueRelative(pixel);
    }
  }

  private double pixelToValueRelative(final int pixel) {
    synchronized (_synchObject) {
      final double[] minMaxView = _histogramDataGroup.getMinMaxView();
      final double min = minMaxView[0];
      final double max = minMaxView[1];
      final double valuePerPixel = (max - min) / PaletteFix.getSize();
      return pixel * valuePerPixel;
    }
  }

  /*
   * Converts image value to histogram onscreen horizontal pixel.
   */
  private int valueToPixel(final double value) {
    synchronized (_synchObject) {
      final double[] minMaxView = _histogramDataGroup.getMinMaxView();
      final double min = minMaxView[0];
      final double max = minMaxView[1];
      final int pixel = (int) (PaletteFix.getSize() * (value - min) / (max - min));
      return pixel;
    }
  }

  /*
   * Updates histogram and color bar during the fit.
   */
  private void changed(
      final double minView, final double maxView, final double minLUT, final double maxLUT) {
    synchronized (_synchObject) {
      // TODO ARG added this 9/27 to change hDG internal minMaxViews
      _histogramDataGroup.setMinMaxView(minView, maxView);
      _histogramPanel.setStatistics(_histogramDataGroup.getStatistics(WIDTH));
      _colorBarPanel.setMinMax(minView, maxView, minLUT, maxLUT);
      // TODO changed is currently called from two places:
      // i) the HistogramData listener will call it periodically during the
      // fit.
      // ii) if the user types in a new LUT range this gets called.
      // iii) in the future more UI interactions will wind up here
      //
      // TODO if the user drags a new LUT range this doesn't get called!

      // IJ.log("changed min/maxView " + minView + " " + maxView +
      // " min/maxLUT " + minLUT + " " + maxLUT);

      _histogramDataGroup.redisplay();
    }
  }

  /*
   * Given a value representing the minimum or maximum cursor bound,
   * calculates a pixel location for the cursor.
   *
   * @param max whether this is the maximum cursor or not
   */
  private int cursorPixelFromValue(final boolean max, final double value) {
    int pixel = INSET + valueToPixel(value);
    if (max) {
      // cursor brackets the value
      ++pixel;
    }
    return pixel;
  }

  // TODO TIDY THIS UP do we need "reset cursor positions"
  private void zoomToLUT() {
    final double minMaxLUT[] = _histogramDataGroup.getMinMaxLUT();
    final double minLUT = minMaxLUT[0];
    final double maxLUT = minMaxLUT[1];

    // zoom to fit current min/max LUT settings
    final double minView = minLUT;
    final double maxView = maxLUT;

    changed(minView, maxView, minLUT, maxLUT);

    // reset cursor positions
    _histogramPanel.resetCursors();
  }

  /** Inner class listens for changes during the fit. */
  private class HistogramDataListener implements IHistogramDataListener {

    @Override
    public void minMaxChanged(
        final HistogramDataGroup histogramDataGroup,
        final double minView,
        final double maxView,
        final double minLUT,
        final double maxLUT) {
      // make sure it's the same current fitted image
      if (_histogramDataGroup == histogramDataGroup) {
        changed(minView, maxView, minLUT, maxLUT);
        _uiPanel.setMinMaxLUT(minLUT, maxLUT);
      }
    }
  }

  /** Inner class to listen for the user moving the cursor on the histogram. */
  private class HistogramPanelListener implements IHistogramPanelListener {

    private Timer _timer = null;
    private volatile int _dragPixels;

    /**
     * Listens to the HistogramPanel, gets minimum and maximum cursor bar positions in pixels.
     * Called when the cursor bar is moved and the mouse button released. A new LUT range has been
     * specified.
     *
     * @param min
     * @param max
     */
    @Override
    public void setMinMaxLUTPixels(final int min, final int max) {
      killTimer();

      // get new minimum and maximum values for LUT
      final double minLUT = pixelToValue(min);
      final double maxLUT = pixelToValue(max + 1);

      // set min and max on UI panel
      _uiPanel.setMinMaxLUT(minLUT, maxLUT);

      // redraw color bar
      _colorBarPanel.setMinMaxLUT(minLUT, maxLUT);

      // save and redraw image
      synchronized (_synchObject) {
        _histogramDataGroup.setMinMaxLUT(minLUT, maxLUT);
        if (_excludePixels) {
          _histogramDataGroup.excludePixels(true);
        }
        zoomToLUT();
      }
    }

    /**
     * Listens to the HistogramPanel, gets minimum and maximum cursor bar positions in pixels.
     * Called during a drag operation.
     *
     * @param min
     * @param max
     */
    @Override
    public void dragMinMaxPixels(final int min, final int max) {
      if (min < 0 || max >= PaletteFix.getSize()) {
        // cursor is out of bounds, set up a periodic task to stretch
        // the bounds, if not already running
        if (min < 0) {
          _dragPixels = min;
        } else {
          _dragPixels = max - PaletteFix.getSize() + 1;
        }
        if (null == _timer) {
          _timer = new Timer();
          _timer.schedule(new PeriodicTask(), 0, TASK_PERIOD);
        }
      } else {
        // dragging within bounds now, kill the periodic task, if any
        killTimer();
        final double minLUT = pixelToValue(min);
        final double maxLUT = pixelToValue(max + 1);
        _uiPanel.dragMinMaxLUT(minLUT, maxLUT);
      }
    }

    @Override
    public void exited() {
      // dragged off the panel, kill the periodic task
      killTimer();
    }

    // stop our timer for animating view expansion
    private void killTimer() {
      if (null != _timer) {
        _timer.cancel();
        _timer = null;
      }
    }

    /**
     * Inner class used to animate view expansion, triggered by the initial report of a mouse drag
     * event off the edge of the histogram.
     */
    private class PeriodicTask extends TimerTask {

      @Override
      public void run() {
        // how much are we dragging, converted to our value
        final double value = pixelToValueRelative(_dragPixels);
        synchronized (_synchObject) {
          // get current LUT bounds
          final double[] minMaxLUT = _histogramDataGroup.getMinMaxLUT();
          double minLUT = minMaxLUT[0];
          double maxLUT = minMaxLUT[1];

          // adjust the appropriate left or right side of the view
          final double[] minMaxView = _histogramDataGroup.getMinMaxView();
          double minView = minMaxView[0];
          double maxView = minMaxView[1];
          if (value < 0) {
            minView += value;
            minLUT = minView;
          } else {
            maxView += value;
            maxLUT = maxView;
          }

          // update histogram data min and max view
          _histogramDataGroup.setMinMaxView(minView, maxView);

          // keep other cursor fixed
          int pixel;
          if (value < 0) {
            if (maxView != maxLUT) {
              pixel = cursorPixelFromValue(true, maxLUT);
              // pixel = valueToPixel(maxLUT);
              // IJ.log("PeriodicTask.run maxLUT is " + maxLUT +
              // " cursor is at " + pixel);
              // TODO ARG this is still a little bit off
              // _histogramPanel.setCursors(null, INSET + pixel + 1);
              // TODO ARG this is still a little off
              _histogramPanel.setCursors(null, pixel);
            }
          } else {
            if (minView != minLUT) {
              pixel = cursorPixelFromValue(false, minLUT);
              // pixel = valueToPixel(minLUT);
              // IJ.log("PeriodicTask.run minLUT is " + minLUT +
              // " cursor is at " + pixel);
              // TODO ARG this is still a little bit off
              // _histogramPanel.setCursors(INSET + pixel, null);
              // TODO ARG this is still a little off
              _histogramPanel.setCursors(pixel, null);
            }
          }

          // get updated histogram data and show it
          _histogramPanel.setStatistics(_histogramDataGroup.getStatistics(WIDTH));
          _colorBarPanel.setMinMax(minView, maxView, minLUT, maxLUT);

          // update numbers in UI panel
          _uiPanel.dragMinMaxLUT(minLUT, maxLUT);
        }
      }
    }
  }

  /**
   * Restores size from Java Preferences.
   *
   * @return size
   */
  private Dimension getSizeFromPreferences() {
    final Preferences prefs = Preferences.userNodeForPackage(this.getClass());
    return new Dimension(prefs.getInt(WIDTH_KEY, SET_WIDTH), prefs.getInt(HEIGHT_KEY, SET_HEIGHT));
  }

  /**
   * Saves the size to Java Preferences.
   *
   * @param size
   */
  private void saveSizeInPreferences(final Dimension size) {
    final Preferences prefs = Preferences.userNodeForPackage(this.getClass());
    prefs.putInt(WIDTH_KEY, size.width);
    prefs.putInt(HEIGHT_KEY, size.height);
  }

  /** Inner class listens to the UI panel */
  private class UIPanelListener implements IUIPanelListener {

    @Override
    public void setAutoRange(final boolean autoRange) {
      synchronized (_synchObject) {
        // TODO shows up twice now!!!!TRY CALLING AFTER
        _histogramDataGroup.setAutoRange(autoRange);

        if (autoRange) {
          // TODO ARG:
          // It is not always true that the cursors will be (null, null)
          // [same as (0, 254)], the exception happens if you are showing
          // all channels but only autoranging your channel.
          _histogramPanel.setCursors(null, null);
        } else {
          _histogramPanel.setCursors(INSET, INSET + WIDTH - 1);
        }

        // this will propagate min/max changes
        // _histogramDataGroup.setAutoRange(autoRange);
      }
    }

    @Override
    public void setExcludePixels(final boolean excludePixels) {
      _excludePixels = excludePixels;
      synchronized (_synchObject) {
        // pass along the changes
        _histogramDataGroup.excludePixels(excludePixels);

        // get updated histogram data and show it
        _histogramPanel.setStatistics(_histogramDataGroup.getStatistics(WIDTH));
      }
    }

    @Override
    public void setCombineChannels(final boolean combineChannels) {
      synchronized (_synchObject) {
        _histogramDataGroup.setCombineChannels(combineChannels);
      }
    }

    @Override
    public void setDisplayChannels(final boolean displayChannels) {
      synchronized (_synchObject) {
        _histogramDataGroup.setDisplayChannels(displayChannels);

        // get updated histogram data and show it
        _histogramPanel.setStatistics(_histogramDataGroup.getStatistics(WIDTH));
      }
    }

    @Override
    public void setLogarithmicDisplay(final boolean log) {
      _histogramPanel.setLogarithmicDisplay(log);
    }

    @Override
    public void setSmoothing(final boolean smooth) {
      _histogramPanel.setSmoothing(smooth);
    }

    @Override
    public void setBandwidth(final double bandwidth) {
      _histogramPanel.setBandwidth(bandwidth);
    }

    @Override
    public void setFamilyStyle1(final boolean on) {
      _histogramPanel.setFamily1(on);
    }

    @Override
    public void setFamilyStyle2(final boolean on) {
      _histogramPanel.setFamily2(on);
    }

    @Override
    public void setMinMaxLUT(final double minLUT, final double maxLUT) {
      boolean changed = false;
      double minView = 0;
      double maxView = 0;

      // silently ignores errors; c/b temporary condition such as setting
      // maximum before minimum
      if (minLUT < maxLUT) {
        changed = true;

        // TODO redo half-assed synchronization in 1.2
        synchronized (_synchObject) {
          // adjust view range to LUT range
          minView = minLUT;
          maxView = maxLUT;
          _histogramDataGroup.setMinMaxView(minView, maxView);
          _histogramDataGroup.setMinMaxLUT(minLUT, maxLUT);
        }
      }

      if (changed) {
        // update histogram and color bar
        changed(minView, maxView, minLUT, maxLUT);

        if (_excludePixels) {
          _histogramDataGroup.excludePixels(true);
        }
      }
    }
  }
}