function rgbToHsv(r, g, b) {
  r /= 255, g /= 255, b /= 255;

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

  var d = max - min;
  s = max == 0 ? 0 : d / max;

  if (max == min) {
    h = 0; // achromatic
  } else {
    switch (max) {
      case r: h = (g - b) / d + (g < b ? 6 : 0); break;
      case g: h = (b - r) / d + 2; break;
      case b: h = (r - g) / d + 4; break;
    }

    h /= 6;
  }

  return [ h, s, v ];
}


/**
 * Converts an HSV color value to RGB. Conversion formula
 * adapted from http://en.wikipedia.org/wiki/HSV_color_space.
 * Assumes h, s, and v are contained in the set [0, 1] and
 * returns r, g, and b in the set [0, 255].
 *
 * @param   Number  h       The hue
 * @param   Number  s       The saturation
 * @param   Number  v       The value
 * @return  Array           The RGB representation
 */
function hsvToRgb(h, s, v) {
  var r, g, b;

  var i = Math.floor(h * 6);
  var f = h * 6 - i;
  var p = v * (1 - s);
  var q = v * (1 - f * s);
  var t = v * (1 - (1 - f) * s);

  switch (i % 6) {
    case 0: r = v, g = t, b = p; break;
    case 1: r = q, g = v, b = p; break;
    case 2: r = p, g = v, b = t; break;
    case 3: r = p, g = q, b = v; break;
    case 4: r = t, g = p, b = v; break;
    case 5: r = v, g = p, b = q; break;
  }

  return [ r * 255, g * 255, b * 255 ];
}


var extractColors = function( ctx, where, k, debug){


  var points = [].concat.apply( [], where.map( function( polygon ){
    return triangle.apply( null, polygon );
  } ) );

  var minX = Infinity, minY = Infinity,
    maxX = -Infinity, maxY = -Infinity;

  for( var i = 0, _i = points.length; i < _i; i++ ){
    var point = points[ i ];
    if( minX > point[ 0 ] ) minX = point[ 0 ];
    if( minY > point[ 1 ] ) minY = point[ 1 ];
    if( maxX < point[ 0 ] ) maxX = point[ 0 ];
    if( maxY < point[ 1 ] ) maxY = point[ 1 ];
  }

  var w = maxX - minX + 1;
  var imageData = ctx.getImageData( minX, minY, w, maxY - minY + 1 ),
    data = imageData.data;

  var r, g, b;
  var colors = [];
  for( var i = 0, _i = points.length; i < _i; i++ ){
    var p = points[ i ], pointer = ( p[ 0 ] - minX + ( p[ 1 ] - minY ) * w ) * 4;
    r = data[ pointer ];
    g = data[ pointer + 1 ];
    b = data[ pointer + 2 ];
    //data[ pointer + 2 ] = 255
    colors.push( rgbToHsv( r, g, b ) );
  }

  //ctx2.putImageData( imageData, minX, minY );


  var means = new KMeans();

  var result = { means: means, clusters: means.cluster( colors, k, hsvDistance ) };
  var amount = 0;
  for( var i = 0, _i = result.clusters.length; i < _i; i++ ){
    var cluster = result.clusters[ i ] || [];
    amount += cluster.length;
  }

  var filteredResult = result.clusters.map( function( c, i ){
    return { color: c, normalized: c.length / amount };
  } )
    .filter( function( c ){
      return c.normalized > 1 / k;
    } )
    .sort( function( a, b ){
      return a.normalized - b.normalized;
    } )
    .map( function( c ){
      return c.color[0];//result.means.centroids.slice().sort(distanceToHSV(c.color[0]))[0];
    } );

  var similars = filteredResult.map(function(c, i) {
    return {color: c, i: i, similar: [i]}
  });
  var similarColor = 0.03;

  // O^2 similarity search
  for( var i = 0, _i = similars.length; i < _i; i++ ){
    var c1 = similars[ i ];
    for( var j = 0, _j = similars.length; j < _j; j++ ){
      if(i === j)
        continue;
      var c2 = similars[ j ];
      if(Math.abs(hsvDistance(c1.color, c2.color))<similarColor){
        if(c1.similar.indexOf(j) === -1){
          c1.similar.push( j );
        }
      }
    }
  }
  var used = {};

  var different = similars.reduce(function(store, c) {
    if(!(c.i in used))
      store.push(c);

    for( var i = 0, _i = c.similar.length; i < _i; i++ ){
      var similarElement = c.similar[ i ];
      var item = similars[similarElement];
      if(!(item.i in used)){
        used[item.i] = true;
      }
    }
    return store;
  }, []);

  if(debug){
    similars, result
  }

  filteredResult = different.map(function(item) {
    return item.color;
  });


  return filteredResult.map(function(color) {
    return {color: color, distance: hsvDistance(color, filteredResult[0])};
  }).filter(function(color) {
    var delta = Math.abs(color.distance);
    return delta <0.0000001 || delta > 0.03;
  });
};
var abs = Math.abs, min = Math.min;
var hsvDistance = function( c1, c2 ){
  var dh = min( abs( c2[ 0 ] - c1[ 0 ] ), 360 - abs( c2[ 0 ] - c1[ 0 ] ) ) / 180.0,
    ds = abs( c2[ 1 ] - c1[ 1 ] ), dv = c2[ 2 ] - c1[ 2 ];

  return dh*30 + ds / 12 + dv / 6;
};

var extractor = function(ctx, from, k, debug) {
  return extractColors(ctx, from, k, debug)
    .map(function(m) {
      return {color: hsvToRgb.apply(null, m.color).map(function(a){return a|0}), distance: m.distance};
    })
    .map(function(m){
      return {
        html: '<span style="display: inline-block;width:32px;height:32px;background:rgb('+m.color.join(',')+');border-radius:3px;border:2px solid #000;margin:2px">'+m.distance.toFixed(2)+'</span>'
      };
    });
};