/**
 * Ioko class
 * 
 * Base functionality class
 * 		Handles:
 * 				- namespacing
 * 				- cross browser safe debugging
 * 				- loading various asset types (script, stylesheet, images)
 * 				- lazy loading and keeping track of modules and their dependencies
 * 
 * 
 * @author Alec Hill
 */
var Ioko = (function() {

    // PRIVATE /////////////////

    var _major = '0';
    var _minor = '1';

    // track what modules are loaded and events set on their load
    var _modules = {};
    var _module_load_events = {};
    var _module_load_listeners = [];
    var _module_load_poller = false;
    var _module_ready_events = {};
    var _module_ready_listeners = [];
    var _module_ready_poller = false;

    // track window load and events to run on it
    var _window_loaded = false;
    var _window_load_events = [];


    /**
    * _onWindowLoad
    *
    * Called when the window / dom has loaded. 
    * Executes the functions that have been added to the _window_load_events array
    * 
    */
    var _onWindowLoad = function() {
        for (var i = 0, len = _window_load_events.length; i < len; i++) {
            var obj = _window_load_events[i];
            obj.func.apply(obj.bind, obj.args);
        }
        _window_loaded = true;
    }

    // start listening for the dom ready / window load events ///////////////////////////////////
    var _fn = _onWindowLoad;
    if (window.top == window) {
        if (window.MooTools) {
            window.addEvent('domready', _fn);
        } else if (window.jQuery) {
            window.jQuery(function($) { _fn(); });
        } else if (window.Prototype) {
            document.observe('dom:loaded', _fn);
        } else if (window.YAHOO) {
            YAHOO.util.Event.onDOMReady(_fn);
        } else {
            if (window.addEventListener) {
                window.addEventListener('load', _fn, false);
            } else if (window.attachEvent) {
                window.attachEvent('load', _fn);
            } else {
                var old_onload = window.onload;
                if (old_onload != null) {
                    window.onload = function() {
                        old_onload();
                        _fn();
                    }
                }
                else {
                    window.onload = function() { _fn(); }
                }
            }
        }
    }
    else {
        var old_onload = window.onload;
        if (old_onload != null) {
            window.onload = function() {
                old_onload();
                _fn();
            }
        }
        else {
            window.onload = function() { _fn(); }
        }

    }
    /////////////////////////////////////////////////////////////////////////////////////////////

    /**
    * _namealize
    *
    * normalizes names of modules for using as object property names ( replaces dot notation with underscores )
    *
    * @param name {String} - name of the module
    * @return {String}
    */
    var _namealize = function(name) {
        return name.replace(/\./g, '_');
    }

    /**
    * _pathalize
    *
    * changes namespace of module into a path to js file
    *
    * @param name {String} - name of the module
    * @return {String}
    */
    var _pathalize = function(name) {
        return name.replace(/\./g, '/') + '.js';
    }

    /**
    * _isModuleRequested
    *
    * checks if a module has been asked to load yet
    *
    * @param name {String} - name of the module
    * @return {Boolean}
    */
    var _isModuleRequested = function(name) {
        return _modules[_namealize(name)] != undefined;
    }

    /**
    * _isModuleRegistered
    *
    * checks if a module has been registered (ie script loaded - but the code may not have been executed yet)
    *
    * @param name {String} - name of the module
    * @return {Boolean}
    */
    var _isModuleRegistered = function(name) {
        var name = _namealize(name);
        return _modules[name] && _modules[name].registered;
    }

    /**
    * _isModuleReady
    *
    * checks if a module is ready to have its code executed (all dependencies are fully loaded)
    *
    * @param name {String} - name of the module
    * @return {Boolean}
    */
    var _isModuleReady = function(name) {
        var name = _namealize(name);
        if (_modules[name] && _modules[name].registered) {
            for (var i = 0, len = _modules[name].dependencies.length; i < len; i++) {
                if (!_isModuleLoaded(_modules[name].dependencies[i])) return false;
            }
            return true;
        }
        return false;
    }

    /**
    * _isModuleLoaded
    *
    * checks if a module is fully loaded
    *
    * @param name {String} - name of the module
    * @return {Boolean}
    */
    var _isModuleLoaded = function(name) {
        var name = _namealize(name);
        return _modules[name] && _modules[name].loaded;
    }

    /**
    * _pollForModuleEvent
    *
    * adds a listener for a module event, starts / clears an interval that polls for that particular event type
    *
    * @param event_type {String} - type of the event
    * @param name {String} - name of the module
    */
    var _pollForModuleEvent = function(event_type, name) {
        // select listeners, poller, and method for checking status of module - depending upon event type
        switch (event_type) {
            case 'load':
                var listeners = _module_load_listeners;
                var poller = _module_load_poller;
                var checkStatus = _isModuleLoaded;
                break;
            case 'ready':
                var listeners = _module_ready_listeners;
                var poller = _module_ready_poller;
                var checkStatus = _isModuleReady;
                break;
            default:
                return;
        }
        //add to listeners array if not already there
        var found = false;
        for (var i = 0, len = listeners.length; i < len; i++) {
            if (listeners[i] == name) {
                found = true;
                break;
            }
        }
        if (!found) listeners.push(name);
        // check if module poller is already running
        if (poller) return;
        // module poller not started yet so start it up...
        poller = setInterval(function() {
            // must run in reverse so we can remove listeners along the way
            for (var i = listeners.length - 1; i >= 0; i--) {
                var name = listeners[i];
                if (checkStatus(name)) {
                    // remove from listeners array
                    listeners.splice(i, 1);
                    _doModuleEvents(event_type, name);
                }
            }
            // if nothing to listen for load any more, clear the poller
            if (!listeners.length) {
                clearInterval(poller);
                poller = false;
            }
        }, 50);

    }

    /**
    * _doModuleEvents
    *
    * calls the functions that have been set to be run on a module event
    * 
    * @param event_type {String} - type of the event
    * @param name {String} - name of the module
    */
    var _doModuleEvents = function(event_type, name) {
        switch (event_type) {
            case 'load': var events = _module_load_events; break;
            case 'ready': var events = _module_ready_events; break;
            default: return;
        }
        var name = _namealize(name);
        for (var i = 0, len = events[name].length; i < len; i++) {
            var obj = events[name][i];
            obj.func.apply(obj.bind, obj.args);
        }
    }

    /**
    * _onModuleEvent
    *
    * generic event subscriber for modules
    *
    * @param event_type {String} - type of the event
    * @param name {String} - name of the module
    * @param func {Function} - reference to function to add
    * @param args {Array} - an array of arguments to be passed to the function
    * @param bind {Object} - the object to to which 'this' will refer to within the scope of the function
    * @return {String}
    */
    var _onModuleEvent = function(event_type, name, func, args, bind) {
        switch (event_type) {
            case 'load': var events = _module_load_events; break;
            case 'ready': var events = _module_ready_events; break;
            default: return;
        }
        var name = _namealize(name);
        events[name] = events[name] || [];
        events[name].push({ func: func, args: args || [], bind: bind || window });
        _pollForModuleEvent(event_type, name);
    }



    // PUBLIC ///////////////////

    return {

        /**
        * version
        *
        * returns the version
        *
        * @return {String}
        */
        version: function() {
            return _major + '.' + _minor
        },

        /**
        * cross browser safe console logging
        *
        * outputs a debug message / object for inspection to console if available 
        * 
        * @param something {Mixed} - something to inspect / log
        */
        debug: function(something) {
            if (!Ioko.namespace('config').debug) return;
            if (typeof something == typeof '' || typeof something == typeof 1 || typeof something == typeof true) var something = 'DEBUG' + ' - ' + something;
            if (window['console']) {
                console.log(something);
            } else {
                if (!document.body || !Ioko.namespace('config').log_panel) return this;
                var pane = document.getElementById('iokoLog')
                if (!pane) {
                    pane = document.createElement('div');
                    pane.id = 'iokoLog';
                    var s = pane.style;
                    s.position = 'absolute';
                    s.left = pane.style.bottom = 0;
                    s.width = '100%';
                    s.height = '100px';
                    s.backgroundColor = '#eeeeee';
                    s.borderTop = '3px solid #666666';
                    s.zIndex = '9999';
                    s.overflow = 'auto';
                    document.body.appendChild(pane);
                }
                var item = document.createElement('div');
                item.style.borderBottom = '1px solid #cccccc';
                pane.appendChild(item);
                var top = item.offsetTop; // ie7 likes this here or makes pane shrink
                item.appendChild(document.createTextNode(something));
                pane.scrollTop = top;
            }
            return this;
        },



        /**
        * namespace
        *
        * creates / fetches a namespace
        *
        * @param ns {String} - string representation of name space (i.e. first.second)
        * @param base {Mixed} - the base object to add the namespaces to (defaults to this) OR a string name of the base object
        * @return {Object}
        */
        namespace: function(ns, base) {
            var namespaces = ns.split('.');
            if (typeof base == typeof '') {
                if (window[base] == undefined) window[base] = {};
                base = window[base];
            }
            var ret = base || this;
            for (var i = 0, len = namespaces.length; i < len; i++) {
                var name = namespaces[i];
                if (ret[name]) {
                    if (typeof ret[name] == typeof {} || typeof ret[name] == typeof function() { }) {
                        ret = ret[name];
                    } else {
                        throw new Error("namespace - cannot use '" + name + "' as part of '" + ns + "' namespace as it is already being used for other purposes");
                    }
                } else {
                    ret = ret[name] = {};
                }
            }
            return ret;
        },

        /**
        * isWindowLoaded
        *
        * returns whether the window has loaded or not
        *
        * @return {String}
        */
        isWindowLoaded: function() {
            return _window_loaded;
        },

        /**
        * onWindowLoad
        *
        * adds a function to run on the window.onload / dom ready event
        *
        * @param func {Function} - reference to function to add
        * @param args {Array} - an array of arguments to be passed to the function
        * @param bind {Object} - the object to to which 'this' will refer to within the scope of the function
        * @return {Object}
        */
        onWindowLoad: function(func, args, bind) {
            if (this.isWindowLoaded()) {
                func.apply(bind || window, args || []);
                return this;
            }
            _window_load_events.push({ func: func, args: args || [], bind: bind || window });
            return this;
        },

        /**
        * isModuleLoaded
        *
        * checks if a module is fully loaded
        *
        * @param name {String} - name of the module
        * @return {String}
        */
        isModuleLoaded: function(name) {
            return _isModuleLoaded(name);
        },

        /**
        * registerModule
        *
        * registers a module to be used and starts up fetching its dependencies
        *
        * @param props {Object} - properties object:
        * 								- name {String}: the namespace of the module, should match the path to its file  
        * 								- uses {Array}: an array of string names of modules this one depends on
        * 								- images {Array}: an array of strings srcs of images to be preloaded for use in module
        * 								- styles {Array}: an array of strings hrefs of stylesheets to be included for use in module
        * 								- code {Function}: a function containing the module's code, this will be run when the dependencies are all loaded
        */
        registerModule: function(props) {
            var name = props.name;
            var uses = props.uses || [];
            var images = props.images || [];
            var styles = props.styles || [];
            var code = props.code || function() { };
            // register it
            Ioko.debug('Registering module - ' + name);
            _modules[_namealize(name)] = {
                registered: true,
                loaded: false,
                dependencies: uses
            }
            // load dependencies
            for (var i = 0, len = uses.length; i < len; i++) {
                this.loadModule(uses[i]);
            }
            // @todo - check for load status of stylesheets and images as well as js???
            for (var i = 0, len = styles.length; i < len; i++) {
                this.loadStylesheet(styles[i]);
            }
            this.loadImages(images);
            // run the module code after all the dependencies have been made ready
            _onModuleEvent('ready', name, function() {
                // run module code
                code.call(this);
                _modules[_namealize(name)].loaded = true;
                Ioko.debug('Loaded module - ' + name);
            });
        },

        /**
        * loadModule
        *
        * loads a module script
        *
        * @param name {String} - name of the module
        * @return {String}
        */
        loadModule: function(name) {
            if (!_isModuleRequested(name)) {
                _modules[_namealize(name)] = {
                    registered: false,
                    loaded: false,
                    dependencies: []
                };
                this.loadScript((this.namespace('config').base_code_path || '') + _pathalize(name));
            }
            return this;
        },

        /**
        * onModuleLoad
        *
        * adds a function to run when the module is fully loaded and can be used
        *
        * @param name {String} - name of the module
        * @param func {Function} - reference to function to add
        * @param args {Array} - an array of arguments to be passed to the function
        * @param bind {Object} - the object to to which 'this' will refer to within the scope of the function
        * @return {Object}
        */
        onModuleLoad: function(name, func, args, bind) {
            _onModuleEvent('load', name, func, args, bind);
            return this;
        },

        /**
        * loadScript
        *
        * includes an external script element in the head
        *
        * @param src {String} - the src of the script
        * @param callback {Function} - a function to be executed on load
        */
        loadScript: function(src, callback) {
            Ioko.debug('Loading script ' + src);
            var el = document.createElement('script');
            el.setAttribute('src', src);
            el.setAttribute('type', 'text/javascript');
            if (callback) {
                el.onload = callback;
                el.onreadystatechange = function() {
                    if (/(complete|loaded)/.test(this.readyState)) callback();
                }
            }
            document.getElementsByTagName('head')[0].appendChild(el);
            return this;
        },

        /**
        * loadStyles
        *
        * includes an external script element in the head
        * 
        * @param href {String} - the href of the stylesheet
        * @param media {String} - the media stylesheet should apply to - defaults to screen
        */
        loadStylesheet: function(href, media) {
            Ioko.debug('Loading stylesheet ' + href);
            var el = document.createElement('link');
            el.setAttribute('href', href);
            el.setAttribute('type', 'text/css');
            el.setAttribute('rel', 'stylesheet');
            el.setAttribute('media', media || 'screen');
            document.getElementsByTagName('head')[0].appendChild(el);
            return this;
        },

        /**
        * loadImages
        *
        * preloads images
        * 
        * @param src {Mixed} - string src of image to preload, or an array of string srcs
        */
        loadImages: function(src) {
            var preloaded = [];
            var src = (typeof src == typeof []) ? src : [src];
            for (var i = 0, len = src.length; i < len; i++) {
                preloaded.push(new Image());
                preloaded[preloaded.length - 1].src = src[i];
            }
            return this;
        }

    }

})();


/* // example config and bootstrap... 
(function(){
	// path to where Ioko dir lives
	Ioko.namespace('config').base_code_path = '';
	
	// set application name
	// - this is the directory and namespace of application specific code 
	// - directory should live at same level as Ioko directory
	Ioko.namespace('config').app_name = 'App';
	
	// bootstrap function for the SiteController to use
	Ioko.namespace('config').bootStrap = function(){
		// map body ids to page controller classes
		Ioko.controllers.SiteController.addPageController('tester', 'Tester');
	}
	
	// load site controller and dependencies
	Ioko.loadModule('Ioko.controllers.SiteController');
})();
*/
