// Color
function Color() {
  this.hue = 0;
  this.saturation = 0;
  this.luminosity = 0;
}

// Palette
function ColorScheme() {
  this.color1 = new Color();
  this.color2 = new Color();
  this.color3 = new Color();
  this.color4 = new Color();
  this.color5 = new Color();
};

//
function displayRgb(r, g, b) {
  var hexVal = 0x1000000 + r * 0x010000 + g * 0x000100 + b * 0x000001;
  return '#' + hexVal.toString(16).substring(1);
}

// Pick an element from an array at random
function pickFrom(arr) {
  var idx = Math.floor(arr.length * Math.random());
  return arr[idx];
}

function pickBoolean() {
  return Math.random() < 0.5;
}

function clamp(val, mn, mx) {
  return Math.min(mx, Math.max(mn, val));
}

function hToRgb(v1, v2, h) {
  var vH = h;
  if (h < 0) {
    vH = h + 1;
  }
  if (h > 1) {
    vH = h - 1;
  }
  var res;
  if ((6 * vH) < 1) {
    res = v1 + ((v2 - v1) * 6 * vH);
  } else if ((2 * vH) < 1) {
    res = v2;
  } else if ((3 * vH) < 2) {
    res = v1 + ((v2 - v1) * 6 * ((2 / 3) - vH));
  } else {
    res = v1;
  }
  return res;
}

function hslToRgb(color) {
  var h = color.hue; // 0..360
  var s = color.saturation; // 0..100
  var l = color.luminosity; // 0..100
  //
  var r = 0;
  var g = 0;
  var b = 0;
  //
  if (s == 0) {
    r = l / 100;
    g = l / 100;
    b = l / 100;
  } else {
    var hr = h / 360;
    var lr = l / 100;
    var sr = s / 100;
    var v2;
    if (lr < 0.5) {
      v2 = lr * (1 + sr);
    } else {
      v2 = (lr + sr) - (lr * sr);
    }
    var v1 = 2 * lr - v2;

    r = hToRgb(v1, v2, hr + (1 / 3));
    g = hToRgb(v1, v2, hr);
    b = hToRgb(v1, v2, hr - (1 / 3));
  }

  //
  r = Math.floor(clamp(255 * r, 0, 255));
  g = Math.floor(clamp(255 * g, 0, 255));
  b = Math.floor(clamp(255 * b, 0, 255));
  return displayRgb(r, g, b);
}

function hsvToRgb(color) {
  var h = color.hue; // 0..360
  var s = color.saturation; // 0..100
  var v = color.luminosity; // 0..100
  //
  if (s == 0) {
    r = v / 100;
    g = v / 100;
    b = v / 100;
  } else {
    var hr = h / 360;
    var vr = v / 100;
    var sr = s / 100;
    //
    var vH = hr * 6;
    if (vH == 6) {
      vH = 0;
    }
    var vI = Math.floor(vH);
    var v1 = vr * (1 - sr);
    var v2 = vr * (1 - sr * (vH - vI));
    var v3 = vr * (1 - sr * (1 - vH + vI));
    //
    if (vI == 0) {
      r = vr;
      g = v3;
      b = v1;
    } else if (vI == 1) {
      r = v2;
      g = vr;
      b = v1;
    } else if (vI == 2) {
      r = v1;
      g = vr;
      b = v3;
    } else if (vI == 3) {
      r = v1;
      g = v2;
      b = vr;
    } else if (vI == 4) {
      r = v3;
      g = v1;
      b = vr;
    } else {
      r = vr;
      g = v1;
      b = v2;
    }
  }
  //
  r = Math.floor(clamp(255 * r, 0, 255));
  g = Math.floor(clamp(255 * g, 0, 255));
  b = Math.floor(clamp(255 * b, 0, 255));
  return displayRgb(r, g, b);
}

function xyzToRgb(x, y, z) {
  var vX = x / 100; // Note: x from 0 to 95.047      (Observer = 2°, Illuminant = D65)
  var vY = y / 100; // Note: y from 0 to 100
  var vZ = z / 100; // Note: z from 0 to 108.883
  //
  var vR = (3.2406 * vX) + (-1.5372 * vY) + (-0.4986 * vZ);
  var vG = (-0.9689 * vX) + (1.8758 * vY) + (0.0415 * vZ);
  var vB = (0.0557 * vX) + (-0.2040 * vY) + (1.0570 * vZ);
  //
  var transform = function(v) {
    var res;
    if (v > 0.0031308) {
      res = (1.055 * Math.pow(v, 1 / 2.4)) - 0.055;
    } else {
      res = 12.92 * v;
    }
    return res;
  }
  //
  var r = Math.floor(clamp(255 * transform(vR), 0, 255));
  var g = Math.floor(clamp(255 * transform(vG), 0, 255));
  var b = Math.floor(clamp(255 * transform(vB), 0, 255));
  return displayRgb(r, g, b);
}

function LabToRgb(l, a, b) {
  var vY = (l + 16) / 116;
  var vX = vY + (a / 500);
  var vZ = vY - (b / 200);
  //
  var transform = function(v) {
    var res;
    var v3 = v * v * v;
    if (v3 > 0.008856) {
      res = v3;
    } else {
      res = (v - (16 / 116)) / 7.787;
    }
    return res;
  }
  // Observer= 2°, Illuminant= D65
  var x =  95.047 * transform(vX);
  var y = 100.000 * transform(vY);
  var z = 108.883 * transform(vZ);
  return xyzToRgb(x, y, z);
}

function LCHabToRgb(color) {
  var l = color.luminosity; // 0..100
  var c = color.saturation; // 0..100
  var h = color.hue; // 0..360
  //
  var a = c * Math.cos(h * Math.PI / 180);
  var b = c * Math.sin(h * Math.PI / 180);
  return LabToRgb(l, a, b);
}

function LuvToRgb(l, u, v) {
  var vY;
  if (l > 8) {
    vY = Math.pow((l + 16) / 116, 3);
  } else {
    vY = l * Math.pow(3 / 29, 3);
  }

  // Observer= 2°, Illuminant= D65
  var rX =  95.047;
  var rY = 100.000;
  var rZ = 108.883;

  var rU = (4 * rX) / (rX + (15 * rY) + (3 * rZ));
  var rV = (9 * rY) / (rX + (15 * rY) + (3 * rZ));

  var vU = rU + (u / (13 * l));
  var vV = rV + (v / (13 * l));

  var y = 100 * vY;
  var x = y * ((9 * vU) / (4 * vV));
  var z = y * ((12 - (3 * vU) - (20 * vV)) / (4 * vV));

  return xyzToRgb(x, y, z);
}

function LCHuvToRgb(color) {
  var l = color.luminosity; // 0..100
  var c = color.saturation; // 0..100
  var h = color.hue; // 0..360
  //
  var u = c * Math.cos(h * Math.PI / 180);
  var v = c * Math.sin(h * Math.PI / 180);
  return LuvToRgb(l, u, v);
}

// Choose the first color
function pickInitialColor() {
  var result = new Color();
  result.hue = 360 * Math.random();
  result.saturation = 60 + (30 * Math.random());
  result.luminosity = 60 + (30 * Math.random());
  return result;
}

function lerp(a, b, t) {
  return a + t * (b - a);
}

function onWheel(val) {
  if (val < 0) {
    return onWheel(val + 360);
  } else if (val >= 360) {
    return onWheel(val - 360);
  } else {
    return val;
  }
}

// Gradient between complementaries colors
function algorithmGradient(initialColor) {
  var scheme = new ColorScheme();
  scheme.color1 = initialColor;
  scheme.color5.hue = onWheel(scheme.color1.hue + 180);

  scheme.color5.saturation = 100 - scheme.color1.saturation;
  scheme.color5.luminosity = 100 - scheme.color1.luminosity;
  var directHue = pickBoolean();
  var minHue = scheme.color1.hue;
  var maxHue = scheme.color5.hue;
  var minSat = scheme.color1.saturation;
  var maxSat = scheme.color5.saturation;
  var minLum = scheme.color1.luminosity;
  var maxLum = scheme.color5.luminosity;
  var computeColor = function(color, step) {
    if (directHue) {
      color.hue = onWheel(lerp(minHue, maxHue, step));
    } else {
      color.hue = onWheel(lerp(maxHue, minHue, 1 - step));
    }
    color.saturation = lerp(minSat, maxSat, step);
    color.luminosity = lerp(minLum, maxLum, step);
  }
  computeColor(scheme.color2, 0.25);
  computeColor(scheme.color3, 0.5);
  computeColor(scheme.color4, 0.75);

  return scheme;
}

function pickValueAnalog(x) {
  var a = 50 + x / 2;
  var b = 3 * x / 2 - 50;
  var mn = Math.max(50, Math.min(a, b));
  var mx = Math.min(90, Math.max(a, b));
  return lerp(mn, mx, Math.random());
}

function pickValueComplementary(x) {
  var a = x / 4;
  var b = 3 * x / 4;
  var mn = Math.max(a, 25);
  var mx = Math.min(b, 50);
  return lerp(mn, mx, Math.random());
}

// initial, 2 analogs, 2 complementaries
function algorithmFiveHues(initialColor) {
  var scheme = new ColorScheme();
  scheme.color2 = initialColor;
  var analogDistance = 25 + Math.floor(25 * Math.random());
  scheme.color1.hue = onWheel(scheme.color2.hue + analogDistance);
  scheme.color3.hue = onWheel(scheme.color2.hue - analogDistance);
  scheme.color4.hue = onWheel(scheme.color2.hue + (analogDistance / 2) + 180);
  scheme.color5.hue = onWheel(scheme.color2.hue - (analogDistance / 2) + 180);
  /**/
  scheme.color1.saturation = pickValueAnalog(scheme.color2.saturation);
  scheme.color3.saturation = pickValueAnalog(scheme.color2.saturation);
  scheme.color1.luminosity = pickValueAnalog(scheme.color2.luminosity);
  scheme.color3.luminosity = pickValueAnalog(scheme.color2.luminosity);
  if (pickBoolean()) {
    scheme.color4.saturation = pickValueComplementary(scheme.color2.saturation);
    scheme.color4.luminosity = pickValueComplementary(scheme.color2.luminosity);
  } else {
    scheme.color4.saturation = pickValueAnalog(scheme.color2.saturation);
    scheme.color4.luminosity = pickValueAnalog(scheme.color2.luminosity);
  }
  if (pickBoolean()) {
    scheme.color5.saturation = pickValueComplementary(scheme.color2.saturation);
    scheme.color5.luminosity = pickValueComplementary(scheme.color2.luminosity);
  } else {
    scheme.color5.saturation = pickValueAnalog(scheme.color2.saturation);
    scheme.color5.luminosity = pickValueAnalog(scheme.color2.luminosity);
  }

  return scheme;
}

// Create a new color scheme
function newScheme() {
  // Pick a color
  var firstColor = pickInitialColor();
  // choose a scheme generator algorithm
  var algorithm = pickFrom([algorithmGradient, algorithmFiveHues, algorithmFiveHues, algorithmFiveHues, algorithmFiveHues]);
  //
  return algorithm(firstColor);
}

// Entry point
function generateScheme() {
  var scheme = newScheme();
  updateBlock('hsl', scheme, hslToRgb);
  updateBlock('hsv', scheme, hsvToRgb);
  updateBlock('lchab', scheme, LCHabToRgb);
  updateBlock('lchuv', scheme, LCHuvToRgb);
}

// Update a block
function updateBlock(blockId, scheme, colorFun) {
  updateColor('color-1-' + blockId, scheme.color1, colorFun);
  updateColor('color-2-' + blockId, scheme.color2, colorFun);
  updateColor('color-3-' + blockId, scheme.color3, colorFun);
  updateColor('color-4-' + blockId, scheme.color4, colorFun);
  updateColor('color-5-' + blockId, scheme.color5, colorFun);
  displayWheel('wheel.' + blockId, colorFun, 50);
}

// Update the background (and foreground) color of element
function updateColor(id, color, func) {
  var elem = document.getElementById(id);
  var value = func(color);
  elem.style.background = value;
  elem.innerHTML = value;
  if (color.luminosity < 45) {
    elem.style.color = 'white';
  } else {
    elem.style.color = 'black';
  }
}

// Display the wheel at sat = 100
function displayWheel(id, colorFun, lum) {
  var elem = document.getElementById(id);
  var contents = '';
  var col = new Color();
  col.saturation = 100;
  col.luminosity = lum;
  for (i = 0; i < 359; i += 2) {
    col.hue = i;
    contents += '<span style="display:inline-block;width:1px;height:20px;background:';
    contents += colorFun(col);
    contents += ';"></span>';
  }
  elem.innerHTML = contents;
}
