import 'dart:math' as Math;

import 'image_exception.dart';
import 'internal/clamp.dart';

/**
 * Image pixel colors are instantiated as an int object rather than an instance
 * of the Color class in order to reduce object allocations. Image pixels are
 * stored in 32-bit RGBA format (8 bits per channel). Internally in dart, this
 * will be stored in a "small integer" on 64-bit machines, or a
 * "medium integer" on 32-bit machines. In Javascript, this will be stored
 * in a 64-bit double.
 *
 * The Color class is used as a namespace for color operations, in an attempt
 * to create a cleaner API for color operations.
 */
class Color {
  /**
   * Create a color value from RGB values in the range [0, 255].
   */
  static int fromRgb(int red, int green, int blue) {
    return getColor(red, green, blue);
  }

  /**
   * Create a color value from RGBA values in the range [0, 255].
   */
  static int fromRgba(int red, int green, int blue, int alpha) {
    return getColor(red, green, blue, alpha);
  }

  /**
   * Create a color value from HSL values in the range [0, 1].
   */
  static int fromHsl(num hue, num saturation, num lightness) {
    var rgb = hslToRGB(hue, saturation, lightness);
    return getColor(rgb[0], rgb[1], rgb[2]);
  }

  /**
   * Create a color value from HSV values in the range [0, 1].
   */
  static int fromHsv(num hue, num saturation, num value) {
    var rgb = hsvToRGB(hue, saturation, value);
    return getColor(rgb[0], rgb[1], rgb[2]);
  }

  /**
   * Create a color value from XYZ values.
   */
  static int fromXyz(num x, num y, num z) {
    var rgb = xyzToRGB(x, y, z);
    return getColor(rgb[0], rgb[1], rgb[2]);
  }

  /**
   * Create a color value from CIE-L*ab values.
   */
  static int fromLab(num L, num a, num b) {
    var rgb = labToRGB(L, a, b);
    return getColor(rgb[0], rgb[1], rgb[2]);
  }
}


/// Red channel of a color.
const int RED = 0;
/// Green channel of a color.
const int GREEN = 1;
/// Blue channel of a color.
const int BLUE = 2;
/// Alpha channel of a color.
const int ALPHA = 3;
/// Luminance of a color.
const int LUMINANCE = 4;

/**
 * Get the color with the given [r], [g], [b], and [a] components.
 *
 * The channel order of a uint32 encoded color is RGBA, to be consistent
 * with the image data of a canvas html element.
 */
int getColor(int r, int g, int b, [int a = 255]) =>
    (clamp255(a) << 24) |
    (clamp255(b) << 16) |
    (clamp255(g) << 8) |
    clamp255(r);

/**
 * Get the [channel] from the [color].
 */
int getChannel(int color, int channel) =>
    channel == 0 ? getRed(color) :
    channel == 1 ? getGreen(color) :
    channel == 2 ? getBlue(color) :
    getAlpha(color);

/**
 * Returns a new color, where the given [color]'s [channel] has been
 * replaced with the given [value].
 */
int setChannel(int color, int channel, int value) =>
    channel == 0 ? setRed(color, value) :
    channel == 1 ? setGreen(color, value) :
    channel == 2 ? setBlue(color, value) :
    setAlpha(color, value);

/**
 * Get the red channel from the [color].
 */
int getRed(int color) =>
    (color) & 0xff;

/**
 * Returns a new color where the red channel of [color] has been replaced
 * by [value].
 */
int setRed(int color, int value) =>
    (color & 0xffffff00) | (clamp255(value));

/**
 * Get the green channel from the [color].
 */
int getGreen(int color) =>
    (color >> 8) & 0xff;

/**
 * Returns a new color where the green channel of [color] has been replaced
 * by [value].
 */
int setGreen(int color, int value) =>
    (color & 0xffff00ff) | (clamp255(value) << 8);

/**
 * Get the blue channel from the [color].
 */
int getBlue(int color) =>
    (color >> 16) & 0xff;

/**
 * Returns a new color where the blue channel of [color] has been replaced
 * by [value].
 */
int setBlue(int color, int value) =>
    (color & 0xff00ffff) | (clamp255(value) << 16);

/**
 * Get the alpha channel from the [color].
 */
int getAlpha(int color) =>
    (color >> 24) & 0xff;

/**
 * Returns a new color where the alpha channel of [color] has been replaced
 * by [value].
 */
int setAlpha(int color, int value) =>
    (color & 0x00ffffff) | (clamp255(value) << 24);

/**
 * Returns a new color of [src] alpha-blended onto [dst]. The opacity of [src]
 * is additionally scaled by [fraction] / 255.
 */
int alphaBlendColors(int dst, int src, [int fraction = 0xff]) {
  double a = (getAlpha(src) / 255.0);
  if (fraction != 0xff) {
    a *= (fraction / 255.0);
  }

  int sr = (getRed(src) * a).round();
  int sg = (getGreen(src) * a).round();
  int sb = (getBlue(src) * a).round();
  int sa = (getAlpha(src) * a).round();

  int dr = (getRed(dst) * (1.0 - a)).round();
  int dg = (getGreen(dst) * (1.0 - a)).round();
  int db = (getBlue(dst) * (1.0 - a)).round();
  int da = (getAlpha(dst) * (1.0 - a)).round();

  return getColor(sr + dr, sg + dg, sb + db, sa + da);
}

/**
 * Returns the luminance (grayscale) value of the [color].
 */
int getLuminance(int color) {
  int r = getRed(color);
  int g = getGreen(color);
  int b = getBlue(color);
  return (0.299 * r + 0.587 * g + 0.114 * b).round();
}

/**
 * Returns the luminance (grayscale) value of the color.
 */
int getLuminanceRGB(int r, int g, int b) =>
  (0.299 * r + 0.587 * g + 0.114 * b).round();

/**
 * Convert an HSL color to RGB, where h is specified in normalized degrees
 * [0, 1] (where 1 is 360-degrees); s and l are in the range [0, 1].
 * Returns a list [r, g, b] with values in the range [0, 255].
 */
List<int> hslToRGB(num hue, num saturation, num lightness) {
  if (saturation == 0) {
    int gray = (lightness * 255.0).toInt();
    return [gray, gray, gray];
  }

  hue2rgb(num p, num q, num t) {
    if (t < 0.0) {
      t += 1.0;
    }
    if (t > 1) {
      t -= 1.0;
    }
    if (t < 1.0 / 6.0) {
      return p + (q - p) * 6.0 * t;
    }
    if (t < 1.0 / 2.0) {
      return q;
    }
    if (t < 2.0 / 3.0) {
      return p + (q - p) * (2.0/3.0 - t) * 6.0;
    }
    return p;
  }

  var q = lightness < 0.5
          ? lightness * (1.0 + saturation)
          : lightness + saturation - lightness * saturation;
  var p = 2.0 * lightness - q;

  var r = hue2rgb(p, q, hue + 1.0 / 3.0);
  var g = hue2rgb(p, q, hue);
  var b = hue2rgb(p, q, hue - 1.0 / 3.0);

  return [(r * 255.0).round(), (g * 255.0).round(), (b * 255.0).round()];
}

/**
 * Convert an HSV color to RGB, where h is specified in normalized degrees
 * [0, 1] (where 1 is 360-degrees); s and l are in the range [0, 1].
 * Returns a list [r, g, b] with values in the range [0, 255].
 */
List<int> hsvToRGB(num hue, num saturation, num brightness) {
  if (saturation == 0) {
    var gray = (brightness * 255.0).round();
    return [gray, gray, gray];
  }

  double h = (hue - hue.floor()) * 6.0;
  double f = h - h.floor();
  double p = brightness * (1.0 - saturation);
  double q = brightness * (1.0 - saturation * f);
  double t = brightness * (1.0 - (saturation * (1.0 - f)));

  switch (h.toInt()) {
    case 0:
      return [(brightness * 255.0).round(),
              (t * 255.0).round(),
              (p * 255.0).round()];
    case 1:
      return [(q * 255.0).round(),
              (brightness * 255.0).round(),
              (p * 255.0).round()];
    case 2:
      return [(p * 255.0).round(),
              (brightness * 255.0).round(),
              (t * 255.0).round()];
    case 3:
      return [(p * 255.0).round(),
              (q * 255.0).round(),
              (brightness * 255.0).round()];
    case 4:
      return [(t * 255.0).round(),
              (p * 255.0).round(),
              (brightness * 255.0).round()];
    case 5:
      return [(brightness * 255.0).round(),
              (p * 255.0).round(),
              (q * 255.0).round()];
    default:
      throw new ImageException('invalid hue');
  }
}

/**
 * Convert an RGB color to HSL, where r, g and b are in the range [0, 255].
 * Returns a list [h, s, l] with values in the range [0, 1].
 */
List<double> rgbToHSL(num r, num g, num b) {
  r /= 255.0;
  g /= 255.0;
  b /= 255.0;
  var max = Math.max(r, Math.max(g, b));
  var min = Math.min(r, Math.min(g, b));
  var h;
  var s;
  var l = (max + min) / 2.0;

  if (max == min){
    return [0.0, 0.0, l];
  }

  var d = max - min;

  s = l > 0.5 ? d / (2.0 - max - min) : d / (max + min);

  if (max == r) {
    h = (g - b) / d + (g < b ? 6.0 : 0.0);
  } else if (max == g) {
    h = (b - r) / d + 2.0;
  } else {
    h = (r - g) / d + 4.0;
  }

  h /= 6.0;

  return [h, s, l];
}

/**
 * Convert a CIE-L*ab color to XYZ.
 */
List<int> labToXYZ(num l, num a, num b) {
  var y = (l + 16.0) / 116.0;
  var x = y + (a / 500.0);
  var z = y - (b / 200.0);
  if (Math.pow(x, 3) > 0.008856) {
    x = Math.pow(x, 3);
  } else {
    x = (x - 16.0 / 116) / 7.787;
  }
  if (Math.pow(y, 3) > 0.008856) {
    y = Math.pow(y, 3);
  } else {
    y = (y - 16.0 / 116.0) / 7.787;
  }
  if (Math.pow(z, 3) > 0.008856) {
    z = Math.pow(z, 3);
  } else {
    z = (z - 16.0 / 116.0) / 7.787;
  }

  return [(x * 95.047).toInt(), (y * 100.0).toInt(), (z * 108.883).toInt()];
}

/**
 * Convert an XYZ color to RGB.
 */
List<int> xyzToRGB(num x, num y, num z) {
  var b, g, r;
  x /= 100;
  y /= 100;
  z /= 100;
  r = (3.2406 * x) + (-1.5372 * y) + (-0.4986 * z);
  g = (-0.9689 * x) + (1.8758 * y) + (0.0415 * z);
  b = (0.0557 * x) + (-0.2040 * y) + (1.0570 * z);
  if (r > 0.0031308) {
    r = (1.055 * Math.pow(r, 0.4166666667)) - 0.055;
  } else {
    r *= 12.92;
  }
  if (g > 0.0031308) {
    g = (1.055 * Math.pow(g, 0.4166666667)) - 0.055;
  } else {
    g *= 12.92;
  }
  if (b > 0.0031308) {
    b = (1.055 * Math.pow(b, 0.4166666667)) - 0.055;
  } else {
    b *= 12.92;
  }

  return [(r * 255).toInt().clamp(0, 255),
          (g * 255).toInt().clamp(0, 255),
          (b * 255).toInt().clamp(0, 255)];
}

/**
 * Convert a CMYK color to RGB, where c, m, y, k values are in the range
 * [0, 255]. Returns a list [r, g, b] with values in the range [0, 255].
 */
List<int> cmykToRGB(num c, num m, num y, num k) {
  c /= 255.0;
  m /= 255.0;
  y /= 255.0;
  k /= 255.0;
  return [(255.0 * (1.0 - c) * (1.0 - k)).round(),
          (255.0 * (1.0 - m) * (1.0 - k)).round(),
          (255.0 * (1.0 - y) * (1.0 - k)).round()];
}

/**
 * Convert a CIE-L*ab color to RGB.
 */
List<int> labToRGB(num l, num a, num b) {
  const double ref_x = 95.047;
  const double ref_y = 100.000;
  const double ref_z = 108.883;

  double y = (l + 16.0) / 116.0;
  double x = a / 500.0 + y;
  double z = y - b / 200.0;

  double y3 = Math.pow(y, 3);
  if (y3 > 0.008856) {
    y = y3;
  } else {
    y = (y - 16 / 116) / 7.787;
  }

  double x3 = Math.pow(x,  3);
  if (x3 > 0.008856) {
    x = x3;
  } else {
    x = (x - 16 / 116) / 7.787;
  }

  double z3 = Math.pow(z, 3);
  if (z3 > 0.008856) {
    z = z3;
  } else {
    z = (z - 16 / 116) / 7.787;
  }

  x *= ref_x;
  y *= ref_y;
  z *= ref_z;

  x /= 100.0;
  y /= 100.0;
  z /= 100.0;

  // xyz to rgb
  double R = x * 3.2406 + y * (-1.5372) + z * (-0.4986);
  double G = x * (-0.9689) + y * 1.8758 + z * 0.0415;
  double B = x * 0.0557 + y * (-0.2040) + z * 1.0570;

  if (R > 0.0031308) {
    R = 1.055 * (Math.pow(R, 1.0 / 2.4)) - 0.055;
  } else {
    R = 12.92 * R;
  }

  if (G > 0.0031308) {
    G = 1.055 * (Math.pow(G, 1.0 / 2.4)) - 0.055;
  } else {
    G = 12.92 * G;
  }

  if (B > 0.0031308) {
    B = 1.055 * (Math.pow(B, 1.0 / 2.4)) - 0.055;
  } else {
    B = 12.92 * B;
  }

  return [(R * 255.0).toInt().clamp(0,  255),
          (G * 255.0).toInt().clamp(0,  255),
          (B * 255.0).toInt().clamp(0,  255)];
}
