// UKNova general-purpose utilities

// Language utilities _________________________________________________________

// Use prototypes to implement a workable class system
//
function js_class(classname, superclassname, defs) {
    if (superclassname)
        defs.prototype= js_prototypes[superclassname];
    var classobj= js_classobj()
    js_prototypes[classname]=classobj.prototype= new defs;
    window[classname]= classobj;
}

function js_classobj() {
    return function() { if (this._init) this._init.apply(this, arguments); }
}

var js_prototypes= new Object();


// Use a closure to allow a method to be called back with 'this' pointing to
// the actual owner object. Indirect through a look-up Array to avoid the
// browser objects getting circular references (memory leaks in IE). Allow
// arguments to accumulate from creation and call-time.
//
function js_callback(obj, method) {
    if (!window.js_callback_objects) return; // callback post close
    var i= js_callback_objects.length;
    js_callback_objects[i]= obj;
    js_callback_methods[i]= method;
    js_callback_arguments[i]= new Array();
    for (var j= 2; j<arguments.length; j++)
        js_callback_arguments[i].push(arguments[j]);
    return js_callback_make(i);
}

function js_callback_make(i) {
    return function() {
        if (window.closed) return;
        var that= js_callback_objects[i];
        var args= js_callback_arguments[i].slice();
        for (var j= 0; j<arguments.length; j++)
            args.push(arguments[j]);
        return that[js_callback_methods[i]].apply(that, args);
    };
};

var js_callback_objects= new Array();
var js_callback_methods= new Array();
var js_callback_arguments= new Array();

// Trivial lambdas
//
function js_returnfalse() { return false; };
function js_returntrue() { return true; };
function js_returnnull() { return null; };


// DOM utilities ______________________________________________________________

// Get adjacent siblings ignoring text nodes
//
function dom_nextElement(node) {
    var next= node;
    do {
        next= next.nextSibling;
    } while (next && next.nodeType!=ELEMENT_NODE);
    return next;
}
function dom_previousElement(node) {
    var previous= node;
    do {
        previous= previous.previousSibling;
    } while (previous && previous.nodeType!=ELEMENT_NODE);
    return previous;
}

// Get comment descendents
//
function dom_getComments(node) {
    var results= new Array();
    for (var childi= 0; childi<node.childNodes.length; childi++) { var child= node.childNodes[childi];
        if (child.nodeType==COMMENT_NODE) {
            results[results.length]= child;
        } else if (child.nodeType==ELEMENT_NODE) {
            var childresults= dom_getComments(child);
            for (var resulti= 0; resulti<childresults.length; resulti++) { var result= childresults[resulti];
                results[results.length]= result;
            }
        }
    }
    return results;
}


// Remove all content in a node
//
function dom_emptyNode(node) {
    if (!node) return;
    while (node.firstChild)
        node.removeChild(node.firstChild);
}

// Check containment
//
function dom_isinside(element, ancestor) {
    for (var i= ancestor.childNodes.length; i-->0;) {
        var child= ancestor.childNodes[i];
        if (child===element) return true;
        if (child.nodeType==ELEMENT_NODE && dom_isinside(element, child)) return true;
    }
    return false;
}

// Space-separated class name handling
//
function dom_isClass(el, classname) {
    if (!el.className) return false;
    if (el.className==classname) return true;
    return array_index(el.className.split(' '), classname)!=-1;
}
function dom_setClass(el, classname, active) {
    var classes= el.className.split(' ');
    var ix= array_index(classes, classname);
    if (ix==-1 && active) {
        classes[classes.length]= classname;
    } else if (ix!=-1 && !active) {
        classes.splice(ix, 1);
    }
    el.className= classes.join(' ');
}
function dom_classArg(el, classname) {
    var prefix= classname+'-';
    var classes= el.className.split(' ');
    for (var i= classes.length; i-->0;)
        if (classes[i].substring(0, prefix.length)==prefix)
            return classes[i].substring(prefix.length);
    return null;
}

// Import contents of XML element as HTML into a given parent+sibling position
// (sibling null for append). (May fail in IE for some elements not used here.)
//
function dom_importInto(el, parent, sibling) {
    for (var childi= 0; childi<el.childNodes.length; childi++) { var child= el.childNodes[childi];
        var clone= null;
        if (child.nodeType==TEXT_NODE) {
            clone= document.createTextNode(child.data);
        }
        else if (child.nodeType==ELEMENT_NODE) {
            clone= document.createElement(child.nodeName);
            for (var attri= child.attributes.length; attri-->0;) { var attr= child.attributes[attri];
                clone.setAttribute(attr.name, attr.value);
            }
            dom_importInto(child, clone, null);
        }
        if (clone!=null)
            parent.insertBefore(clone, sibling);
    }
}

// Get element position
//
function domext_getPosition(el) {
    if (el.offsetLeft==window.undefined) return null;
    var x= 0;
    var y= 0;
    var w= el.offsetWidth;
    var h= el.offsetHeight;
    while (el && el.nodeType==ELEMENT_NODE) {
        x+= el.offsetLeft;
        y+= el.offsetTop;
        el= el.offsetParent;
    }
    return new Array(x, y, w, h);
}

// IE fixes ___________________________________________________________________

// Work around IE CSS problems
//
var IEWIN= false;
var IE7= false;
/*@cc_on
@if (@_win32)
    IEWIN= true;
@end @*/
var IE7= IEWIN && document.documentElement.style.maxHeight!==window.undefined;
var IE8= IEWIN && document.documentMode>=8;

var CURSOR_POINTER= IEWIN? 'hand' : 'pointer';

// Constants that should be defined on Node, but aren't in IE
//
var ELEMENT_NODE= 1;
var TEXT_NODE= 3;
var COMMENT_NODE= 8;

// IE7 is deranged and doesn't let site use window.prompt() any more.
// Simulate its action, returning to a supplied callback when done.
//
js_class('ErsatzPrompt', null, function() {
    this._init= function(question, initial) {
        this.question= question;
        this.initial= initial;
    };
    this.callback= function(target, method, args) {
        this.target= target;
        this.method= method;
        this.args= args;
        if (!IE7) return this.respond(window.prompt(this.question));
 
        this.shade= document.createElement('div');
        this.shade.style.position= 'fixed'; this.shade.style.zIndex= '100';
        this.shade.style.left= '0'; this.shade.style.top= '0';
        this.shade.style.width= '100%'; this.shade.style.height= '100%';
        this.shade.style.backgroundColor= 'black'; this.shade.style.filter= 'alpha(opacity=50)';
        this.form= document.createElement('form');
        this.form.style.position= 'fixed'; this.form.style.zIndex= '101';
        this.form.style.left= '30%'; this.form.style.top= '25%';
        this.form.style.width= '40%'; this.form.style.backgroundColor= 'ThreeDFace';
        this.form.style.border= 'outset white 2px'; this.form.style.padding= '8px 14px 8px 8px';
        this.form.onsubmit= js_callback(this, 'submit');

        var heading= document.createElement('p');
        heading.style.color= 'ButtonText';
        heading.appendChild(document.createTextNode(this.question));
        this.form.appendChild(heading);
        this.form.appendChild(document.createElement('p'));
        this.field= document.createElement('input');
        this.field.type= 'text'; this.field.value= this.initial;
        this.field.style.width= '100%';
        this.form.lastChild.appendChild(this.field);
        this.form.appendChild(document.createElement('p'));
        var button= document.createElement('input');
        button.type= 'submit'; button.className='default'; button.value= 'OK';
        this.form.lastChild.appendChild(button);
        var button= document.createElement('input');
        button.type= 'submit'; button.value= 'Cancel'; button.style.marginLeft= '8px';
        button.onclick= js_callback(this, 'cancel');
        this.form.lastChild.appendChild(button);

        document.body.insertBefore(this.form, document.body.firstChild);
        document.body.insertBefore(this.shade, document.body.firstChild);
        if (this.field.createTextRange!==window.undefined) {
            var range= this.field.createTextRange();
            range.setEndPoint('StartToEnd', range);
            range.select();
        } else {
            this.field.focus();
        }
    };
    this.submit= function() {
        var response= this.field.value;
        this.form.parentNode.removeChild(this.form);
        this.shade.parentNode.removeChild(this.shade);
        this.respond(response);
        return false;
    };
    this.cancel= function() {
        this.form.parentNode.removeChild(this.form);
        this.shade.parentNode.removeChild(this.shade);
        this.respond(null);
        return false;
    }
    this.respond= function(response) {
        var args= this.args.slice(0);
        args.splice(0, 0, response);
        this.target[this.method].apply(this.target, args)
    };
});

// Image utilities ____________________________________________________________

// KHTML's Image constructor doesn't create a DOM Node, but
// IE's createElement call doesn't create a real image. Choose which is best
//
function img_create(src, alt, title) {
    var img= IEWIN? new Image() : document.createElement('img');
    img.src= src;
    if (alt!=null) img.alt= alt;
    if (title!=null) img.title= title;
    return img;
}

// Preload images (on-page in a hidden div)
//
function img_preload(url) {
    if (img_preloads[url]) return;
    img_preloads[url]= true;
    var preload= document.getElementById('img_preload');
    if (!preload) {
        preload= document.createElement('div');
        preload.id= 'img_preload';
        preload.style.position= 'absolute'; preload.style.overflow= 'hidden';
        preload.style.left= '-128px'; preload.style.width= '64px';
        document.body.insertBefore(preload, document.body.firstChild);
    }
    preload.appendChild(img_create(url, ''));
}

var img_preloads= new Object();


// String utilities ___________________________________________________________

// Get occurances of a character in a string. Plus optionally a ridiculous
// extra one (see textmarkup.js)
//
function string_count(s, c, ridiculous) {
    var count= 0;
    if (ridiculous && s.charAt(0)==c) count++;
    var ix= 0;
    while (ix!=-1) {
       ix= s.indexOf(c, ix+1);
       if (ix!=-1) count++;
    }
    return count;
}

// Remove leading/trailing spaces
//
function string_strip(s, newlines) {
    var spaces= newlines? '\n\r\t ' : '\t ';
    var ix1= s.length;
    while (ix1>0 && spaces.indexOf(s.charAt(ix1-1))!=-1)
        ix1--;
    var ix0= 0;
    while (ix0<ix1 && spaces.indexOf(s.charAt(ix0))!=-1)
        ix0++;
    return s.substring(ix0, ix1);
}

// Convenience substring matches
//
function string_startswith(s, prefix) {
    return s.substring(0, prefix.length)==prefix;
}
function string_endswith(s, suffix) {
    return s.substring(s.length-suffix.length)==suffix;
}
function string_startswithany(s, prefices) {
    for (var i= prefices.length; i-->0;)
        if (string_startswith(s, prefices[i]))
            return true;
    return false;
}
function string_endswithany(s, suffices) {
    for (var i= suffices.length; i-->0;)
        if (string_endswith(s, suffices[i]))
            return true;
    return false;
}
var string_QUERYSPLIT= new RegExp('[&;]');
function string_getquery(query, argname) {
    var args= query.split(string_QUERYSPLIT);
    for (var argi= args.length; argi-->0;) { var arg= args[argi];
        var splitix= arg.indexOf('=');
        if (splitix!=-1 && decodeURIComponent(arg.substring(0, splitix))==argname)
            return decodeURIComponent(arg.substring(splitix+1));
    }
    return '';
}

// Find string in array
//
function array_index(array, item) {
    for (var i= 0; i<array.length; i++)
        if (array[i]==item)
            return i;
    return -1;
}

// UI utilities _______________________________________________________________

// UIButton: Non-form element with disabled, enabled, hover, and active state
// responding to interaction, plus selected state that may be set by script.
// Abstract base class
//
js_class('UIButton', null, function() {
    this._init=  function(element, disabled, hovered, pressed, selected) {
        // Store args, which should be true if button is initially in that
        // state, false if it isn't, or null if that state cannot be achieved.
        // eg. (el, null, false, null, null) for a simple hover-only button.
        //
        this.element= element;
        this.disablable= disabled!==null;
        this.hoverable= hovered!==null;
        this.pressable= pressed!==null;
        this.selectable= selected!==null;
        this.disabled= disabled==true;
        this.hovered= hovered==true;
        this.pressed= pressed==true;
        this.selected= selected==true;

        this.preload();

        // Attach element events to ourselves
        //
        this.element.onmouseover= js_callback(this, 'onmouseover');
        this.element.onmouseout= js_callback(this, 'onmouseout');
        this.element.onmousedown= js_callback(this, 'onmousedown');
        this.element.onmouseup= js_callback(this, 'onmouseup');
        this.element.onclick= js_callback(this, 'onclick');

        // Hack links and buttons so that they don't autofocus on click causing
        // an ugly outline (without sabotaging their ability to focus from
        // keyboard events for accessibility)
        //
        element.style.cursor= CURSOR_POINTER;
        var tag= this.element.tagName.toLowerCase();
        if (tag=='a' || tag=='input')
            this.element.onfocus= js_callback(this, 'onfocus');
        if (IEWIN)
            this.element.ondragstart= js_returnfalse;
    };

    // Set state in response to interaction
    //
    this.onmouseover= function() {
        this.hovered= true;
        if (this.hoverable) this.update();
    };
    this.onmouseout= function() {
        this.hovered=this.pressed= false;
        if (this.hoverable||this.pressable) this.update();
    };
    this.onmousedown= function() {
        this.pressed= true;
        if (this.pressable) this.update();
    };
    this.onmouseup= function() {
        this.pressed= false;
        if (this.pressable) this.update();
    };
    this.onfocus= function() {
        if (this.pressed) this.element.blur();
    };

    // Separate-image-based updates
    //
    this.preload= function() {
        var url= this.geturl();
        if (this.disablable) img_preload(this.changeurl(url, 0));
        img_preload(this.changeurl(url, 1));
        if (this.hoverable) img_preload(this.changeurl(url, 2));
        if (this.pressable || this.selectable) img_preload(this.changeurl(url, 3));
        this.update();
    };
    this.update= function() {
        var disabled= this.disablable && this.disabled
        var hovered= this.hoverable && this.hovered
        var pressed= this.pressable && this.pressed
        var selected= this.selectable && this.selected
        var state= disabled? 0 : (selected||pressed)? 3 : hovered? 2 : 1;
        var url= this.geturl();
        if (url) {
            var newrl= this.changeurl(url, state);
            if (newrl!=url) this.seturl(newrl);
        }
    };
    this.changeurl= function(url, state) {
        var ix= url.lastIndexOf('.');
        if (ix==-1) return url;
        return url.substring(0, ix-1)+state+url.substring(ix);
    };

    // Subclass must provide these
    //
    this.geturl= js_returnnull;
    this.seturl= js_returnnull;

    // User may wish to override to get click events from the target
    //
    this.onclick= js_returntrue;
});

// UIButton using a plain HTML <img> element
//
js_class('ImageButton', 'UIButton', function() {
    this.geturl= function() { return this.element.src; };
    this.seturl= function(url) { this.element.src= url; };
});

// UIButton using an element with an inline CSS background image
// Initialiser should be passed a base image URL in the case that the
// background image is not set in the (easily-readable) inline style.
//
js_class('BackgroundButton', 'UIButton', function() {
    this._init= function(element, url, disabled, hovered, pressed, selected) {
        element.style.backgroundImage= 'url('+url+')';
        UIButton.prototype._init.call(this, element, disabled, hovered, pressed, selected);
    };
    this.geturl= function() {
        var url= this.element.style.backgroundImage;
        if (!url) return null;
        return url.substring(4, url.length-1);
    };
    this.seturl= function(url) {
        this.element.style.backgroundImage= 'url('+url+')';
    };
});

// UIButton that reflects state just as a class
//
js_class('SpriteButton', 'UIButton', function() {
    this.preload= function() {};
    this.update= function() {
        dom_setClass(this.element, 'sprite-disabled', this.disablable && this.disabled);
        dom_setClass(this.element, 'sprite-hovered', this.hoverable && this.hovered);
        dom_setClass(this.element, 'sprite-pressed', this.pressable && this.pressed);
        dom_setClass(this.element, 'sprite-selected', this.selectable && this.selected);
    };
});


// AJAX utilities ____________________________________________________________

// Get an object implementing XMLHttpRequest
//
function xmlhttp_make() {
    // IE7, Safari 1.2, Mozillae: use the XMLHttpRequest constructor.
    // Override response type to avoid odd errors in old Mozilla versions.
    //
    if (window.XMLHttpRequest) {
        var request= new XMLHttpRequest();
        if (request.overrideMimeType)
            request.overrideMimeType('text/xml');
        return request;
    }
    // IE<7/Win: try to get an XmlHttp ActiveX object. May fail due to security
    // settings etc.
    //
    if (window.ActiveXObject) {
        try {
            return new ActiveXObject('MSXML2.XmlHttp');
        } catch (e) {}
    }
    return null;
}


// Put a form-urlencoded query string together
//
function xmlhttp_query(pars) {
    if (!pars) return '';
  var query= '';
  for (par in pars) {
    if (query!='') query+= '&';
    query+= encodeURIComponent(par)+'='+encodeURIComponent(pars[par]);
  }
  return query;
}


// XMLHttpRequest asynchronous request helper object
// Create or subclass, set oncomplete and/or oncancel and call open(post, path, pars, timeout)
//
var ASYNCREQUEST_MAXGET= 200;
js_class('AsyncRequest', null, function() {
    this.req=this.timer= null;

    this.open= function(post, path, pars, timeout) {
        this.req= xmlhttp_make();
        if (this.req==null) {
            setTimeout(js_callback(this, 'timeout'), 0);
            return false;
        }
        this.req.onreadystatechange= js_callback(this, 'onchange');
        var query= xmlhttp_query(pars);
        if (query.length>ASYNCREQUEST_MAXGET) post= true;
        if (post) {
            this.req.open('POST', path, true);
            this.req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
            this.req.send(query);
        } else {
            if (query!='')
                path= path+'?'+query
            this.req.open('GET', path, true);
            this.req.send('');
        }
        this.timer= setTimeout(js_callback(this, 'timeout'), timeout);
        return true;
    };
    this.close= function() {
        if (this.timer!=null) clearTimeout(this.timer);
        if (this.req!=null) {
            this.req.onreadystatechange= js_returnnull;
            this.req.abort();
            this.req= null;
        }
    };

    this.onchange= function() {
        if (this.req.readyState!=4) return;
        if (this.timer!=null) clearTimeout(this.timer);
        var root= this.req.responseXML.documentElement;
        var response= null;
        if (root && root.nodeName=='js') { // JavaScript return value
            response= eval('('+root.firstChild.data+')');
            this.close();
            if (this.oncomplete!=null)
                this.oncomplete(response);
        } else if (root && root.nodeName=='dom') { // XML return value
            if (this.oncomplete!=null)
                this.oncomplete(root);
        } else {
            this.timeout();
        }
    };
    this.timeout= function() {
        this.timer= null;
        this.close();
        if (this.oncancel!=null) this.oncancel();
    };

    this.oncomplete= null;
    this.oncancel= null;
});


// Pick up site code version and user's theme setting from info in document
//
var PAGEDATA= function() {
    var comments= dom_getComments(document.getElementsByTagName('head')[0]);
    if (comments.length==0)
        return {'theme': '-dark'}
    return eval('('+comments[0].data+')');
}();
var THEME= PAGEDATA['theme'];

