/*
 * Using a neural network, determine whether two colors are an aesthetically
 * pleasing combination
 * 
 * Copyright (C) 2008 Gregor Richards
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

function hexToR(h) {return parseInt((cutHex(h)).substring(0,2),16)}
function hexToG(h) {return parseInt((cutHex(h)).substring(2,4),16)}
function hexToB(h) {return parseInt((cutHex(h)).substring(4,6),16)}
function cutHex(h) {return (h.charAt(0)=="#") ? h.substring(1,7):h}

function NeuralNet(dims, nodes, ivals) {
    var i = 0;
    this.vals = new Array(dims-1);

    // make all the dimensions
    for (var dim = 1; dim < dims; dim++) {
        var curdim = this.vals[dim-1] = new Array(nodes[dim]);

        // make all the nodes
        for (var n = 0; n < curdim.length; n++) {
            var curnode = curdim[n] = new Array(nodes[dim-1]+1);

            // make all the values
            for (var ni = 0; ni < curnode.length; ni++) {
                curnode[ni] = ivals[i];
                i++;

            }

        }

    }
}

// calculate the value of this neural net for a given input
NeuralNet.prototype.calculate = function(inp) {
    var results = new Array(this.vals.length + 1);
    results[0] = inp;

    // go dimension-by-dimension
    for (var dim = 1; dim < results.length; dim++) {
        var curdim = this.vals[dim-1];
        var curinp = results[dim-1];
        results[dim] = new Array(curdim.length);

        // now for each node ...
        for (var n = 0; n < curdim.length; n++) {
            var curnode = curdim[n];

            // calculate the result
            var res = 0.0;

            // from the inputs
            var i;
            for (i = 0; i < curnode.length - 1; i++) {
                res += curnode[i] * curinp[i];
            }
            // and the constant
            res += curnode[i];

            // all fed through a sigmoid function
            res = 1.0 / (1.0 + Math.pow(Math.E, -res));

            results[dim][n] = res;
        }
    }

    // the result is just the first value in the last dimension
    return results[results.length-1][0];
}

var colormatchGeneration = 4;
var masterNN = new NeuralNet(3, [18, 4, 1],
[491.6114739961, 107.6452489844, 82.5035763468, 741.4981981021, -702.9926128157, 437.3216187924, -253.8267418984, 560.1137081697, 322.1755892160, 191.3897104844, -473.1349215245, 230.8042509018, 389.4456751317, -220.3305724054, 415.1122941461, -437.4858451931, -690.2769757668, -70.7771472495, -222.2075287235, -192.3989475780, 249.6397373475, 460.6033940009, -557.2950053209, 164.3738787096, -711.7808701521, -950.4131438910, -4.8465360177, 34.7202654753, 262.7865674563, 99.7697372199, 202.3317755834, -180.1238482888, -338.8261109285, -309.4317345506, 747.4856725748, -144.1410309994, -79.1648989518, 88.2741421830, -70.4584366480, 208.3399647121, 186.6216600911, 393.6884273970, -25.3014300287, -187.4150309892, 880.4838419777, 23.9420037656, 201.6913739340, 303.1553269441, -74.5287639193, -66.7634272195, -86.1155896113, 15.6382627286, 203.9047680036, -890.1282032766, -16.4832456192, -204.4363577744, 348.7937971435, -307.0254746631, 116.8926920735, -71.2735916747, 241.2009836783, -26.6167236670, 224.5658788491, 579.5442774216, 73.5455642043, -154.3540093825, 343.6016872790, -618.7843625640, -660.2672750294, -253.1569257945, -89.3470815940, -94.3513189639, -597.1503544290, -149.0831725437, 29.5438018021, -214.5985141081, -388.3398239893, 234.3663841729, 147.4154632719, 122.3987208010, -157.8645778652, 0.0]
);

// check if two colors match
function checkColors(c1, c2)
{
    var res = masterNN.calculate(c1.concat(c2));
    return res;
}

// convert a hex number to a CM color
function hexToCM(hc)
{
    var r = hexToR(hc) / 255.0;
    var g = hexToG(hc) / 255.0;
    var b = hexToB(hc) / 255.0;
    var rgb = [r, g, b];

    // make HSV ...
    var hsv = rgbToHSV(rgb);

    // and Lab ...
    var lab = rgbToLAB(rgb);

    // then merge them
    return rgb.concat(hsv, lab);
}

// random hex string of the given length
function randHex(len)
{
    if (len == 0) {
        return "";
    } else {
        var ret = "";

        var rnd = Math.floor(Math.random() * 16);

        if (rnd < 10) {
            ret += rnd;
        } else {
            ret += String.fromCharCode(55+rnd);
        }

        ret += randHex(len-1);

        return ret;
    }
}

// random color in hex format
function randomColor()
{
    return "#" + randHex(6);
}

/* given a color in CM format, create a random matching color in [hex, cm]
 * format. If 'match' is true, create a matching color, if 'match' is false,
 * create a non-matching color */
function randomColorMatch(c1, match)
{
    var c2h, c2c;

    var i;
    for (i = 0; i < 100; i++) {
        c2h = randomColor();
        c2c = hexToCM(c2h);
        var ret = checkColors(c1, c2c);

        if (match) {
            if (ret > 0.5) return [c2h, c2c];
        } else {
            if (ret < 0.5) return [c2h, c2c];
        }
    }

    return false;
}

/* generate an n-ary color scheme given some input colors, returns false if it
 * fails. If 'page' is true, will generate colors such that the first is a dark
 * background and the rest are light text colors. Will retry on failure
 * 'retry'-many times. If page is set, will generate 'background'-many
 * background colors */
function randomColorScheme(cs, n, page, retry, background)
{
    if (page === undefined) page = false;
    if (retry === undefined) retry = 100;
    if (background === undefined) background = 1;

    for (; retry >= 0; retry--) {
        // if cs.length == n, we're done
        if (cs.length == n)
            return cs;

        var cnh, cnc;

        var i, j;
        for (i = 0; i < 100; i++) {
            cnh = randomColor();
            cnc = hexToCM(cnh);

            // make sure the value is OK
            if (page) {
                if (background > 0 && cs.length == 0 && cnc[4] > 0.20 && cnc[6] > 5) {
                    continue;

                } else if (cs.length > 0 && cs.length < background &&
                           ((cnc[4] > 0.20  && cnc[6] > 20) ||
                            (cnc[4] <= 0.20 && cs[0][1][4] > 0.20) ||
                            (cnc[6] <= 20   && cs[0][1][6] > 20))) {
                    /* this is perhaps a good background, but doesn't go with
                     * the current background */
                    continue;

                } else if (cs.length >= background && Math.abs(cnc[6] - cs[0][1][6]) <= 60) {
                    continue;

                }
            }

            var match = true;
            for (j = 0; j < cs.length; j++) {
                var ret = checkColors(cs[j][1], cnc);
                if (ret <= 0.5) {
                    match = false;
                    break;
                }
            }
        
            if (match) {
                // recurse
                var sch = randomColorScheme(cs.concat([[cnh, cnc]]), n, page, 0, background);
                if (sch != false) return sch;
            }
        }

        // fail!
    }

    return false;
}

/* generate a page color scheme with n-many colors, background-many of which
 * are background colors */
function randomPageColorScheme(n, background)
{
    if (n === undefined) n = 3;
    return randomColorScheme([], n, true, 100, background);
}

/* assign a randomly-generated color scheme with background-many background
 * colors to the current page */
function assignPageColorScheme(to, background)
{
    if (background === undefined) background = 1;

    if (to != false) {
        if (document.body === null) {
            // we must be in the head, write a style tag
            document.write("<style type='text/css'>\nbody{" +
                    "background-color:" + to[0][0] +
                    ";color:" + to[background][0] +
                    ";}\na:link{color:" + to[background+1][0] +
                    ";}\na:visited{color:" + to[background+1][0] + "}</style>");

        } else {
            document.body.style.backgroundColor = to[0][0];
            document.body.style.color = to[background][0];

            // update all links
            for (i = 0; i < document.links.length; i++) {
                document.links[i].style.color = to[background+1][0];
            }

        }
    }

    return to;
}

/* generate and cache page color schemes for 'days' days, perhaps forcing it,
 * with the given number of background colors and foreground colors */
function autoScheme(force, days, background, foreground)
{
    if (force === undefined) force = false;
    if (days === undefined) days = 1;
    if (background === undefined) background = 1;
    if (foreground === undefined) foreground = 2;

    // make a cookie name for the color scheme
    var cname = "colorScheme";
    if (background != 1)
        cname += background;
    cname += "=";

    var sch;

    // check if it's already cached
    var c_start = document.cookie.indexOf(cname);
    if (c_start == -1 || force) {
        // generate a new one
        sch = randomPageColorScheme(background+foreground, background);
        if (sch == false) {
            // bad!
            return;
        }

        // make a cookie
        var date = new Date();
        date.setDate(date.getDate() + days);
        var c = cname + "[";
        for (var i = 0; i < sch.length; i++) {
            if (i != 0) c += ",";
            c += "[\"" + sch[i][0] + "\"]";
        }
        c += "]; expires=" +
            date.toGMTString() + "; path=/";
        document.cookie = c;

    } else {
        c_start += cname.length; // length of "colorScheme="
        var c_end = document.cookie.indexOf(";", c_start);
        if (c_end == -1) c_end = document.cookie.length;

        // the scheme is in the cookie
        sch = eval(document.cookie.substring(c_start, c_end));

    }

    // now assign it
    assignPageColorScheme(sch, background);

    return sch;
}

// some conversion functions
/// Convert an RGB value to HSV
function rgbToHSV(rgb)
{
    var hsv = new Array(3);

    var rgbmax, rgbmin;

    if (rgb[0] > rgb[1]) {
        if (rgb[0] > rgb[2]) {
            rgbmax = rgb[0];

            if (rgb[1] > rgb[2]) {
                rgbmin = rgb[2];
            } else {
                rgbmin = rgb[1];
            }
        } else {
            rgbmax = rgb[2];
            rgbmin = rgb[1];
        }
    } else {
        if (rgb[1] > rgb[2]) {
            rgbmax = rgb[1];

            if (rgb[0] > rgb[2]) {
                rgbmin = rgb[2];
            } else {
                rgbmin = rgb[0];
            }
        } else {
            rgbmax = rgb[2];
            rgbmin = rgb[0];
        }
    }

    // V = max(R,G,B)
    hsv[2] = rgbmax;

    // S = (V-min(R,G,B))/V if V!=0, 0 otherwise
    if (rgbmax != 0) {
        hsv[1] = (rgbmax-rgbmin)/rgbmax;
    } else {
        hsv[1] = 0;
    }

    /* H =
     *     (G - B)/6/S if V = R
     *     1/2+(B - R)/6/S if V = G
     *     2/3+(R - G)/6/S if V = B */
    if (hsv[1] != 0) {
        if (rgbmax == rgb[0]) {
            hsv[0] = (rgb[1] - rgb[2]) / 6.0 / hsv[1];
        } else if (rgbmax == rgb[1]) {
            hsv[0] = (1.0/2.0) + (rgb[2] - rgb[0]) / 6.0 / hsv[1];
        } else {
            hsv[0] = (2.0/3.0) + (rgb[0] - rgb[1]) / 6.0 / hsv[1];
        }
    } else {
        hsv[0] = 0.0;
    }

    return hsv;
}

// Algorithms below from http://www.easyrgb.com/index.php?X=MATH

/// RGB colorspace to XYZ colorspace
function rgbToXYZ(rgb)
{
    var var_R, var_G, var_B;
    var_R = rgb[0];
    var_G = rgb[1];
    var_B = rgb[2];

    if ( var_R > 0.04045 ) var_R = Math.pow( ( var_R + 0.055 ) / 1.055, 2.4 );
    else                   var_R = var_R / 12.92;
    if ( var_G > 0.04045 ) var_G = Math.pow( ( var_G + 0.055 ) / 1.055, 2.4 );
    else                   var_G = var_G / 12.92;
    if ( var_B > 0.04045 ) var_B = Math.pow( ( var_B + 0.055 ) / 1.055, 2.4 );
    else                   var_B = var_B / 12.92;

    //Observer. = 2°, Illuminant = D65
    var X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805;
    var Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722;
    var Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505;
    return [X, Y, Z];
}

/// XYZ colorspace to LAB colorspace
function xyzToLAB(xyz)
{
    var var_X = xyz[0] / 0.95047;          //ref_X =  95.047   Observer= 2°, Illuminant= D65
    var var_Y = xyz[1] / 1.00000;          //ref_Y = 100.000
    var var_Z = xyz[2] / 1.08883;          //ref_Z = 108.883

    if ( var_X > 0.008856 ) var_X = Math.pow(var_X, 1.0/3.0);
    else                    var_X = ( 7.787 * var_X ) + ( 16.0 / 116.0 );
    if ( var_Y > 0.008856 ) var_Y = Math.pow(var_Y, 1.0/3.0);
    else                    var_Y = ( 7.787 * var_Y ) + ( 16.0 / 116.0 );
    if ( var_Z > 0.008856 ) var_Z = Math.pow(var_Z, 1.0/3.0);
    else                    var_Z = ( 7.787 * var_Z ) + ( 16.0 / 116.0 );

    var L = ( 116 * var_Y ) - 16;
    var a = 500 * ( var_X - var_Y );
    var b = 200 * ( var_Y - var_Z );

    return [L, a, b];
}

/// RGB to LAB (via XYZ)
function rgbToLAB(rgb)
{
    return xyzToLAB(rgbToXYZ(rgb));
}
