/**
 * @aside guide invoke
 * The Invoke API allows Applications running inside a Sencha Web Application Client
 * to communicate. Applications can securely exchange data with each other.
 *
 * When one application requests data from another, that application loads, and the
 * user is shown the called app. Once the user is done interacting with the called
 * app, the called app returns data back to the calling application, and Sencha Web
 * Application Client returns the user to the original application.
 *
 * The two primary functions for Invoke are `Ext.space.Invoke.get` and
 * `Ext.space.Invoke.onMessage`
 *
 * For additional information on how to use please see our
 * [Invoke Guide](#!/guide/invoke) and [example applications](#!/guide/examples)
 */
Ext.define('Ext.space.Invoke', {
    singleton: true,
 
    messageId: 0,
 
 
   /**
    * @private
    */
    constructor: function() {
        this.pendingReceivePromises = {};
        this.connections = {};
        this.connectQueue = [];
        this.messageQueue = [];
        this.proxies = [];
    },
 
 
   /**
    * @private
    */
    invoke: function(messages) {
        var me = this;
 
        if (!Array.isArray(messages)) {
            throw new Error('[Invoke#invoke] Invalid messages, must be an array');
        }
 
        // Unblock native thread 
        setTimeout(function() {
            messages.forEach(function(message) {
                me.onReceived(message);
            });
        }, 1);
    },
 
    /**
     * Get a connection to another application.
     *
     *   Ext.space.Invoke.get('photos').then(send, failure);
     *
     *   var failure = function(error) {
     *       Ext.space.Logger.error('Received error:', error);
     *   }
     *
     *   var send = function(connection) {
     *       connection.send(data, background).then(
     *           success,
     *           failure
     *       );
     *   };
     * @param {String} receiverId The ID of the application to connect to. Get this ID from #broadcast
     * @returns {Ext.space.Promise}
     */
    get: function(receiverId) {
        var connections = this.connections,
            connection = connections[receiverId];
 
        if (connection) {
            return Ext.space.Promise.from(connection);
        }
        else {
            return this.broadcast(receiverId).then(function(receiverIds) {
                connections[receiverId] = connection = new Ext.space.invoke.Connection(receiverIds[0].id);
                return connection;
            }.bind(this));
        }
    },
 
    /**
     * Send a message
     * @private
     * @param {String} receiverId The ID of the application to connect to. Get this ID from #broadcast
     * @param {*} message The message to send, can be an object, as long as it is JSON-able.
     * @param {Boolean} [foreground] Whether or not to bring the receiver app to the foreground.
     * @returns {Ext.space.Promise}
     */
    send: function(receiverId, message, foreground) {
        var messageId = this.messageId++,
            receivePromise = new Ext.space.Promise(),
            sendPromise = this.doSend(receiverId, messageId, message, foreground),
            pendingReceivePromises = this.pendingReceivePromises;
 
        pendingReceivePromises[messageId] = receivePromise;
 
        sendPromise.error(function(reason) {
            delete pendingReceivePromises[messageId];
            receivePromise.reject(reason);
        });
 
        return receivePromise;
    },
 
    /**
     * @private
     * Assign the callback to handle a new connection.
     * The Boolean returned value determines whether or not
     * to accept the connection.
     * @param {Function} callback 
     */
    onConnect: function(callback) {
        var queue = this.connectQueue.slice(0),
            i, ln, args;
 
        this.connectQueue.length = 0;
 
        if (callback) {
            this.connectCallback = callback;
 
            for (= 0, ln = queue.length; i < ln; i++) {
                args = queue[i];
                this.onReceived.apply(this, args);
            }
        }
    },
 
    /**
    * @private
    *
    *
    */
    register: function(name, obj) {
 
        var proxy = this.proxies[name];
 
        //someone could be waiting for this proxy to be registered 
        //if not create a new promise. 
        if(!proxy) {
            proxy = new Ext.space.Promise();
            this.proxies[name] =proxy;
        }
 
        var temp = {
            name: name,
            methods: []
        };
 
        /*
        * Extract all the functions from the passed object.
        */
        for(var property in obj) {
            if(obj.propertyIsEnumerable(property) && typeof obj[property] == "function"){
                Ext.space.Logger.info(property, obj.propertyIsEnumerable(property));
                temp.methods.push(property);
            }
        }
 
        proxy.fulfill(temp);
 
    },
 
    /**
     *   onMessage registers a function to be called each time another application
     *   invokes this application.
     *
     *   For example for the photos application to respond to a request to get photos:
     *
     *       Invoke.onMessage(function(appId, message) {
     *          var promise = new Ext.space.Promise();
     *
     *          Ext.space.Logger.log('Got message from ' + appId + ' ' + message);
     *
     *          // Do whatever is needed asynchronously before returning the result
     loaded          //  (fulfilling the promise)
     *          setTimeout(function(){
     *             promise.fulfill('Yeah I got it');
     *          }, 3000);
     *
     *          return promise;
     *      });
     * @param {Function} callback 
     */
    onMessage: function(callback) {
        var queue = this.messageQueue.slice(0),
            i, ln, args;
 
        this.messageQueue.length = 0;
 
        if (callback) {
            this.messageCallback = callback;
 
            for (= 0, ln = queue.length; i < ln; i++) {
                args = queue[i];
                this.onReceived.apply(this, args);
            }
        }
    },
 
    /**
     * @private
     */
    onAppConnect: function() {
        return this.connectCallback.apply(this, arguments);
    },
 
    /**
     * @private
     */
    onAppMessage: function(appId, message) {
        var connection = this.connections[appId],
            response;
 
        if (connection) {
            response = connection.receive(message);
        }
 
        if (typeof response == 'undefined') {
            response = this.messageCallback.apply(this, arguments);
        }
 
        return response;
    },
 
    /**
     * @private
     */
    onReceived: function(data) {
        var appId = data.appId,
            message = data.message,
            messageId = data.id,
            foreground = data.foreground,
            pendingReceivePromises = this.pendingReceivePromises,
            pendingPromise = pendingReceivePromises[messageId],
            connectCallback = this.connectCallback,
            messageCallback = this.messageCallback,
            response;
 
        delete pendingReceivePromises[messageId];
 
        // A response 
        if (pendingPromise) {
            if (message.error) {
                pendingPromise.reject(message.error);
            }
            else {
                pendingPromise.fulfill(message.success);
            }
        }
        // A request 
        else {
            try {
                if (message === '__CONNECT__') {
                    if (!connectCallback) {
                        this.connectQueue.push(arguments);
                        return;
                    }
                    else {
                        response = this.onAppConnect(appId);
                    }
                } else if (message.$control){
                    Ext.space.Logger.info("Got control object!", message);
                    response = this.handleControlMethod(appId, message.$control, messageId, foreground);
                } else {
                    if (!messageCallback) {
                        this.messageQueue.push(arguments);
                        return;
                    }
                    else {
                        response = this.onAppMessage(appId, message);
                    }
                }
 
                var invoke = this;
                if (response && typeof response.then == "function") {
                    response.then(function(result) {
                        invoke.doSend(appId, messageId, {
                            success: result
                        }, foreground);
                    }, function(reason) {
                        invoke.doSend(appId, messageId, {
                            error: reason
                        }, foreground);
                    });
                }
                else {
                    this.doSend(appId, messageId, {
                        success: response
                    }, foreground);
                }
            }
            catch (e) {
                this.doSend(appId, messageId, {
                    error: e
                }, foreground);
                throw e;
            }
        }
 
    },
 
    /**
    *@private
    *
    Handle app to app control messages
        Fetch an RPC proxy
        Call a proxy method
        add or remove an event listener
 
    control: {
        action: "getProxy|callProxy|addListener|removeListener"
        name: 'Test', Name of either proxy or event
        method: 'foo'   name of proxy method
    }
 
 
    */
    handleControlMethod: function(appId,control, messageId, foreground){
        var result;
        var self = this;
        var handlers = {
            getProxy: function(){
                var proxy = self.proxies[control.name];
                if(!proxy){
                    proxy = new Ext.space.Promise();
                    self.proxies[name] =proxy;
                }
                return proxy;//{methods:["first", "second", "third"]}; 
            },
            callProxy: function(){
                return {"a" : "b"};
            }
        };
 
        if(control.action && handlers[control.action]) {
            result = handlers[control.action]();
        } else {
            throw 'Invalid control action';
        }
        return result;
 
        /*this.doSend(appId, messageId, {
            success:
        }, foreground);*/
    },
 
    /**
     * @private
     * Broadcast a message (intent) to look for receivers who can respond to the message.
     * @param message
     * @returns {Ext.space.Promise} A promise that provides an array of objects to fulfill.
     * Each object contains information about a receiver, with 'id', 'name', and 'icon' keys.
     */
    broadcast: function(message) {
        var promise = new Ext.space.Promise;
 
        Ext.space.Communicator.send({
            command: 'Invoke#connect',
            callbacks: {
                success: function(result) {
                    if (!result || result.length === 0) {
                        promise.reject({
                            code: 1,
                            message: "There are no receivers for this connection"
                        });
                        return;
                    }
 
                    promise.fulfill(result);
                },
                failure: function(reason) {
                    promise.reject(reason);
                }
            },
            message: message
        });
 
        return promise;
    },
 
    /**
     * @private
     * @param receiverId
     * @param messageId
     * @param message
     * @param foreground
     * @returns {Ext.space.Promise}
     */
    doSend: function(receiverId, messageId, message, foreground) {
        var promise = new Ext.space.Promise,
            appId = Ext.space.Communicator.appId;
 
        var success = function(result) {
            promise.fulfill(result);
        };
 
        success.args = arguments;
 
        Ext.space.Communicator.send({
            command: 'Invoke#send',
            callbacks: {
                success: success,
                failure: function(reason) {
                    promise.reject(reason);
                }
            },
            receiverId: receiverId,
            foreground: foreground,
            message: {
                id: messageId,
                appId: appId,
                message: message,
                foreground: foreground
            }
        });
 
        return promise;
    }
});