/*
 * jNPad v0.3 - jNPad's an Simple Text Editor written in Java
 *
 * Copyright (C) 2014-2017  rgs
 *
 * Require JDK 1.6 (or later)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program 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 program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 *
 * Info, Questions, Suggestions & Bugs Report to rgsevero@gmail.com
 */

package jnpad.ui;

import java.awt.Color;

/**
 * 
 * @version 0.3
 * @since   jNPad v0.1
 */
public final class ColorUtilities {
  /** no instances */
  private ColorUtilities() {
    super();
  }

  /**
   *
   * @param c Color
   * @return String
   */
  public static String getColorHexString(Color c) {
    String colString = Integer.toHexString(c.getRGB() & 0xffffff);
    return "#000000".substring(0, 7 - colString.length()).concat(colString); //$NON-NLS-1$
  }

  /**
   * Creates an array of color values. The colors created will be a gradient from color c1 to color c1 with a count
   * of steps values.
   *
   * @param c1 the starting color
   * @param c2 the ending color
   * @param steps the number of steps between c1 and c2 (the size of the created array)
   *
   * @return the array of color values
   */
  public static Color[] createColorArr(Color c1, Color c2, int steps) {
    if (c1 == null || c2 == null) {
      return null;
    }

    Color colors[] = new Color[steps];
    double r = c1.getRed();
    double g = c1.getGreen();
    double b = c1.getBlue();
    double dr = (c2.getRed() - r) / steps;
    double dg = (c2.getGreen() - g) / steps;
    double db = (c2.getBlue() - b) / steps;
    colors[0] = c1;
    for (int i = 1; i < steps - 1; i++) {
      r += dr;
      g += dg;
      b += db;
      colors[i] = new Color((int) r, (int) g, (int) b);
    }
    colors[steps - 1] = c2;
    return colors;
  }

  /**
   * Creates a color that is the brighter version of the color parameter c.
   *
   * @param c the color
   * @param p the factor of the brightness in percent from 0 to 100
   * @return a new color value that is a brighter version of the color parameter c
   */
  public static Color brighter(Color c, double p) {
    if (c == null) {
      return null;
    }

    double r = c.getRed();
    double g = c.getGreen();
    double b = c.getBlue();

    double rd = 255.0 - r;
    double gd = 255.0 - g;
    double bd = 255.0 - b;

    r += (rd * p) / 100.0;
    g += (gd * p) / 100.0;
    b += (bd * p) / 100.0;
    return new Color( (int) r, (int) g, (int) b);
  }

  /**
   * Creates a color that is the darker version of the color parameter c.
   *
   * @param c the color
   * @param p the factor to shade the color c in percent from 0 to 100
   *
   * @return a new color value that is a darker version of the color parameter c
   */
  public static Color darker(Color c, double p) {
    if (c == null) {
      return null;
    }

    double r = c.getRed();
    double g = c.getGreen();
    double b = c.getBlue();

    r -= (r * p) / 100.0;
    g -= (g * p) / 100.0;
    b -= (b * p) / 100.0;

    return new Color( (int) r, (int) g, (int) b);
  }

  /**
   * Returns a color value which is the media between the colors c1 and c1
   *
   * @param c1 the first color
   * @param c2 the second color
   *
   * @return the median color value of the two colors c1 and c1
   */
  public static Color median(Color c1, Color c2) {
    if ( (c1 == null || c2 == null)) {
      return null;
    }

    int r = (c1.getRed() + c2.getRed()) / 2;
    int g = (c1.getGreen() + c2.getGreen()) / 2;
    int b = (c1.getBlue() + c2.getBlue()) / 2;
    return new Color(r, g, b);
  }

  /**
   * Gets a derived color from an existing color. The derived color is either
   * lighter or darker version of the given color with the same hue.
   *
   * NOTA: Si color = NEGRO, no juega bien!.
   *
   * @param color the given color.
   * @param ratio the ratio. 0.5f if the same color. Any ratio greater than 0.5f
   * will make the result color lighter. Smaller than 0.5f will make the color darker.
   * @return the derived color.
   */
  public static Color deriveColor(Color color, float ratio) {
    if (color != null) {
      float[] hsl = RGBtoHSL(color);
      if (hsl[2] < 0.4) {
        hsl[2] = 0.4f;
      }
      if (ratio > 0.5) {
        hsl[2] += (1f - hsl[2]) * 2 * (ratio - 0.5);
      }
      else {
        hsl[2] -= hsl[2] * 2 * (0.5 - ratio);
      }
      int colorRGB = HSLtoRGB(hsl);
      return new Color(colorRGB);
    }
    return Color.GRAY;
  }

  /**
   * Converts a color from RBG to HSL color space.
   *
   * @param colorRGB the Color.
   * @return color space in HSL.
   */
  public static float[] RGBtoHSL(Color colorRGB) {
    float r, g, b, h, s, l; //this function works with floats between 0 and 1
    r = colorRGB.getRed() / 256.0f;
    g = colorRGB.getGreen() / 256.0f;
    b = colorRGB.getBlue() / 256.0f;

    // Then, minColor and maxColor are defined. Min color is the value of the color component with
    // the smallest value, while maxColor is the value of the color component with the largest value.
    // These two variables are needed because the Lightness is defined as (minColor + maxColor) / 2.
    float maxColor = Math.max(r, Math.max(g, b));
    float minColor = Math.min(r, Math.min(g, b));

    // If minColor equals maxColor, we know that R=G=B and thus the color is a shade of gray.
    // This is a trivial case, hue can be set to anything, saturation has to be set to 0 because
    // only then it's a shade of gray, and lightness is set to R=G=B, the shade of the gray.

    //R == G == B, so it's a shade of gray
    if (Math.abs(r - g) < .0000001 && Math.abs(g - b) < .0000001) { // (r == g && g == b) Keep FindBugs happy
      h = 0.0f; //it doesn't matter what value it has
      s = 0.0f;
      l = r; //doesn't matter if you pick r, g, or b
    }

    // If minColor is not equal to maxColor, we have a real color instead of a shade of gray, so more calculations are needed:

    // Lightness (l) is now set to it's definition of (minColor + maxColor)/2.
    // Saturation (s) is then calculated with a different formula depending if light is in the first half of the second half. This is because the HSL model can be represented as a double cone, the first cone has a black tip and corresponds to the first half of lightness values, the second cone has a white tip and contains the second half of lightness values.
    // Hue (h) is calculated with a different formula depending on which of the 3 color components is the dominating one, and then normalized to a number between 0 and 1.
    else {
      l = (minColor + maxColor) / 2;

      if (l < 0.5) s = (maxColor - minColor) / (maxColor + minColor);
      else s = (maxColor - minColor) / (2.0f - maxColor - minColor);

      if (Math.abs(r - maxColor) < .0000001) // (r == maxColor) Keep FindBugs happy
        h = (g - b) / (maxColor - minColor);
      else if (Math.abs(g - maxColor) < .0000001) // (g == maxColor) Keep FindBugs happy
        h = 2.0f + (b - r) / (maxColor - minColor);
      else 
        h = 4.0f + (r - g) / (maxColor - minColor);

      h /= 6; //to bring it to a number between 0 and 1
      if (h < 0) h++;
    }

    // Finally, H, S and L are calculated out of h, s and l as integers between 0 and 255 and "returned"
    // as the result. Returned, because H, S and L were passed by reference to the function.
    float[] hsl = new float[3];
    hsl[0] = h;
    hsl[1] = s;
    hsl[2] = l;
    return hsl;
  }

  /**
   * Converts from HSL color space to RGB color.
   *
   * @param hsl the hsl values.
   * @return the RGB color.
   */
  public static int HSLtoRGB(float[] hsl) {
    float r, g, b, h, s, l; //this function works with floats between 0 and 1
    float temp1, temp2, tempr, tempg, tempb;
    h = hsl[0];
    s = hsl[1];
    l = hsl[2];

    // Then follows a trivial case: if the saturation is 0, the color will be a grayscale color,
    // and the calculation is then very simple: r, g and b are all set to the lightness.

    //If saturation is 0, the color is a shade of gray
    if (s == 0) {
        r = g = b = l;
    }
    // If the saturation is higher than 0, more calculations are needed again.
    // red, green and blue are calculated with the formulas defined in the code.
    // If saturation > 0, more complex calculations are needed
    else {
      //Set the temporary values
      if (l < 0.5) temp2 = l * (1 + s);
      else temp2 = (l + s) - (l * s);
      temp1 = 2 * l - temp2;
      tempr = h + 1.0f / 3.0f;
      if (tempr > 1) tempr--;
      tempg = h;
      tempb = h - 1.0f / 3.0f;
      if (tempb < 0) tempb++;

      //Red
      if (tempr < 1.0 / 6.0) r = temp1 + (temp2 - temp1) * 6.0f * tempr;
      else if (tempr < 0.5) r = temp2;
      else if (tempr < 2.0 / 3.0) r = temp1 + (temp2 - temp1) * ((2.0f / 3.0f) - tempr) * 6.0f;
      else r = temp1;

      //Green
      if (tempg < 1.0 / 6.0) g = temp1 + (temp2 - temp1) * 6.0f * tempg;
      else if (tempg < 0.5) g = temp2;
      else if (tempg < 2.0 / 3.0) g = temp1 + (temp2 - temp1) * ((2.0f / 3.0f) - tempg) * 6.0f;
      else g = temp1;

      //Blue
      if (tempb < 1.0 / 6.0) b = temp1 + (temp2 - temp1) * 6.0f * tempb;
      else if (tempb < 0.5) b = temp2;
      else if (tempb < 2.0 / 3.0) b = temp1 + (temp2 - temp1) * ((2.0f / 3.0f) - tempb) * 6.0f;
      else b = temp1;
    }

    // And finally, the results are returned as integers between 0 and 255.
    int result = 0;
    result += ((int) (r * 255) & 0xFF) << 16;
    result += ((int) (g * 255) & 0xFF) << 8;
    result += ((int) (b * 255) & 0xFF);

    return result;
  }

  /**
   * Converts from a color to gray scale color.
   *
   * @param c a color.
   * @return a color in gray scale.
   */
  public static Color toGrayscale(Color c) {
    int gray = (int) (c.getRed() * 0.3 + c.getGreen() * 0.59 + c.getBlue() * 0.11);
    return new Gray(gray);
  }

  /**
   *
   * @param c Color
   * @return Color
   */
  public static Color giveYellowTouch(Color c) {
    int oldb = c.getBlue();
    return new Color(c.getRed(), c.getGreen(), (oldb > 128 ? oldb * 10 / 11 : oldb * 11 / 10));
  }

  /**
   *
   * @param c Color
   * @return Color
   */
  public static Color createPureColor(Color c) {
    if (c == null) {
      return Color.RED;
    }
    return new Color(c.getRed(), c.getGreen(), c.getBlue());
  }

  /**
   *
   * @param c Color
   * @return Color
   */
  public static Color removeAlpha(Color c) {
    return withAlpha(c, 255);
  }

  /**
   * With alpha.
   *
   * @param c the c
   * @param alpha the alpha
   * @return the color
   * @throws IllegalArgumentException the illegal argument exception
   */
  public static Color withAlpha(Color c, int alpha) throws IllegalArgumentException {
    if (alpha < 0 || alpha > 255) {
      throw new IllegalArgumentException("Valor alpha inválido"); //$NON-NLS-1$
    }
    return c == null ? null : new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha);
  }

  /**
   * Adds the.
   *
   * @param c1 Color
   * @param c2 Color
   * @return Color
   */
  public static Color add(Color c1, Color c2) {
    return c1 == null ? c2 : c2 == null ? c1 :
        new Color(Math.min(255, c1.getRed() + c2.getRed()),
                  Math.min(255, c1.getGreen() + c2.getGreen()),
                  Math.min(255, c1.getBlue() + c2.getBlue()),
                  c1.getAlpha());
  }

  /**
   * Copy.
   *
   * @param c Color
   * @return Color
   */
  public static Color copy(Color c) {
    return c == null ? null : new Color(c.getRed(), c.getGreen(), c.getBlue(), c.getAlpha());
  }

  /**
   * Oposite.
   *
   * @param c Color
   * @return Color
   */
  public static Color oposite(Color c) {
    return new Color(255 - c.getRed(), 255 - c.getGreen(), 255 - c.getBlue(), c.getAlpha());
  }

  /**
   * Gets the color_1_2.
   *
   * @param c1 Color
   * @param c2 Color
   * @return Color
   */
  public static Color getColor_1_2(Color c1, Color c2) {
    return new Color(propInt(c1.getRed(), c2.getRed(), 2),
                     propInt(c1.getGreen(), c2.getGreen(), 2),
                     propInt(c1.getBlue(), c2.getBlue(), 2));
  }

  /**
   *
   * @param c1 Color
   * @param c2 Color
   * @return Color
   */
  public static Color getColor_1_3(Color c1, Color c2) {
    return new Color(propInt(c1.getRed(), c2.getRed(), 3),
                     propInt(c1.getGreen(), c2.getGreen(), 3),
                     propInt(c1.getBlue(), c2.getBlue(), 3));
  }

  /**
   *
   * @param a int
   * @param b int
   * @param prop int
   * @return int
   */
  private static int propInt(int a, int b, int prop) {
    return b + ( (a - b) / prop);
  }

  /**
   *
   * @param c Color
   * @param factor float
   * @return Color
   */
  public static Color getSimiliarColor(Color c, float factor) {
    return new Color(between( (int) (c.getRed() * factor), 0, 255),
                     between( (int) (c.getGreen() * factor), 0, 255),
                     between( (int) (c.getBlue() * factor), 0, 255), c.getAlpha());
  }

  /**
   *
   * @param v int
   * @param min int
   * @param max int
   * @return int
   */
  private static int between(int v, int min, int max) {
    return Math.max(min, Math.min(v, max));
  }

  /**
   * Dependiendo del color de background retorna el color de foreground (blanco
   * o negro).
   * 
   * @param bg el color de fondo (background)
   * @return <code>Color.BLACK</code> o <code>Color.WHITE</code>
   */
  public static Color computeForeground(Color bg) {
    float[] rgb = bg.getRGBColorComponents(null);
    float y = .3f * rgb[0] + .59f * rgb[1] + .11f * rgb[2];

    return y > .5f ? Color.BLACK : Color.WHITE;
  }

  /**
   *
   * @param fg Color
   * @return boolean
   */
  public static boolean isLightForeground(final Color fg) {
    return fg.getRed() > 0xa0 && fg.getGreen() > 0xa0 && fg.getBlue() > 0xa0;
  }

  /**
   * Checks if is dark.
   *
   * @param c the Color
   * @return true, if is dark
   */
  public static boolean isDark(final Color c) {
    // based on perceptional luminosity, see
    return (1 - (0.299 * c.getRed() + 0.587 * c.getGreen() + 0.114 * c.getBlue()) / 255) >= 0.5;
  }
  
  /** Valor de la luminancia, a partir del cual el color se considera "muy claro". */
  public static final int LIGHT_LUMINANCE = 229;

  /** Valor de la luminancia, por debajo del cual el color se considera "muy oscuro". */
  public static final int DARK_LUMINANCE = 51;

  /**
   * Retorna la luminosidad aproximada del color como un número entero entre 0
   * y 255, donde 0 es negro y 255 es de color blanco.
   * 
   * @param color Color
   * @return int
   */
  public static int getApproximateLuminance(Color color) {
    return ((color.getRed() >> 2) + (color.getGreen() >> 1) + (color.getBlue() >> 2));
  }

  /**
   *
   * @param color Color
   * @return boolean
   */
  public static boolean isVeryDark(Color color) {
    return getApproximateLuminance(color) < DARK_LUMINANCE;
  }

  /**
   *
   * @param color Color
   * @return boolean
   */
  public static boolean isVeryLight(Color color) {
    return getApproximateLuminance(color) > LIGHT_LUMINANCE;
  }

  /**
   *
   * @param color1 Color
   * @param color2 Color
   * @param ratio double
   * @return Color
   */
  public static Color blend(Color color1, Color color2, double ratio) {
    float r = (float) ratio;
    float ir = (float) 1.0 - r;

    float rgb1[] = new float[3];
    float rgb2[] = new float[3];

    color1.getColorComponents(rgb1);
    color2.getColorComponents(rgb2);

    return new Color(rgb1[0] * r + rgb2[0] * ir,
                     rgb1[1] * r + rgb2[1] * ir,
                     rgb1[2] * r + rgb2[2] * ir);
  }

  /**
   *
   * @param color1 Color
   * @param color2 Color
   * @return Color
   */
  public static Color blend(Color color1, Color color2) {
    return blend(color1, color2, 0.5);
  }

  /**
   * Computes "middle" color in terms of rgb color space. Ignores alpha
   * (transparency) channel
   * 
   * @param c1 Color
   * @param c2 Color
   * @return Color
   */
  public static Color getMiddle(Color c1, Color c2) {
    return new Color((c1.getRed() + c2.getRed()) / 2,
                     (c1.getGreen() + c2.getGreen()) / 2,
                     (c1.getBlue() + c2.getBlue()) / 2);
  }

  /**
   *
   * @param c Color
   * @param rDiff int
   * @param gDiff int
   * @param bDiff int
   * @return Color
   */
  public static Color adjustColor(Color c, int rDiff, int gDiff, int bDiff) {
    int red = Math.max(0, Math.min(255, c.getRed() + rDiff));
    int green = Math.max(0, Math.min(255, c.getGreen() + gDiff));
    int blue = Math.max(0, Math.min(255, c.getBlue() + bDiff));
    return new Color(red, green, blue);
  }

  /**
   *
   * @param a Color
   * @param b Color
   * @return boolean
   */
  public static boolean isBrighter(Color a, Color b) {
    int[] ac = new int[] {a.getRed(), a.getGreen(), a.getBlue()};
    int[] bc = new int[] {b.getRed(), b.getGreen(), b.getBlue()};
    int dif = 0;
    for (int i = 0; i < 3; i++) {
      int currDif = ac[i] - bc[i];
      if (Math.abs(currDif) > Math.abs(dif)) {
        dif = currDif;
      }
    }
    return dif > 0;
  }

  /**
   * Return the "distance" between two colors. The rgb entries are taken
   * to be coordinates in a 3D space [0.0-1.0], and this method returnes
   * the distance between the coordinates for the first and second color.
   *
   * @param r1 double
   * @param g1 double
   * @param b1 double
   * @param r2 double
   * @param g2 double
   * @param b2 double
   * @return  Distance bwetween colors.
   */
  public static double colorDistance(double r1, double g1, double b1,
                                     double r2, double g2, double b2) {
    double a = r2 - r1;
    double b = g2 - g1;
    double c = b2 - b1;

    return Math.sqrt(a * a + b * b + c * c);
  }

  /**
   * Return the "distance" between two colors.
   *
   * @param color1  First color [r,g,b].
   * @param color2  Second color [r,g,b].
   * @return        Distance bwetween colors.
   */
  public static double colorDistance(double[] color1, double[] color2) {
    return colorDistance(color1[0], color1[1], color1[2],
                         color2[0], color2[1], color2[2]);
  }

  /**
   * Return the "distance" between two colors.
   *
   * @param color1  First color.
   * @param color2  Second color.
   * @return        Distance between colors.
   */
  public static double colorDistance(Color color1, Color color2) {
    float rgb1[] = new float[3];
    float rgb2[] = new float[3];

    color1.getColorComponents(rgb1);
    color2.getColorComponents(rgb2);

    return colorDistance(rgb1[0], rgb1[1], rgb1[2], rgb2[0], rgb2[1], rgb2[2]);
  }

}
