'use strict';
// General-purpose JavaScript modernisation. Ensure features are available in
// older browsers, whilst using fast native implementations in newer browsers.


// ECMAScript Fifth Edition ___________________________________________________

// Add Fifth Edition methods directly to native objects, where they can be
// safely implemented in Third Edition

// Object
//
if (!('keys' in Object)) {
    Object.keys= function(o) {
        var a= [];
        for (var k in o)
            a.push(k);
        return a;
    };
}
if (!('getOwnPropertyNames' in Object)) {
    Object.getOwnPropertyNames= function() {
        var a= [];
        for (var k in o)
            if (o.hasOwnProperty(k))
                a.push(k);
        return a;
    };
}

// Function
//
if (!('bind' in Function.prototype)) {
    Function.prototype.bind= function(owner) {
        var that= this;
        if (arguments.length<=1) {
            return function() {
                return that.apply(owner, arguments);
            };
        } else {
            var args= Array.prototype.slice.call(arguments, 1);
            return function() {
                return that.apply(owner, arguments.length===0? args : args.concat(Array.prototype.slice.call(arguments)));
            };
        }
    };
}

// String
//
if (!('trim' in String.prototype)) {
    String.prototype.trim= function() {
        return (''+this).replace(/^\s+/, '').replace(/\s+$/, '');
    };
}

// Array
// Consider ArrayLike object to call these methods and also slice, concat (join?)
// on a non-Array object, without copying to a new Array? like:
// new ArrayLike(element.childNodes).forEach(...)
//
if (!('indexOf' in Array.prototype)) {
    Array.prototype.indexOf= function(find, i /*opt*/) {
        if (i===undefined) i= 0;
        if (i<0) i+= this.length;
        if (i<0) i= 0;
        for (var n= this.length; i<n; i++)
            if (i in this && this[i]===find)
                return i;
        return -1;
    };
}
if (!('lastIndexOf' in Array.prototype)) {
    Array.prototype.lastIndexOf= function(find, i /*opt*/) { /* care: i is inclusive */
        if (i===undefined) i= this.length-1;
        if (i<0) i+= this.length;
        if (i>this.length-1) i= this.length-1;
        for (i++; i-->0;)
            if (i in this && this[i]===find)
                return i;
        return -1;
    };
}
if (!('forEach' in Array.prototype)) {
    Array.prototype.forEach= function(action, that /*opt*/) {
        for (var i= 0, n= this.length; i<n; i++)
            if (i in this)
                action.call(that, this[i], i, this);
    };
}
if (!('map' in Array.prototype)) {
    Array.prototype.map= function(mapper, that /*opt*/) {
        var other= new Array(this.length);
        for (var i= 0, n= this.length; i<n; i++)
            if (i in this)
                other[i]= mapper.call(that, this[i], i, this);
        return other;
    };
}
if (!('filter' in Array.prototype)) {
    Array.prototype.filter= function(filter, that /*opt*/) {
        var other= [], v;
        for (var i=0, n= this.length; i<n; i++)
            if (i in this && filter.call(that, v= this[i], i, this))
                other.push(v);
        return other;
    };
}
if (!('every' in Array.prototype)) {
    Array.prototype.every= function(tester, that /*opt*/) {
        for (var i= 0, n= this.length; i<n; i++)
            if (i in this && !tester.call(that, this[i], i, this))
                return false;
        return true;
    };
}
if (!('some' in Array.prototype)) {
    Array.prototype.some= function(tester, that /*opt*/) {
        for (var i= 0, n= this.length; i<n; i++)
            if (i in this && tester.call(that, this[i], i, this))
                return true;
        return false;
    };
}

if (!('isArray' in Array)) {
    Array.isArray= function(o) {
        return Object.prototype.toString.call(o)==='[object Array]';
    };
}

// JSON
//
if (!('JSON' in window)) {
    window.JSON= {};
}
if (!('parse' in JSON)) {
    JSON.parse= function(text, reviver) {
        if (reviver!==undefined) throw 'JSON.parse reviver not supported';
        return eval('('+text.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029')+')');
    };
}
if (!('stringify' in JSON)) {
    JSON.stringify= function(value, replacer, space) {
        if (reviver!==undefined) throw 'JSON.stringify replacer not supported';
        if (value===null) return 'null';
        if (typeof value==='boolean') return ''+value;
        if (typeof value==='number') return ''+value;
        if (typeof value==='string')
            return '"'+value.replace(/[\x00-\x1F\x7F"\\]/g, function(match) {
                var ix= '\"\\\b\t\n\f\r'.indexOf(match[0]);
                if (ix!==-1) return '\\'+'"\\btnfr'.charAt(ix);
                return '\\u'+match[0].charCodeAt(0).toString(16).padLeft(4, '0');
            })+'"';
        if (Array.isArray(value))
            return '['+Array.prototype.map.call(value, JSON.stringify).join(', ')+']';
        return '{'+Object.getOwnPropertyNames(value).map(function(k) {
            return JSON.stringify(k)+': '+JSON.stringify(value[k]);
        }).join(', ')+'}';
    };
}

// XMLHttpRequest
//
if (!window.XMLHttpRequest && 'ActiveXObject' in window) {
    window.XMLHttpRequest= function() {
        return new ActiveXObject('MSXML2.XMLHttp');
    }
}


// DOM ________________________________________________________________________

// DOM nodes may be 'host objects' and are thus not reliably prototypable. So
// make available as `Interface_methodName(target, args)` instead of
// `target.methodName(args)`

// DOM Level 2 Events
//
function EventTarget_addEventListener(target, event, listener) {
    if ('addEventListener' in target)
        target.addEventListener(event, listener, false);
    else if ('attachEvent' in target)
        target.attachEvent('on'+event, EventTarget_addEventListener._attached.bind(target, listener));
    else
        target['on'+event]= EventTarget_addEventListener._propped.bind(listener, target['on'+event]);
}
EventTarget_addEventListener._attached= function(listener, event) {
    if (!('preventDefault' in event) && 'returnValue' in event)
        event.preventDefault= EventTarget_addEventListener._prevent;
    if (!('stopPropagation' in event) && 'cancelBubble' in event)
        event.stopPropagation= EventTarget_addEventListener._stop;
    listener.call(this, event);
};
EventTarget_addEventListener._prevent= function() {
    this.returnValue= false;
};
EventTarget_addEventListener._stop= function() {
    this.cancelBubble= true;
};
EventTarget_addEventListener._propped= function(listener, previous, event) {
    var r= previous.call(this, event);
    if (r!==undefined && !r)
        return false;
    this.returnValue= true;
    this.cancelBubble= false;
    event.stopPropagation= EventTarget_addEventListener._prevent;
    event.preventDefault= EventTarget_addEventListener._stop;
    listener.call(this, event);
    return event.returnValue;
};

// ElementTraversal API
//
function Element_nextElementSibling(element) {
    if ('nextElementSibling' in element)
        return element.nextElementSibling;
    do
        element= element.nextSibling;
    while (element!==null && element.nodeType!==1);
    return element;
}
function Element_previousElementSibling(element) {
    if ('previousElementSibling' in element)
        return element.previousElementSibling;
    do
        element= element.previousSibling;
    while (element!==null && element.nodeType!==1);
    return element;
}
function Element_firstElementChild(element) {
    if ('firstElementChild' in element)
        return element.firstElementChild;
    var child= element.firstChild;
    while (child!==null && child.nodeType!==1)
        child= child.nextSibling;
    return child;
}
function Element_lastElementChild(element) {
    if ('lastElementChild' in element)
        return element.lastElementChild;
    var child= element.lastChild;
    while (child!==null && child.nodeType!==1)
        child= child.previousSibling;
    return child;
}
function Element_childElementCount(element) {
    if ('childElementCount' in element)
        return element.childElementCount;
    var count= 0;
    for (var i= element.childNodes.length; i-->0;)
        if (element.childNodes[i].nodeType===1)
            count++;
    return count;
}

// HTML5
//
function Element_classList(element) {
    if  ('classList' in element)
        return element.classList;
    if (!(this instanceof Element_classList))
        return new Element_classList(element);
    this.element= element;
}
Element_classList.prototype.item= function(ix) {
    return this.element.className.trim().split(/\s+/)[ix];
};
Element_classList.prototype.contains= function(name) {
    var classes= this.element.className.trim().split(/\s+/);
    return classes.indexOf(name)!==-1;
};
Element_classList.prototype.add= function(name) {
    var classes= this.element.className.trim().split(/\s+/);
    if (classes.indexOf(name)===-1) {
        classes.push(name);
        this.element.className= classes.join(' ');
    }
};
Element_classList.prototype.remove= function(name) {
    var classes= this.element.className.trim().split(/\s+/);
    var ix= classes.indexOf(name);
    if (ix!==-1) {
        classes.splice(ix, 1);
        this.element.className= classes.join(' ');
    }
};
Element_classList.prototype.toggle= function(name) {
    var classes= this.element.className.trim().split(/\s+/);
    var ix= classes.indexOf(name);
    if (ix!==-1)
        classes.splice(ix, 1);
    else
        classes.push(name);
    this.element.className= classes.join(' ');
};

// Nearly HTML5, but returned NodeLists cannot be 'live'
//
function Node_getElementsByClassName(node, classnames, taghint) {
    if ('getElementsByClassName' in node)
        return Array.fromList(node.getElementsByClassName(classnames));

    var exps= classnames.split(/\s+/).map(function(name) {
        return new RegExp('(^|\\s)'+RegExp.escape(name)+'(\\s|$)');
    });
    var els= node.getElementsByTagName(taghint || '*');
    var matches= [];
    for (var i= 0, n= els.length; i<n; i++) {
        var el= els[i];
        if (exps.every(function(exp) {
            return exp.test(el.className);
        }))
            matches.push(el);
    }
    return matches;
}


// Unstandardised methods _____________________________________________________

// This are not endorsed by any standards document, but generally useful

Function.returnFalse= function() { return false; }
Function.returnTrue= function() { return true; }

// Backslash-escape string for literal use in a RegExp
//
RegExp.escape= function(s) {
    return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
};

// Prototype/MS-AJAX string ops
//
if (!('startsWith' in String.prototype)) {
    String.prototype.startsWith= function(s) {
        return this.lastIndexOf(s, 0)===0;
    };
}
if (!('endsWith' in String.prototype)) {
    String.prototype.endsWith= function(s) {
        var i= this.length-s.length;
        return i>=0 && this.indexOf(s, i)===i;
    };
}
if (!('padLeft' in String.prototype)) {
    String.prototype.padLeft= function(n, c) { 
        if (c===undefined) c= ' ';
        return new Array(n-this.length+1).join(c)+this;
    };
}

// A lightweight class/instance pattern
//
Function.prototype.makeSubclass= function() {
    function Class() {
        if (!(this instanceof Class))
            throw 'Constructor function requires new operator';
        if ('_init' in this)
            this._init.apply(this, arguments);
    }
    if (this!==Object) {
        Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
        Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
    }
    return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};

// Make Array from Array-like. Like Array slice(), but slice() isn't guaranteed
// to work on host objects like NodeList.
//
Array.fromList= function(list, alwayscopy) {
    if (alwayscopy===undefined) alwayscopy= true;
    if (list instanceof Array && !alwayscopy)
        return list;
    var array= new Array(list.length);
    for (var i= 0, n= list.length; i<n; i++)
        array[i]= list[i];
    return array;
}

// Tree-order traversal
//
function Node_nextNode(node) {
    if (node.firstChild)
        return node.firstChild;
    while (node.nextSibling===null) {
        node= node.parentNode;
        if (node===null)
            return node;
    }
    return node.nextSibling;
}
function Node_previousNode(node) {
    if (node.previousSibling!==null) {
        node= node.previousSibling;
        while (node.lastChild!==null)
            node= node.lastChild;
        return node;
    }
    return node.parentNode;
}

// Use classNames to receive arbitrary data
//
function Element_getClassArgument(element, name) {
    var classes= element.className.trim().split(/\s+/);
    for (var i= 0, n=classes.length; i<n; i++)
        if (classes[i].startsWith(name+'-'))
            return decodeURIComponent(classes[i].substring(name.length+1));
    return null;
}

// Element creation convenience function
//
function Element_make(tag, attrs, content) {
    var el= document.createElement(tag);
    if (attrs!==undefined)
        for (var k in attrs)
            if (attrs.hasOwnProperty(k))
                el[k]= attrs[k];
    if (content!==undefined) {
        if (typeof(content)==='string')
            el.appendChild(document.createTextNode(content));
        else
            for (var i= 0; i<content.length; i++)
                el.appendChild(content[i]);
    }
    return el;
}

function Element_getPageXY(element) {
    var x= 0, y= 0;
    do {
        x+= element.offsetLeft+element.clientLeft;
        y+= element.offsetTop+element.clientTop;
    } while (element= element.offsetParent);
    return [x, y];
}

function Element_hasAncestor(element, ancestor) {
    while (element= element.parentNode)
        if (element===ancestor)
            return true;
    return false;
}

// Catch changes to inputs as soon as possible
//
var ChangeWatcher= Object.makeSubclass();
ChangeWatcher.prototype._init= function(input, callback) {
    this.input= input;
    this.callback= callback;
    this.value= input.value;
    this.check= this.check.bind(this);
    EventTarget_addEventListener(input, 'change', this.check);
    EventTarget_addEventListener(input, 'keyup', this.check);
    if (input.type==='checkbox' || input.type==='radio')
        EventTarget_addEventListener(input, 'click', setTimeout.bind(window, this.check, 0));
    else
        setInterval(this.check, 500);
};
ChangeWatcher.prototype.check= function() {
    var v= this.input.value;
    if (v!==this.value) {
        this.value= v;
        this.callback.call(this.input, v);
    }
};
ChangeWatcher.prototype.set= function(v) {
    this.value=this.input.value= v;
};

// Simple XHR wrapper. Can be used as a one-shot (new HttpChannel().open(...)),
// or kept and re-used, aborting any existing request taking place.
//
// Callback function is called with args (success, response), where success is
// true for a normal 200 (with the decoded JSON response), false for any other
// response or timeout (with an error message in response), or null if the
// request was aborted due to a new request coming in on the channel.
//
var HttpChannel= Object.makeSubclass();
HttpChannel.prototype._init= function(maxtime /* opt */) {
    this.maxtime= maxtime || 5000;
    this.callback= null;
    this.timer= null;
    this.ontimedout= this.ontimedout.bind(this);
    this.request= new XMLHttpRequest();
    this.request.onreadystatechange= this.onchanged.bind(this);
}
HttpChannel.prototype.close= function(callback /* default */) {
    if (this.timer!==null) {
        clearTimeout(this.timer);
        this.timer= null;
        this.request.abort();
        if (callback || callback===undefined)
            this.callback(null, 'Request aborted');
    }
};
HttpChannel.prototype.open= function(method, url, form, callback) {
    this.close();
    var q= [];
    if (form)
        if (typeof form==='string')
            q.push(form);
        else
            for (k in form) if (form.hasOwnProperty(k))
                q.push(encodeURIComponent(k)+'='+encodeURIComponent(form[k]));
    q.push('client=xhr');
    q= q.join('&');

    this.callback= callback || null;
    this.timer= setTimeout(this.ontimedout, this.maxtime);
    method= method.toUpperCase()

    if (method==='GET' || method==='HEAD')
        url+= (url.indexOf('?')===-1? '?' : '&')+q
    this.request.open(method, url, true);
    if (method==='POST' || method==='PUT') {
        this.request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        this.request.send(q);
    } else {
        this.request.send();
    }
};
HttpChannel.prototype.onchanged= function() {
    if (this.request.readyState!==4)
        return;
    if (this.timer!==null) {
        clearTimeout(this.timer);
        this.timer= null;
    }
    if (this.request.status!==200 || !this.request.responseText.startsWith('//'))
        this.callback(false,
            'Request failed with status '+this.request.status+': '+
            this.request.responseText.slice(0, 40)+(this.request.responseText.length>40? '...' : '')
        );
    else
        this.callback(true, JSON.parse(this.request.responseText.slice(2)));
};
HttpChannel.prototype.ontimedout= function() {
    this.timer= null;
    this.request.abort();
    this.callback(false, 'Request timed out');
    this.callback= null;
};


// Shortcut names _____________________________________________________________

function id(s) {
    return document.getElementById(s);
}
function cn(classes, taghint) {
    return Node_getElementsByClassName(document, classes, taghint);
}
var cl= Element_classList;
var el= Element_make;
var ev= EventTarget_addEventListener;

function describe(o) {
    var pars= [];
    for (var k in o)
        pars.push(k);
    return o+': '+pars.join(', ');
};


