/**
 * Ext Direct aims to streamline communication between the client and server
 * by providing a single interface that reduces the amount of common code
 * typically required to validate data and handle returned data packets
 * (reading data, error conditions, etc).
 *
 * The `Ext.direct` namespace includes several classes for a closer integration
 * with the server-side. The Ext.data namespace also includes classes for working
 * with Ext.data.Stores which are backed by data from an Ext Direct method.
 *
 * # Specification
 *
 * For additional information consult the [Ext Direct Specification][1].
 *
 * # Providers
 *
 * Ext Direct uses a provider architecture, where one or more providers are used
 * to transport data to and from the server. There are several providers that exist
 * in the core at the moment:
 *
 * - {@link Ext.direct.JsonProvider JsonProvider} for simple JSON operations
 * - {@link Ext.direct.PollingProvider PollingProvider} for repeated requests
 * - {@link Ext.direct.RemotingProvider RemotingProvider} exposes server side to the client.
 *
 * A provider does not need to be invoked directly, providers are added via
 * {@link Ext.direct.Manager #addProvider}. RemotingProviders' API declarations
 * can also be loaded with {@link Ext.direct.Manager #loadProvider}, with
 * Provider instance created automatically after successful retrieval.
 *
 * # Router
 *
 * Ext Direct RemotingProviders utilize a "router" on the server to direct
 * requests from the client to the appropriate server-side method. Because
 * the Ext Direct API is platform-agnostic, you could completely swap out
 * a Java based server solution and replace it with one that uses C#
 * without changing the client side JavaScript at all, or vice versa.
 *
 * # Server side events
 *
 * Custom events from the server may be handled by the client by adding listeners, for example:
 *
 *     {"type":"event","name":"message","data":"Successfully polled at: 11:19:30 am"}
 *
 *     // add a handler for a 'message' event sent by the server
 *     Ext.direct.Manager.on('message', function(e){
 *         out.append(String.format('<p><i>{0}</i></p>', e.data));
 *         out.el.scrollTo('t', 100000, true);
 *     });
 *
 *    [1]: http://sencha.com/products/extjs/extdirect
 *
 * @singleton
 * @alternateClassName Ext.Direct
 */
Ext.define('Ext.direct.Manager', {
    singleton: true,
 
    requires: [
        'Ext.util.MixedCollection'
    ],
 
    mixins: [
        'Ext.mixin.Observable'
    ],
 
    /**
     * Exception types.
     */
    exceptions: {
        TRANSPORT: 'xhr',
        PARSE: 'parse',
        DATA: 'data',
        LOGIN: 'login',
        SERVER: 'exception'
    },
    
    // Classes of Providers available to the application
    providerClasses: {},
    
    // Remoting Methods registered with the Manager
    remotingMethods: {},
    
    config: {
        /**
         * @cfg {String} [varName="Ext.REMOTING_API"]
         * Default variable name to use for Ext Direct API declaration.
         */
        varName: 'Ext.REMOTING_API'
    },
    
    apiNotFoundError: 'Ext Direct API was not found at {0}',
    
    /**
     * @event event
     *
     * Fires after an event.
     *
     * @param {Ext.direct.Event} event The {@link Ext.direct.Event Event} that occurred.
     * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}
     * that provided the event.
     */
 
    /**
     * @event exception
     *
     * Fires after an event exception.
     *
     * @param {Ext.direct.Event} event The {@link Ext.direct.Event Event} that occurred.
     * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}
     * that provided the event.
     */
    
    /**
     * @event providerload
     *
     * Fired by {@link #loadProvider} after successfully loading RemotingProvider API
     * declaration and creating a new Provider instance.
     *
     * @param {String} url The URL used to retrieve remoting API.
     * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}
     * instance that was created.
     */
    
    /**
     * @event providerloaderror
     * 
     * Fired by {@link #loadProvider} when remoting API could not be loaded, or
     * Provider instance could not be created.
     *
     * @param {String} url The URL used to retrieve remoting API.
     * @param {String} error The error that occured.
     */
 
    /**
     * @private
     */
    constructor: function() {
        var me = this;
        
        me.mixins.observable.constructor.call(me);
 
        me.transactions = new Ext.util.MixedCollection();
        me.providers = new Ext.util.MixedCollection();
    },
 
    /**
     * Adds an Ext Direct Provider and creates the proxy or stub methods to execute
     * server-side methods for RemotingProviders. If the provider is not already connected,
     * it will auto-connect.
     *
     *      var pollProv = new Ext.direct.PollingProvider({
     *          id: 'polling1',
     *          url: 'php/poll2.php'
     *      });
     *
     *      Ext.direct.Manager.addProvider({
     *          id: 'remoting1',
     *          type: 'remoting',           // create a Ext.direct.RemotingProvider
     *          url:  'php/router.php',     // url to connect to the Ext Direct server-side router.
     *          actions: {                  // each property in actions object represents an Action
     *              TestAction: [{          // array of Methods within each server side Action
     *                  name: 'doEcho',     // name of method
     *                  len:  1
     *              }, {
     *                  name: 'multiply',
     *                  len:  1
     *              }, {
     *                  name: 'doForm',
     *                  formHandler: true   // handle form on server with Ext.direct.Transaction
     *              }]
     *          },
     *          namespace: 'myApplication', // namespace to create the Remoting Provider in
     *      }, {
     *          id:   'polling2',
     *          type: 'polling',            // create an Ext.direct.PollingProvider
     *          url:  'php/poll.php'
     *      },
     *      pollProv);                      // reference to previously created instance
     *
     * @param {Ext.direct.Provider/Object...} provider
     *
     * Accepts any number of Provider descriptions (an instance or config object for
     * a Provider). Each Provider description instructs Ext Direct how to create
     * client-side stub methods.
     */
    addProvider: function(provider) {
        var me = this,
            args = arguments,
            relayers = me.relayers || (me.relayers = {}),
            i, len;
 
        if (args.length > 1) {
            for (= 0, len = args.length; i < len; ++i) {
                me.addProvider(args[i]);
            }
            
            return;
        }
 
        // if provider has not already been instantiated
        if (!provider.isProvider) {
            provider = Ext.create('direct.' + provider.type + 'provider', provider);
        }
        
        me.providers.add(provider);
        provider.on('data', me.onProviderData, me);
        
        if (provider.relayedEvents) {
            relayers[provider.id] = me.relayEvents(provider, provider.relayedEvents);
        }
 
        if (!provider.isConnected()) {
            provider.connect();
        }
 
        return provider;
    },
    
    /**
     * Load Ext Direct Provider API declaration from the server and construct
     * a new Provider instance. The new Provider will then auto-connect and
     * create stub functions for the methods exposed by the server side. See 
     * {@link #addProvider}.
     * 
     *      Ext.direct.Manager.loadProvider({
     *          url: 'php/api.php',
     *          varName: 'MY_REMOTING_API' // defaults to 'Ext.REMOTING_API'
     *      });
     *
     * @param {Object} config Remoting API configuration.
     * @param {String} config.url URL to retrieve remoting API declaration from.
     * @param {String} config.varName Name of the variable that will hold 
     * RemotingProvider configuration block, including its Actions.
     * @param {Function} [callback] Optional callback to execute when
     * Provider is created, or when an error has occured.
     * @param {Object} [scope] Optional scope to execute callback function in.
     *
     * For additional information see the [Ext Direct specification][1].
     */
    loadProvider: function(config, callback, scope) {
        var me = this,
            classes = me.providerClasses,
            type, url, varName, provider, i, len;
        
        if (Ext.isArray(config)) {
            for (= 0, len = config.length; i < len; i++) {
                me.loadProvider(config[i], callback, scope);
            }
            
            return;
        }
        
        // We may have been passed config object containing enough
        // information to create a Provider without further ado.
        type = config.type;
        url = config.url;
        
        if (classes[type] && classes[type].checkConfig(config)) {
            provider = me.addProvider(config);
            
            me.fireEventArgs('providerload', [url, provider]);
            Ext.callback(callback, scope, [url, provider]);
            
            // We're deliberately not returning the provider here
            // to make way for the future Promises based implementation
            // that should be consistent with the remote API declaration
            // retrieval below.
            return;
        }
        
        // For remote API declaration retrieval we need to know the
        // service discovery URL and variable name, at the minimum.
        // We have a default for the variable name but not for URL.
        varName = config.varName || me.getVarName();
        delete config.varName;
        
        //<debug>
        if (!url) {
            Ext.raise("Need API discovery URL to load a Remoting provider!");
        }
        //</debug>
        
        // The URL we are requesting API from is not the same as the
        // service URL, and we don't need them to mix.
        delete config.url;
        
        // Have to use closures here as Loader does not allow passing
        // options object from caller to callback.
        Ext.Loader.loadScript({
            url: url,
            scope: me,
            
            onLoad: function() {
                this.onApiLoadSuccess({
                    url: url,
                    varName: varName,
                    config: config,
                    callback: callback,
                    scope: scope
                });
            },
            
            onError: function() {
                this.onApiLoadFailure({
                    url: url,
                    callback: callback,
                    scope: scope
                });
            }
        });
    },
    
    /**
     * Retrieves a {@link Ext.direct.Provider provider} by the id specified when the
     * provider is added.
     *
     * @param {String/Ext.direct.Provider} id The id of the provider, or the provider instance.
     */
    getProvider: function(id) {
        return id.isProvider ? id : this.providers.get(id);
    },
 
    /**
     * Removes the provider.
     *
     * @param {String/Ext.direct.Provider} provider The provider instance or the id of the provider.
     *
     * @return {Ext.direct.Provider} The provider, null if not found.
     */
    removeProvider: function(provider) {
        var me = this,
            providers = me.providers,
            relayers = me.relayers,
            id;
 
        provider = provider.isProvider ? provider : providers.get(provider);
 
        if (provider) {
            provider.un('data', me.onProviderData, me);
 
            id = provider.id;
            
            if (relayers[id]) {
                relayers[id].destroy();
                delete relayers[id];
            }
            
            providers.remove(provider);
            
            return provider;
        }
        
        return null;
    },
 
    /**
     * Adds a transaction to the manager.
     *
     * @param {Ext.direct.Transaction} transaction The transaction to add
     *
     * @return {Ext.direct.Transaction} transaction
     *
     * @private
     */
    addTransaction: function(transaction) {
        this.transactions.add(transaction);
        
        return transaction;
    },
 
    /**
     * Removes a transaction from the manager.
     *
     * @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction
     * to remove
     *
     * @return {Ext.direct.Transaction} transaction
     *
     * @private
     */
    removeTransaction: function(transaction) {
        var me = this;
        
        transaction = me.getTransaction(transaction);
        me.transactions.remove(transaction);
        
        return transaction;
    },
 
    /**
     * Gets a transaction
     *
     * @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction to get
     *
     * @return {Ext.direct.Transaction} 
     *
     * @private
     */
    getTransaction: function(transaction) {
        return typeof transaction === 'object' ? transaction : this.transactions.get(transaction);
    },
 
    onProviderData: function(provider, event) {
        var me = this,
            i, len;
 
        if (Ext.isArray(event)) {
            for (= 0, len = event.length; i < len; ++i) {
                me.onProviderData(provider, event[i]);
            }
            
            return;
        }
        
        if (event.name && event.name !== 'event' && event.name !== 'exception') {
            me.fireEvent(event.name, event);
        }
        else if (event.status === false) {
            me.fireEvent('exception', event);
        }
        
        me.fireEvent('event', event, provider);
    },
    
    /**
     * Parses a direct function. It may be passed in a string format, for example:
     * "MyApp.Person.read".
     *
     * @param {String/Function} fn The direct function
     *
     * @return {Function} The function to use in the direct call. Null if not found
     */
    parseMethod: function(fn) {
        var current = Ext.global,
            i = 0,
            resolved, parts, len;
        
        if (Ext.isFunction(fn)) {
            resolved = fn;
        }
        else if (Ext.isString(fn)) {
            resolved = this.remotingMethods[fn];
            
            // Support legacy resolution as top-down lookup
            // from the window scope
            if (!resolved) {
                parts = fn.split('.');
                len = parts.length;
 
                while (current && i < len) {
                    current = current[parts[i]];
                    ++i;
                }
            
                resolved = Ext.isFunction(current) ? current : null;
            }
        }
        
        return resolved || null;
    },
        
    /**
     * @private
     * Iterate over an API object containing function names and resolve Direct functions.
     *
     * @param {Object} api API object
     * @param {Ext.Base} caller The object calling
     *
     * @return {Object} The API object with each property resolved to a Direct function.
     */
    resolveApi: function(api, caller) {
        var prefix, action, method, fullName, fn;
        
        prefix = api && api.prefix;
        
        if (prefix && prefix.substr(prefix.length - 1) !== '.') {
            prefix += '.';
        }
        
        for (action in api) {
            method = api[action];
            
            if (action !== 'prefix' && typeof method !== 'function') {
                fullName = (prefix || '') + method;
                fn = this.parseMethod(fullName);
                
                if (typeof fn === 'function') {
                    api[action] = fn;
                }
                //<debug>
                else {
                    Ext.raise("Cannot resolve Direct API method '" + fullName + "' for " + action +
                              " action in " + caller.$className + " instance with id: " +
                              (caller.id != null ? caller.id : 'unknown'));
                }
                //</debug>
            }
        }
        
        return api;
    },
    
    privates: {
        addProviderClass: function(type, cls) {
            this.providerClasses[type] = cls;
        },
        
        onApiLoadSuccess: function(options) {
            var me = this,
                url = options.url,
                varName = options.varName,
                api, provider, error;
            
            try {
                // Variable name could be nested (default is Ext.REMOTING_API),
                // so we use eval() to get the actual value.
                api = Ext.apply(options.config, eval(varName));
                
                provider = me.addProvider(api);
            }
            catch (e) {
                error = e + '';
            }
            
            if (error) {
                me.fireEventArgs('providerloaderror', [url, error]);
                Ext.callback(options.callback, options.scope, [url, error]);
            }
            else {
                me.fireEventArgs('providerload', [url, provider]);
                Ext.callback(options.callback, options.scope, [url, provider]);
            }
        },
        
        onApiLoadFailure: function(options) {
            var url = options.url,
                error;
            
            error = Ext.String.format(this.apiNotFoundError, url);
            
            this.fireEventArgs('providerloaderror', [url, error]);
            Ext.callback(options.callback, options.scope, [url, error]);
        },
        
        registerMethod: function(name, method) {
            this.remotingMethods[name] = method;
        },
        
        // Used for testing
        clearAllMethods: function() {
            this.remotingMethods = {};
        }
    }
}, function() {
    // Backwards compatibility
    Ext.Direct = Ext.direct.Manager;
});