//
// fputils.js
//
// Written by Matthew K. Coughlin
//


(function(){

var window = this;   // Needed for web workers, etc

//=========================================================
//
// FpUtils constructor
//
// Static properties
//   imageCache
//   isHighContrast
//   browser
//
// Static methods:
//   rand(x)
//   mod(i, max)
//   limit(i, max)
//   roundAngle(angle)
//   angleToRadians(angle)
//   swap(a, b, aScope, bScope)
//
//   replace(s, pattern)
//   sort(x)
//
//   getBaseUrl()
//   getUrlParams(url)
//   getKey(e)
//
//   cacheImage(img)
//   createElement(data)
//   detectHighContrast()
//   verifyMouseOutEvent(e, elem)
//
FpUtils = window.FpUtils = window.$fp = function() {}


// Static properties

FpUtils.imageCache = {};

FpUtils.isHighContrast = false;

var appName = navigator.appName.toLowerCase();
var userAgent = navigator.userAgent.toLowerCase();

FpUtils.browser = {
    // Broswer engine
    version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [0,'0'])[1],
    ie: ((appName == "microsoft internet explorer") || (userAgent.indexOf("msie") != -1)),
    gecko: ((userAgent.indexOf("gecko") != -1) && (userAgent.indexOf("like gecko") == -1)),
    webkit: (userAgent.indexOf("webkit") != -1),
    presto: (userAgent.indexOf("presto") != -1),

    // Specific broswer
    ie6: (userAgent.indexOf("msie 6") != -1),
    ie7: (userAgent.indexOf("msie 7") != -1),
    ie8: (userAgent.indexOf("msie 8") != -1),
    firefox: (userAgent.indexOf("firefox") != -1),
    firefoxmac: ((userAgent.indexOf("firefox") != -1) && (userAgent.indexOf('mac') != -1)),
    safari: ((userAgent.indexOf("safari") != -1) && (userAgent.indexOf("chrome") == -1)),
    chrome: (userAgent.indexOf("chrome") != -1),
    opera: (userAgent.indexOf('opera') != -1)
}


// Static methods

//=========================================================
//
// rand(x)
//
// Return value
//   - If x is a number, returns a random number in the range [0 .. x].
//   - If x is an indexed array, returns a random element from the array.
//
FpUtils.rand = function(x) {
    if (typeof x === 'number') {
        var now = new Date();
        var seed = (now.getSeconds() * 1000) + now.getMilliseconds();
        return Math.round(Math.random(seed) * x);
    }
    else if ((typeof x === 'object') && (x.length != undefined)) {
        return x[ FpUtils.rand(x.length - 1) ];
    }
    else { throw Error('Invalid argument in function rand()'); }
}


//===================================================================
//
// mod(i, max)
//
// Modulus function that works for both positive and negative i values.
//
// The result is in the range [0 .. max)
//
// Examples
//   mod(-3, 3)   // 0
//   mod(-2, 3)   // 1
//   mod(-1, 3)   // 2
//   mod( 0, 3)   // 0
//   mod( 1, 3)   // 1
//   mod( 2, 3)   // 2
//   mod( 3, 3)   // 0
//
FpUtils.mod = function(i, max) {
    if ((typeof i !== 'number') || (typeof max !== 'number') || (max === 0)) {
        throw Error('Missing or invalid arguments in function mod()');
    }
    return ((i % max) + max) % max;
}


//===================================================================
//
// limit(i, max)
//
// Limit the lower and upper value for i.
//
// The result is in the range [0 .. max]
//
FpUtils.limit = function(i, max) {
    if ((typeof i !== 'number') || (typeof max !== 'number')) {
        throw Error('Missing or invalid arguments in function limit()');
    }
    return Math.min( Math.max(i, 0), max );
}


//=========================================================
//
// angleToRadians(angle)
//
FpUtils.angleToRadians = function(angle) {
    return angle * (Math.PI / 180.0);
}


//=========================================================
//
// roundAngle(angle)
//
// Bring the angle within the range [0.0 .. 360.0)

FpUtils.roundAngle = function(angle) {
    while (angle < 0.0)    { angle += 360.0; }
    while (angle >= 360.0) { angle -= 360.0; }
    return angle;
}


//=========================================================
//
// swap(a, b, aScope, bScope)
//
// Indirectly swaps two values by reference.
//
// Usage:
//   The first two parameters specify the variables or array indexes to be swapped.
//   Additional parameters specify the scope to use. If the last scope is omitted,
//   the same scope will be used for both.
//
//   Doesn't work for primitives with local scope.
//
// Examples:
//   swap('x', 'y', a);      // Swap:  a.x    a.y
//   swap('x', 'y', a.b);    // Swap:  a.b.x  a.b.y
//   swap('x', 'x', a, b);   // Swap:  a.x    b.x
//   swap(0, 1, a);          // Swap:  a[0]   a[1]
//
FpUtils.swap = function(a, b, aScope, bScope) {
    if ( (a == undefined) || ((b == undefined) || (aScope == undefined)                         // Missing arguments
      || (typeof a !== 'string') || (typeof b !== 'string') || (typeof aScope !== 'object')     // Invalid arguments
      || ((bScope !== undefined) && (typeof bScope !== 'object'))                               
      || (aScope)[a] == undefined) || ((bScope||aScope)[b] == undefined) )                      // Undefined targets
    {
        throw Error('Missing or invalid arguments in function swap()');
    }
    var tmp = (aScope)[a];
    (aScope)[a] = (bScope||aScope)[b];
    (bScope||aScope)[b] = tmp;
}


//=========================================================
//
// replace(s, pattern)
//
// Apply a set of substitutions to a set of strings.
//
// Examples
//   replace('abc', 'a', 'd')                           // 'dbc'
//   replace('abc', ['a','d'])                          // 'dbc'
//   replace('abc', [['a','d'], ['c','f']])             // 'dbf'
//
//   replace(['abc','app'], 'a', 'd')                   // ['dbc','dpp']
//   replace(['abc','app'], ['a','d'])                  // ['dbc','dpp']
//   replace(['abc','uvw'], [['a','d'], ['u','x']])     // ['dbc','xvw']
//
FpUtils.replace = function(s, pattern) {

    // If s is an array, recursively call the function for each element
    if (s instanceof Array) {
        for (var i = 0; i < s.length; i++) { s[i] = FpUtils.replace(s[i], arguments[1], arguments[2]); }
    }
    // If pattern is an array of arrays, recursively call the function for each element
    else if (pattern[0] instanceof Array) {
        for (var i = 0; i < pattern.length; i++) { s = FpUtils.replace(s, pattern[i]); }
    }
    // replace(s, from, to)
    else if (arguments[2] != undefined) {
        s = s.replace(arguments[1], arguments[2]);
    }
    // replace(s, [from, to])
    else {
        s =  s.replace(pattern[0], pattern[1]);
    }
    return s;
}


//=========================================================
//
// sort(x)
//
FpUtils.sort = function(x) {
    if (typeof x === 'string') { return x.split('').sort().join(''); }   // Sort the characters in the string
    else { return x; }
}


//===================================================================
//
// getBaseUrl()
//
// Returns the base URL (minus URL parameters and named anchor).
//
FpUtils.getBaseUrl = function(url) {
    if (!url || (typeof url !== 'string')) { url = window.location.href; }
    var paramLoc = url.indexOf('?');
    var anchorLoc = url.indexOf('#');
    return (paramLoc > -1) ? url.substring(0, paramLoc) : ((anchorLoc > -1) ? url.substring(0, anchorLoc) : url);
}


//===================================================================
//
// getUrlParams(url)
//
FpUtils.getUrlParams = function(url) {
    if (!url || (typeof url !== 'string')) { url = window.location.href; }

    // Remove the anchor
    var params = {}, i = url.indexOf('#');
    if (i != -1) { url = url.substring(0, i); }

    // Extract the full URL parameters
    var i = url.indexOf('?');
    if ((i != -1) && (i < (url.length - 1))) {

        // Isolate each individual URL parameter
        for (var arr = url.substring(i + 1).split('&'), j = 0; j < arr.length; j++) {

            // Separate the parameter into a key-value pair
            if (arr[j].indexOf('=') != -1) { var curParam = arr[j].split('='); params[curParam[0]] = curParam[1]; }
            else { params[arr[j]] = ''; }
        }
    }
    return params;
}


//=========================================================
//
// getKey(e)
//
// Avoid using this function in combination with any library event function (jQuery, etc) that sanitizes
// the event properties. The original unsanitized event properties are needed in some cases for Opera.
//
// Return value properties
//   isPrintable     [true,false]
//   isAlphanumeric  [true,false]
//   isLetter        [true,false]
//   isNumber        [true,false]
//   ifFunctionKey   [true,false]
//   repeated        [true,false]
//   shift           [true,false]
//
//   ch              ['a'-'z','A'-'Z','0'-'9','!','@',...,'']
//   letter          ['a'-'z','']
//   letterOrd       [0-23,'']
//   number          [0-9,'']
//   functionKey     [1-12,'']
//   type            ['number','letter','blankspace','symbol','fkey','left','right','home','end',...,'']
//
FpUtils.getKey = function(e) {
    e = (e == undefined ? event : e);
    if (e.altKey || e.ctrlKey) { return false; }
    if (e.type == 'keyup') { return false; }
    if ((!(FpUtils.browser.ie || FpUtils.browser.webkit)) && (e.type == 'keydown')) { return false; }   // FF, Opera

    var printableChars = '                                 !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~';
    //alert( printableChars.charAt(125) );

    var mapKeyCode = {8:'backspace', 9:'tab', 13:'enter', 27:'esc', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down', 45:'insert', 46:'delete'};

    var key = {isPrintable:false, isAlphanumeric:false, isLetter:false, isNumber:false, isFunctionKey:false,
               shift:e.shiftKey, repeated:false,
               ch:'', letter:'', letterOrd:'', number:'', functionKey:'', type:''};

    // Sanitize which
    var which = ( (!e.which && ((e.charCode || e.charCode === 0) ? e.charCode : e.keyCode)) ? (e.charCode || e.keyCode) : e.which );

    // {   (special handling for left curly brace)
    if ( ((FpUtils.browser.ie || FpUtils.browser.webkit) && (e.type == 'keypress') && (e.keyCode == 123))   // IE, WebKit
      || (FpUtils.browser.opera && (e.which == 123)) ) {                                                    // Opera
        key.isPrintable = true;
        key.ch = '{';
    }
    // F1..F12
    else if ( (e.keyCode >= 112) && (e.keyCode <= 123)
      && ((FpUtils.browser.ie || FpUtils.browser.webkit) == (e.type == 'keydown'))   // IE, WebKit
      && (!(FpUtils.browser.opera && e.which)) ) {                                   // Opera
        key.isFunctionKey = true;
        key.functionKey = e.keyCode - 111;
        key.type = 'fkey';
    }
    // Additional nonprintable characters
    else if ( mapKeyCode[e.keyCode]
      && ((FpUtils.browser.ie || FpUtils.browser.webkit) == (e.type == 'keydown'))   // IE, WebKit
      && (!(FpUtils.browser.opera && e.which && (e.which > 13))) ) {                 // Opera
        key.type = mapKeyCode[e.keyCode];
    }
    // Printable characters
    else if (e.type == 'keypress') {

        // 0..9
        if ((which >= 48) && (which <= 57)) {
            key.isPrintable = key.isAlphanumeric = key.isNumber = true;
            key.number = which - 48;
            key.ch = printableChars.charAt(which);
            key.type = 'number';
        }
        // A..Z
        else if ((which >= 65) && (which <= 90)) {
            key.isPrintable = key.isAlphanumeric = key.isLetter = true;
            key.letterOrd = which - 65;
            key.letter = key.ch = printableChars.charAt(which);
            key.type = 'letter';
        }
        // a..z
        else if ((which >= 97) && (which <= 122)) {
            key.isPrintable = key.isAlphanumeric = key.isLetter = true;
            key.letterOrd = which - 97;
            key.letter = key.ch = printableChars.charAt(which);
            key.type = 'letter';
        }
        // Additional printable characters
        else if ( (which >= 32) && (which <= 126) && ((printableChars.charAt(which) != ' ') || (which == 32)) ) {
            key.isPrintable = true;
            key.ch = printableChars.charAt(which);
            key.type = ((which == 32) ? 'blankspace' : 'symbol');
        }
    }

    // In IE and WebKit, use keypress for printable characters and keydown for nonprintable characters
    // (all other browsers use keypress).
    //
    // Explanation:
    //   * For all browsers, keypress events must be used for alphabetic keys to detect if caps lock is on.
    //   * In IE and WebKit, keypress events aren't fired for nonprintable repeated keys (arrow keys, function keys, ...).
    //   * In Opera on Windows, Firefox on Linux, and Konqueror on Linux, keydown events aren't fired for repeated keys.

    if (FpUtils.browser.ie || FpUtils.browser.webkit) {
        if ((e.type == 'keypress') != key.isPrintable) { return false; }
    }
    return key;
}


//=========================================================
//
// cacheImage(img)
//
FpUtils.cacheImage = function(img) {
    if (img && (typeof img === 'string') && !FpUtils.imageCache[img]) {
        FpUtils.imageCache[img] = new Image();
        FpUtils.imageCache[img].src = img;
    }
    else if (img && (typeof img === 'object')) {
        for (var key in img) { FpUtils.cacheImage(img[key]); }
    }
}


//=========================================================
//
// createElement(data)
//
FpUtils.createElement = function(data) {
    var obj = document.createElement(data.tag);

    // Add attributes
    for (var name in data.attr) {
        var value = data.attr[name];
        if (name == 'class') { obj.className = value; }           // IE6/7
        else                 { obj.setAttribute(name, value); }
    }
    return obj;
}


//=========================================================
//
// detectHighContrast()
//
// Determine if high-contrast mode is being used in Windows
//
FpUtils.detectHighContrast = function() {
    var obj = document.createElement('div');
    var id = 'testHighContrast';
    obj.id = id;
    obj.style.cssText = 'border:1px solid; border-color:red green; position:absolute; height:5px; top:-999px; background-image:url("images/pixel_invis.gif");';
    document.body.appendChild(obj);

    if (document.defaultView != undefined) {
        var curStyle = document.defaultView.getComputedStyle(obj, null);
    }
    else {
        var curStyle = document.getElementById(id).currentStyle;
    }

    var bgImg = curStyle.backgroundImage;
    FpUtils.isHighContrast = (curStyle.borderTopColor == curStyle.borderRightColor) || (bgImg != null && (bgImg == "none" || bgImg == "url(invalid-url:)" ));
    document.body.removeChild(obj);
    return FpUtils.isHighContrast;
}


//=========================================================
//
// verifyMouseOutEvent(e, elem)
//
// Verify that a mouseout event wasn't caused by a child element
//
FpUtils.verifyMouseOutEvent = function(e, elem) {
    e = e || window.event;
    var related = e.relatedTarget || e.toElement;

    while (related != undefined && related != elem && related.nodeName != 'BODY') {
        related = related.parentNode;
    }
    return (related != elem);
}


})();

