/**
 * This class provides a Javascript-API for contrexx
 * 
 * @param theOptions Object {
 *     extensions: ['extension_name', ... ]
 * }
 * @requires JQuery / JQuery-includeMany
 */
var ContrexxJs = function(theOptions) {
    //we want jQuery at $ internally
    var $ = jQuery;
    //has this object been initialited by contrexx?
    var initialized = false;
    //an array of functions that have to be executed as soon as the api finished loading
    var readyWork = [];
    //is the api ready?
    var ready = false;
    //are the extensions loaded? dependencies handled separately via unloadedDependenciesCounter
    var extensionsLoaded = false;
    //remembers how many dependencies we have to load until we're ready
    var unloadedDependenciesCounter = 0;

    //extensions loaded by default
    var defaultExtensions = ['ui'];


    var options = theOptions;
    if(!options) //no options specified by user
        options = {}; //create empty object to simplify option checking

    var extensions;
    //check which extensions to load
    if(options.extensions)
        extensions = options.extensions;
    else
        extensions = defaultExtensions;

    //stores and retrieves data
    var DataHolder = function() {
        var scopes = {};
        return {
            set: function(key,value,scope) {
                //if the scope parameter is not set, we're dealing with one of the following cases:
                //a) the key parameter is an array of multiple key-value pairs
                //   => in this case, the scope is in parameter value
                //b) no scope was specified
                //   => in this case, we use the default scope 'global'
                //c) a) and b) occur
                var multipleValues = typeof(key) != "string";
                if(!scope) {
                    if(multipleValues) //case a)
                        scope = value;
                    if(!scope) //case b) or c)
                        scope = 'global';                    
                }

                //create scope if it doesn't exist
                if(!scopes[scope])
                    scopes[scope] = {};
                
                if(!multipleValues) {
                    //assign the data
                    scopes[scope][key] = value;
                }
                else {
                    var targetScope = scopes[scope];
                    $.each(key, function(key,value){
                        targetScope[key] = value;
                    });
                }
            },
            get: function(key,scope) {
                //set default scope 'global' if no scope is specified
                if(!scope)
                    scope = 'global';

                //handle unexisting scopes
                if(!scopes[scope])
                    return null;
                
                //retrieve the data
                return scopes[scope][key];
            }
        };
    };

    var variables = new DataHolder();
    //we use this to make object instances generated by contrexx accessible without polluting the global namespace
    var instances = new DataHolder();

    /**
     * holds files currently loading and work waiting to be done after their load.
     * [
     *   { 
     *     id: <queue_id>, 
     *     done: <boolean> - has the loading been finished?
     *     work: [<first_function>, <second_function>, ...]
     *     working. <boolean> - are we currently processing this queue?
     *   },
     *   ...
     * ]
     **/ 
    var loadingQueue = [];
    //a counter to generate unique and ascending loading ids
    var currentLoadingId = 0;

    //adds a function to loading queue where specified
    var addToLoadingQueue = function(id, work) {
        var recordExisted = false; //remember if we found a record
        for(var i = 0; i < loadingQueue.length; i++) {
            if(loadingQueue[i].id == id) { //this is our record, add the work
                loadingQueue[i].work.push(work); //add work at end of queue
                recordExisted = true;
                break;
            }
        }
        if(!recordExisted) { //no record found, create one
            var record = {
                id: id,
                work: [work],
                working: false, //remembers if we're processing the work currently
                done: false //remembers if the files are loaded
            };
            loadingQueue.push(record);
        }
    };

    //executes all work in a queue item sequentially and maintains the status of
    //the done-property
    var processQueueItem = function(queueItem, finishedCallback) {
        queueItem.working = true;
        if(queueItem.work.length == 0) {
            finishedCallback();
        }
        else {
            queueItem.work[0]();
            //remove first element - it's the work we just done
            queueItem.work.splice(0,1);
            processQueueItem(queueItem, finishedCallback);
        }
    };

    //steps through queue from the beginning and executes all work until a non-ready item is found
    var processQueue = function() {

        if(loadingQueue.length == 0) //nothing to do
            return;
        if(loadingQueue[0].working == true) //already processing work, don't do it twice
            return;
        if(loadingQueue[0].done) { //files loaded, process work
            processQueueItem(loadingQueue[0], function() {
                loadingQueue.splice(0,1); //remove first item, loading is finished & work done
                processQueue();
            });
        }
    };

    //notifies the queue of loaded files
    var loadingFinished = function(id) {
        for(var i = 0; i < loadingQueue.length; i++) {
            if(loadingQueue[i].id == id) { //this is our record, act
                loadingQueue[i].done = true;
                processQueue();
                break;
            }
        }
    };

    //this holds all files we've already loaded.
    var alreadyLoadedFiles = [];

    /**
     * Dynamical inclusion of files
     * 
     * @param array | string files ['the/first/file', 'another', ... ]
     * @param function callback is called as soon as all files are loaded
     * @param boolean lazy optional. if set to true, .ready(f,true) won't wait for this.
     *                use if you are including stuff that could possibly fail. 
     * @param boolean initcall internal, optional. if set to true, callback is called without ready-check.
     *                         use this for inclusion of files needed before contrexx is loaded.
     * @param boolean chain optional. if set to true, files are loaded one after another rather than sim-
     *                      ultaneously.
     */
    var include = function(files,callback,lazy, initcall, chain) {
        if((!initialized || !ready) && !initcall) {//include later if api is not ready
            readyFunc(function(){
                include(files,callback,lazy);
            });
            return;
        }

        var expandedFiles = []; //we'll place the files with a fully specified path here
        var pathPrefix = variables.get('cmsPath','contrexx')+'/';
        
        //create array if single file was specified as string        
        if(typeof(files) == "string")
            files = [files];

        $(files).each(function(index,file) {
            //prevent double inclusion
            if(!alreadyLoadedFiles[file]) {
                //TODO: implement this properly, this is a design issue
                //problems occur with /cadmin requests
                var expandedFile = pathPrefix+file;
                if(expandedFile.substring(0,2) == '//') //workaround: we do not want double leading slashes
                    expandedFile = expandedFile.substring(1);

                expandedFiles.push(expandedFile);
                alreadyLoadedFiles[file] = true;
            }
        });
        if(expandedFiles.length > 0) { //only load files if there _are_ files
            $(function() { //wait for include-plugin to load
                if(lazy || initcall) { //easy going, just include
                    if(!chain)
                        $.include(expandedFiles,callback);
                    else
                        $.chainclude(expandedFiles,callback);
                }
                else { //queue has to be managed
                    addToLoadingQueue(++currentLoadingId,callback); //make sure callback gets executed first after loading
                    var loadingIdOfThis = currentLoadingId;
                    if(!chain) {
                        $.include(expandedFiles,function() {
                            loadingFinished(loadingIdOfThis); //set queue callback                            
                        });
                    }
                    else {
                        $.chainclude(expandedFiles,function() {
                            loadingFinished(loadingIdOfThis); //set queue callback                            
                        });                        
                    }
                }
            });
        }
        else { 
            callback(); //make sure callback gets called
        }            
    };
    
    //executes functions stored for after-loading
    var doReadyWork = function() {
        ready = true;
        for(var i = 0; i < readyWork.length; i++) {
            readyWork[i]();
        };
    };

    //executes readyWork if we're ready
    var checkReady = function() {
        if(initialized && extensionsLoaded && unloadedDependenciesCounter == 0)
            doReadyWork();
    };

    //called as soon as all extension files are loaded
    var extensionLoadingFinished = function()
    {
        extensionsLoaded = true;
        checkReady();
    };

    //called as soon as a dependency is loaded
    var dependencyLoaded = function(){
        unloadedDependenciesCounter--;
        checkReady();
    };

    //loads all extensions specified
    var loadExtensions = function(){
        var expandedExtensions = []; //we'll place the files with a fully specified path here
        var pathPrefix = 'lib/javascript/cx/';
        $.each(extensions,function(index,extension){
            expandedExtensions.push(pathPrefix+extensions+'.js');
        });
        include(expandedExtensions,extensionLoadingFinished, true, true);
    };

    //this is exposed as 'ready'.
    //pass a function func that will be executed as soon as the cx api loaded
    //set waitforqueue to true to wait for all include-dependent code to finish
    var readyFunc = function(func, waitForQueue) {
        $(function(){//make sure jQuery is ready
            if(!ready) { //retry again when we're ready
                readyWork.push(function() {
                    //call this again so we can decide whether to execute or add to loadingqueue
                    readyFunc(func, waitForQueue); 
                });
                return;
            }
            if(!waitForQueue) {
                func();
            }
            else { //schedule for execution as soon as everything is loaded
                if(loadingQueue.length == 0) //special case: nothing in loading queue
                    readyFunc(func); //no waiting to do
                else
                    addToLoadingQueue(currentLoadingId,func);
            }            
        });
    };

    //public properties of ContrexxJs
    return {
        ready: readyFunc,
        variables: variables,
        instances: instances,
        include: include,
        jQuery: $,
        //contrexx internal stuff, do not temper with.
        internal: {
            setCxInitialized: function() {
                initialized = true;
                //now that we have the contrexx paths, let'se load!
                loadExtensions();
            },
            //used by extensions to include their dependencies
            dependencyInclude: function(files, callback, chain){
                unloadedDependenciesCounter++;
                include(files,function(){
                    callback();
                    dependencyLoaded();
                }, true, true, chain);
            }
        }
    };
};

//the only, global instance
cx = new ContrexxJs();