/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <https://www.compadre.org/osp/>
 */

package org.opensourcephysics.display2d;
import java.awt.Color;
import javax.swing.JFrame;
import javax.swing.WindowConstants;
import org.opensourcephysics.display.DisplayRes;
import org.opensourcephysics.display.InteractivePanel;
import org.opensourcephysics.display.axes.XAxis;
import org.opensourcephysics.display.axes.XYAxis;

public class VectorColorMapper {
  public static final int SPECTRUM = 0;
  public static final int RED = 1;
  public static final int BLUE = 2;
  public static final int GREEN = 3;
  public static final int GRAY = 4;
  public static final int BLACK = 5; // solid black
  private static final Color RED_COMP;
  private static final Color GREEN_COMP;
  private static final Color BLUE_COMP;
  private Color background = Color.WHITE;
  private Color[] colors;
  private Color[] compColors;
  private double ceil, floor;
  private int numColors;
  private int paletteType;
  private JFrame legendFrame;
  private VectorPlot legendPlot;
  private InteractivePanel legendPanel;

  static {
    float[] hsb = Color.RGBtoHSB(255, 0, 0, null);
    RED_COMP = Color.getHSBColor((hsb[0]+0.5f)%1, hsb[1], hsb[2]);   // complement red hue
    hsb = Color.RGBtoHSB(0, 255, 0, null);
    GREEN_COMP = Color.getHSBColor((hsb[0]+0.5f)%1, hsb[1], hsb[2]); // complement green hue
    hsb = Color.RGBtoHSB(0, 255, 0, null);
    BLUE_COMP = Color.getHSBColor((hsb[0]+0.5f)%1, hsb[1], hsb[2]);  // complement blue hue
  }

  /**
   * Constructor VectorColorMapper
   * @param _numColors
   * @param _ceil
   */
  public VectorColorMapper(int _numColors, double _ceil) {
    ceil = _ceil;
    numColors = _numColors;
    floor = (numColors<2) ? 0 : ceil/(numColors-1);
    paletteType = SPECTRUM;
    createSpectrumPalette(); // default colors
  }

  /**
   * Sets the number of colors
   * @param _numColors
   */
  public void setNumberOfColors(int _numColors) {
    if(_numColors==numColors) {
      return;
    }
    numColors = _numColors;
    floor = (numColors<2) ? 0 : ceil/(numColors-1);
    setPaletteType(paletteType);
  }

  public double getFloor() {
    return floor;
  }

  public double getCeiling() {
    return ceil;
  }

  /**
 * Sets the color palette.
 * @param _paletteType
 */
  public void setPaletteType(int _paletteType) {
    if(paletteType==_paletteType && numColors==colors.length) {
      return;
    }
    floor = (numColors<2) ? 0 : ceil/(numColors-1);
    paletteType = _paletteType;
    switch(paletteType) { // computes new palette
       case RED :
         createRedSpectrumPalette();
         return;
       case BLUE :
         createBlueSpectrumPalette();
         return;
       case GREEN :
         createGreenSpectrumPalette();
         return;
       case BLACK :       // always black; no need to compute anything
         return;
       case GRAY :
         createGraySpectrumPalette();
         return;
       default :          // spectrum of colors from light blue toward blue toward red toward black
         createSpectrumPalette();
    }
  }

  /**
   * Checks to see if background color matches the color palette background.
   * @param backgroundColor
   */
  public void checkPallet(Color backgroundColor) {
    if(background==backgroundColor) {
      return; // backgrounds match
    }
    background = backgroundColor;
    switch(paletteType) { // compute palette using new background
       case RED :
         createRedSpectrumPalette();
         return;
       case BLUE :
         createBlueSpectrumPalette();
         return;
       case GREEN :
         createGreenSpectrumPalette();
         return;
       case BLACK :       // always black; no need to compute anything
         return;
       case GRAY :
         createGraySpectrumPalette();
         return;
       default :          // spectrum of colors from light blue toward blue toward red toward black
         createSpectrumPalette();
    }
  }

  /**
   * Sets the scale.
   *
   * @param _ceil
   */
  public void setScale(double _ceil) {
    ceil = _ceil;
    floor = (numColors<2) ? 0 : ceil/(numColors-1);
  }

  /**
   * Converts a double to a the complementary color.
   * @param mag
   * @return the color
   */
  public Color doubleToCompColor(double mag) {
    if(mag<=floor) { // magnitudes less than floor are clear
      return background;
    }
    int index;
    switch(paletteType) {
       case RED :
         if(mag>=ceil) {
           return RED_COMP;
         }
         index = (int) ((numColors-1)*(mag/ceil));
         return compColors[index];                                               // shade of red
       case BLUE :
         if(mag>=ceil) {
           return BLUE_COMP;
         }
         index = (int) ((numColors-1)*(mag/ceil));
         return compColors[index];                                               // shade of blue
       case GREEN :
         if(mag>=ceil) {
           return GREEN_COMP;
         }
         index = (int) ((numColors-1)*(mag/ceil));
         return compColors[index];                                               // shade of green
       case BLACK :                                                              // complement of BLACK is WHITE
         return Color.WHITE;
       case GRAY :                                                               // cannot complement gray
         if(mag>=ceil) {
           return Color.black;
         }
         index = (int) ((numColors-1)*(mag/ceil));
         return colors[index];                                                   // shade of gray from white
       default :                                                                 // spectrum of colors from light blue toward blue toward red toward black
         Color c = getSpectrumColor(mag);
         float[] hsb = Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), null);
         float[] hsbBack = Color.RGBtoHSB(background.getRed(), background.getGreen(), background.getBlue(), null);
         return Color.getHSBColor((2*hsbBack[0]-hsb[0]+0.5f)%1, hsb[1], hsb[2]); // complementary hue
    }
  }

  /**
   * Converts a double to a color.
   * @param mag
   * @return the color
   */
  public Color doubleToColor(double mag) {
    if(mag<=floor) { // magnitudes less than floor are clear
      return background;
    }
    int index;
    switch(paletteType) {
       case RED :
         if(mag>=ceil) {
           return Color.RED;
         }
         index = (int) ((numColors-1)*(mag/ceil));
         return colors[index]; // shade of red
       case BLUE :
         if(mag>=ceil) {
           return Color.BLUE;
         }
         index = (int) ((numColors-1)*(mag/ceil));
         return colors[index]; // shade of blue
       case GREEN :
         if(mag>=ceil) {
           return Color.GREEN;
         }
         index = (int) ((numColors-1)*(mag/ceil));
         return colors[index]; // shade of green
       case BLACK :            // always black
         return Color.black;
       case GRAY :
         if(mag>=ceil) {
           return Color.black;
         }
         index = (int) ((numColors-1)*(mag/ceil));
         return colors[index]; // shade of gray from white
       default :               // spectrum of colors
         return getSpectrumColor(mag);
    }
  }

  private Color getSpectrumColor(double mag) {
    if((background==Color.BLACK)&&(mag>=ceil)) { // magnitude greater than max tends from RED toward pure WHITE
      int s = (int) (255*(1-ceil/mag));
      return new Color(255, s, s);
    } else if(mag>=ceil) {                       // magnitude greater than max tends from RED toward pure BLACK
      return new Color((int) (255.0*ceil/mag), 0, 0);
    }
    int index = (int) ((numColors-1)*(mag/ceil));
    return colors[index];
  }

  private void createRedSpectrumPalette() {
    colors = new Color[numColors];
    compColors = new Color[numColors];
    int bgr = background.getRed();
    int bgg = background.getGreen();
    int bgb = background.getBlue();
    for(int i = 0; i<numColors; i++) {                              // start with the background and increase toward pure RED
      int tr = bgr+(255-bgr)*i/numColors;                           // increase the red
      int tg = bgg-bgg*i/numColors;                                 // decrease the green
      int tb = bgb-bgb*i/numColors;                                 // decrease the blue
      colors[i] = new Color(tr, tg, tb);
      float[] hsb = Color.RGBtoHSB(tr, tg, tb, null);
      Color c = Color.getHSBColor((hsb[0]+0.5f)%1, hsb[1], hsb[2]); // complementary hue
      tr = bgr+(c.getRed()-bgr)*i/numColors;
      tg = bgg+(c.getGreen()-bgg)*i/numColors;
      tb = bgb+(c.getBlue()-bgb)*i/numColors;
      compColors[i] = new Color(tr, tg, tb);
    }
  }

  private void createGreenSpectrumPalette() {
    colors = new Color[numColors];
    compColors = new Color[numColors];
    int bgr = background.getRed();
    int bgg = background.getGreen();
    int bgb = background.getBlue();
    for(int i = 0; i<numColors; i++) {                              // start with the background and increase toward pure GREEN
      int tr = bgr-bgr*i/numColors;                                 // decrease the red
      int tg = bgg+(255-bgg)*i/numColors;                           // increase the green
      int tb = bgb-bgb*i/numColors;                                 // decrease the blue
      colors[i] = new Color(tr, tg, tb);
      float[] hsb = Color.RGBtoHSB(tr, tg, tb, null);
      Color c = Color.getHSBColor((hsb[0]+0.5f)%1, hsb[1], hsb[2]); // complementary hue
      tr = bgr+(c.getRed()-bgr)*i/numColors;
      tg = bgg+(c.getGreen()-bgg)*i/numColors;
      tb = bgb+(c.getBlue()-bgb)*i/numColors;
      compColors[i] = new Color(tr, tg, tb);
    }
  }

  private void createBlueSpectrumPalette() {
    colors = new Color[numColors];
    compColors = new Color[numColors];
    int bgr = background.getRed();
    int bgg = background.getGreen();
    int bgb = background.getBlue();
    for(int i = 0; i<numColors; i++) {                              // start with the background and increase toward pure BLUE
      int tr = bgr-bgr*i/numColors;                                 // decrease the red
      int tg = bgg-bgg*i/numColors;                                 // decrease the green
      int tb = bgb+(255-bgb)*i/numColors;                           // increase the blue
      colors[i] = new Color(tr, tg, tb);
      float[] hsb = Color.RGBtoHSB(tr, tg, tb, null);
      Color c = Color.getHSBColor((hsb[0]+0.5f)%1, hsb[1], hsb[2]); // complementary hue
      tr = bgr+(c.getRed()-bgr)*i/numColors;
      tg = bgg+(c.getGreen()-bgg)*i/numColors;
      tb = bgb+(c.getBlue()-bgb)*i/numColors;
      compColors[i] = new Color(tr, tg, tb);
    }
  }

  private void createGraySpectrumPalette() {
    compColors = colors = new Color[numColors]; // cannot complement gray color
    if(background==Color.BLACK) {  // special case; increase toward white
      for(int i = 0; i<numColors; i++) {
        int sat = 255*i/numColors; // increase the saturation
        colors[i] = new Color(sat, sat, sat);
      }
      return;
    }
    int bgr = background.getRed();
    int bgg = background.getGreen();
    int bgb = background.getBlue();
    for(int i = 0; i<numColors; i++) { // start with the background and increase toward BLACK
      int tr = bgr-bgr*i/numColors;    // decrease the red
      int tg = bgg-bgg*i/numColors;    // decrease the green
      int tb = bgb-bgb*i/numColors;    // decrease the blue
      colors[i] = new Color(tr, tg, tb);
    }
  }

  private void createSpectrumPalette() {
    compColors = colors = new Color[numColors]; // use same colors for complement
    int n1 = numColors/3;
    n1 = Math.max(1, n1);
    int bgr = background.getRed();
    int bgg = background.getGreen();
    int bgb = background.getBlue();
    for(int i = 0; i<n1; i++) { // start with the background and increase toward all blue
      int tr = bgr-bgr*i/n1;
      int tg = bgg-bgg*i/n1;
      int tb = Math.min(255,bgg+(255-bgb)*i/n1);
      colors[i] = new Color(tr, tg, tb);
    }
    for(int i = n1; i<numColors; i++) { // decrease blue and increase green and then red
      double sigma = n1/1.2;
      double arg1 = (i-n1)/sigma;
      double arg2 = (i-2*n1)/sigma;
      double arg3 = (i-numColors)/sigma;
      int b = (int) (255*Math.exp(-arg1*arg1));
      int g = (int) (255*Math.exp(-arg2*arg2));
      int r = (int) (255*Math.exp(-arg3*arg3));
      r = Math.min(255, r);
      b = Math.min(255, b);
      g = Math.min(255, g);
      colors[i] = new Color(r, g, b);
    }
  }

  public JFrame showLegend() {
    double floor = 0;
    double ceil = this.ceil*2;
    legendPanel = new InteractivePanel();
    legendPanel.setPreferredSize(new java.awt.Dimension(300, 120));
    legendPanel.setPreferredGutters(0, 0, 0, 35);
    legendPanel.setClipAtGutter(false);
    legendPanel.setSquareAspect(false);
    if((legendFrame==null)||!legendFrame.isDisplayable()) {
      legendFrame = new JFrame(DisplayRes.getString("GUIUtils.Legend")); //$NON-NLS-1$
    }
    legendFrame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
    legendFrame.setResizable(true);
    legendFrame.setContentPane(legendPanel);
    int numVecs = 30;
    GridPointData pointdata = new GridPointData(numVecs, 2, 3);
    double[][][] data = pointdata.getData();
    double delta = 1.5*ceil/numVecs;
    double cval = floor-delta/2;
    for(int i = 0, n = data.length; i<n; i++) {
      data[i][1][2] = cval;
      data[i][1][3] = 0;
      data[i][1][4] = 4;
      cval += delta;
    }
    pointdata.setScale(0, 1.5*ceil+delta, 0, 1);
    legendPlot = new VectorPlot(pointdata);
    legendPlot.setPaletteType(paletteType);
    legendPlot.setAutoscaleZ(false, 0.5*ceil, ceil);
    legendPlot.update();
    legendPanel.addDrawable(legendPlot);
    XAxis xaxis = new XAxis(""); //$NON-NLS-1$
    xaxis.setLocationType(XYAxis.DRAW_AT_LOCATION);
    xaxis.setLocation(-0.0);
    xaxis.setEnabled(true);
    legendPanel.addDrawable(xaxis);
    legendFrame.pack();
    legendFrame.setVisible(true);
    return legendFrame;
  }

  public JFrame getLegendFrame() {
    return legendFrame;
  }

  public void updateLegend() {
    if(legendPlot==null) {
      return;
    }
    legendPlot.setPaletteType(paletteType);
    legendPlot.setAutoscaleZ(false, 0.5*ceil, ceil);
    legendPlot.update();
    legendPanel.repaint();
  }

}

/*
 * Open Source Physics software is free software; you can redistribute
 * it and/or modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License,
 * or(at your option) any later version.

 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be released
 * under the GNU GPL license.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2019  The Open Source Physics project
 *                     https://www.compadre.org/osp
 */