var Color = (function() {

  var nameSpace = {};

  var COMPONENT_PREFIX = '_to_';
  var SUBSTITUTE_PREFIX = '_with_';

  var CSS_THREE_CHAR_HEX = /^#([A-F0-9])([A-F0-9])([A-F0-9])$/i;
  var CSS_SIX_CHAR_HEX = /^#([A-F0-9]{6})$/i;
  var CSS_RGB = /^rgb\(\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*\)/;
  var CSS_RGBA = /^rgba\(\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*\,\s*(.+)\s*\)/;

  nameSpace.InvalidArgumentException = 'Invalid argument.';

  /**
   * All conversions performed by dat.Color respect these values as the
   * maximum for that component's range. This is also the range we
   * expect that they adhere to when you pass them as parameters.
   */
  nameSpace.bounds = {
    a: 1,
    r: 255,
    g: 255,
    b: 255,
    h: 360,
    s: 100,
    v: 100
  };

  /**
   * Creates a dat.Color object from the specified RGBA components. Those
   * components should respect the ranges specified by dat.Color.bounds.
   * The alpha is assumed to be fully opaque.
   *
   * @param r red.
   * @param g green.
   * @param b blue.
   */
  nameSpace.rgb = function(r, g, b, a) {
    return nameSpace.rgba(r, g, b, a || nameSpace.bounds['a']);
  };

  /**
   * Creates a dat.Color object from the specified RGBA components. Those
   * components should respect the ranges specified by dat.Color.bounds.
   *
   * @param r red.
   * @param g green.
   * @param b blue.
   * @param a alpha / opacity.
   */
  nameSpace.rgba = function(r, g, b, a) {
    return new dat.Color(r, g, b, a || nameSpace.bounds['a']);
  };

  /**
   * Creates a dat.Color object from the specified HSV components. Those
   * components should respect the ranges specified by dat.Color.bounds.
   * The alpha is assumed to be fully opaque.
   *
   * @param h hue.
   * @param s saturation.
   * @param v value / brightness.
   */
  nameSpace.hsv = function(h, s, v, a) {
    return nameSpace.hsva(h, s, v, a || nameSpace.bounds['a']);
  };

  /**
   * Creates a dat.Color object from the specified HSVA components. Those
   * components should respect the ranges specified by dat.Color.bounds.
   *
   * @param h hue.
   * @param s saturation.
   * @param v value / brightness.
   * @param a alpha / opacity.
   */
  nameSpace.hsva = function(h, s, v, a) {
    var asRGB = nameSpace.hsva_to_rgba(h, s, v, a || nameSpace.bounds['a']);
    return new dat.Color(asRGB.r, asRGB.g, asRGB.b, asRGB.a);
  };

  /**
   * Does a best guess to convert the data in arguments to a 32-bit hex
   * representation of color.
   *
   * @param a
   */
  nameSpace.args_to_hex = function(a) {

    if (typeof a == 'string') {

      var string_match = testString(a);
      if (string_match != null) {
        return string_match;
      }

    } else if (a.length == 1) {

      if (typeof a[0] == 'number') {

        return a[0];

      } else if (typeof a[0] == 'array') {

        return nameSpace.rgba_to_hex(a[0][0], a[0][1], a[0][2], a[0][3] || 1);

      } else if (typeof a[0] == 'object' && hasRGBAProps(a[0])) {

        return nameSpace.rgba_to_hex(a[0].r, a[0].g, a[0].b, a[0].a);

      } else if (typeof a[0] == 'object' && hasRGBProps(a[0])) {

        return nameSpace.rgb_to_hex(a[0].r, a[0].g, a[0].b);

      } else if (typeof a[0] == 'string') {

        var string_match = testString(a[0]);
        if (string_match != null) {
          return string_match;
        }

      }

    } else if (a.length == 3) {

      return nameSpace.rgb_to_hex(a[0], a[1], a[2]);

    } else if (a.length == 4) {

      return nameSpace.rgba_to_hex(a[0], a[1], a[2], a[3]);

    }

    return 0;

  }

  function testString(string) {

    var test = string.match(CSS_THREE_CHAR_HEX);

    if (test != null) {

      var concat = '0x' + test[1].toString() + test[1].toString() + test[2].toString() + test[2].toString() + test[3].toString() + test[3].toString();

      return parseInt(concat);

    }

    test = string.match(CSS_SIX_CHAR_HEX);

    if (test != null) {

      var concat = '0x' + test[1].toString();
      return parseInt(concat);

    }

    test = string.match(CSS_RGBA);

    if (test != null) {

      var r = parseFloat(test[1]) / 255 * nameSpace.bounds.r;
      var g = parseFloat(test[2]) / 255 * nameSpace.bounds.g;
      var b = parseFloat(test[3]) / 255 * nameSpace.bounds.b;
      var a = parseFloat(test[4]) * nameSpace.bounds.a;

      // TODO Would be nice to be able to expose r,g,b and a before they get
      // lossy converted to hex
      return nameSpace.rgba_to_hex(r, g, b, a);

    }


    test = string.match(CSS_RGB);

    if (test != null) {

      var r = parseFloat(test[1]) / 255 * nameSpace.bounds.r;
      var g = parseFloat(test[2]) / 255 * nameSpace.bounds.g;
      var b = parseFloat(test[3]) / 255 * nameSpace.bounds.b;


      // TODO Would be nice to be able to expose r,g,b and a before they get
      // lossy converted to hex
      return nameSpace.rgb_to_hex(r, g, b);

    }

    return null;

  }

  /**
   * Converts a color in HSVA space to a color in RGBA space as
   * represented by { r:?, g:?, b:?, a:? }
   *
   * @param h hue.
   * @param s saturation.
   * @param v value / brightness.
   * @param a alpha / opacity.
   */
  nameSpace.hsva_to_rgba = function(h, s, v, a) {

    h = isNaN(h) ? 0 : h;
    h %= nameSpace.bounds['h'];
    h = h / nameSpace.bounds['h'] * 360;

    s /= nameSpace.bounds['s'];
    v /= nameSpace.bounds['v'];
    a = a || nameSpace.bounds['a'];

    var hi = Math.floor(h / 60) % 6;

    var f = h / 60 - Math.floor(h / 60);
    var p = v * (1.0 - s);
    var q = v * (1.0 - (f * s));
    var t = v * (1.0 - ((1.0 - f) * s));
    var c = [
      [v, t, p],
      [q, v, p],
      [p, v, t],
      [p, q, v],
      [t, p, v],
      [v, p, q]
    ][hi];

    return {
      r: c[0] * nameSpace.bounds['r'],
      g: c[1] * nameSpace.bounds['g'],
      b: c[2] * nameSpace.bounds['b'],
      a: a || nameSpace.bounds['a']
    };

  }

  nameSpace.hsv_to_rgb = nameSpace.hsva_to_rgba;

  /**
   * Converts an object in RGBA space to HSVA space as represented by
   * { h:?, s:?, v:?, a:? }. Returns NaN for the hue of greyscale values.
   *
   * @param r red.
   * @param g green.
   * @param b blue.
   * @param a alpha / opacity.
   */
  nameSpace.rgba_to_hsva = function(r, g, b, a) {

    r /= nameSpace.bounds['r'];
    g /= nameSpace.bounds['g'];
    b /= nameSpace.bounds['b'];

    var min = Math.min(r, g, b),
        max = Math.max(r, g, b),
        delta = max - min,
        h, s, v = max;

    v = max;
    if (max != 0) {
      s = delta / max;
    } else {
      return {
        h: 0,
        s: 0,
        v: 0,
        a: a || nameSpace.bounds['a']
      };
    }

    if (r == max) {
      h = (g - b) / delta;
    } else if (g == max) {
      h = 2 + (b - r) / delta;
    } else {
      h = 4 + (r - g) / delta;
    }
    h /= 6;
    if (h < 0) {
      h += 1;
    }

    return {
      h: h * nameSpace.bounds['h'],
      s: s * nameSpace.bounds['s'],
      v: v * nameSpace.bounds['v'],
      a: a || nameSpace.bounds['a']
    };

  };

  nameSpace.rgb_to_hsv = nameSpace.rgba_to_hsva;

  /**
   * Converts a color in RGBA space to a 32-bit hex integer.
   *
   * @param r red.
   * @param g green.
   * @param b blue.
   * @param a alpha / opacity.
   */
  nameSpace.rgba_to_hex = function(r, g, b, a) {
    var hex = 0;
    hex = nameSpace['hex' + SUBSTITUTE_PREFIX + 'a'](hex, a || nameSpace.bounds['a']);
    hex = nameSpace['hex' + SUBSTITUTE_PREFIX + 'r'](hex, r);
    hex = nameSpace['hex' + SUBSTITUTE_PREFIX + 'g'](hex, g);
    return nameSpace['hex' + SUBSTITUTE_PREFIX + 'b'](hex, b);
  };
  nameSpace.rgb_to_hex = nameSpace.rgba_to_hex;


  /**
   * Converts a 32-bit hex integer representing a color into RGBA space,
   * as represented by { r:?, g:?, b:?, a:? }.
   *
   * @param hex 32-bit hex integer.
   */
  nameSpace.hex_to_rgba = function(hex) {
    return {
      a: nameSpace['hex' + COMPONENT_PREFIX + 'a'](hex),
      r: nameSpace['hex' + COMPONENT_PREFIX + 'r'](hex),
      g: nameSpace['hex' + COMPONENT_PREFIX + 'g'](hex),
      b: nameSpace['hex' + COMPONENT_PREFIX + 'b'](hex)
    };
  };
  nameSpace.hex_to_rgb = nameSpace.hex_to_rgba;

  /**
   * Converts a 32-bit hex integer into a CSS or fillStyle compatible
   * string.
   *
   * @param hex 32-bit hex integer.
   */
  nameSpace.hex_to_string = function(hex) {
    var a = nameSpace['hex' + COMPONENT_PREFIX + 'a'](hex) / nameSpace.bounds['a'];
    var r = Math.round(nameSpace['hex' + COMPONENT_PREFIX + 'r'](hex) / nameSpace.bounds['r'] * 255);
    var g = Math.round(nameSpace['hex' + COMPONENT_PREFIX + 'g'](hex) / nameSpace.bounds['g'] * 255);
    var b = Math.round(nameSpace['hex' + COMPONENT_PREFIX + 'b'](hex) / nameSpace.bounds['b'] * 255);
    return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
  };

  nameSpace.invert_hex = function(hex) {
    var rgba = nameSpace.hex_to_rgba(hex);
    var invert = nameSpace.invert_rgba(rgba.r, rgba.g, rgba.a, rgba.a);
    return nameSpace.rgba_to_hex(invert.r, invert.g, invert.b, invert.a);
  }

  nameSpace.invert_rgba = function(r, g, b, a) {
    return {
      r: nameSpace.bounds.r - r,
      g: nameSpace.bounds.g - g,
      b: nameSpace.bounds.b - b,
      a: a || nameSpace.bounds.a
    };
  };

  nameSpace.lerp_rgb = nameSpace.lerp_rgba = function(c1, c2, t) {
    return new Color(lerp(c1.r, c2.r, t), lerp(c1.g, c2.g, t), lerp(c1.b, c2.b, t), lerp(c1.a, c2.a, t));
    function lerp(a, b, l) {
      return (b - a) * l + a;
    }
  };

  nameSpace.mix = function(colorArray) {
    var r = 0, g = 0, b = 0, a = 0, l = colorArray.length;
    for (var i = 0; i < l; i++) {
      r += colorArray[i].r;
      g += colorArray[i].g;
      b += colorArray[i].b;
      a += colorArray[i].a;
    }
    r /= l;
    g /= l;
    b /= l;
    a /= l;
    return new dat.Color(r, g, b, a);
  };

  nameSpace.random = function() {

    return new Color(Math.random() * nameSpace.bounds['r'],
        Math.random() * nameSpace.bounds['g'],
        Math.random() * nameSpace.bounds['b']);

  };

  nameSpace.ivnert_rgb = nameSpace.invert_rgba;

  nameSpace.greyscale_hex = function(hex) {
    var rgba = nameSpace.hex_to_rgba(hex);
    var grey = nameSpace.greyscale_rgba(rgba.r, rgba.g, rgba.a, rgba.a);
    return nameSpace.rgba_to_hex(grey.r, grey.g, grey.b, grey.a);
  };

  nameSpace.greyscale_rgba = function(r, g, b, a) {
    var hsva = nameSpace.rgba_to_hsva(r, g, b, a);
    hsva.s = 0;
    return nameSpace.hsva_to_rgba(hsva.h, hsva.s, hsva.v, hsva.a);
  };

  nameSpace.greyscale_rgb = nameSpace.greyscale_rgba;

  for (var i = 0; i < 4; i++) {
    defineARGBComponent(i);
  }

  for (i = 0; i < 3; i++) {
    defineHSVComponent('hsv'[i]);
  }

  function defineARGBComponent(i) {
    var name = 'bgra'[i];
    var i8 = i * 8;
    nameSpace['hex' + COMPONENT_PREFIX + name] = function(hex) {
      return ((hex >> (i8)) & 0xFF) / 0xFF * nameSpace.bounds[name];
    };
    nameSpace['hex' + SUBSTITUTE_PREFIX + name] = function(hex, c) {
      c = c / nameSpace.bounds[name] * 0xFF;
      return (c << (i8) | (hex & ~ (0xFF << (i8))));
    };
  }

  function defineHSVComponent(c) {
    nameSpace['hex' + COMPONENT_PREFIX + c] = function(hex) {
      var rgba = nameSpace.hex_to_rgba(hex);
      var hsva = nameSpace.rgba_to_hsva(rgba.r, rgba.g, rgba.b, rgba.a);
      return hsva[c];
    };
    nameSpace['hex' + SUBSTITUTE_PREFIX + c] = function(hex, v) {
      var rgba = nameSpace.hex_to_rgba(hex);
      var hsva = nameSpace.rgba_to_hsva(rgba.r, rgba.g, rgba.b, rgba.a);
      hsva[c] = v;
      rgba = nameSpace.hsva_to_rgba(hsva.h, hsva.s, hsva.v, hsva.a);
      var tr = nameSpace.rgba_to_hex(rgba.r, rgba.g, rgba.b, rgba.a);
      return tr;
    };
  }

  function hasRGBAProps(obj) {
    return obj.r != undefined && obj.g != undefined && obj.b != undefined && obj.a != undefined;
  }

  ;

  function hasRGBProps(obj) {
    return obj.r != undefined && obj.g != undefined && obj.b != undefined;
  }

  ;

  var Color = function() {

    var a = arguments;

    var _this = this;
    var _hex = nameSpace.args_to_hex(a);
    //console.log('That sounds like ' + _hex.toString(16));

    var _rgbaStore = {};
    var _hsvStore = {};

    // Did the object we pass in already specify rgba props?
    // If so we shouldn't reconvert from hex.
    if (a.length == 1 && hasRGBAProps(a[0])) {
      _rgbaStore.r = a[0].r;
      _rgbaStore.g = a[0].g;
      _rgbaStore.b = a[0].b;
      _rgbaStore.a = a[0].a;
    } else if (a.length == 1 && hasRGBProps(a[0])) {
      _rgbaStore.r = a[0].r;
      _rgbaStore.g = a[0].g;
      _rgbaStore.b = a[0].b;
      _rgbaStore.a = nameSpace.bounds.a;
    } else {
      _rgbaStore = nameSpace.hex_to_rgba(_hex);
    }

    if (a.length == 1 && a[0] instanceof Color) {
      _hsvStore.h = a[0].h;
      _hsvStore.s = a[0].s;
      _hsvStore.v = a[0].v;
    } else {
      recalculateHSVComponents();
    }

    this.__defineSetter__('hex', function(v) {
      _hex = v;
      _rgbaStore = nameSpace.hex_to_rgba(_hex);
      recalculateHSVComponents();
    });

    this.__defineGetter__('hex', function() {
      return _hex;
    });

    this.toString = function() {
      return nameSpace.hex_to_string(_hex);
    };

    this.copy = function() {
      return new Color(_this);
    };

    this.invert = function() {
      _this.r = nameSpace.bounds.r - _this.r;
      _this.g = nameSpace.bounds.g - _this.g;
      _this.b = nameSpace.bounds.b - _this.b;
    };

    // Make functional programming masquerade as oop-style:
    var rgbaComponents = 'argb';
    var hsvComponents = 'hsv';

    for (var i = 0; i < rgbaComponents.length; i++) {
      defineRGBAComponent(rgbaComponents[i]);
    }

    for (var i = 0; i < hsvComponents.length; i++) {
      defineHSVComponent(hsvComponents[i]);
    }

    function recalculateHSVComponents() {
      var tmp = nameSpace.rgba_to_hsva(_rgbaStore.r, _rgbaStore.g, _rgbaStore.b, _rgbaStore.a);
      for (var i in tmp) {
        if (!isNaN(tmp[i])) {
          _hsvStore[i] = tmp[i];
        }
      }
    }

    function defineRGBAComponent(componentName) {

      var setter = nameSpace['hex' + SUBSTITUTE_PREFIX + componentName];

      _this.__defineGetter__(componentName, function() {
        return _rgbaStore[componentName];
      });

      _this.__defineSetter__(componentName, function(value) {

        if (typeof value != 'number') {
          throw nameSpace.InvalidArgumentException;
        }

        // Remember value
        _rgbaStore[componentName] = value;

        // Update hex
        _hex = setter.call(_this, _hex, value);

        // Update HSV
        recalculateHSVComponents();

      });

    }

    function defineHSVComponent(componentName) {

      _this.__defineSetter__(componentName, function(value) {

        if (typeof value != 'number') {
          throw nameSpace.InvalidArgumentException;
        }

        // This is NOT a hack! Greyscale colors SHOULD return NaN for the
        // hue, and if this doesn't specify a hue, we should keep whatever hue we
        // thought it might have been previously.
        if (!isNaN(value)) {
          _hsvStore[componentName] = value;
        }

        _rgbaStore = nameSpace.hsva_to_rgba(_hsvStore.h, _hsvStore.s, _hsvStore.v, _rgbaStore.a);

        _hex = nameSpace.rgba_to_hex(_rgbaStore.r, _rgbaStore.g, _rgbaStore.b, _rgbaStore.a);

      });

      _this.__defineGetter__(componentName, function() {
        return _hsvStore[componentName];
      });

    }

  };

  for (var i in nameSpace) {
    Color[i] = nameSpace[i];
  }

  return Color;

})();

