package nxm.sys.libg;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.RescaleOp;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MidasColor;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.Util;

/**
  A class to handle colormaps.

  @author Jeff Schoen
*/
public class MColor implements MidasColor {


  int ncolor=0;
  Color tcolor, tcolors[];

  /** Constructor */
  public MColor () {
    super();
  }
  /** Constructor
    @param name  JGS not used
    @param color Java color object
  */
  public MColor (String name, Color color) {
    super();
    ncolor = 0;
    tcolor = color;
  }
  /** Constructor
    @param name  JGS not used
    @param colors Array of Java color objects
  */
  public MColor (String name, Color[] colors) {
    super();
    ncolor = colors.length;
    tcolors = colors;
  }

  /** Get a single color.  This is a convenience wrapper for the Java constructor
      which has component ranges from 0 to 1.0 instead of 0-100.
    @param r Red component: 0-100
    @param g Green component: 0-100
    @param b Blue component: 0-100
    @return A Java color object
  */
  public static Color getColor (int r, int g, int b) {
    return MidasColor.getColor(r, g, b);
  }

  /** Get a single color by name or number string. This calls {@link #getColor(Object)} and then
      applies the given alpha component to the color.
      @param obj   The name/value of the color.
      @param alpha The alpha component to use (0-255) where 255 = Completely Opaque, 0 = Completely
                   Transparent.
      @return a Java Color object
      @since NeXtMidas 2.3.1
  */
  public static Color getColor (Object obj, int alpha) {
    Color color = getColor(obj);
    if ((color != null) && (color.getAlpha() != alpha)) {
      color = new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha);
    }
    return color;
  }

  /** Get a single color by name or number string. This checks to see if the object
      passed in is an instance of {@link Color}, if so it just returns that object,
      otherwise it converts the object to a string and calls {@link #getLegacyColor(String)}.
      @param obj The name/value of the color.
      @return a Java Color object (this never returns null, if no match it returns {@link #DEFAULT_COLOR}.)
      @since NeXtMidas 2.3.1
  */
  public static Color getColor (Object obj) {
    return MidasColor.getColor(obj);
  }

  /** Get a color defined in a String as given by Color.toString. (e.g java.awt.Color[r=0,g=255,b=255])
      @since neXtMidas 2.9.2
   */
  private static Color getColorFromJavaString (String str) {
    if ((str.indexOf("r=")<0) || (str.indexOf("g=")<0) || (str.indexOf("b=")<0)) return null;
    int red   = getIntfromColorString('r',str);
    int green = getIntfromColorString('g',str);
    int blue  = getIntfromColorString('b',str);
    if (str.indexOf("a=")>=0) {   // has an alpha value
      int alpha = getIntfromColorString('a',str);
      return new Color(red,green,blue,alpha);
    }
    return new Color(red,green,blue);
  }

  private static int getIntfromColorString (char color, String str) {
    String tmp = str;
    int startIndex = tmp.indexOf(color+"=");
    String colorString = tmp.substring(startIndex);
    int endIndex = colorString.indexOf(",")<0 ? colorString.indexOf("]") : colorString.indexOf(",");
    int colorInt = Integer.parseInt(colorString.substring(2,endIndex));
    return colorInt;
  }

  /** Get a single color from a indexed list
    JGS--are these standard colors?
    @param index the index number in the color list
    @return a Java Color object
  */
  public static Color getColorByIndex (int index) {
    return MidasColor.getColorByIndex(index);
  }

  /** Get a colormap by typeString or create one via an array of color names with the specified
      alpha component for transparency. See {@link #getColorMap(String, int)} for additional info.
    @param colormapType Color name in the color map list
    @param numColors Number of colors
    @param alpha transparency value for colors in the colormap (0=transparent, 255=opaque)
    @return an array of Color objects
    @since NeXtMidas 2.5.0
  */
  public static Color[] getColorMap (String colormapType, int numColors, int alpha) {
    Color colormap[] = getColorMap(colormapType, numColors);
    for (int i=0, len=colormap.length; i<len; i++) {
      Color color = MColor.getColor(colormap[i], alpha);
      colormap[i] = color;
    }
    return colormap;
  }

  /** Get a colormap by typeString or create one via an array of color names
      @param s Color name in the color map list
      @param ncolors Number of colors
      @return an array of Color objects
  */
  public static Color[] getColorMap (String s, int ncolors) {
    //if s is a defined color map in colorMapList, find it
    if (s.indexOf(',') < 0) {
      int type = Parser.find(colorMapList,s,0);
      if (type>0) return getColorMap(type, ncolors);
    }
    // if not a defined color map, assume an array of colors needs to be parsed
    if (s.charAt(0)=='"'||s.charAt(0)=='(') s = s.substring(1,s.length()-1);
    Parser p = new Parser(s,true);  // added TRUE in 3.1.0 (Bug 2456) to trim white spaces
    if (ncolors<0) ncolors = p.elements();
    Color[] colors = getColorMap(-1, ncolors); // get an ~empty color array
    for (int i=1; i<=ncolors; i++) {
      String cs = p.get(i);
      if (cs.length()>0) colors[i] = getColor(cs);
      else colors[i] = Color.black;
    }
    return colors;
  }

  /** Get a colormap by typeIndex
    @param type Color map number in {@link #colorMapList}
    @param ncolors Number of colors
    @return a Java Color object
  */
  public static Color[] getColorMap (int type, int ncolors) {
    Color[] colors;
    switch (type) {
    case GRAYSCALE:
      if (ncolors<0) ncolors=DEFCOLORS;
      colors = new Color[ncolors+1];
      interpColors (colors, CM_GRAY);
      break;
    case RAMP:
      if (ncolors<0) ncolors=DEFCOLORS;
      colors = new Color[ncolors+1];
      interpColors (colors, CM_RAMP);
      break;
    case XRAMP:
      if (ncolors<0) ncolors=DEFCOLORS;
      colors = new Color[ncolors+1];
      interpColors (colors, CM_XRAMP);
      break;
    case WHEEL:
      if (ncolors<0) ncolors=DEFCOLORS;
      colors = new Color[ncolors+1];
      interpColors (colors, CM_WHEEL);
      break;
    case TERRAIN:
      if (ncolors<0) ncolors=DEFCOLORS;
      colors = new Color[ncolors+1];
      interpColors (colors, CM_TERRAIN);
      break;
    case MULTI:
      if (ncolors<0) ncolors=DEFCOLORS;
      colors = new Color[ncolors+1];
      interpColors (colors, CM_MULTI);
      break;
    case SPECTRUM:
      if (ncolors<0) ncolors=DEFCOLORS;
      colors = new Color[ncolors+1];
      interpColors (colors, CM_SPECTRUM);
      break;
    case XDATALIST:
      if (ncolors<0) ncolors=16; /* JC4: Changed from 17 to 16 to match XDATALIST (28NOV06).*/
      colors = new Color[ncolors+1];
      interpColors (colors, CM_SPECTRUM);
      break;
    case T5:
      if (ncolors<5) ncolors=5;  // since 2.5.4: prevent ArrayOutOfBoundException when ncolors<5
      colors = new Color[ncolors+1];
      colors[T5_INDEX_CEP]     = T5_COLOR1_CEP; //Since 3.5.3 - use Color constants
      colors[T5_INDEX_HORIZON] = T5_COLOR2_HORIZON;
      colors[T5_INDEX_TDOA]    = T5_COLOR3_TDOA;
      colors[T5_INDEX_FDOA]    = T5_COLOR4_FDOA;
//Pink colors[T5_INDEX_TRACE5000]   = getColor(80,00,80);
      colors[T5_INDEX_TRACE5000]   = T5_COLOR5_TRACE5000;
      break;
    case MAPLINE:
      if (ncolors<8) ncolors=8; // since 2.5.4: prevent ArrayOutOfBoundException when ncolors<8
      colors = new Color[ncolors+1];
      colors[C_COASTLINE] = getColor(30,40,55);
      colors[C_POLITICAL] = getColor(45,45,45);
      colors[C_INTERPOL]  = getColor(20,30,30);
      colors[C_RIVER]     = getColor(00,20,50);
      colors[C_RAIL]      = getColor(25,15,15);
      colors[C_ROAD]      = getColor(40,40,00);
      colors[C_POL_OLD] = colors[C_POLITICAL];
      colors[C_INTPOL_OLD]  = colors[C_INTERPOL];
      break;
    case GRID:
      if (ncolors<2) ncolors=2; // since 2.5.4: prevent ArrayOutOfBoundException when ncolors<2
      colors = new Color[ncolors+1];
      colors[C_AXIS] = getColor(60,60,60);
      colors[C_GRID] = getColor(25,25,25);
      break;
    default:
      if (ncolors<0) ncolors=1;
      colors = new Color[ncolors+1];
      break;
    }
    colors[0] = Color.black;
    return colors;
  }

  /** map legend list */
  public static String mapLegendList = "CoastLine,Political,InterPol,River,Rail,Road";

  /** Returns a NeXtMidas Table of the current set of colors
      @return NeXtMidas Table of the current set of colors
   */
  public static Table getMapColorTable() {
    Color[] colors = getColorMap(MAPLINE,-1);
    Table ct = new Table();
    Parser p = new Parser(mapLegendList);
    for (int i=1; i<=p.elements(); i++) {
      String name = p.get(i).toUpperCase();
//      ct.put(name,colors[i]);
      ct.put(name,MColor.toString(colors[i]));
    }
    return ct;
  }

  /** Returns a set of colors given a NeXtMidas Table of colors
      @param ctab NeXtMidas Table of colors
      @return set of colors as an Array
   */
  public static Color[] getColorMap (Table ctab) {
    Table.Iterator ti = ctab.iterator();
    Color[] colors = new Color[ti.size+1];
    colors[0] = Color.black;
    for (int i=0; i<ti.size; i++) colors[i+1] = getColor(ti.values[i].toString());
    return colors;
  }

  /** Takes a given colormap (array of colors) and simply reverse the order of the colors,
      excluding position 0 (since it is reserved for the background in colormap).
      @param cmap ColorMap to be modified. Null returns null
      @return cmap (but also modifies the input array directly)
      @since NeXtMidas 2.8.2
   */
  public static Color[] reverseColorMap (Color[] cmap) {
    return Util.reverseArray(cmap, 1);
  }

  /** Takes a given colormap (array of colors) and inverts the colors, excluding position 0,
      (since it is reserved for the background in colormap). Inverting creates a photo negative
      effect. Doing this twice on a color map will return it back to normal.
      @param cmap ColorMap to be modified. null returns null.
      @return colors (but also modifies the input array directly)
      @since NeXtMidas 2.9.0
   */
  public static Color[] invertColorsMap (Color[] cmap) {
    return invertColors(cmap, 1);
  }

  /** Takes a given colormap (array of colors) and inverts the colors, excluding position 0.
      Inverting creates a photo negative effect. Doing this twice on a color map will
      return it back to normal.
      @param colors ColorMap to be modified. Null returns null
      @param startIndex index (inclusive) into colors to begin reversing the colors.
      @return colors (but also modifies the input array directly)
      @since NeXtMidas 2.9.0
   */
  public static Color[] invertColors (Color[] colors, int startIndex) {
    if (colors==null) return null;
    int len = colors.length;
    for (int ii = startIndex; ii < len; ii++) {
      colors[ii] = invert(colors[ii]);
    }
    return colors;
  }

  /** Inverts the given Color. The alpha (transparency) component will remain the same.
      Inverting creates a photo negative effect. Doing this twice on a Color will
      return it back to normal.
      @param inColor Color to be inverted. null returns null.
      @return new Color representing the inverted color of the input color
      @since NeXtMidas 2.9.0
   */
  public static Color invert (Color inColor) {
    return MidasColor.invert(inColor);
  }

  /** Interpolate colors from X-Midas color map definitions.
    @param colors The pre-allocated color array (sized to match ncolors+1).
    @param table  The interpolation table (usually copied from X-Midas).
  */
  public static void interpColors (Color[] colors, byte[] table) {
    int i,j,ncolors = colors.length-1;
    float p,pp,pq,r,g,b;
    for (i=1; i<=ncolors; i++) {
      if (ncolors==1) { p = 0.0f; } // BUG 1344: prevent p from being NaN yet still use appropriate interpolation
      else            { p = 100*((float)(i-1))/(float)(ncolors-1); }

      for (j=4; j<table.length && table[j]<(int)p; j+=4);
      pp = (p-(float)table[j-4])/(float)(table[j]-table[j-4]);
      pp = Math.max(0.0F,Math.min(1.0F,pp)); pq = 1-pp;
      r = Math.min(1.0F,(pp*(float)table[j+1] + pq*(float)table[j+1-4])/100);
      g = Math.min(1.0F,(pp*(float)table[j+2] + pq*(float)table[j+2-4])/100);
      b = Math.min(1.0F,(pp*(float)table[j+3] + pq*(float)table[j+3-4])/100);
      colors[i]=new Color(r,g,b);
    }
  }

  /** Get a Java color model. This is used by {@link MImage} when rendering the raster image for
      {@link Layer2D}. Beginning with NeXtMidas 2.5.4, the alpha (transparency) component of the
      color is used in the returned ColorModel.
      @param colors The colors in the color map.
      @return An AWT ColorModel.
  */
  public static ColorModel getColorModel (Color[] colors) {
    int size=colors.length;
    ColorModel cm;
    int bits = 8;  // we always use a byte array for the map
    byte[] r = new byte[size];
    byte[] g = new byte[size];
    byte[] b = new byte[size];
    byte[] a = new byte[size];
    for (int i=0; i<size; i++) {
      r[i] = (byte)colors[i].getRed();
      g[i] = (byte)colors[i].getGreen();
      b[i] = (byte)colors[i].getBlue();
      a[i] = (byte)colors[i].getAlpha();
    }
    cm = new IndexColorModel(bits,size,r,g,b,a);
    return cm;
  }

  /** Get a mixed color. This is frequently used when shading.
    @param c0  The first Java Color object to be mixed.
    @param c1  The second Java Color object to be mixed.
    @param mix The percentage of c0 to use (<code>1.00-mix</code> is the percentage used for c1).
               The acceptable range of mix is <code>0.00 - 1.00</code>.  If the value is outside this
               range it is clipped.
    @return A color between c0 and c1.
  */
  public static Color getColorMix (Color c0, Color c1, double mix) {
    return MidasColor.getColorMix(c0, c1, mix);
  }

//  /** Gets a color based on it RGB color code. It is assumed that the color is opaque unless its
//      value is larger than <code>0xFFFFFF</code> or specified using the "#aarrggbb" syntax.
//    @param color The integer string for the color.
//    @return The color matching the given string or <code>null</code> if the string is invalid.
//  */
//  private static Color getIntColor (String color) {
//    Color colorObj = null;
//    if (color.startsWith("#") || color.toUpperCase().startsWith("0X")) {
//      if (color.startsWith("#")) color = "0x"+color.substring(1);
//      colorObj = new Color(Convert.o2l(color), (color.length() > 8));
//    } else if (StringUtil.isNumber(color)) {
//      int val = Convert.o2l(color);
//      //colorObj = new Color(val, (val > 0xFFFFFF));  // Old way
//      colorObj = new Color(val, (val & 0xFF000000) != 0 );
//    }
//    return colorObj;
//  }

  /** Converts a color to it's string form.
      If the color matches a named color that name will be returned.
      otherwise the Hex form of the color will be returned. If the color is
      OPAQUE, the hex form will be six digits, otherwise it will be eight.
      <br>
      Examples (color arg shown in hex):
      <pre>
        toString(ff888888)   --&gt; "GREY"
        toString(ff123456)   --&gt; "#123456"     (OPAQUE      - no alpha)
        toString(7f123456)   --&gt; "#7f123456"   (Semi-OPAQUE - has alpha)
      </pre>
      <br>

    @param color The color.
    @return The string form of the color (or <code>"null"</code> if the color is null).
    NOTE: This is a fuller implementation than MidasColor and this one should
      be used by graphical classes
  */
  public static String toString (Color color) {
    if (color == null) { return "null"; }

    String str = NamedColors.getColorName(color);  // Check if named color

    /** If not named get the RGB value and strip out the Alpha (if OPAQUE) */
    if (str == null) {
      int    rgb = color.getRGB();
      String hex = Integer.toHexString(rgb);
      hex = StringUtil.padLeft(hex, 8, '0');
      if ((rgb & 0xFF000000) == 0xFF000000) {
        hex = hex.substring(2); // Strip off the Alpha
      }
      str = "#" + hex;
    }
    return str;
  }

  /** Converts a color map to it's string form. If the color matches a named color that
      name will be returned, otherwise the Hex form of the color will be returned.
      @param colors The colors.
      @return The string form of the color map.
  */
  public static String toString (Color[] colors) {
    StringBuilder str = new StringBuilder();
    for (int i = 0; i < colors.length; i++) {
      if (i != 0) { str.append(','); }
      str.append(toString(colors[i]));
    }

    return str.toString();
  }

  /** Adjusts the image for brightness, contrast, alpha, and greyscale.
      @param img        The image to adjust
      @param xMin       The lower x value for drawing the image
      @param yMin       The lower y value for drawing the image
      @param xMax       The upper x value for drawing the image
      @param yMax       The upper y value for drawing the image
      @param contrast   The contrast.   Range is 0..100 (50 means no change).
      @param brightness The brightness. Range is 0..100 (50 means no change).
      @param alpha      The 8-bit alpha value to change. Range is 0x00 to 0xFF (-1 means no change).
      @param greyscale  Convert to greyscale (false means no change).
      @return The adjusted image (this may be a *new* image or may just be a reference to the one
              passed in if no changes were required.
      @since NeXtMidas 2.7.0
      @see #adjustColor(int,int,int,int,boolean)
   */
  public static BufferedImage adjustImage (BufferedImage img, int xMin, int yMin, int xMax, int yMax,
                                           int contrast, int brightness, int alpha, boolean greyscale) {
    if ((contrast == 50) && (brightness == 50) && (alpha == -1) && !greyscale) {
      return img; // No changes needed.
    }
    // JC4: I tried several of the image conversion tools in AWT, but all were sloooooooooooooow
    //      so I came up with this algorithm
    int height = yMax - yMin;
    int width  = xMax - xMin;
    BufferedImage out = new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB);

    int[] buf = img.getRGB(xMin, yMin, width, height, new int[width*height], 0, width);
    float cnt = (contrast   / 50f); // Multiplier for contrast
    float brt = (brightness / 50f); // Multiplier for brightness
    int   alp = (alpha == -1)? -1 : Math.min(0xFF, Math.max(0,alpha));
    for (int i = 0; i < buf.length; i++) {
      buf[i] = adjustColor0(buf[i], cnt, brt, alp, greyscale);
    }
    out.setRGB(xMin, yMin, width, height, buf, 0, width);
    return out;
  }

  /** Adjusts the image for brightness, contrast, alpha, and greyscale.
      brightness only adjustments uses RescaleOp to improve performance
      @param img        The image to adjust
      @param xMin       The lower x value for drawing the image
      @param yMin       The lower y value for drawing the image
      @param xMax       The upper x value for drawing the image
      @param yMax       The upper y value for drawing the image
      @param contrast   The contrast.   Range is 0..100 (50 means no change).
      @param brightness The brightness. Range is 0..100 (50 means no change).
      @param alpha      The 8-bit alpha value to change. Range is 0x00 to 0xFF (-1 means no change).
      @param greyscale  Convert to greyscale (false means no change).
      @return The adjusted image (this may be a *new* image or may just be a reference to the one
              passed in if no changes were required.
      @since NeXtMidas 2.7.0
      @see #adjustColor(int,int,int,int,boolean)
*/
  static BufferedImage adjustImageFast (BufferedImage img, int xMin, int yMin, int xMax, int yMax,
      int contrast, int brightness, int alpha, boolean greyscale) {
    boolean contrastUpdate = contrast != 50;
    boolean brightnessUpdate = brightness != 50;
    boolean alphaUpdate = alpha != -1;

    if (!(contrastUpdate || brightnessUpdate || alphaUpdate || greyscale)) {
    return img; // No changes needed.
    }

    // JC4: I tried several of the image conversion tools in AWT, but all were sloooooooooooooow
    //      so I came up with this algorithm
    int height = yMax - yMin;
    int width  = xMax - xMin;
    BufferedImage out = null; //new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB);

    float brt = (brightness / 50f); // Multiplier for brightness
    if ((Shell.getJavaMajorVersion() >= 9) && brightnessUpdate && !(contrastUpdate || alphaUpdate || greyscale)) {
      // make faster adjustment if only brightness being adjusted NXM-3747
      // Note: RescaleOp code has JDK 8 bugs, so only use this if running JDK 9+
      RescaleOp op = new RescaleOp(brt,0,null);
      out = op.filter(img, null);
    }
    else {
      out = new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB);
      int[] buf = img.getRGB(xMin, yMin, width, height, new int[width*height], 0, width);
      float cnt = (contrast   / 50f); // Multiplier for contrast
      int   alp = (alpha == -1)? -1 : Math.min(0xFF, Math.max(0,alpha));
      for (int i = 0; i < buf.length; i++) {
        buf[i] = adjustColor0(buf[i], cnt, brt, alp, greyscale);
      }
      out.setRGB(xMin, yMin, width, height, buf, 0, width);
    }
    return out;
  }

  /** Adjust the brightness, contrast, and alpha of a given aRGB value.
      <pre>
      Note 1: When repeatedly adjusting contrast and brightness without refreshing the original RGB values,
              changing the brightness or contrast of a R,G, or B component above the max (255)
              or below the min (0) will effectively "lose" the color, since that component
              will stop incrementing but the other components will not stop until they
              themselves hit the max/min. See LayerShape for sample implementation.
      Note 2: Adjusting the alpha value from either 0 (none) or 255 (solid) results in a
              significant performance impact due to the graphics blending required.
      </pre>
      @param rgb        The aRGB value.
      @param contrast   The contrast.   Range is 0..100 (50 means no change).
      @param brightness The brightness. Range is 0..100 (50 means no change).
      @param alpha      The 8-bit alpha value to change. Range is 0x00 to 0xFF (-1 means no change).
      @param greyscale  Convert to greyscale (false means no change).
      @return The 32 bit aRGB value
      @since NeXtMidas 2.7.0
   */
  public static int adjustColor(int rgb, int contrast, int brightness, int alpha, boolean greyscale) {
    float cnt = (contrast   / 50f); // Multiplier for contrast
    float brt = (brightness / 50f); // Multiplier for brightness
    int   alp = (alpha == -1)? -1 : Math.min(0xFF, Math.max(0,alpha));
    if ((contrast == 50) && (brightness == 50) && (alpha == -1) && !greyscale) return rgb; // No conversion required

    return adjustColor0(rgb, cnt, brt, alp, greyscale);
  }

  /** Internal version of adjustColor but without some of the checks and converts that can come
      outside of the loop in adjustImage(..).
   */
  private static int adjustColor0 (int rgb, float cnt, float brt, int alp, boolean greyscale) {
    int alpha = (rgb >> 24) & 0xFF;
    int red   = (rgb >> 16) & 0xff;
    int green = (rgb >>  8) & 0xff;
    int blue  = (rgb >>  0) & 0xff;
    int aa    = (alp != -1)? alp : alpha; // alpha (adjusted)
    int rr;                               // red   (adjusted)
    int gg;                               // green (adjusted)
    int bb;                               // blue  (adjusted)

    if (greyscale) {
      int grey = (red + green + blue) / 3;
      int gs = (int)((((grey * brt) - 128) * cnt) + 128); // grey scale

      rr = gg = bb = Math.min(0xFF, Math.max(0,gs));
    }
    else {
      // jwb - 18 Jun 08 - changed algorithm to apply brightness changes first per advice online
      int r = (int)((((red   * brt) - 128) * cnt) + 128); // red
      int g = (int)((((green * brt) - 128) * cnt) + 128); // green
      int b = (int)((((blue  * brt) - 128) * cnt) + 128); // blue

      rr = Math.min(0xFF, Math.max(0,r));
      gg = Math.min(0xFF, Math.max(0,g));
      bb = Math.min(0xFF, Math.max(0,b));
    }
    return (aa << 24) | (rr << 16) | (gg << 8) | (bb << 0);
  }

  /** Apply the specified transparency (alpha) component to every element in the image buffer array
      containing ARGB color values.
      @param rgbBuf Image array of ARGB color (32 bit) color values to have it's alpha value changed
      @param alpha  The 8-bit alpha value to change for all colors in the input imageBuf array (this
                  takes in an int rather than a byte to avoid issues with alpha being unsigned).
      @since NeXtMidas 2.7.0
   */
  public static void applyAlpha (int[] rgbBuf, int alpha) {
    if (rgbBuf==null || rgbBuf.length==0) return;
    int a = (alpha & 0xFF) << 24;
    for (int i=0, len=rgbBuf.length; i<len; i++) {
      rgbBuf[i] = a | (rgbBuf[i] & 0xFFFFFF);
    }
  }

  /** Specific to opacity, converts a percentage (0-100) to an alpha
      component (0-255).
      @param int percentage value between (0-100)
      @return int alpha value between (0-255)
      @since NeXtMidas 3.3.4
   */
  protected static int pct2alpha(int percent)
  {
    float pct = percent / 100.0f;
    int   alpha = (int)(255*pct);
    alpha       = Math.min(alpha, 255);
    alpha       = Math.max(alpha, 0);
    return alpha;
  }

  /** Specific to opacity, converts an alpha component value (0-255) to a
      percentage (0-100).
      @param int alpha value between (0-255)
      @return int percentage value between (0-100)
      @since NeXtMidas 3.3.4
   */
  protected static int alpha2pct(int alpha)
  {
    int pct = (int)(alpha / 2.55);
    pct     = Math.min(pct, 100);
    pct     = Math.max(pct, 0);
    return pct;
  }

  /** Blend two colors.<br>
      Note: To preserve transparency, blending is NOT done on the Alpha component
      @param color1  First color to blend.
      @param color2  Second color to blend.
      @param ratio   Blend ratio. 0.5 will give even blend, 1.0 will return
                     color1, 0.0 will return color2 and so on.
      @return        Blended color.
      @since         NeXtMidas 2.7.1
   */
  public static Color blend (Color color1, Color color2, double ratio) {
    return MidasColor.blend(color1, color2, ratio);
  }

  /** Simple formula for determining a difference between colors.
      Primarily used to determine how close colors are visually. This is based on the CIE76
      formula since it easy to implement and sufficient to represent the color difference.
      @see <a href="http://en.wikipedia.org/wiki/Color_difference">Color Difference</a>
      @param color1 First color to compare (cannot be null).
      @param color2 Second color to compare (cannot be null).
      @return double value (normalized) between 0.0 (comparing same color) and 1.0 (i.e. comparing black with white)
      @since  3.5.0
   */
  public static double colorDifference (Color color1, Color color2) {
    return MidasColor.colorDifference(color1, color2);
  }

  /** Make an even blend between two colors. <br>
      Note: To preserve transparency, blending is NOT done on the Alpha component.
      @param color1     First color to blend.
      @param color2     Second color to blend.
      @return       Blended color.
      @since        NeXtMidas 2.7.1
   */
  public static Color blend (Color color1, Color color2) {
    return MidasColor.blend (color1, color2, 0.5);
  }

  /** Modifies a given image by adding colors
      @param img   image to be modified
      @param color color to be applied to the image
      @param ratio blend ratio
      @return newly modified image
   */
  public static BufferedImage colorImage (BufferedImage img, Color color, float ratio) {
    if (color != null && img != null) {
      int h = img.getHeight();
      int w = img.getWidth();
      int[] rgb = new int[h * w];
      img.getRGB(0, 0, w, h, rgb, 0, w);
      for (int xx=0; xx< rgb.length; xx++) {
        rgb[xx] = blend(new Color(rgb[xx], true), color, ratio).getRGB();
      }
      img.setRGB(0,0,w,h,rgb,0,w);
    }
    return img;
  }

  /** This is not for public use outside the NeXtMidas framework.
     This method captures 'special'  color numbers that can be in 1998, 1999 or 5000 files.
     X-Midas XGEOPLOT keys off these special numbers to adds 5 before calling mx$draw_lines  A DR
     (g765978-1) was written in Jan 2008 to document all special XGEOPLOT color numbers. This does
     not yet do all the special handling documented in this DR to include the COLORMAP=RAW
     COLORMAP=USER and foreground and background colors where I have chosen white or black.

     See docs in XM 4.5.5. or later.
     @param colNum color number
     @return the corresponding Color
  */
  @InternalUseOnly
  public static java.awt.Color getSpecialXMidasColor (int colNum) {
    return MidasColor.getSpecialXMidasColor(colNum);
  }

  /** Convert a KML-style formatted color (aabbggrr) to standard (aarrggbb)
      @param bgr The color  to be converted
      @return The re-arranged color
      @since NeXtMidas 3.3.2
   */
  public static int BGRtoRGB (int bgr) {
    return RGBtoBGR(bgr);  // does the same thing
  }

  /** Convert a KML-style formatted color (aabbggrr) to standard (aarrggbb)
      @param bgrColor The color  to be converted
      @return The re-arranged color
      @since NeXtMidas 3.3.2
   */
  public static Color BGRtoRGB (Color bgrColor) {
    return RGBtoBGR(bgrColor);  // does the same thing
  }

  /** Convert a standard color (aarrggbb) to KML-style formatted (aabbggrr) and vice versa
      @param rgbColor The color to be converted
      @return The re-arranged color
      @since NeXtMidas 3.3.2
   */
  public static int RGBtoBGR (int rgbColor) {
    int aa = (rgbColor >> 24) & 0xFF;    // alpha
    int bb = (rgbColor >> 16) & 0xFF;    // blue / red
    int gg = (rgbColor >>  8) & 0xFF;    // green
    int rr = (rgbColor >>  0) & 0xFF;    // red / blue

    return (aa << 24) | (rr << 16) | (gg << 8) | (bb << 0);
  }

  /** Convert a standard color (aarrggbb) to KML-style formatted (aabbggrr) and vice versa
      @param rgbColor The color  to be converted
      @return The re-arranged color
      @since NeXtMidas 3.3.2
   */
  public static Color RGBtoBGR (Color rgbColor) {
    int bgr = rgbColor.getRGB();
    int argb = RGBtoBGR(bgr);
    boolean hasAlpha = ((bgr & 0xFF000000) != 0);

    return new Color(argb, hasAlpha);
  }

  /** This InternalUseOnly method was moved to MidasColor for JDK 11 modularity
   *  @param s color string
   *  @return the Color
   */
  @Deprecated
  public static Color getLegacyColor (String s) {
    return MidasColor.getLegacyColor(s);
  }
}
