// movenetworks.js - Useful stuff we use over and over
// Copyright (c) 2003-2005 Move Networks
// Contains stuff from prototype.js as well as MochiKit - I'd like to standardize on
// some third party framework at some point - as long as it's not huge

if (!String.prototype.strip)
String.prototype.strip = function()
{   // returns a copy of this string with leading and trailing whitespace removed
    var temp = this.replace( /^\s+/g, "" ); // strip leading space
    return temp.replace( /\s+$/g, "" ); // strip trailing space
}

if (!String.prototype.endswith)
String.prototype.endswith = function(s)
{   // returns true if this ends with s's value
    return this.substr(this.length-s.length) == s;
}

if (!String.prototype.startswith)
String.prototype.startswith = function(s)
{   // returns true if this starts with s's value
    return this.substring(0,s.length) == s;
}

if (!String.prototype.format)
String.prototype.format = function()
{   // Given a template, replaces '%s' with remaining parameters, in order.
    // ex: 'hello, %s, you are %s feet tall!'.format('Frank', 6)
    var params = arguments;
    var i = 0;
    var x = function() { return params[i++]; }
    return this.replace(/(%s)/g, x);
}

if (!window.$)
$ = function()
{   // Given one or more elements or IDs, returns the elements. If multiple are requested,
    // returns them as an array.
    // Ex: $('foo').innerHTML = '<br>Grr'
    // inspired by prototype.js - thank you!
    var ret = [];
    for (var i = 0; i < arguments.length; i++)
    {
        var e = arguments[i];
        if (typeof e == 'string')
            e = document.getElementById(e);

        if (arguments.length == 1)
            return e;

        ret.push(e);
    }

    return ret;
}

// MN is the top-level namespace for all general-purpose code
var MN =
{
    // Note that this next line is autogenerated - don't mess with it!
    RELEASE_VERSION : '06040501',

    Class : function(parentClass)
    {   // Creates a "class" (mostly just syntactic sugar - but does bind all methods)
        func = function() { MN.BindMethods(this); this.initialize.apply(this, arguments); }
        if (parentClass)
            func.prototype = new parentClass;
        return func;
    },

    MakeBound : function(inst, meth)
    {   // returns the given method as a function bound to the given instance, so that
        // it could e.g. be used as a callback function and still know its instance
        var __method = meth;
        if (__method.__originalMethod) // don't double-bind methods
            __method = __method.__originalMethod;

        var bound = function() { return __method.apply(inst, arguments); }
        bound.__originalMethod = __method;
        return bound;
    },

    BindMethods : function(inst)
    { // from various - prototype, MochiKit, etc. Binds all methods in instance
        for (var prop in inst)
        {
            var meth = inst[prop];
            if (typeof(meth)  == 'function' && prop != 'initialize')
                inst[prop] = MN.MakeBound(inst, meth);
                //inst[prop] = meth.bind(inst); //Bind(meth, inst);
        }
    },

    EvalJSON : function(raw)
    {   // Given a JSON string, returns it as an object or an empty object on failure
        raw = raw || '{}';

        var obj = {};
        try
        {
            eval('obj=' + raw); // wheee
        }
        catch (e)
        {
            logError('EvalJSON failed', e);
            log(raw)
        }
        return obj;
    },

    PadDigits : function(n, totalDigits, padChar)
    {   // returns a number as a string pre-padded to a length of totalDigits using
        // the given padChar (defaults to '0')
        if (!padChar)
            padChar = '0';

        n = n.toString();
        var pad = '';
        for (var i=0; i < (totalDigits - n.length); i++)
            pad += '0';
        return pad + n;
    },

    GetPageParams : function()
    {   // returns all page query parameters as a dictionary
        return MN.URL.GetParams(window.location.search);
    },

    GetWindowSize : function()
    {   // returns the window's current width and height as [w, h]
        var w,h;
        if (self.innerHeight) // all except Explorer
        {
            w = self.innerWidth;
            h = self.innerHeight;
        }
        else if (document.documentElement && document.documentElement.clientHeight)
            // Explorer 6 Strict Mode
        {
            w = document.documentElement.clientWidth;
            h = document.documentElement.clientHeight;
        }
        else if (document.body) // other Explorers
        {
            w = document.body.clientWidth;
            h = document.body.clientHeight;
        }
        return [w,h];
    },

    GetTimestamp : function()
    {   // returns a string 'HH:MM:SS' that represents now
        var now = new Date();
        return '%s:%s:%s'.format(MN.PadDigits(now.getHours(), 2), MN.PadDigits(now.getMinutes(), 2), MN.PadDigits(now.getSeconds(), 2));
    },

    ConvertToTimestamp : function(s, includeHours)
    {   // converts a number of seconds into a string in 'HH:MM:SS' format
        if (s < 0)
            s = 0;

        var h = Math.floor(s / 3600);
        s -= h * 3600;
        var m = Math.floor(s / 60);
        s -= m * 60;
        s = Math.floor(s);

        // don't include the hours unless h>0
        var ret = MN.PadDigits(m, 2) + ':' + MN.PadDigits(s, 2);
        if (h > 0 || includeHours)
            ret = MN.PadDigits(h, 2) + ':' + ret;
        return ret;
    },

    ToArray : function(bad)
    { // converts a quasi-array (like function.arguments) into a real one
        var good = [];
        for (var i=0; i < bad.length; i++)
            good.push(bad[i]);
        return good;
    },

    Update : function(dest, src)
    {   // for each property in src, copy to dest
        for (var prop in src)
            dest[prop] = src[prop];
        return dest;
    },

    LoadTestLib : function()
    {   // Creates an invisible logging pane - inspired by MochiKit
        MN.Log.ShowPane(0);
        MN.RQA = {};
        if (MN.RQA._pane)
            return;
        MN.RQA._form = document.createElement('form');
        MN.RQA._form.name = 'remoteQAForm';
        MN.RQA._form.id = 'remoteQAForm';
        MN.RQA._form.innerHTML = 'Test Info Area<br>';
        MN.RQA._script = document.createElement('script');
        MN.RQA._script.src ='testRemote.js';
        MN.RQA._script.type='text/javascript';
        document.body.appendChild(MN.RQA._script);
        MN.RQA._pane = document.createElement('div');
        MN.Update(MN.RQA._pane.style, {'display':'block', 'width':'100%', 'overflow':'auto',
        'position':'fixed', 'left':'0px', 'bottom':'0px', 'font':'8pt Verdana,sans-serif',
        'height':'10em', 'backgroundColor':'white', 'borderTop':'2px solid black'});

        // display the version number in the log pane
        MN.RQA._pane.appendChild(MN.RQA._form);
        document.body.appendChild(MN.RQA._pane);
    }
};

MN.URL =
{   // Functions for manipulating URLs

    Root : function(url)
    {   // Returns the URL root, e.g. http://www.foo.com/biff/baz/frank.html --> http://www.foo.com/
        // On empty input or invalid URL, returns the empty string
        if (!url) return '';

        // Find and skip ://
        var i = url.indexOf('://');
        if (i == -1) return '';
        i += 3;

        i = url.indexOf('/', i);
        if (i == -1)
            return url+'/'; // the URL is already a base URL, just tack on a slash
        return url.substr(0, i+1);
    },

    Base : function(url)
    {   // Returns the URL for the directory, e.g. http://www.foo.com/biff/baz/frank.html --> http://www.foo.com/biff/baz/
        // On empty input or invalid URL, returns the empty string
        if (!url) return '';

        // Find :// to detect a URL that is already a root
        var i = url.indexOf('://');
        if (i == -1) return '';
        i += 2; // now points to last slash in ://

        var last = url.lastIndexOf('/');
        if (i == last)
            return url + '/'; // url is a root URL, so the base is just the root, but also tack on a slash

        return url.substr(0, last+1);
    },

    Join : function(base, rel)
    {   // Creates an absolute URL from base + rel. Info from base is used only as needed. Ex:
        // http://www.google.com/biff/foo.html, bar.html --> http://www.google.com/biff/bar.html
        // http://www.google.com/biff/foo.html, /bar.html --> http://www.google.com/bar.html
        // http://www.a.com/b/c.html, http://www.b.com/a.html --> http://www.b.com/a.html
        // Does not (yet) auto fixup rel with '..' in them. If base is empty, it is assumed
        // that the caller means 'relative to current page', so window.location.href is used.
        if (!rel) return base; // nothing relative to an empty base is just the base
        if (rel.indexOf('://') != -1)
            return rel; // already a "full" URL
        if (!base)
            base = window.location.href;
        if (rel.charAt(0) == '/')
            return MN.URL.Root(base) + rel.substr(1);
        return MN.URL.Base(base) + rel;
    },

    GetParams : function(url)
    {   // reads off a parameter string and returns it as a dictionary
        var i = url.indexOf('?');
        if (i == -1)
            return {};

        var ret = {};
        var params = url.substr(i+1).split('&');
        for (var i=0; i<params.length; i++)
        {
            var param = params[i].split('=', 2);
            if (param.length == 1)
                ret[param[0]] = true;
            else if (param.length == 2)
                ret[param[0]] = decodeURIComponent(param[1]);
        }
        return ret;
    },

    SetParams : function(url, params)
    {   // returns the url (minus old query, if any) + the given params (a dictionary) as a query string
        // strip off old query string
        var i = url.indexOf('?');
        if (i != -1)
            url = url.substr(0, i);

        var pairs = [];
        for (var i in (params || {}))
            pairs.push(encodeURIComponent(i) + '=' + encodeURIComponent(params[i]));

        if (pairs.length == 0) // the caller is just clearing the URL
            return url;
        return url + '?' + pairs.join('&');
    }
}

// Event shtuff - thank you prototype.js
MN.Event = {
  pointerX: function(event) {
    return event.pageX || (event.clientX +
      (document.documentElement.scrollLeft || document.body.scrollLeft));
  },

  pointerY: function(event) {
    return event.pageY || (event.clientY +
      (document.documentElement.scrollTop || document.body.scrollTop));
  },

  _listenerFuncs: [],

  Observe : function(obj, name, cb)
  {
    var obj = $(obj);
    var listName = '_%sListeners'.format(name.toLowerCase());
    if (obj[listName] == null)
    {
        obj[listName] = [];
        var listeners = obj[listName];
        var func = function()
        {
            for (var i=0; i<listeners.length; i++)
                try
                {
                    listeners[i].apply(this, arguments);
                }
            catch (e) {}
        }

        if (obj.addEventListener)
            obj.addEventListener(name, func, false);
        else if (obj.attachEvent)
            obj.attachEvent('on'+name, func);
        MN.Event._listenerFuncs.push([obj, name, func]);
    }

    obj[listName].push(cb);
  },

  StopObserving : function (obj, name, cb)
  {
    var obj = $(obj);
    var listName = '_%sListeners'.format(name.toLowerCase());
    var listeners = obj[listName];
    if (listeners == null)
        return;

    for (var i=0; i < listeners.length; i++)
    {
        if (listeners[i] == cb)
        {
            listeners.splice(i, 1);
            if (listeners.length == 0)
                obj[listName] = null; // completely clear it out
            break;
        }
    }
  },

  RemoveAllObservers : function()
  {
    for (var i=0; i< MN.Event._listenerFuncs.length; i++)
    {
        var entry = MN.Event._listenerFuncs[i];
        var obj = entry[0];
        var name = entry[1];
        obj['_%sListeners'.format(name.toLowerCase())] = null;
        var func = entry[2];
        entry[0] = null;
        if (obj.removeEventListener)
            obj.removeEventListener(name, func, false);
        else if (obj.detachEvent)
            obj.detachEvent('on'+name, func);
        MN.Event._listenerFuncs[i] = null;
    }
  }
}

// prevent memory leaks in IE - without this, it seems like circular references exist
MN.Event.Observe(window, 'unload', MN.Event.RemoveAllObservers);
//MN.Event.observe(window, 'unload', Event.unloadCache, false);

// "base class" of all EventSource children
MN.EventSource = MN.Class();
MN.EventSource.prototype.initialize = function()
{
    this.events = {}; // event type --> array of listeners
}

// Called by clients to add an event callback. Has no effect if already added
MN.EventSource.prototype.addEventListener = function(event, newListener)
{
    var event = event.toLowerCase();

    // add this as a new event type if we don't already have it
    if (!this.events[event])
        this.events[event] = [];

    // add this listener only if it's not already present
    var listeners = this.events[event];
    for (var i in listeners)
    {
        var listener = listeners[i];
        if (newListener == listener)
            return; // already here
    }

    listeners.push(newListener);
}

// IE compatibility function. Sort of.
MN.EventSource.prototype.attachEvent = function(event, newListener)
{
    if (!event.toLowerCase().startswith('on'))
        logError('attachEvent expects "on<eventname>" events not', event);
    else
        this.addEventListener(event.substr(2), newListener);
}

// removes a previously added event listener, if present.
MN.EventSource.prototype.removeEventListener = function(event, oldListener)
{
    var event = event.toLowerCase();
    var listeners = this.events[event];
    if (!listeners)
        return;

    var keepers = [];
    for (var i in listeners)
    {
        var listener = listeners[i];
        if (listener != oldListener)
            keepers.push(listener);
    }
    this.events[event] = keepers;
}

MN.EventSource.prototype.FireEvent = function(event)
{
    var event = event.toLowerCase();
    var listeners = this.events[event];
    if (!listeners)
        return;

    var args = MN.ToArray(arguments).slice(1);
    for (var i in listeners)
    {
        var listener = listeners[i];
        try
        {
            listener.apply(this, args); // is window the right thisObj to use??
        }
        catch (e)
        {
            logError('Listener', listener, 'for event', event, 'had uncaught exception', e.message || e);
        }
    }
}

// Simple AJAX routines - in most cases simply uses MN.AJAX.Get or Post
MN.AJAX =
{
    Create : function()
    {   // creates a new XMLHttpRequest-ish object
        try { return new XMLHttpRequest(); } catch (e) {};
        try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) {};
        try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {};
        return null;
    },

    // Get and Post are simple, convenient forms of Call
    Get : function(url, cb)
    {   // sends an HTTP GET request to the given URL. Any parameters after the required two
        // get converted to part of the query string and are expected to be key-value pairs.
        // For example, Get('http://foo', mycb, 'biff', 10, 'bar', 12) --> http://foo?biff=10&bar=12
        // The callback should take a single param that receives a request-like object once the work
        // is done.
        // Convert extra arguments into a parameters object
        var aParams = MN.ToArray(arguments).slice(2);
        var params = {};
        for (var i=0; i < aParams.length/2; i++)
            params[aParams[i*2]] = aParams[i*2+1];

        return MN.AJAX.Call({ method:'GET', url:url, callback:cb, params:params});
    },

    Post : function(url, cb)
    {   // Just like Get, but uses an HTTP POST
        // Convert extra arguments into a parameters object
        var aParams = MN.ToArray(arguments).slice(2);
        var params = {};
        for (var i=0; i < aParams.length/2; i++)
            params[aParams[i*2]] = aParams[i*2+1];

        return MN.AJAX.Call({ method:'POST', url:url, callback:cb, params:params});
    },

    Submit : function(form, cb)
    {   // Submits the given form (form is either the form itself or the form ID) using
        // the form's specified action (URL) via POST. Additional parameters can be
        // passed in in pairs - they'll get added to the submission as well.
        // NOTE: we handle conversion of parameters a little differently here because in forms
        // the keys may not be unique
        var form = $(form);
        var opt = { method:'POST', url:form.action, cache:false, callback:cb };
        var params = [];
        var MakeParam = function(name, value) { params.push(encodeURIComponent(name) + '=' + encodeURIComponent(value)); };

        // handle form inputs
        for (var i=0; i < form.length; i++)
        {
            var el = form.elements[i];
            if (!el.name)
                continue;

            switch(el.type.toLowerCase())
            {
                case 'hidden':
                case 'textarea':
                case 'text':
                case 'password':
                    MakeParam(el.name, el.value);
                    break;
                case 'radio':
                case 'checkbox':
                    if (el.checked)
                        MakeParam(el.name, el.value);
                    break;
                case 'button':
                case 'submit':
                    // we currently ignore types 'button' and 'submit' - they probably shouldn't be sent unless clicked, right?
                    break;
                case 'select-one':
                    var selected = el.selectedIndex;
                    if (selected >= 0)
                    {
                        var option = el.options[selected];
                        var value = option.value;
                        if (!value)
                            value = option.text; // if values aren't provided, use option label
                        MakeParam(el.name, value);
                    }
                    break;
                case 'select-multiple':
                    for (var j=0; j < el.length; j++)
                    {
                        var option = el.options[j];
                        if (option.selected)
                        {
                            var value = option.value;
                            if (!value)
                                value = option.text; // if values aren't provided, use option label
                            MakeParam(el.name, value);
                        }
                    }
                    break;
                default:
                    logError('MN.AJAX.Submit not handling', el.name, el.type, el.value);
                    break;
            }
        }

        // also toss in extra inputs in the function call
        var aParams = MN.ToArray(arguments).slice(2);
        for (var i=0; i < aParams.length/2; i++)
            MakeParam(aParams[i*2], aParams[i*2+1]);

        opt.body = params.join('&');
        return MN.AJAX.Call(opt);
    },

    Call : function(options)
    {   // Performs an AJAX request based on the given options, an object with some
        // or all of the following: method (GET, POST, or HEAD), cache (bool indicating
        // whether or not to allow the response to come from cache), url (the URL to
        // call), callback (the callback function to call when done), body (the
        // request body to send), headers (an object of extra HTTP request headers),
        // tag (an opaque value to be passed to the callback), params (the request
        // parameters to include as an object)

        var fail = null;
        var method = options.method || 'GET';
        var url = options.url;
        if (!url)
            fail = 'Missing AJAX URL';
        if (!options.callback)
            fail = 'Missing AJAX callback';

        var req = MN.AJAX.Create();
        if (!req)
            fail = 'AJAX not supported';

        // Don't go any further if the options are incorrect or if AJAX support isn't present
        if (fail)
        {   // fake a failure call to the callback
            if (options.callback)
                return options.callback({status:400, statusText:fail, responseText:''});
            else
                logError('No AJAX callback given');
        }

        // Process any parameters, including any that were already in the URL
        var params = options.params || {};
        MN.Update(params, MN.URL.GetParams(url));
        url = MN.URL.SetParams(url, {}); // clear them too

        var headers = options.headers || {};
        if (MN.AJAX.forceNoCache || options.cache == false)
        {
            headers['Cache-control'] = 'no-cache';
            if (method == 'GET') // TODO: add && we're on IE
                params['__bah'] = (new Date()).getTime();
        }
        if (method == 'POST')
            headers['Content-type'] = 'application/x-www-form-urlencoded';

        // Come up with the final URL - if it's a GET request, put all parameters onto the URL as
        // a query string. If it's a POST, put them all into the body.
        var body = options.body || '';
        if (method == 'POST')
        {
            var extra = MN.URL.SetParams('', params).substr(1); // chop off '?' the front
            if (extra)
            {
                if (body)
                    body = body + '&' + extra;
                else
                    body = extra;
            }
            headers['Content-length'] = body.length;
        }
        else // GET or HEAD
        {
            body = null; // not sure we always have to force this, but ...
            url = MN.URL.SetParams(url, params);
        }

        var tag = options.tag;
        var cb = options.callback;
        //log('AJAX.Call', method, url);
        req.open(method, url);

        // Apply extra HTTP headers if needed
        for (var h in headers)
        {
            //log('setting request header', h, headers[h]);
            req.setRequestHeader(h, headers[h]);
        }

        req.onreadystatechange = function()
        {
            if (req.readyState == 4 && cb)
            {   // mock up a request-like object
                var resp = {status:req.status, statusText:req.statusText, responseText:req.responseText, tag:tag};
                cb(resp);
            }
        }

        //log('AJAX.Call body', body);
        req.send(body || null);
    },

    forceNoCache : false // when true, forces all requests to be non-cacheable

    // later: convert and submit form, return code, reuse request objects
};

MN.Position =
{   // also from prototype.js
  cumulativeOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
    } while (element);
    return [valueL, valueT];
  }
}

// routines for loading other Javascript modules
MN._FindLibBase = function()
{   // Returns the base URL to use when loading JS (bases it off movenetworks.js)
    var tags = document.getElementsByTagName('script');
    var libURLs = [];
    for (var i=0; i < tags.length; i++)
    {
        var tag = tags[i];
        var src = tag.src;
        if (!src || !src.endswith('movenetworks.js'))
            continue;

        return src.substring(0, src.lastIndexOf('movenetworks.js'));
    }
    return '';
}

MN._allLibs = {}; // all libraries dynamically loaded so far to prevent reloading
MN._libBase = MN._FindLibBase(); // base URL to movenetworks.js

MN.UpdateAllLibs = function()
{   // Updates MN._allLibs with the src URLs from all script tags (sometimes needed
    // because stuff gets loaded after the initial check)
    var tags = document.getElementsByTagName('script');
    for (var i=0; i < tags.length; i++)
    {
        var src = tags[i].getAttribute('src');
        if (src)
            MN._allLibs[src] = true;
    }
}

MN.FindLibraryURL = function(lookFor)
{   // Looks through MN._allLibs to find the first URL that matches the given text
    MN.UpdateAllLibs();
    for (var i in MN._allLibs)
    {
        if (i.indexOf(lookFor) != -1)
            return i;
    }
    return null;
}

MN.LoadLibrary = function(src)
{   // Loads another Javascript library. 'src' can be a full URL or can be treated as relative to movenetworks.js.
    // An extension is added as needed.
    // You have no guarantee that the library will be loaded before the page is fully loaded, so if you need to
    // do setup that uses functions in the library to load, use Event.observe on window.onload.
    src = MN.URL.Join(MN._libBase, src);
    if (!src.endswith('.js'))
        src = src + '.js';

    if (MN._allLibs[src])
        return; // already loaded
    MN._allLibs[src] = true;

    document.write('<sc' + 'ript type="text/javascript" src="' + src + '">' + '<' + '/scri' + 'pt>');
}

MN.LoadLibrary('logging');

MN.LoadAllLibraries = function()
{   // Called when page loads - loads any scripts listed in 'import' attribute of movenetwork.js script tag
    var tags = document.getElementsByTagName('script');
    var libURLs = [];
    for (var i=0; i < tags.length; i++)
    {
        var tag = tags[i];
        var imports = tag.getAttribute('import');
        if (!imports)
            continue;
        imports = imports.split(' ');
        for (var j in imports)
            libURLs.push(imports[j]);
    }

    for (var i in libURLs)
        MN.LoadLibrary(libURLs[i]);
}

MN.LoadAllLibraries();

