#!/usr/bin/env kjscmd
/*************************************************************************
    Copyright notice

    Copyright 2009 Franck Qulain <shift (at) free.fr>
    All rights reserved

    This file is part of KjsCAC.

    KjsCAC is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.

    KjsCAC is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with KjsCAC.  If not, see <http://www.gnu.org/licenses/>.
***************************************************************************/

/* Constants for dichromat calculations */
const RGBtoLMS = new Array(new Array(0.05059983, 0.08585369, 0.00952420),
                        new Array(0.01893033, 0.08925308, 0.01370054),
                        new Array(0.00292202, 0.00975732, 0.07145979));
const LMStoRGB = new Array(new Array(30.830854, -29.832659, 1.610474),
                        new Array(-6.481468, 17.715578, -2.532642),
                        new Array(-0.375690, -1.199062, 14.273846));
const Gamma = new Array(2.1 , 2.0, 2.1);

const LE = 0.14597772;
const ME = 0.12188395;
const SE = 0.08413913;

const DICHROMAT_TYPE_NORMAL = "";
const DICHROMAT_TYPE_PROTANOPIA = "_prot";
const DICHROMAT_TYPE_DEUTERANOPIA = "_deut";
const DICHROMAT_TYPE_TRITERANOPIA = "_trit";

const BRIGHTNESS_TRESHOLD = 125;
const COLOR_TRESHOLD = 500;

// Load GUI
var win = Factory.loadui("ressources/kjsCAC.ui", this, win);

var ExitAction = win.child('fileExitAction');
var AboutAction = win.child('helpAboutAction');
var ForegroundColorButton = win.child('foregroundColorButton');
var BackgroundColorButton = win.child('backgroundColorButton');

// Connect action and widget
ExitAction.connect(ExitAction, 'activated()', this, 'exit');
AboutAction.connect(AboutAction, 'activated()', this, 'about');
ForegroundColorButton.connect(ForegroundColorButton, 'changed(const QColor&)', this, 'setForegroundColor');
BackgroundColorButton.connect(BackgroundColorButton, 'changed(const QColor&)', this, 'setBackgroundColor');

// Initialise the colors
var foregroundColor = '#000000';
var backgroundColor = '#ffffff';
setForegroundColor(foregroundColor);
setBackgroundColor(backgroundColor);

// Show and execute the application
win.show();
application.exec();

/**
 * Display about dialog box
 */
function about() {
    alert("KjsCAC - Color Accessibility Checker\nVersion 3.0\n\n Franck Qulain (shift at free.fr)");
}

/**
 * Modification of the foreground color
 * @param color Color chosen
 */
function setForegroundColor(color) {
    foregroundColor = color;
    updateResult();
}

/**
 * Modification of the background color
 * @param color Color chosen
 */
function setBackgroundColor(color) {
    backgroundColor = color;
    updateResult();
}

/**
 * Update the result
 */
function updateResult(){

    // background color
    var br = parseInt(backgroundColor.substring(1,3),16);
    var bg = parseInt(backgroundColor.substring(3,5),16);
    var bb = parseInt(backgroundColor.substring(5,7),16);

    // foreground color
    var fr = parseInt(foregroundColor.substring(1,3),16);
    var fg = parseInt(foregroundColor.substring(3,5),16);
    var fb = parseInt(foregroundColor.substring(5,7),16);

    // ------ Normal
    updateResultForDichromatType(br, bg, bb, fr, fg, fb, DICHROMAT_TYPE_NORMAL);

    // ----- Protanopia
    updateResultForDichromatType(br, bg, bb, fr, fg, fb, DICHROMAT_TYPE_PROTANOPIA);

    // ----- Deuteranopia
    updateResultForDichromatType(br, bg, bb, fr, fg, fb, DICHROMAT_TYPE_DEUTERANOPIA);

    // ----- Triteranopia
    updateResultForDichromatType(br, bg, bb, fr, fg, fb, DICHROMAT_TYPE_TRITERANOPIA);
}

/**
 * Update result for dichromat type
 * @param br Background red
 * @param bg Background green
 * @param bb Background blue
 * @param fr Foreground red
 * @param fg Foreground green
 * @param fb Foreground blue
 * @param dichromatType Dichromat type
 */
function updateResultForDichromatType(br, bg, bb, fr, fg, fb, dichromatType) {

    var BrightnessDiffEdit = win.child('brightnessDiffEdit' + dichromatType);
    var ColorDiffEdit = win.child('colorDiffEdit' + dichromatType);
    var ResultEdit = win.child('resultEdit' + dichromatType);
    var DemoLabel = win.child('demoLabel' + dichromatType);

    var backgroundColorConverted = convertDichromatColors(br, bg, bb, dichromatType);
    var foregroundColorConverted = convertDichromatColors(fr, fg, fb, dichromatType);

    DemoLabel.paletteForegroundColor = RGBToColor(foregroundColorConverted[0], foregroundColorConverted[1], foregroundColorConverted[2]);
    DemoLabel.paletteBackgroundColor = RGBToColor(backgroundColorConverted[0], backgroundColorConverted[1], backgroundColorConverted[2]);

    var results = calculateDiff(backgroundColorConverted[0], backgroundColorConverted[1], backgroundColorConverted[2], foregroundColorConverted[0], foregroundColorConverted[1], foregroundColorConverted[2]);

    var brightnessDifference = results[0];
    var colorDifference = results[1];

    BrightnessDiffEdit.text = brightnessDifference;
    ColorDiffEdit.text = colorDifference;

    if ((brightnessDifference >= BRIGHTNESS_TRESHOLD) && (colorDifference >= COLOR_TRESHOLD))   {
        ResultEdit.text = "YES!"; // compliant
        ResultEdit.paletteForegroundColor = "blue";
    }else if ((brightnessDifference >= BRIGHTNESS_TRESHOLD) || (colorDifference >= COLOR_TRESHOLD)){
        ResultEdit.text = "sort of..."; // sort of compliant
        ResultEdit.paletteForegroundColor = "red";
    }else{
        ResultEdit.text = "NO!"; // not compliant "Poor visibility between text and background colors."
        ResultEdit.paletteForegroundColor = "red";
    }
}

/** Calculate differences (color and brightness)
 * @param br Background red
 * @param bg Background green
 * @param bb Background blue
 * @param fr Foreground red
 * @param fg Foreground green
 * @param fb Foreground blue
 * @return Array(brightness difference, color difference)
 */
function calculateDiff(br, bg, bb, fr, fg, fb) {
    var bY=((br * 299) + (bg * 587) + (bb * 114)) / 1000;
    var fY=((fr * 299) + (fg * 587) + (fb * 114)) / 1000;
    var brightnessDifference = Math.abs(bY-fY);

    var colorDifference = (Math.max (fr, br) - Math.min (fr, br)) +
                          (Math.max (fg, bg) - Math.min (fg, bg)) +
                          (Math.max (fb, bb) - Math.min (fb, bb));

    return new Array(brightnessDifference, colorDifference);
}

/**
 * Convert RGB color to RGB color seen by a color blind person
 * @param red Red
 * @param green Green
 * @param blue, Blue
 * @param dichromatType Dichromat type
 * @return Array(r, g, b)
 */
function convertDichromatColors(red, green, blue, dichromatType) {

    var a1, b1, c1, a2, b2, c2, a, t;

    if(dichromatType == DICHROMAT_TYPE_NORMAL) {
        return new Array(red, green, blue);
    }

    red = Math.pow(red / 255, Gamma[0]);
    green = Math.pow(green / 255, Gamma[1]);
    blue = Math.pow(blue / 255, Gamma[2]);

    oldRed = red;
    oldGreen = green;

    red = oldRed * RGBtoLMS[0][0] + oldGreen * RGBtoLMS[0][1] + blue * RGBtoLMS[0][2];
    green = oldRed * RGBtoLMS[1][0] + oldGreen * RGBtoLMS[1][1] + blue * RGBtoLMS[1][2];
    blue = oldRed * RGBtoLMS[2][0] + oldGreen * RGBtoLMS[2][1] + blue * RGBtoLMS[2][2];

    switch(dichromatType) {
        case DICHROMAT_TYPE_PROTANOPIA :
        case DICHROMAT_TYPE_DEUTERANOPIA :
            //575nm
            a1 = -0.0614;
            b1 = 0.0833;
            c1 = -0.0141;
            //475nm
            a2 = 0.0584;
            b2 = -0.0794;
            c2 = 0.0136;

            break;
        case DICHROMAT_TYPE_TRITERANOPIA :
            //660nm
            a1 = 0;
            b1 = 0.0076;
            c1 = -0.011;
            //485nm
            a2 = 0.0254;
            b2 = -0.0416;
            c2 = 0.0163;

            break;
    }

    switch(dichromatType) {
        case DICHROMAT_TYPE_PROTANOPIA :

            if(green > 0) {
                a = blue / green;
            } else {
                a = 0;
            }

            t = SE / ME;

            if(a < t) {
                red = -(b1 * green + c1 * blue) / a1;
            } else {
                red = -(b2 * green + c2 * blue) / a2;
            }

            break;
        case DICHROMAT_TYPE_DEUTERANOPIA :

            if(red > 0) {
                a = blue / red;
            } else {
                a = 0;
            }

            t = SE/LE;

            if(a < t) {
                green = -(a1 * red + c1 * blue) / b1;
            } else {
                green = -(a2 * red + c2 * blue) / b2;
            }

            break;
        case DICHROMAT_TYPE_TRITERANOPIA :

            if(red > 0) {
                a = green / red;
            } else {
                a = 0;
            }

            t = ME/LE;

            if(a < t) {
                blue = -(a1 * red + b1 * green) / c1;
            } else {
                blue = -(a2 * red + b2 * green) / c2;
            }

            break;
    }

    oldRed = red;
    oldGreen = green;
    red = oldRed * LMStoRGB[0][0] + oldGreen * LMStoRGB[0][1] + blue * LMStoRGB[0][2];
    green = oldRed * LMStoRGB[1][0] + oldGreen * LMStoRGB[1][1] + blue * LMStoRGB[1][2];
    blue = oldRed * LMStoRGB[2][0] + oldGreen * LMStoRGB[2][1] + blue * LMStoRGB[2][2];

    if(red > 0) {
        red = 255 * Math.pow(red, 1 / Gamma[0]);
    } else {
        red = 0;
    }

    if(green > 0) {
        green = 255 * Math.pow(green, 1 / Gamma[1]);
    } else {
        green = 0;
    }

    if(blue > 0) {
        blue = 255 * Math.pow(blue, 1 / Gamma[2]);
    } else {
        blue = 0;
    }

    red = Math.floor(ensureRange(red, 0, 255));
    green = Math.floor(ensureRange(green, 0, 255));
    blue = Math.floor(ensureRange(blue, 0, 255));
    return new Array(red, green, blue);
}

/**
 * Ensure that a value is in a range
 * @param value Value to ensure range
 * @param min Minimum range
 * @param max Maximum range
 */
function ensureRange(value, min, max) {
    return Math.max(min, Math.min(value, max));
}

/**
 * Convert color in RGB
 *
 * @param Color #......
 * @return r,g,b array
 */
function colorToRGB(color) {
    var r = parseInt(color.substring(1,3),16);
    var g = parseInt(color.substring(3,5),16);
    var b = parseInt(color.substring(5,7),16);

    return new Array(r, g, b);
}

function RGBToColor(r, g, b) {
    return "#" + decToHex(r) + decToHex(g) + decToHex(b);
}

/**
 * Decimal to hexadecimal conversion.
 *
 * @param dec Decimal number between 0 and 255
 * @return Hexadecimal number in 2 characters
 */
function decToHex(dec) {
    var hexStr = "0123456789ABCDEF";
    var low = dec % 16;
    var high = (dec - low)/16;
    hex = "" + hexStr.charAt(high) + hexStr.charAt(low);
    return hex;
}
