/**
 * Copyright ClearWiki Limited 2006
 * @author Paul Tuckey & Paul Wilton
 * @author $Author: paulw $
 * @version $Revision: 3211 $ $Date: 2008-10-12 19:35:16 +0100 (Sun, 12 Oct 2008) $
 *
 * j.js - main javascript lib
 * g functions are for gui
 * l functions are for logging
 *
 */

var gVer = "$Revision: 3211 $";
var gTimers = new Array();
var gIsOnFileSystem = document.URL.indexOf("file:") == 0;


// Static code block that initialises the timezone cookie
// don't do this on local file system
if (! gIsOnFileSystem) {
    // setup timezone cookie so we can read it at server
    var tzExpiry = new Date(new Date().getTime() + 7*24*60*60*1000); //expires in 1 week
    document.cookie = "tz=" + (new Date()).getTimezoneOffset() + ";path=/;expires=" + tzExpiry.toGMTString();
    // validate browser can persist cookies
    var cookieOk = false;
    if (document.cookie) {
        if (document.cookie.indexOf("tz=") != -1) cookieOk = true;
    }
    if (! cookieOk) {
        document.location = "/auth/enable-cookies/";
    }
}


function gLoadUrl(url){
    document.location = url;
}

/**
 * Gets a name from a path e.g. given /one/two/three will return three
 * @param path String the path to parse
 * @return String - the name
 */
function gGetNameFromPath(path) {
    var re = new RegExp(/([^\/\\]+)$/);
    var m = re.exec(path);
    if (m == null) {
        return null;
    } else {
        return m[0];
    }
}

/**
 * Escpapes an html attribute value for display
 * @param str String the attribute value to escape
 * @return String - the escaped atrribute value
 */
function gEscAttr(str) {
    if (! str) return str;
    return str.replace('\\', '\\\\').replace("'", "&apos;").replace("\"", "&quot;").replace('\n', ' ');
}

function gHide(id) {
    $("#" + id).fadeOut(1000);
}
function gHideQ(id) {
    $("#" + id).fadeOut(150);
}
function gHideNow(id) {
    $("#" + id).hide();
}
function gInvisible(id) {
    $("#" + id).css("visibility", "hidden");
}
function gVisible(id) {
    $("#" + id).css("visibility", "visible");
}

var gSD = new Array();
/**
 * Shows (unhides) a dom element (sets its style to display:"") after 150ms
 * @param id dom id of element to make show
 */
function gShow(id) {
    if (gSD[id]) clearTimeout(gSD[id]);
    if (gTimers[id]) clearTimeout(gTimers[id]);
    gSD[id] = setTimeout("$(\"#" + id + "\").show();", 150);
}

function gShowNow(id) {
    $("#" + id).show();
}

function gSetStyle(id, cssAttr, cssValue) {
    $("#" + id).css(cssAttr, cssValue);
}

function gSetClass(id, cn) {
    $("#" + id).addClass(cn);
}

function get(id) {
    var o;
    try {
        o = document.getElementById(id);
        if (!o) {
            lError("get(id): ! id '" + id + "'");
        }
        if (o == null) {
            lError("get(id): '" + id + "' == null");
        }
    } catch (e) {
        lError("get(id): exception " + id);
        lDebug(e);
    }
    return o;
}

function gExists(id) {
    return $("#" + id).size() > 0;
}

function gIsEmpty(s) {
    return $.trim(s).length == 0;
}

function gIsNodeValueEmpty(id) {
    return $.trim($("#" + id).val()).length == 0;
}

function gGetNodeValueClean(id) {
    return $.trim($("#" + id).val());
}

/**
 * Appends html to the innerHtml of a DOM element
 * @param id - id of element
 * @param html - the html to append
 */
function gAppend(id, html) {
    get(id).innerHTML += html;
}

function gChange(id, html) {
    $("#" + id).html(html);
}


/**
 * Replaces innerHtml of a DOM element, and subsequently clears it after 10 seconds
 * @param id - id of element
 * @param html - the html to replace
 */
function gChangeAndClear(id, html){
    $("#" + id).html(html);
    if (gTimers["clr" + id]) clearTimeout(gTimers["clr" + id]);
    gTimers["clr" + id] = eval("setTimeout(\"$('#" + id + "').html('');\", 10000);");
}

function gChangeIfExists(id, html) {
    $("#" + id).html(html);
}

/**
 * Replaces innerHtml of a DOM element after a 1 second delay
 * @param id - id of element
 * @param html - the html to replace
 */
function gDelayChange(id, html) {
    if (gTimers[id]) clearTimeout(gTimers[id]);
    gTimers[id] = setTimeout("$(\"#" + id + "\").html(\"" + html + "\");", 1000);
}

/**
 * Replaces innerHtml of a DOM element after a number of seconds delay
 * @param id - id of element
 * @param html - the html to replace
 * @param secs - the nu ber of seconds to deplay the change
 */
function gDelayChangeSecs(id, html, secs) {
    if (gTimers[id]) clearTimeout(gTimers[id]);
    gTimers[id] = setTimeout("$(\"#" + id + "\").html(\"" + html + "\");", secs * 1000);
}

/**
 * Gets the array element identified by its key
 * @param arr - an array
 * @param key - the key (or index) of the array element to get
 * @return the array element identified by key
 */
function gArrayContainsKey(arr, key) {
    if (! arr) return false;
    return arr[key];
}

/**
 * Gets the array element that contains a given value
 * @param arr - an array
 * @param val - the value of the array element to get
 * @return the array element identified by its value or false if the element does not exist
 */
function gArrayContainsValue(arr, val) {
    for (var e = 0; e < arr.length; e++) {
        if (arr[e] == val) {
            return true;
        }
    }
    return false;
}

/**
 * Counts the words in a given string which may contain html
 * @param str - the string to count the words in
 * @return the word count
 */
function gCountWords(str) {
    var wordCount = 0;
    if (str && str.length > 1) {
        var trimmedStr = str.replace(/^\s+|\s+$/g, "");
        var cleanedStr = trimmedStr.replace(/\s+/gi, " ");
        /* make sure all white space chars are spaces */
        lDebug("cleanedStr " + cleanedStr);
        wordCount = cleanedStr.split(" ").length;
    }
    return wordCount;
}

/**
 * Strips XML from a string
 * @param str - the string to strip
 * @return the stripped string
 */
function gStripXML(str) {
    var filter = /<[^>]*>/gi;
    return str.replace(filter, "");
}

/**
 * Is the string a given password match based on rules
 * 6 or more chars including at least one numeral
 * @param str - the password string to test
 * @return true if valid password else false
 */
function gIsValidPwd(str) {
    if (!str) return false;
    if (str.length < 6) return false;
    var filter = /^[a-zA-Z0-9]*[0-9]+[a-zA-Z0-9]*$/;
    return filter.test(str);
}

/**
 * Is the string a number
 * @param str - the string to test
 * @return true if number else false
 */
function gIsValidNumber(str) {
    if (!str) return false;
    var filter = /^[0-9]*$/;
    return filter.test(str);
}

/**
 * Is the string a valid email address
 * @param str - the string to test
 * @return true if valid email else false
 */
function gIsValidEmail(str) {
    if (!str) return false;
    var filter = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
    return filter.test(str);
}

function gIsValidDomain(str) {
    if (!str) return false;
    var filter = /^(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
    return filter.test(str);
}

/**
 * Is the string a valid phone number
 * @param str - the string to test
 * @return true if valid phone number else false
 */
function gIsValidPhone(str) {
    if (!str) return false;
    var filter = /^[\d\+\-\(\)x\s]*$/;
    return filter.test(str);
}

/**
 * Is the string a valid year
 * @param str - the string to test
 * @return true if valid year else false
 */
function gIsValidYear(str) {
    if (!str) return false;
    var filter = /^[0-9]{4}$/;
    return filter.test(str);
}

function gValue(id) {
    return $("#" + id).val();
}

/**
 * Sets the value of a DOM element
 * @param id - of the DOM element
 * @param newValue - the new value to set
 */
function gSetValue(id, newValue) {
    if (! newValue) newValue = "";
    return  $("#" + id).val(newValue);
}

/**
 * Select the value specified of a select list.
 * @param id the id of the select element
 * @param newSelectedValue the value to make selected
 */
function gSelectValue(id, newSelectedValue) {
    lDebug(id, newSelectedValue);
    var selList = get(id);
    for (var x in selList.options) {
        if (selList.options[x] && selList.options[x].value == newSelectedValue) {
            selList.selectedIndex = x;
            lDebug("selected index " + x);
        }
    }
}

/**
 * Get the value value of the selected option.
 * @param id the id of the select element
 */
function gGetSelectedValue(id) {
    var selList = get(id);
    var seled = selList.options[selList.selectedIndex].value;
    lDebug(seled);
    return seled;
}

function gGetCheckedRadioValue(radioName) {
    var eles = document.getElementsByName(radioName);
    for (i = 0; i < eles.length; i++) {
        if (eles[i].checked) {
            return eles[i].value;
        }
    }
    return "";
}

/**
 *  gFtypes - array of form element types
 */
var gFtypes = ["button","checkbox","file","hidden","password","radio","reset","select-one","select-multiple","submit","text","textarea"];


/**
 * Is the given DOM element a form element
 * @param el -  the id of the DOM element
 * @return true if it is a form element
 */
function gIsFormElement(el) {
    if (!el) return false;
    if (typeof el.disabled != "boolean") return false;
    if (typeof el.type != "string") return false;
    return ((',' + gFtypes.toString() + ',').indexOf(',' + el.type + ',') !== -1);
}

/**
 * Disables all elements in the given form
 * @param fid the DOM id of the form to disable
 */
function gDisable(fid) {
    lInfo("disabling form " + fid);
    if (!fid) return;
    var f = get(fid);
    // may be an old timeout around clean up
    if (gTimers[fid]) clearTimeout(gTimers[fid]);
    for (var x in f.elements) {
        if (gIsFormElement(f.elements[x])) {
            f.elements[x].disabled = true;
        }
    }
    // make sure we reenable the form after 2 mins in case an error occurs
    gTimers[fid] = setTimeout("gEnable(\"" + fid + "\");", 1000 * 120);
}

/**
 * Enable all form elements in form f
 * @param f - id of a form
 */
function gEnable(f) {
    lInfo("enabling form " + f);
    if (! f) return;
    var fObj = get(f);
    if (! fObj) {
        lError("could not find form " + f);
        return;
    }
    for (var x in fObj.elements) {
        if (gIsFormElement(fObj.elements[x])) fObj.elements[x].disabled = false;
    }
}

/**
 * Disable element f (sets attribute disabled to true)
 * @param f - id of an element to disable
 */
function gDisableElement(f) {
    lInfo("disabling " + f);
    if (gTimers[f]) clearTimeout(gTimers[f]);
    $("#" + f).attr("disabled", "disabled");
    // make sure we reenable the element after 2 mins in case an error occurs (clearing any existing timer first)
    gTimers[f] = setTimeout("gEnableElement(\"" + f + "\");", 1000 * 120);
}

/**
 * Enable element f (sets attribute disabled to false)
 * @param f - id of an element to enable
 */
function gEnableElement(f) {
    lInfo("enabling " + f);
    if (gTimers[f]) clearTimeout(gTimers[f]);
    $("#" + f).removeAttr("disabled");
}


/**
 * Toggles a DOM element between enabled and disabled
 * @param f - id of the element to enable or disable
 */
function gToggle(f) {
    if (! f) return;
    f = get(f);
    f.disabled = !f.disabled;
}

/**
 * Is the given form element checked (typically used for a checkbox or radio button)
 * @param f - id of the element to check
 * @return true if the element f is checked
 */
function gIsChecked(f) {
    if (! f) return false;
    f = get(f);
    return f.checked;
}

/**
 * Sets the checked attribute of given form element (typically used for a checkbox or radio button)
 * @param f - id of the element to check
 */
function gCheck(f, checked) {
    if (! f) return;
    f = get(f);
    if (!f) return;
    f.checked = checked;
}

/**
 * focus a field, but do it 155ms after now in another "thread" to avoid issues with item being hidden.
 * @param id - id of the element to focus
 */
function gFocus(id) {
    if (gFC[id]) clearTimeout(gFC[id]);
    gFC[id] = setTimeout("$(\"#" + id + "\").focus();", 155);
}
// store the focus timers in case we need to cancel it
var gFC = new Array();


/**
 * Initialise a DWR RPC batch (a batch of AJAX calls)
 * Also writes a status message if one is supplied
 * @param statusElId - id of the status element to write a message to (typically a span)
 * @param msg - the status message to display
 */
function gCallNowInit(statusElId, msg) {
    lDebug("init rpc call");
    if (statusElId) {
        if (gTimers[statusElId]) clearTimeout(gTimers[statusElId]);
        gDelayChange(statusElId, "<span class=jsProc>&nbsp;" + (gIsEmpty(msg) ? "Processing..." : msg) + "</span>");
    }
    if (!gIsOnFileSystem) {
        DWREngine.beginBatch();
    }
}

var gTranArgs = new Array();  // an array of DWR (Ajax) transaction arguments
var gTranId = 0; // a DWR (Ajax) transaction id

/**
 * Adds a DWR (AJAX) call into a batch
 * @param actionClass - the remote class to invoke
 * @param method - method to be invoked on remote class
 * @param successMsg - text to be displayed in statusElId on success
 * @param errorMsg - text to be displayed in statusElId on error, if null, the message from the server-side exception is displayed
 * @param statusElId - id of element of which status messages to be written
 * @param fnCallback - optional function that will be called after all other RPC callback code - or null
 * @param params - params to be passed to rpc method
 */
function gCallNowAdd(actionClass, method, successMsg, errorMsg, statusElId, fnCallback, params, asyncParam) {
    gTranId++;
    if ( gTranId > 2000 ) {
        lError("Reached max transactions for a page load please reload page. IGNORING ADD");
        return;
    }
    // inc tran id
    var args = ['/rpc', actionClass, method];
    var i = 3;
    if (typeof params == "object") {
        for (var x in params) {
            args[i] = params[x];
            i++;
        }
    } else {
        args[i++] = params;
    }
    var runAsync = asyncParam == null ? true : asyncParam;
    eval("var callbackProxy = function(result) {gCallbackWrapper(result, " + gTranId + ");};");
    eval("var errCallbackProxy = function(errorString, exception) {gCallbackErrWrapper(errorString, exception, " + gTranId + ");};");
    args[i] = {async: runAsync, callback:callbackProxy, timeout:60000, errorHandler:errCallbackProxy };

    lInfo("adding rpc call", gTranId, actionClass, method, params);
    // save the transaction
    gTranArgs[gTranId] = [actionClass, method, successMsg, errorMsg, statusElId, fnCallback, params, new Date()];

    if (!gIsOnFileSystem) {
        DWREngine._execute.apply(this, args);
    }
}

/**
 * The callback method for handling DWR (AJAX) errors generated on server side
 * @param errorString - text of the error message (if exception this is the exception.getMessage() )
 * @param exception - a server side exception serialised into javascript
 * @param tranId - the DWR (Ajax) transcation id
 */
function gCallbackErrWrapper(errorString, exception, tranId) {
    lError(errorString, exception);
    if (exception.message.indexOf("no auth info found") >= 0) {
        document.location = "/auth/login/";
        return;
    }
    var errId = gTranArgs[tranId][4];
    var errMsg = gTranArgs[tranId][3];
    if (errId) {
        if (gTimers["clr" + errId]) clearTimeout(gTimers["clr" + errId]);
        if (gTimers[errId]) clearTimeout(gTimers[errId]);
        gDelayChange(errId, "<span class=jsErr>&nbsp;" + (exception ? exception.message : (errMsg ? errMsg : errorString)) + "&nbsp;</span>");
    }
    var fnCallback = gTranArgs[tranId][5];
    if (fnCallback) fnCallback(true, null); // true for error, null = no result
    gTranArgs[tranId] = null;
}


/**
 * The callback method invoked when a DWR (AJAX) call executes successfully on the server side
 * @param result - a server side object serialised into javascript
 * @param tranId - the DWR (Ajax) transcation id
 */
function gCallbackWrapper(result, tranId) {
    // we want to make sure things don't happen too fast for the user :)
    // it's confusing and your brain can't keep up
    // so slow it down if it was too fast
    gTranArgs[tranId][8] = result;
    var st = gTranArgs[tranId][7].getTime();
    var took = new Date().getTime() - st;
    lInfo("tranId", tranId, "took", took + "ms");
    var minTime = 500;
    // note: set to 1500 when testing
    setTimeout("gCallbackWrapperTimed(" + tranId + ");", took > minTime ? 10 : minTime - took);
}

/**
 * A subsequent callback function called by gCallbackWrapper after a short delay
 * @param tranId - the DWR (Ajax) transcation id
 */
function gCallbackWrapperTimed(tranId) {
    var msg = gTranArgs[tranId][2];
    var statusId = gTranArgs[tranId][4];
    var fnCallback = gTranArgs[tranId][5];
    var result = gTranArgs[tranId][8];
    lDebug("result", tranId, result);
    if (gTimers[statusId]) clearTimeout(gTimers[statusId]);
    if (statusId && gExists(statusId)) {
        if (msg) {
            gChangeAndClear(statusId, "<span class=jsMsg>&nbsp;" + msg + "&nbsp;</span>");
        } else {
            gChange(statusId, "");
        }
    }
    if (fnCallback) fnCallback(false, result);  // false = no error
    gTranArgs[tranId] = null;
}

/**
 * Will execute a batch of DWR (ajax) calls now, call now exec will run in parallel if something else is running
 */
function gCallNowExec() {
    lDebug("rpc calling");
    if (!gIsOnFileSystem) {
        DWREngine.endBatch();
    } else {
        lError("RPC not support via file system");
    }
}

function gIsAnyCallActive() {
    for ( var x in gTranArgs ) {
        if ( ! gTranArgs[x] ) continue;
        if ( gTranArgs[x] != null ) return true;
    }
    return false;
}

/**
 * l is for log  -this is a colelction of logging methods
 * Log copies the style of log4j and jakarta commons logging.
 *
 * @author Paul Tuckey
 */

var lLevel = "ERROR";

function lDebug() {
    if (lLevel != "DEBUG") return;
    lWrite("DEBUG", lDebug.arguments, lDebug.caller);
}

function lInfo() {
    if (lLevel != "INFO" && lLevel != "DEBUG") return;
    lWrite("INFO", lInfo.arguments, lInfo.caller);
}

var lErrReported = false;

function lError() {
    lWrite("ERROR", lError.arguments, lError.caller);
    if ( !lErrReported ) {
        lErrReported = true;
        try {
            gCallNowInit();
            gCallNowAdd("WikiRPCAction", "reportJSError", "", "", "", function() {}, [$("#lLogContent").html()]);
            gCallNowExec();
        } catch(e) {
            // nothing here
        }
    }
}

function lClean(o) {
    if (o == null) return null;
    var s = "";
    if (typeof o == "object") {
        var c = "";
        $.each(o, function(k, v) {
            c += (c == "" ? "" : ", " ) + k + ": " + lClean(v);
        });
        s = "{" + c + "}";
    } else if ($.isFunction(o)) {
        s = "function()";
    } else {
        s = (o + "").length > 512 ? o.substring(0, 512) + "..." : o;
    }
    return s;
}

/**
 * Handles writing of log lines.
 *
 * @param level log level to log for
 * @param args argumnets of log function
 * @param funcObj the log function invoked
 */
function lWrite(level, args, funcObj) {
    if ($("#lLogContent").length != 1) return;
    var fs = funcObj + "";
    var funcName = fs.substring(fs.indexOf(" ") + 1, fs.indexOf("("));
    var s = "";
    $.each(args, function(i, o) {
        s += (i > 0 ? ", " : "") + lClean(o);
    });
    s = s.replace(/</g, "&lt;").replace(/>/g, "&gt;");
    var style = "ERROR" == level ? "color: red; font-weight: bold;" : ("DEBUG" == level ? "color: gray;" : "");
    var timeNow = new Date();
    $("#lLogContent").append("<span style='color: #c0c0c0;'>" + timeNow.getHours() + ":" + timeNow.getMinutes() + ":" +
                             timeNow.getSeconds() + "." + timeNow.getMilliseconds() + " " + level + " " + funcName +
                             "</span><span style=\"" + style + "\"> " + s + "</span><br/>");
}

/** init logging if jquery available **/
if (typeof $ == "function") {
    $(document).ready(function() {
        $("#lLogLink").html("<span id=lLog class=logArea style=\"display: none;\">" +
                            "<b>JavaScript Log</b> &nbsp; &nbsp; " +
                            "<a href=\"javascript:$('#lLogContent').html('');void(0);\" class='jsl'>Clear</a> &nbsp; "+
                            "<a href=\"javascript:(!lErrReported && confirm('Send?') ? lError('Test') : lError('Once Only!'));void(0);\" class='jsl'>Send</a> &nbsp; "+
                            "<a href=\"#\" id=\"lLevelLink\" class='jsl'>Level</a> &nbsp; &nbsp; &nbsp; &nbsp; " +
                            "<b><a href=\"javascript:$('#lLog').toggle();void(0)\" class='jsl'>Hide</a></b><br/>" +
                            "<span id=lLogContent></span></span>" +
                            "<span id=lLogMsg style=\"cursor:pointer; font-size: xx-small; color:gray;\" onclick=\"$('#lLog').toggle();return false;\">.</span>");
        $("#lLevelLink").click(function() {
            lLevel = lLevel == "ERROR" ? "DEBUG" : (lLevel == "DEBUG" ? "INFO" : "ERROR");
            lWrite(lLevel, ["level set to " + lLevel], this);
            return false;
        });
    });
}

// setup global error handler
window.onerror = new Function("v1", "v2", "v3", "lError(v1, v2, v3); return true;");

// alter dwr's error handler
if (typeof DWREngine == "function") {
    DWREngine.setErrorHandler(function(message) {
        lError(message);
    });
    DWREngine.setWarningHandler(function(message) {
        lError(message);
    });
}
