/*
 * Copyright (c) 2007 Andrew Sumin (http://jsx.ru/)
 * 
 * Licensed under Creative Commons Atribution 3.0 license
 * http://creativecommons.org/licenses/by/3.0/
 * 
 * Permission is granted to modify this work as stated
 * in license given that this notice is preserved. 
 */

/**
 * Core object contains methods for dynamic loading of scripts.
 */
var jsx = new function(){
    this.build = '0003';
    this.aliases = [];
    this.expectedAliases = {};
    this.charsets = [];
    this.scriptsByFileName = [];
    this._getAliasRegexp = /^\{[^\}]+\}/;
    /**
     * This method loads script and executes listener after script loading.
     * @param {String} String like "foo.bar". This string means to load file bar.js from framework folder subfolder foo.
     * @param {Function} Function for execution after loading script.
     * @method
     */
    this.require = function(url, listener){
        this.Loader.require(url, listener);
    };
    /**
     * This method is for scripts that are loaded. They must execute this method (jsx.loaded) after all initialization functions.
     * @param {String} String like "foo.bar". This string means to load file bar.js from framework folder subfolder foo.
     * @method
     */
    this.loaded = function(url){
        this.Loader.loaded(url);
    };
    /**
     * Returns path to file in src attribute
     * @param {String} file name
     * @method
     */
    this.getBase = function(file, listener){
        function returnBase(script){
            var src = script.getAttribute('src');
            listener(src.substring(0, src.indexOf(file)));
        }
        this.getScriptByFileName(file, returnBase);
    };
    
    /**
     * Returns tag "script" by file name or null if no such tag.
     * @param {String} file name
     * @param {Function} listener to call with founded file as parameter
     * @method
     */    
    this.getScriptByFileName = function(file, listener, /* private */ tries){
        if(this.scriptsByFileName[file]){
           listener(this.scriptsByFileName[file]);
           return;
        }
        tries = tries || 0;

        function findScript(script){
            var src = script.getAttribute('src');
            return src && src.indexOf(file) >= 0;
        }
        this.scriptsByFileName[file] = $A(document.getElementsByTagName('script')).find(findScript);

        if(this.scriptsByFileName[file]){
            listener(this.scriptsByFileName[file]);
        }else if(tries < 1000){
            var _this = this;
            window.setTimeout(function (){_this.getScriptByFileName(file, listener, ++tries)}, 10);
        }
    }
    
    /**
     * Sets the alias as path to file
     * @param {String} alias
     * @param {String} file name
     * @param {String} charset
     */
    this.getBaseAndSetAlias = function(alias, file, charset){
        charset = charset || null;
        function setAlias(base){
            this.setAliasCharset(alias, charset);
            this.setAlias(alias, base);
        }
        this.getBase(file, setAlias.bind(this));
    };
    /**
     * Sets the alias
     * @param {String} alias
     * @param {String} path
     * @method
     */
    this.setAlias = function(name, value){
        this.aliases[name] = value;
        if (!this.expectedAliases[name]){
            return;
        }
        // maybe some scripts are waiting for this alias        
        var listener;
        while ((listener = this.expectedAliases[name].shift())){
            listener();
        }
    };
    /**
     * Sets the charset for scripts with such alias
     * @param {String} alias
     * @param {String} charset
     * @method
     */
    this.setAliasCharset = function(alias, charset){
        this.charsets[alias] = charset;
    };
    /**
     * Returns the alias
     * @param {String} location
     * @method
     */
    this.getAlias = function(location){
        var alias = this._getAliasRegexp.exec(location);
        return (alias ? alias[0].substr(1, alias[0].length - 2) : null);
    };
};

/**
 * Contains all global vars like base to core (this) file browser type and version...
 */
jsx.Vars = new (function (){
    this.UNDEF = 'undefined';
    this.DEBUG = false;

    this.FALSE = function() { return false;        };
    this.TRUE  = function() { return true;         };
    this.NULL  = function() { return null;         };
    this.EMPTY = function() { /* return nothing */ };
});

/**
 * Creates tag script.
 * @alias jsx.Scripts
 */
jsx.Scripts = new function (){
    /**
     * This method creates tag SCRIPT.
     * @param {Object} Attrbites
     * @param {Function} Listerner for script creation.
     */
    this.createScript = function (attributes, listener, /*private*/ tries){
        tries = tries || 0;
        listener = typeof(listener) == 'function' ? listener : jsx.Vars.NULL;
        var script = this._createScript(attributes);
        if (script == null && tries < 10){
            window.setTimeout(this.createScript.bind(this, attributes, listener, ++tries), 10);
        }else{
            listener(script);
        }
    };

    this._createScript = function (attributes) {
        var script = document.createElement('script');
        script.setAttribute('type', 'text/javascript');        
        for (var i in attributes){
            script.setAttribute(i, attributes[i]);
        }
        // InsertBefore for IE.
        // If head is not closed and use appendChild IE crashes.
        var head = document.getElementsByTagName('head').item(0);
        head.insertBefore(script, head.firstChild);
        return script;
    }
};

/**
 * Creates tag link.
 * @alias jsx.Links
 */
jsx.Links = new function (){
    /**
     * This method creates tag LINK.
     * @param {Object} Attrbites
     */
    this.createLink = function (attributes){
        var link = document.createElement('link');
        attributes.rel = attributes.rel || 'stylesheet';
        attributes.type = attributes.type || 'text/css';
        for (var i in attributes){
            link.setAttribute(i, attributes[i]);
        }
        // InsertBefore for IE.
        // If head is not closed and use appendChild IE crashes.
        var head = document.getElementsByTagName('head').item(0);
        head.insertBefore(link, head.firstChild);
    };
};

/**
 * Creates real URL from strings.
 */
jsx.ConstructURL = new function(){
    /**
     * Creates real URL from strings.
     * @param {String} String like "foo.bar"
     * @param {String} File extention like "js"
     * @return {String} URL like http://www.yandex.ru/foo/bar.js
     */
    this.construct = function(string, type){
        type = type || 'js';
        string = string.replace(/\./g, '/');
        function replacealias(match){
            var alias = match.substr(1, match.length - 2);
            return jsx.aliases[alias];
        }
        string = string.replace(/\{[^\}]+\}/ig, replacealias);
        var http = string.match(/^https?\:\/\//i) || '';
        string = string.replace(/^https?\:\/\//i, '');
        string = string.replace(/\/\//ig, '/');
        return http + string + '.' + type + '?build=' + jsx.build;
    };
};

/**
 * Alias locator. Contains information about files with aliases.
 */
jsx.Locator = new function(){
    this.aliases = [];
    this.get = function(alias){
        return this.aliases[alias] || null;
    };
    this.set = function(name, alias){
        this.aliases[name] = alias;
        if(jsx.expectedAliases[name]){
            this.load(name);
        }
    };
    this.load = function(alias){
        if (!this.get(alias) || this.get(alias).called){
            return;
        }
        this.get(alias).called = true;
        jsx.Scripts.createScript({'src': this.get(alias).src + '?build=' + jsx.build, 'charset': this.get(alias).charset});
    };
};

/**
 * Dynamically loads scripts.
 */
jsx.Loader = new function() {
    this.scripts = {};

    /**
     * This method loads script and executes listener after script loading.
     * @param {String} String like "foo.bar". This string means to load file bar.js from framework folder subfolder foo.
     * @param {Function} Function for execution after loading script.
     */
    this.require = function(urls, listener){
        urls = (typeof(urls) == 'string') ? [urls] : urls;
        this.requireList(urls, listener);
    };

    /**
     * This method is for scripts that are loaded. They must execute this method (jsx.loaded) after all initialization functions.
     * @param {String} String like "foo.bar". This string means to load file bar.js from framework folder subfolder foo.
     */
    this.loaded = function(url){
        if (!jsx.getAlias(url)){
            url = '{jsx}.' + url;
        }
        if (!this.scripts[url]){
            this.scripts[url] = this.createScriptFake(jsx.Vars.NULL);
        }
        var listener;
        while ((listener = this.scripts[url].listeners.shift())){
            listener();
        }
        this.scripts[url].ready = true;
    };

    this.requireList = function(urls, listener){
        var counter = 0, length = urls.length;
        // when JS file will be loaded listWatch will be called
        function listWatch() {
            // increase files counter
            counter++;
            // exec listener if all loaded
            if(counter == length){
                (listener || jsx.Vars.EMPTY)();
            }
        }
        urls.map(this._require.bind(this, listWatch));
    };

    this._require = function (listener, url){
        var alias = jsx.getAlias(url);
        if (!alias){
            url = '{jsx}.' + url;
            alias = 'jsx';
        }
        listener = listener || jsx.Vars.EMPTY;
        if (jsx.aliases[alias]){
            // if alas is defined start load
            this.startLoad(url, listener, jsx.charsets[alias])
        }else{
            // another way may be it will appear later
            this.waitForAlias(alias, url, listener)
        }
    };

    this.startLoad = function(url, listener, charset){
        if (this.scripts[url]){
             this.putOnWaitingList(url, listener);
        }else{
            this.scripts[url] = this.createScriptFake(listener);
            jsx.Scripts.createScript({'src': jsx.ConstructURL.construct(url), 'charset': charset});
        }
    };

    this.waitForAlias = function (alias, url, listener){
        if(!jsx.expectedAliases[alias]){
            jsx.expectedAliases[alias] = [];
        }
        var _this = this;
        jsx.expectedAliases[alias].push(function(){_this._require(listener, url)});
        jsx.Locator.load(alias);
    };
    
    this.createScriptFake = function(listener){
        return {
            listeners: [listener],
            ready: false
        }
    };

    this.putOnWaitingList = function (url, listener){
        if(this.scripts[url].ready){
            (listener || jsx.Vars.EMPTY)();
        }else{
            this.scripts[url].listeners.push(listener);
        }
    };
};

jsx.getBaseAndSetAlias('jsx', 'jsx.js', 'utf-8');

jsx.Console = new function(){
    this.log      = jsx.Vars.NULL;
    this.info     = jsx.Vars.NULL;
    this.warn     = jsx.Vars.NULL;
    this.error    = jsx.Vars.NULL;
    this.trace    = jsx.Vars.NULL;
    this.dir      = jsx.Vars.NULL;
    this.dirxml   = jsx.Vars.NULL;
    this.group    = jsx.Vars.NULL;
    this.groupEnd = jsx.Vars.NULL;
};

/*
window.onerror = function(){
    return !jsx.Vars.DEBUG;
};
*/

// Start init components after window onload
jsx.getScriptByFileName('jsx.js', function(script){
    var pathBuild = script.src.match(/build=([^&^$]*)/);
    jsx.build = pathBuild && pathBuild[1] ? pathBuild[1] : jsx.build;
    jsx.Locator.set('jsxComponents', {
        src: jsx.ConstructURL.construct('{jsx}.jsxComponents.jsxComponents'),
        charset: 'utf-8',
        called: false
    });  
    jsx.Locator.set('jsxAjax', {
        src: jsx.ConstructURL.construct('{jsx}.jsxAjax.jsxAjax'),
        charset: 'utf-8',
        called: false
    });  
    
    if (script.src.indexOf('autoinit=true') == -1){
        return;
    }
    Event.observe(window, 'load', function () {
        jsx.require('Components', function(){jsx.Components.init()});
    });
});

// If you watn to debug something add parameter jsxdebug=on to location.
// To switch off debug mode add parameter jsxdebug=off to location.
if (/jsxdebug/.test(window.location.search) || /jsxdebug/.test(document.cookie)){
    jsx.require('Debug');
}

