<?php
/*  wrapper for curl, sends a single parameter to the esp8266, no attempt is made to interpret
 *  user data, except that we protect curl from possible injection.  Because protection is heavy
 *  handed, end-user data can easily be mangled, as in, if it isn't formatted right it is going to
 *  be forced through a correctly formatted die
 */
function curl($postParam, $len, $route, $varName = "var") {
    $uri = "http://rain-gutter-rgb.lan.rome7.com/{$route}";
    $unsafeUserData = "";
    // start with an empty, off, or "zeroed" buffer
    for($i = $len; $i--;) $unsafeUserData .= "0";
    // [!] protect curl from injection, only allow hexidecimal digits 0..9a..fA..F]
    for($i = $len; $i--; ) $unsafeUserData[$i] = (isset($_POST[$postParam][$i]) && ctype_xdigit($_POST[$postParam][$i]))
        ? $_POST[$postParam][$i]
        : '0';
    $ch = curl_init();
    error_log("sending [{$postParam}] '{$unsafeUserData}' to {$uri}");
    curl_setopt($ch, CURLOPT_URL, $uri);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, "{$postParam}={$unsafeUserData}");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $out = curl_exec($ch);
    curl_close($ch);
    return $out;
}

/*  check for a POST request and use curl to send data to the esp8266
 *  default response is http bad request
 *  passthru contains a simple description of the esp8266 api
 */
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $code = 400; $out = "";
    $passthru = array(
        // POST_VARIABLE => [buffer-length, api-endpoint]
        "color" => array(6, "rgb"),
        "brightness" => array(2, "brightness")
    ); 
    // execute the first matching command
    foreach($passthru as $k => $v) {
        if(!isset($_POST[$k])) continue; $out = curl($k, $v[0], $v[1]); $code = 200; break; }
    // set headers
    header("HTTP/1.0 " . strval($code) . (200 === $code ?" OK" : " Bad Request"));
    $GLOBALS["http_response_code"] = $code;
    //http_response_code($code);
    exit("{$out}");
} /* else GET request, check for quick response parameters -- do not generate whole page */ {
    if(isset($_GET['req'])) {
        // default is to get color as decimal [0..255] values R,G,B
        $req = "dec";
        switch($_GET['req']) {
            case "color": $req = "dec"; break;
            case "brightness": $req = "brightness"; break;
            case "correction": $req = "cfg"; break;
        }
        $uri = "http://rain-gutter-rgb.lan.rome7.com/{$req}";
        // send GET request to the esp8266
        $ch = curl_init();
        error_log("sending [get: {$req}] via {$uri}");
        curl_setopt($ch, CURLOPT_URL, $uri);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $out = curl_exec($ch);
        curl_close($ch);
        exit($out);
    }
}

// otherwise - generate a whole web-page
echo "<html>";
echo "<body>";
// either use mixbox non-commercial use under the CC BY-NC 4.0 license
echo "<script src='mixbox.js'></script>";
// or the spectral.js MIT license
echo "<script src='spectral.js'></script>";
echo "
<script>
// example mixing two colors
var rgb1 = 'rgb(0, 33, 133)';  // blue
var rgb2 = 'rgb(252, 211, 0)'; // yellow
var t = 0.5;                   // mixing ratio
var mixed  = mixbox.lerp(rgb1, rgb2, t);
console.log(mixed);

// example mixing multiple colors
var z1 = mixbox.rgbToLatent(rgb1);
var z2 = mixbox.rgbToLatent(rgb2);
var z3 = mixbox.rgbToLatent(mixed);
var zMix = new Array(mixbox.LATENT_SIZE);
for (var i = 0; i < zMix.length; i++) { // mix:
    zMix[i] = (0.3*z1[i] +       // 30% of rgb1
               0.6*z2[i] +       // 60% of rgb2
               0.1*z3[i]);       // 10% of mixed
}
var rgbMix = mixbox.latentToRgb(zMix);
console.log(rgbMix);

// using spectral.js to mix two colors (does not seem to be working..., returns [0,0,0])
var color = spectral.mix('[0, 33, 133]', '[252, 211, 0]', 0.5, spectral.RGB);
console.log(color);

</script>
";

// common functions
echo "
<script>
// using the newer fetch API, not sure if this is worse than AJAX
async function post(url = '', kw = 'key', data = 'data') {
    // to use _POST superblogal, send body as http multipart/form-data
    let fd = new FormData();
    fd.append(kw, data);
    return await fetch(url, { method: 'POST', body: fd });
}
/*  I assume http get requests may use default
 *  [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
 *  parameters.
 */
function get(arg) { return fetch('?' + new URLSearchParams({req:arg})).then((v) => v.text()); }
function getColor(selectedObj = null) {
    // get the color from the esp8266, returns as decimal [0..255] R,G,B
    let dec = get('color');
    // split into red, green, and blue components:
    let rgb = dec.then((v) => { let a = v.split(',').map((i) => parseInt(i, 10)); return {red: a[0], green: a[1], blue: a[2]}; });
    rgb.then((v) => console.log('esp8266 color: ', v));
    // by default, return as Promise that will resolve to rgb object { red: x, green: y, blue: z }
    if(null === selectedObj) return rgb;
    // otherwise set backround and text of the selected color object
    rgb.then((v) => {
        let a = [v.red, v.green, v.blue];
        color = 'rgb(' + a.join(',') + ')';
        let off = a.every((x) => 0 === x);
        // use gray as a placeholder for 'lights off'
        selectedObj.style.background = off ?'rgb(211, 211, 211)' :color;
        selectedObj.textContent = color;
    });
}
function getBrightness(sliderObj = null) {
    // get the brightness from the esp8266, returns as decimal [0..15]
    let brightness = get('brightness').then((v) => parseInt(v,10));
    brightness.then((i) => { console.log('esp8266 brightness: ', i); });
    // by default, return as Promise that will resolve to integer
    if(null === sliderObj) return brightness;
    // otherwise set slider object's value property to brightness when the promise resolves
    brightness.then((i) => { sliderObj.value = i; });
}
function getCorrection() {
    /*  get the correction config from the esp8266, returns as cfg [ setting:[0..1], ... setting_n:[0..1]]
     *  settings currently include:
     *      quickGamma - corrects for human perception using quadratic function
     *      sCurveGamma - corrects for human perception using sigmoid function
     *      colorCorrection - LED manufacturer correction, differences between doping used for different colors
     *
     *  + quickGamma and sCurveGamma can be turned on or off and are are mutually
     *    exclusive to eachother
     *  + colorCorrection may be turned on or off, and can be applied to either
     *    gamma correction schemes
     */
    var cfg = get('correction');
    return cfg;
}
</script>
";

// render a color selection wheel
echo "
<script>
var canvas = document.createElement('canvas');
// google chrome performance flag
//var context = canvas.getContext('2d');
var context = canvas.getContext('2d', { willReadFrequently: true });
var w = 400; var h = 400;
canvas.width = w;
canvas.height = h;

for(var i = 0; i < 360; i += 0.1) {
    var rad = i * (2 * Math.PI) / 360;
    var x1 = w/2 + w/2 * Math.cos(rad);
    var y1 = h/2 + h/2 * Math.sin(rad);
    var gradient = context.createLinearGradient(w/2, h/2, x1, y1);
    gradient.addColorStop(0.0, 'white');
    gradient.addColorStop(1.0, 'hsla('+i+', 100%, 50%, 1.0)');
    context.strokeStyle = gradient;
    context.beginPath();
    context.moveTo(w/2, h/2);
    context.lineTo(x1, y1);
    context.stroke();
}

document.body.appendChild(canvas);

</script>
";

// simple color picker
echo "
<script>
function tableData(c, head = false) {
  let td = document.createElement(head ?'th' :'td');
  if(typeof c === 'string' || c instanceof String) td.innerText = c;
  else td.appendChild(c);
  return td;
}
// create a table
var table = document.createElement('table');
var tableHead = document.createElement('thead');
table.appendChild(tableHead);
var tableHeadRow = document.createElement('tr');
tableHead.appendChild(tableHeadRow);
var tableBody = document.createElement('tbody');
table.appendChild(tableBody);
var tableBodyRow = document.createElement('tr');
tableBody.appendChild(tableBodyRow);
// add some headers
tableHeadRow.appendChild(tableData('', true));
tableHeadRow.appendChild(tableData('hover', true));
tableHeadRow.appendChild(tableData('selected', true));
// add table data
tableBodyRow.appendChild(tableData(canvas));
hovered = tableData('');       hovered.id = 'hovered-color';
tableBodyRow.appendChild(hovered);
selected = tableData('');    selected.id = 'selected-color';
tableBodyRow.appendChild(selected);

document.body.appendChild(table);

// set the inital value of the selected color cell to the esp8266 lights
getColor(selected);

// listen to events from canvas
function pick(event, destination, cb = false) {
  const bounding = canvas.getBoundingClientRect();
  const x = event.clientX - bounding.left;
  const y = event.clientY - bounding.top;
  const pixel = context.getImageData(x, y, 1, 1);
  const data = pixel.data;
  //console.log(pixel, data);

  const rgb = 'rgb(' + data[0] + ', ' + data[1] + ', ' + data[2] + ')';
  let off = 0 === (data[0] | data[1] | data[2]);
  // use 211,211,211 (gray) as a placeholder for 'lights turned off'
  destination.style.background = off ?'rgb(211, 211, 211)' :rgb;
  destination.textContent = rgb;

  // handle optional callback
  if(false !== cb) cb(data[0], data[1], data[2]);

  return rgb;
}

canvas.addEventListener('mousemove', (event) => pick(event, hovered));
canvas.addEventListener('click', (event) => pick(event, selected));

</script>
";

// add a callback to the click event to send rgb value to esp8266
echo "
<script>
function sg(x) { return Math.round((x*x)/255); }
function callback(red, green, blue) {
    console.log('logged', red, green, blue);
    post(
        /* url */ '',
        /* key */ 'color',
        sg(red).toString(16).padStart(2,'0') +
        sg(green).toString(16).padStart(2,'0') +
        sg(blue).toString(16).padStart(2,'0')
    ).then((data) => { console.log(data, data.text()); });
}
canvas.removeEventListener('click', (event) => pick(event, selected));
canvas.addEventListener('click', (event) => pick(event, selected, callback));

</script>
";

/*  make a brightness slider, esp8266 takes 0..15 levels of brightness
 *  zero is off and should be ignored
 *
 *  currently brightness control is very simple and operates without
 *  modifying end-user color choices
 *
 *  tldr;  users have 24bit color selectivity, in hardware we have a
 *  little over 1.5 bytes per color giving us somewhere near 37bit color
 *  selectivity.  so a user can select 0a3bcc as their color choise and
 *  we can have thousands of shades of colors that are close to that.
 *  The power section of the esp8266 is not optimal so the esp8266 will
 *  limit running the LEDs at maximum current.  This limits the color-space
 *  by about 20%.  The way we implement brightness is to cut give the end
 *  user 24-bits of color selectivity, that fits into 1/20th of the color-space
 *  then we can multiply those values linearly 1..15 before we hit that 80%
 *  power limit.  because we are amplifying colors evenly the selected output
 *  color will not change.
 */
echo "
<script>
var slider = document.createElement('input');
slider.type = 'range';
slider.min = 1;
slider.max = 15;
slider.value = 7;
slider.step = 1;
document.body.appendChild(slider);

// set the inital value of the slider to whatever the current brightness value
getBrightness(slider);

function slid(event, cb = false) {
  if(false !== cb) cb(slider.value);
  console.log('changed slider to ' + slider.value);
  return 'brightness(' + slider.value + ')';
}

slider.addEventListener('change', (event) => slid(event));
</script>
";
// add a callback to the off-click event on the slider
echo "
<script>
function slidercb(valu) {
    console.log('logged', valu);
    post('', 'brightness', parseInt(valu, 10).toString(16).padStart(2,'0')).then((data) => { console.log(data, data.text()); });
}
slider.removeEventListener('change', (event) => slid(event));
slider.addEventListener('change', (event) => slid(event, slidercb));

</script>
";

echo "</body>";
echo "</html>";
