function jaf()
{
    //object references
    this.animate = new jafAnimate();
    this.tool    = new jafTool();
    this.backend = new jafBackend();
    this.check 	 = new jafCheck();
    this.doc     = new jafDoc();
    this.module  = new jafModule();
    this.widget  = new jafModule();
}



/***********************************
* jaf GLOBALS  *
************************************/
/* ERROR CATCHING */
var erCatch = new Array();
var erMsgs  = new Array();//erMsgs['methodKey']="message";

erMsgs['bDoor']     = "Ajax backDoor - There was an error processing a backend request.";
erMsgs['bTalk']     = "Ajax backTalk - @param `sendObj` is not in the correct format. Send To Reference: ";
erMsgs['bXML']      = "Ajax backTranslate_XML - @param `xmlObj` has no data. Send To Reference: ";
erMsgs['dObjID']    = "docObj - @param `id` is required for any Document Object. Type Reference: ";
erMsgs['dObjApd']   = "docObj - @param `html` is not in the correct format. Id: ";
erMsgs['docLink']   = "docLink - @param `html` is required as a display object. Id: ";
erMsgs['docImage']  = "docImage - @param `source` path is required to create an image object. Id: ";
erMsgs['sibReplace']= "docReplaceChildren - @param `html` is not in the correct format. ParentId:";
erMsgs['LText']     = "moduleLoadingText - @param `parentObj` is not a proper dom element. Id: ";

/* TESTING */
var ajaxTestScript = '/jaf/php/_ajaxTest.php';
var ajaxTestXML    = '/jaf/jaf.test.xml';
var ajaxProdScript = 'path to production script';

/* RATING SPECS */
var ratingTitles = new Array(); //!IMPORTANT! - for 5 scale only.
ratingTitles[1] = "Yuck!";
ratingTitles[2] = "I didn't like it.";
ratingTitles[3] = "It was ok.";
ratingTitles[4] = "It was good.";
ratingTitles[5] = "I'll add it to my list of favorites!";

var ratingObj = new Array();
ratingObj['size']     = 15;
ratingObj['spacer']   = 0;
ratingObj['blank']    = 'url(/jaf/img/starBlank.png)'; //url(path)
ratingObj['average']  = 'url(/jaf/img/starAvg.png)';
ratingObj['user']     = 'url(/jaf/img/starUser.png)';

var ratingSpecs = new Array();
ratingSpecs['scale']    = 5;
ratingSpecs['stars']    = ratingObj;
ratingSpecs['titles']   = ratingTitles;
ratingSpecs['backend']  = ajaxTestScript; //defaults to testScript

/* PROGRESS ITEM SPECS */
var loadTxtSpecs = new Array(); //loading text
loadTxtSpecs['node']       = document.createTextNode('');
loadTxtSpecs['increment']  = 0.25;
loadTxtSpecs['iterations'] = 5;

/* FADE EFFECT SPECS */
var fadeSpecs = new Array();
fadeSpecs['fadeIn'] = true;
fadeSpecs['obj']    = new Object();


/* BACKEND RESPONSE HOLDERS */
var xmlResponse  = new Array();
var ymlResponse  = new Array();
var jsonResponse = new Array();


//Message Constants
EMAIL_INVALID       = "Email Address is Empty or Invalid";
EMAIL_CHK_FAILED    = "Email Confirmation Failed";

PASSWORD_MIN_LENGTH     = 8;
PASSWORD_LENGTH_FAILED  = "Password must be at least "+PASSWORD_MIN_LENGTH+" characters.";
PASSWORD_chk_FAILED     = "New passwords were not identical.";
PASSWORD_INVALID        = "Passwords may not contain spaces or special characters.";

PHONE_INVALID           = "Phone is incomplete or invalid.";

LOGIN_FAIL              = 'Email Address and/or Password is invalid';

REQUIRED_FIELD          = "Required Fields Missing";

CHANGEPASSWORD_FAIL     = "Your Original Password was Incorrect.";
CHANGEPASSWORD_OK       = "Your Password has been changed.";

CHANGEEMAIL_FAIL        = "Your Password was Incorrect.";
CHANGEEMAIL_OK          = "Your Email address has been changed.";

/**
* STANDARD GET ELEMENT (BY ID)
*
* @param {string} id
* @return {object}
*/
function getElement(id){return document.getElementById(id); }



/************************************
* jaf AJAX INTERFACE  *
*************************************/
function jafBackend()
{
    this.door  = backDoor;
    this.talk  = backTalk;
    this.tXML  = backTranslate_XML;
    /*this.tJSON = backTranslate_JSON;   //for future use. */
}

/**
* AJAX INTERFACE
*/
function backDoor()
{
    try { this.request = new XMLHttpRequest(); } //standard browsers
    catch (standard_fail)
    {
        try { this.request = new ActiveXObject("Microsoft.XMLHTTP");} //current IE
        catch (currentIE_fail)
        {
            try { this.request = new ActiveXObject("Msxml2.XMLHTTP"); } //old IE
            catch (oldIE_fail)
            {
                this.request = false;
                erCatch.push(erMsgs['bDoor']);
            }
        }
    }
}

/**
* AJAX BACKEND COMMUNICATION
*
* @param {string}backendURL - location of script or xml file
* @param {object}sendObj    - an array or.. string in format: 'postName=postValue' + '&postName2=postValue2' for sending to backend
* @param {object}actionFn   - function to execute upon success
* @param {boolean}synchronous (optional) in case of synchronous results need
*/
function backTalk(backendURL, sendObj, actionFn, synchronous)
{
    var entry  = new backDoor();
    var rqst   = entry.request;
    var async  = !checkIsSet(synchronous);
    var gResponse;

    if(checkIsArray(sendObj))
    {
        var sendString = '';
        for(var key in sendObj)
        {
            if(sendString!=''){sendString+='&'; }

            sendString += key+'='+sendObj[key];
        }
        sendObj = sendString;
    }
    else if(sendObj.toString().search('=')==-1)
    {
        erCatch.push(erMsgs['bTalk']+backendURL);
        return false;
    }

    rqst.open("POST", backendURL, async);
    rqst.onreadystatechange = function ()
    {
        if(rqst.readyState == 4)
        {
            gResponse = (checkIsXML(rqst.responseXML))? backTranslate_XML(rqst.responseXML):rqst.responseText;
            actionFn(gResponse);
        }
    }
    rqst.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    rqst.send(sendObj); //.send() needs to incur, but doesn't need the send object
}

/**
* XML TRANSLATOR FOR AN XML RESPONSE
*
* @param {object} xmlObj
* @param {string} backendURL
* @param {string} parentKey - to prepend to the childname to represent the dimension of the array while maintaining 1-dimension
* @return {array} xmlResponse
*/
function backTranslate_XML(xmlObj, backendURL, parentKey)
{
    if(checkIsSet(backendURL))
    {
        if(!xmlObj.hasChildNodes())
        {
            erCatch.push(erMsgs['bXML']+backendURL);
            return false;
        }
        xmlResponse = new Array();
    }

    var xmlChildren = xmlObj.childNodes;
    var keyPrefix   = (checkIsSet(parentKey))? parentKey+'.':'';
    for(var childKey=0; childKey<xmlChildren.length; childKey++)
    {
        var xmlChild = xmlChildren[childKey];
        if(xmlChild.nodeType==1)
        {
            var childName = keyPrefix + xmlChild.nodeName;

            if(xmlChild.childNodes.length>1)
            {
                xmlResponse[childName] = new Array();
                xmlResponse[childName].push(backTranslate_XML(xmlChild, null, childName));
            }
            else
            {   xmlResponse[childName] = xmlChild.childNodes[0].nodeValue;  }
        }
    }
    return xmlResponse;
}



/************************************
* jaf CHECKING & DEBUGGING  *
*************************************/
function jafCheck()
{
    this.enter      = checkEnter;
    this.errors     = checkErrors;
    this.isArray   	= checkIsArray;
    this.isDefined 	= checkIsDefined;
    this.isDocObj  	= checkIsDocObj;
    this.isObject  	= checkIsObject;
    this.isSet  	= checkIsSet;
    this.isString  	= checkIsString;
    this.objDump    = checkObjectDump;
    this.htmlDump   = checkObjectDump_html;
    this.type 	   	= getType;
}

/**
* CHECK FOR THE TYPE OF AN ENTRY OBJECT
* @param {object} obj
* @return {string}objType
*/
function getType(obj)
{
    var objType = typeof obj;

    if(obj == undefined)
    {
        objType = "undefined";
    }
    else if(obj == null)
    {
        objType = "null";
    }
    else if(obj.setType != undefined && typeof(obj.setType) == 'String')
    {
        objType = obj.setType;
    }
    else if( obj.nodeType == 1)
    {
        objType = "domelement";
    }
    else //check for an array
    {
        if( obj.constructor!=null && obj.constructor==Array)
        {
            objType = "array";
        }
        else if(objType == "function" && (/^\[object.*\]$/i).test(obj.toString()))
        {
            objType = "object";
        }
    }
    //if none of the above conditions are met, the standard type will return
    return objType;
}
/**
* CHECK OBJECT TO SEE IF IT'S AN ARRAY
* @param {object} obj
* @return {boolean} 'return comparison results'
*/
function checkIsArray(obj){ return (getType(obj).toLowerCase() == 'array'); }
/**
* CHECK OBJECT TO SEE IF IT EXISTS AND IS NOT NULL
* @param {object} obj
* @return {boolean} 'return comparison results'
*/
function checkIsDefined(obj)
{
    var objType = getType(obj);
    return (objType != 'undefined' && objType != 'null');
}
/**
* CHECK OBJECT TO SEE IF IT'S A DOM OBJECT
* @param {object} obj
* @return {boolean} 'return comparison results'
*/
function checkIsDocObj(obj){ return (getType(obj).search('dom')!=-1); }
/**
* CHECK OBJECT TO SEE IF IT'S AN OBJECT
* @param {object} obj
* @return {boolean} 'return comparison results'
*/
function checkIsObject(obj){ return (getType(obj).toLowerCase() == 'object'); }
/**
* CHECK OBJECT TO SEE IF IT'S A STRING
* @param {object} obj
* @return {boolean} 'return comparison results'
*/
function checkIsString(obj){ return (getType(obj).toLowerCase() == 'string'); }
/**
* FORCE OBJECT TO BOOLEAN CHECK - INTERPRET NULL & UNDEFINED AS FALSE
* @param {object} obj
* @return {boolean} 'return comparison results'
*/
function checkIsSet(obj){ return (checkIsDefined(obj) && obj!=false); }
/**
* CHECK OBJECT TO SEE IF IT'S A NUMBER
* @param {object} obj
* @return {boolean} 'return comparison results'
*/
function checkIsNumeric(obj){ return !obj.NaN; }
/**
* CHECK OBJECT TO SEE IF IT'S AN XML OBJECT
* @param {object} obj
* @return {boolean} 'return comparison results'
*/
function checkIsXML(obj){ return (checkIsDefined(obj) && obj.childNodes.length>0); }

/**
* CHECK ERROR CATCHER FOR ERRORS
*/
function checkErrors()
{
    if(erCatch.length>0)
    { alert(erCatch.join("\n")); }
}

/**
* DUMP THE CONTENTS OF AN OBJECT/ARRAY INTO A STRING
* @param {object} obj
* @return {string} dumpString
*/
function checkObjectDump(obj)
{
    var z=0;
    var dumpString = '';

    for(var key in obj)
    {
        if(z<obj.length)
        {
            var item = obj[key];
            dumpString +=  key+": "+item+"\n";
        }
        z++;
    }
    return dumpString;
}

/**
* DUMP THE CONTENTS OF AN HTML OBJECT
* @param {object} obj
* @return {string} dumpString
*/
function checkObjectDump_html(obj)
{
    var dumpString = '';
    if (obj != null && obj.hasChildNodes())
    {
        var children = obj.childNodes;
        for(var key in children)
        {
            var child = children[key];

            if (child.childNodes)        { dumpString += checkObjectDump_html(child); }
            if (child.nodeValue != null) { dumpString += child.nodeValue; }
        }
    }
    return dumpString;
}

/**
* Confirms whether the Enter key was pressed (US-Ascii keyboard specific)
* @param {event} e The event object to look at to decide if the key was pressed.
* @return true if 'Enter' key was pressed, false otherwise.
* @type boolean
*/
function checkEnter(e)
{
    var key = null;

    if(e.which) { key = e.which;   }//Standard
    else        { key = e.keyCode; }//IE

    if(key == 13) { return true; }
    return false;
}


/************************************
* jaf DOM CREATION  *
*************************************/
function jafDoc()
{
    this.obj = documentObject;
    this.lnk = documentLink;
    this.img = documentImage;

    this.opacity         = documentOpacity;
    this.removeChildren  = documentRemoveChildren;
    this.replaceChildren = documentReplaceChildren;
}

/**
* BASIC DOM OBJECT
* @param {string}id
* @param {string}type i.e('div', 'span', 'p')
* @param {string}cssClass
* @param {object}html i.e.(text or html)
*/
function documentObject(id, type, cssClass, html)
{
    if(!checkIsString(id))
    {
        erCatch.push(erMsgs['dObjID']+type);
        return false;
    }
    this.obj = document.createElement((checkIsString(type))? type:'div');

    this.obj.id   = id;
    this.obj.name = id;
    this.setType  = 'domConstruct';

    if(checkIsString(cssClass)){this.obj.className = cssClass; }
    if(checkIsDefined(html))
    {
        if(checkIsString(html))
        {
            html = document.createTextNode(unescape(html));
        }
        else if(!checkIsDocObj(html))
        {
            erCatch.push(erMsgs['dObjApd']+id);
            return;
        }
        this.obj.appendChild(html);
    }
}

/**
* HREF LINK OBJECT
* @param {string}id
* @param {string}url i.e('http://www.site.com')
* @param {string}cssClass
* @param {object}html i.e.(text or html)
*/
function documentLink(id, html, url, cssClass)
{
    if(!checkIsSet(html))
    { html = erMsgs['docLink']+id; }

    var domObj = new documentObject(id ,'a', cssClass, html);

    domObj.obj.href = (checkIsString(url))? location:'';

    domObj.obj.style.borderWidth = '0px';
    domObj.obj.style.display     = 'block';

    this.obj = domObj.obj;
}

/**
* HREF IMAGE OBJECT
* @param {string}id
* @param {string}source i.e('/images/image.png')
* @param {string}cssClass
*/
function documentImage(id, source, cssClass)
{
    if(!checkIsDefined(source))
    {
        erCatch.push(erMsgs['docImage']+id);
        return false;
    }
    var domObj = new documentObject(id ,'img', cssClass);

    domObj.obj.src = (checkIsString(source))? source:false;

    domObj.obj.style.borderWidth = '0px';

    this.obj = domObj.obj;
}

/**
* SET TRANSPARENCY OF AN OBJECT
*
* @param {object} obj
* @param {int} opacity
*/
function documentOpacity(obj, opacity)
{
    obj.style.opacity    = opacity/100;
    obj.style.MozOpacity = opacity/100;
    obj.style.filter     = "alpha(opacity="+opacity+")";
}

/**
* REMOVE ALL OF A PARENTS CHILDREN
* @param {object} obj
* @return {boolean} 'return true if success'
*/
function documentRemoveChildren(obj)
{
    if(obj.hasChildNodes())
    {
        while(obj.childNodes.length>0)
        {
            obj.removeChild(obj.firstChild);
        }
    }
}

/**
* REPLACE ALL OF A PARENTS CHILDREN
* @param {object} obj
* @param {object} html - replacement object
* @return {boolean} 'return true if success'
*/
function documentReplaceChildren(obj, html)
{
    if(checkIsString(html))
    {
        html = document.createTextNode(html);
    }
    else if(!checkIsDocObj(html))
    {
        erCatch.push(erMsgs['sibReplace']+obj.id);
        return false;
    }
    documentRemoveChildren(obj);
    obj.appendChild(html);
}


function documentToggleCheckbox(obj)
{
    obj.style.innerHTML = (obj.style.innerHTML=='x')? '':'x';
}


/**
* VALIDATE AND SUBMIT FORM
*
* Naming Conventions:
*
* FIELDS
* fieldID           = formId  + '_' + `UniqueIdentifier`;
* email field       = formId  + '_email'
* password field    = formId  + '_pwd'
* new pword field   = formId  + '_pwd_new'
* phone             = formId  + '_phone'
*
* REQUIRE FIELD
* requiredField     = `uniquePrefix`  + '_rqd'
*
* FIELD CONFIRMATION
* fieldConfirm      = fieldID + '_chk'
*
* ALERTS
* formMessageHolder = formId  + '_msg'
* fieldText         = fieldID + '_txt'
*
* @param {domelement} formId The dom object that contains the form.
* @param {boolean} onEnter - if true, check for enter and continue if pressed
*/
function documentFormSubmit(formId, onEnter)
{
    var emailError;
    var pwdError;
    var rqdError;

    /**
    * MANAGE FIELD HEADER ON ERROR EVENT
    * @param object fieldObj (required)
    * @param boolean isError (required)
    * @param boolean multipleFields (optional) - i.e. phone numbers with separate fields for area code and prefix
    */
    _FieldHeader = function(fieldObj, isError, multipleFields)
    {
        if(fieldObj)
        {
            var headerId  = (multipleFields==true)? (fieldObj.id).replace(/[0-9]/,''):fieldObj.id;
            headerId += '_txt';
            getElement(headerId).style.color = (isError)? '#FF0000':'';
        }
    }

    /**
    * CHECK EMAIL ADDRESS - report error
    * @param object emailObj (required)
    */
    _CheckEmail = function(emailObj)
    {
        if(emailObj)
        {
            var error       = false;
            var confirmObj  = getElement(emailObj.id+'_chk');

            if(confirmObj && confirmObj.value != emailObj.value)
            {
                formErrors.push(EMAIL_CHK_FAILED);
                error = true;
            }
            else
            {
                var emailFilter = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;

                if(emailFilter.test(emailObj.value)!=1)
                {
                    //add error to error list
                    emailError = error = true;
                }
            }
            //manage field header accordingly
            _FieldHeader(confirmObj, error);
            _FieldHeader(emailObj, error);
        }
    }

    /**
    * CHECK PASSWORD FIELDS
    * - Disallows Spaces.
    * - Check length
    * - Check for confirmation
    * @param boolean pwdObj (required)
    * @param string pwdStatus (optional)
    */
    _CheckPassword = function(pwdObj, pwdStatus)
    {
        if(pwdObj)
        {
            var error        = false;
            var pwdLength    = pwdObj.value.length;
            var confirmObj   = getElement(pwdObj.id+'_chk');
            //Characters in password can only be alphanumeric
            var charChecker = /^[a-zA-Z0-9]+$/;

            if(confirmObj && confirmObj.value != pwdObj.value)
            {
                error = PASSWORD_chk_FAILED;
            }
            else if(pwdLength < PASSWORD_MIN_LENGTH)
            {
                error = PASSWORD_LENGTH_FAILED;
            }
            else if(!charChecker.test(pwdObj.value))
            {
                error = PASSWORD_INVALID;
            }
            
            if(error)
            {
                if(pwdStatus == '_new' && error.search('New ')==-1)
                {
                    error = error.replace('Password', 'New Password');
                }
                pwdError = error;
            }
            else
            {
                if(confirmObj)
                {
                    confirmObj.value = toolBase64(confirmObj.value);
                }
                pwdObj.value = toolBase64(pwdObj.value);
            }
            
            _FieldHeader(confirmObj, error);
            _FieldHeader(pwdObj, error);
        }
    }

    /**
    * CHECK REQUIRED FIELD
    * @param object rqdObj (required)
    */
    _CheckRequired = function(rqdObj)
    {
        if(rqdObj)
        {
            var error = false;
            var header = getElement(rqdObj.id+'_txt');
            var headerTxt = header.innerHTML;

            if(rqdObj.value=='')
            {
                if(headerTxt.search(/[*]/)==-1){ header.innerHTML = '*'+headerTxt;}
                rqdError = error = true;
            }
            else
            {
                header.innerHTML= headerTxt.replace('*','');
            }

            _FieldHeader(rqdObj, error);
        }
    }

    /**
    * CHECK PHONE
    * checks for numeric entry
    * checks for proper length depending on entry position
    *
    * @param object phoneObj (required)
    */
    _CheckPhone = function(phoneObj)
    {
        if(phoneObj)
        {
            //for later
            _FieldHeader(phoneObj, phoneError, true);
        }
    }

    var gSubmitted;
    if(onEnter!=null && !checkEnter(onEnter)){ return false; }

    var formDOM  = getElement(formId);

    //Validate
    if(checkIsDocObj(formDOM) && !checkIsDefined(gSubmitted))
    {
        formErrors = new Array();

        var validNodes   = "INPUT::SELECT::TEXTAREA";
        var formElements = formDOM.elements;

        //multipleField Errors
        var rqdError   = false;
        var emailError = false;
        var pwdError   = false;

        for(var id in formElements)
        {
            var element = formElements[id];
            var isValid = element!=null && element.nodeName!=null && validNodes.search(element.nodeName)!=-1;

            if(isValid && id.search('_chk')==-1)
            {
                if(element.id.search('_rqd')!=-1)//fieldID: uniquePrefix + '_rqd'
                {
                    _CheckRequired(element);
                }
                else if(element.id.search('_email')!=-1)//fieldID: formId + '_email'
                {
                    _CheckEmail(element);
                }
                else if(element.id.search('_pwd')!=-1)//fieldID: formId + '_pwd'
                {
                    var pwdStatus = (element.id.search('_new')!=-1)? '_new':'';
                    _CheckPassword(element, pwdStatus);
                }
                else if (element.id.search('_phone')!=-1)
                {
                    _CheckPhone(element);
                }
            }
        }
        //multipleField errors
        if(rqdError)  { formErrors.push('*'+REQUIRED_FIELD); }
        if(emailError){ formErrors.push(EMAIL_INVALID);  }
        if(pwdError)  { formErrors.push(pwdError);  }

        if(formErrors.length > 0) //check for errors
        {
            documentMessage(formId, formErrors, 'fail');
            return false;
        }
        else
        {
            documentMessage(formId);
            formDOM.submit();
            gSubmitted=true;
        }
    }
}



/**
* RETURNS STYLED MESSAGE TO SPECIFIED PARENT OBJECT
* @param {string} parentID The forms base ID to find the forms message holder.
* @param {mixed} msgObj - object of 1 message or list of messages.
* @param {string} status If 'fail' then the text will be styled in error format, otherwise in a success format.
*/
function documentMessage(formId, msgObj, status)
{
    _AppendMessage = function(parentObj, msg, status)
    {
        var msgItem = new documentObject(msg);
        parentObj.appendChild(msgItem.obj);

        //highlight field titles in red when error occurs
        //IMPORTANT - STYLE NEED TO BE SET
        var msgStyle = (status=='fail')? 'ErrorText':'SuccessText';

        if(false)//status=='fail';
        {
            var failMark = new documentObject(msg+'_mark', 'span' , 'floatLeft', 'OOPS: ');
            msgItem.obj.appendChild(failMark.obj);
        }

        var msgText = new documentObject(msg+'_txt', null, 'msgStyle', msg);
        parentObj.appendChild(msgText.obj);

    }//end append message method

    if(checkIsDocObj(getElement(formId+'_msg')))
    {
        var msgParent = getElement(formId+'_msg'); //check for form message holder
        animateFade(msgParent);
    }

    if(!checkIsSet(formId) || !checkIsDocObj(msgParent))
    { return null; }

    //strip current contents of message holder
    documentRemoveChildren(msgParent);

    //append messages or message
    if(checkIsArray(msgObj) && msgObj.length > 0)
    {
        for(var i = 0; i < msgObj.length; i++)
        {
            _AppendMessage(msgParent, msgObj[i], status);
        }
    }
    else if(checkIsString(msgObj))
    {
        _AppendMessage(msgParent, msgObj, status);
    }

}








/************************************
* jaf TOOLS  *
*************************************/
function jafTool()
{
    this.base64      = toolBase64;
    this.cookieJar   = toolCookieJar;
    this.cookieMaker = toolCookieMake;
    this.cookieTrash = toolCookieTrash;
    this.getItemId   = toolGetItemId;
    this.toggle      = toolToggleBool;
    this.bwCheck      = toolSpeedTest;
    //this.md5       = toolMD5; add later
}

/**
* ENCODE AND DECODE BETWEEN ASCII AND BASE 64
* @param {string} input - string to encode/decode.
* @param {boolean} decode (Optional) - if true, it will expect a base64 to decode
* @return {string} output - translated string
*/
function toolBase64(input, decode)
{
    var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    var output = "";
    var character1, character2, character3;
    var encoded1, encoded2, encoded3, encoded4;
    var i = 0;

    if(!checkIsSet(decode))//encode
    {
        do //piece through and convert string to base64
        {
            character1 = input.charCodeAt(i++);
            character2 = input.charCodeAt(i++);
            character3 = input.charCodeAt(i++);

            encoded1 = character1 >> 2;
            encoded2 = ((character1 & 3) << 4) | (character2 >> 4);
            encoded3 = ((character2 & 15) << 2) | (character3 >> 6);
            encoded4 = character3 & 63;

            if(isNaN(character2))       { encoded3 = encoded4 = 64; }
            else if(isNaN(character3))  { encoded4 = 64;            }

            output += keyStr.charAt(encoded1) + keyStr.charAt(encoded2) + keyStr.charAt(encoded3) + keyStr.charAt(encoded4);
            // output +
        }
        while (i < input.length);
    }
    else//decode
    {
        // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
        input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");

        do //piece through and break down converted string back to ascii
        {
            encoded1 = keyStr.indexOf(input.charAt(i++));
            encoded2 = keyStr.indexOf(input.charAt(i++));
            encoded3 = keyStr.indexOf(input.charAt(i++));
            encoded4 = keyStr.indexOf(input.charAt(i++));

            character1 = (encoded1 << 2) | (encoded2 >> 4);
            character2 = ((encoded2 & 15) << 4) | (encoded3 >> 2);
            character3 = ((encoded3 & 3) << 6) | encoded4;

            output += String.fromCharCode(character1);// output +

            if (encoded3 != 64){ output += String.fromCharCode(character2); }// output +
            if (encoded4 != 64){ output += String.fromCharCode(character3); }// output +
        }
        while (i < input.length);
    }
    return output;
}

/**
* LIST OF CURRENT COOKIES
* @return {array}cookies - associative array of current cookies
*/
function toolCookieJar()
{
    var cookies = new Array();

    var cookieList = document.cookie.split(';');
    for(var c in cookieList)
    {
        var cookie = cookieList[c].split('=');
        cookies[cookie[0].toString()] = cookie[1];
    }
    return cookies;
}


/**
* MAKE A BROWSER COOKIE
* @param {string} id    - cookie index
* @param {string} value - cookie value
* @param {int} days     - life of the cookie
*/
function toolCookieMake(id, value, days)
{
    document.cookie = id + "=" + escape(value);
    if(checkIsSet(days) && !isNaN(days))
    {
        var life = new Date();
        life.setDate(life.getDate()+days);

        document.cookie += ";expires=" + life.toGMTString() + ";";
    }
    document.cookie += ";path=/";
}

/**
* THROW AWAY A COOKIE
*
* @param {mixed} id - cookie index
*/
function toolCookieTrash(cookieObj)
{
    if(checkIsArray(cookieObj))
    {
        for(var key in cookieObj)
        {
            toolCookieMake(key,'',-1);
        }
        return true;
    }
    else if(checkIsString(cookieObj))
    {
        toolCookieMake(cookieObj,'',-1);
        return true;
    }
    else return false;
}

/**
* EXTRACT ITEMS ID FROM THE OBJECT ID STRING
* !IMPORTANT! - Assumes and relies on the object's ID is in the format: itemId+'_ObjectSuffix'
* @return {string} `parsed id string`
*/
function toolGetItemId(parentObj)
{
    if(parentObj==null){ return 0; }

    var idArray = (parentObj.id).split('_');
    return idArray[0];
}


/**
* TOGGLE OBJECT VALUE (TRUE/FALSE) or TOGGLE BOOLEAN VALUE
* 
* @param {mixed} obj
*/ 
function toolToggleBool(obj)
{
    if(checkIsObject(obj))
    {
        obj.value = !obj.value;
    }
    else{ return !obj; }
}



/** 2008-12-01
* SpeedTest INTERFACE
* from: http://alexle.net/archives/257
* 
* Instantiates SpeedTest Object.  
* calculates bitrate of client's (browser) internet connection to server.
*/ 
var toolSpeedTest = function() 
{
    /*
    From:  http://techallica.com/kilo-bytes-per-second-vs-kilo-bits-per-second-kbps-vs-kbps/
    256 kbps            31.3 KBps
    384 kbps            46.9 KBps
    512 kbps            62.5 KBps
    768 kbps            93.8 KBps
    1 mbps ~ 1000kbps   122.1 KBps
    */
};

toolSpeedTest.prototype = {
    runCount: 3                 // how many times we want to run the test for
    ,imgUrl: "jaf/img/speedtest.jpg"    // Where the image is located at
    ,size: 59917                // bytes
    
    //__construct()
    ,run: function(options)
    {
        this.results = []; // reset the results
        if( options && options.init ) options.init(); //pseudo __construct()
        this.callback = ( options && options.callback ) ? options.callback : null; //pseudo __destruct()
        this.runTrial(0, options);
    }

    ,runTrial: function(i, options)
    {
        var imgUrl = this.imgUrl + "?r=" + (i+1)*Math.random(); //ensures unique url to avoid caching
        var _this = this;
        var testImage = new Image();
        testImage.onload = function()
        {
            _this.results[i].endTime = ( new Date() ).getTime();
            _this.results[i].runTime = _this.results[i].endTime - _this.results[i].startTime;

            if ( i < _this.runCount - 1 )
            {
                _this.runTrial( i + 1 ); // run the next trial
            }
            else
            {
                // Execute the callback
                if( _this.callback )
                _this.callback( _this.getResults() );
            }
        };
        this.results[i] = { startTime: ( new Date() ).getTime() };
        testImage.src = imgUrl;
    }

    ,getResults: function() 
    {
        var totalRunTime = 0;
        for( var i = 0; i < this.runCount; i++ )
        {
            if( !this.results || !this.results[i].endTime ) return null; // exit if we found no endTime.  --> test's not done yet
            
            else totalRunTime += this.results[i].runTime;
        }
        
        var avgRunTime = totalRunTime / this.runCount; 

        //FOR THE SAKE OF FLASH MEDIA:  weigh this down drastically
        //multiplyer: * 0.20  (reduce by 80% , in other words, take 20% of total run time)
        //this seems to produce consistent results with http://www.flashcomguru.com/apps/bwcheck/
        //and slightly more conservative
        return {
            avgRunTime: avgRunTime
            ,kbps: Math.floor(( this.size * 8 / 1024 / ( avgRunTime / 1000 ) ) * 0.20)
            ,KBps: Math.floor(( this.size / 1024 / ( avgRunTime / 1000 ) ) * 0.20)
        };
    }
}









/************************************
* jaf ANIMATION  *
*************************************/
function jafAnimate()
{
    this.fade   = animateFade;
    this.hover  = animateHover;
    this.move   = animateMove;
}

/**
* FADE AFFECT
*
* @param {object} obj
* @param {boolean} out - will default to null == false == 'fade in'
*/
function animateFade(obj, out)
{
    if(checkIsSet(obj))
    {
        gFadeStep = 0;
        fadeSpecs['fadeIn'] = !checkIsSet(out);
        fadeSpecs['obj']    = obj; //(checkIsObject(obj.obj))? obj.obj:
    }

    if(gFadeStep <= 100)
    {
        documentOpacity(fadeSpecs['obj'], (fadeSpecs['fadeIn'])? gFadeStep:100-gFadeStep);
        gFadeStep+=20;
        t = setTimeout("animateFade()", 10);
    }
    else
    {
        //documentOpacity(fadeSpecs['obj'], (fadeSpecs['fadeIn'])? 100:0);
        gFadeStep = 0;
    }
}

/**
* HOVER FEATURE
* 
* @param {object} obj
* @param {string} mouseoverColor
*/ 
function animateHover(obj, mouseoverColor)
{
    obj.backColor             = obj.style.backgroundColor;
    obj.style.backgroundColor = mouseoverColor;
    obj.onmouseout = function(){ this.style.backgroundColor = this.backColor;  }
}

//CONCEPT
function animateMove(obj, out)
{
    if(checkIsSet(obj))
    {
        gFadeStep = 0;
        fadeSpecs['fadeIn'] = !checkIsSet(out);
        fadeSpecs['obj']    = obj;
    }

    if(gFadeStep <= 100)
    {
        documentOpacity(fadeSpecs['obj'].obj, (fadeSpecs['fadeIn'])? gFadeStep:100-gFadeStep);
        fadeSpecs['obj'].obj.style.marginLeft = (gFadeStep*(2+40/100))+'px';
        fadeSpecs['obj'].obj.style.marginTop = (gFadeStep*(1-100/140))+'px';
        gFadeStep++;
        t = setTimeout("animateMove()", 0);
    }
    else{gFadeStep = 0;}
}




/************************************
* jaf MODULES  *
*************************************/
function jafModule()
{
    this.loadingText     = moduleLoadingText;
    this.rater           = moduleRater;
}

/**
* DYNAMIC "loading..." STRING OBJECT
* @param {object} parentObj
*/
function moduleLoadingText(parentObj)
{
    if(checkIsDocObj(parentObj))
    {
        gLoadStep = 0;
        this.obj  = loadTxtSpecs['node'];
    }

    if(typeof(gLoadStep)=='undefined')
    {
        erCatch.push(erMsgs['LText']+parentObj.id);

        this.obj = loadTxtSpecs['node'];
        loadTxtSpecs['node'].nodeValue = 'loading';
        return;
    }
    else if(gLoadStep <= (loadTxtSpecs['iterations']*4))
    {
        if(gLoadStep%4==0)
        { loadTxtSpecs['node'].nodeValue  = 'loading'; }
        else
        { loadTxtSpecs['node'].nodeValue += '.'; }

        gLoadStep++;
        t = setTimeout("moduleLoadingText()", loadTxtSpecs['increment']*1000);
    }
    else{gLoadStep = 0;}
}

/**
* RATING MODULE
* @param {string} itemId
* @param {int} avgRating
* @param {int} userRating
* @param {boolean} inactive (true = 'turn the rating off')
*/
function moduleRater(itemId, avgRating, userRating, inactive)
{
    var scale            = ratingSpecs['scale'];
    var starSize         = ratingSpecs['stars']['size'];
    var starSpacer       = ratingSpecs['stars']['spacer'];
    var currentRating    = (userRating>0)? userRating:avgRating;//public avg if no userRating
    var defaultStarType  = (userRating>0)? 'user':'average';
    var currentRatingNum = Math.floor(currentRating);
    var raterObjId       = itemId+"_rater";

    //INITIALIZE RATING OBJECT
    var rateObj = new documentObject(raterObjId);

    //RATING OBJECT STYLE
    rateObj.obj.style.cursor   = "pointer";
    rateObj.obj.style.height   = starSize+"px";
    rateObj.obj.style.width    = ((starSize+starSpacer*2)*scale)+"px";

    //FOR WHEN MOUSE LEAVES RATING AREA
    var mouseOut = function() { _MouseOver(raterObjId, currentRatingNum, defaultStarType); }
    try //Standard browsers:`onmouseout`
    { rateObj.obj.onmouseout   = mouseOut; }
    catch(StandardBrowserErr)//IE:`onmouseleave`
    { rateObj.obj.onmouseleave = mouseOut; }

    //CREATE STAR ITEMS (to scale: ie. 5 or 10)
    for(var x=1; x<=scale; x++)
    {
        var starID   = x+'_'+raterObjId;
        var starType = (x <= currentRatingNum)? defaultStarType:'blank'; //use default until passed current rating
        starObj      = new documentObject(starID, 'div');

        //STAR OBJECT STYLE
        starObj.obj.style.height            = starObj.obj.style.width = starSize+'px';
        starObj.obj.style.marginLeft        = starSpacer+'px';
        starObj.obj.style.marginRight       = starSpacer+'px';
        starObj.obj.style.cssFloat          = 'left';
        starObj.obj.style.display           = 'inline'; //IE compatibility
        starObj.obj.style.overflow          = 'hidden'; //IE compatibility
        starObj.obj.style.backgroundRepeat  = 'no-repeat';
        starObj.obj.style.backgroundPosition= 'left bottom';
        starObj.obj.style.backgroundImage   = ratingSpecs['stars'][starType];

        //rating star events
        if(scale==5){starObj.obj.title = ratingSpecs['titles'][x]; }
        starObj.obj.onmouseover = function() { _MouseOver(raterObjId, currentRatingNum, defaultStarType, this); }
        starObj.obj.onclick     = function()
        {
            var ratingParams       = new Array();
            var loadingText        = new moduleLoadingText(rateObj.obj);
            ratingParams['item']   = itemId;
            ratingParams['rating'] = toolGetItemId(this);

            //grab parent obj, replace rateObj with animated loading text
            var parentObj = rateObj.obj.parentNode;
            parentObj.replaceChild(loadingText.obj, rateObj.obj);

            //@param {object}`result` holds the response from the server.
            var talkResponse = function(result)//append new rating object function
            {
                var newRateObj = new moduleRater(itemId, avgRating, result);

                //parentObj.appendChild(document.createTextNode(result+' '));
                parentObj.replaceChild(newRateObj.obj, loadingText.obj);
            }
            backTalk(ratingSpecs['backend'], ratingParams, talkResponse);
        }
        rateObj.obj.appendChild(starObj.obj);
    }
    this.obj = rateObj.obj;

    /**
    * MOUSEOVER FEATURE FOR STARS.
    * @param {string} raterObjId
    * @param {int} currentRating
    * @param {string} currentStarType (keep track of original state)
    * @param {object} obj - star object
    */
    _MouseOver = function(raterObjId, currentRating, currentStarType, starObj)
    {
        var starType;
        var starRank = toolGetItemId(starObj);

        //loop through needed stars to respond to mouse action
        for(var i=ratingSpecs['scale']; i>0; i--)
        {
            var starId    = i+'_'+raterObjId;
            var chgState  = (i>starRank && currentStarType!='user' || starObj==null);
            var setRating = (chgState)? currentRating:starRank;
            var setType   = (chgState)? currentStarType:'user';
            var newType   = (i>setRating)? 'blank':setType;

            getElement(starId).style.backgroundImage = ratingSpecs['stars'][newType];
        }
    }
}


/************************************
* jaf EXAMPLE  *
*************************************

//new jaf object
var jaf = new jaf();
//jaf globals
var obj = jaf.doc.obj;
var lnk = jaf.doc.lnk;
var img = jaf.doc.img;
var rater = jaf.module.rater;
var fader = jaf.animate.fade;

//assign a module
var ratingItem = new rater(125433,6,0);
getElement('divElement').appendChild(ratingItem);

*************************************/