/**
 * @private
 *
 * This object handles communication between the WebView and Sencha's native shell.
 * Currently it has two primary responsibilities:
 *
 * 1. Maintaining unique string ids for callback functions, together with their scope objects
 * 2. Serializing given object data into HTTP GET request parameters
 *
 * As an example, to capture a photo from the device's camera, we use `Ext.device.Camera.capture()` like:
 *
 *     Ext.device.Camera.capture(
 *         function(dataUri){
 *             // Do something with the base64-encoded `dataUri` string
 *         },
 *         function(errorMessage) {
 *
 *         },
 *         callbackScope,
 *         {
 *             quality: 75,
 *             width: 500,
 *             height: 500
 *         }
 *     );
 *
 * Internally, `Ext.device.Communicator.send()` will then be invoked with the following argument:
 *
 *     Ext.device.Communicator.send({
 *         command: 'Camera#capture',
 *         callbacks: {
 *             onSuccess: function() {
 *                 // ...
 *             },
 *             onError: function() {
 *                 // ...
 *             }
 *         },
 *         scope: callbackScope,
 *         quality: 75,
 *         width: 500,
 *         height: 500
 *     });
 *
 * Which will then be transformed into a HTTP GET request, sent to native shell's local
 * HTTP server with the following parameters:
 *
 *     ?quality=75&width=500&height=500&command=Camera%23capture&onSuccess=3&onError=5
 *
 * Notice that `onSuccess` and `onError` have been converted into string ids (`3` and `5`
 * respectively) and maintained by `Ext.device.Communicator`.
 *
 * Whenever the requested operation finishes, `Ext.device.Communicator.invoke()` simply needs
 * to be executed from the native shell with the corresponding ids given before. For example:
 *
 *     Ext.device.Communicator.invoke('3', ['DATA_URI_OF_THE_CAPTURED_IMAGE_HERE']);
 *
 * will invoke the original `onSuccess` callback under the given scope. (`callbackScope`), with
 * the first argument of 'DATA_URI_OF_THE_CAPTURED_IMAGE_HERE'
 *
 * Note that `Ext.device.Communicator` maintains the uniqueness of each function callback and
 * its scope object. If subsequent calls to `Ext.device.Communicator.send()` have the same
 * callback references, the same old ids will simply be reused, which guarantee the best possible
 * performance for a large amount of repetitive calls.
 */
Ext.define('Ext.device.communicator.Default', {
 
    SERVER_URL: 'http://localhost:3000', // Change this to the correct server URL
 
    callbackDataMap: {},
 
    callbackIdMap: {},
 
    idSeed: 0,
 
    globalScopeId: '0',
 
    generateId: function() {
        return String(++this.idSeed);
    },
 
    getId: function(object) {
        var id = object.$callbackId;
 
        if (!id) {
            object.$callbackId = id = this.generateId();
        }
 
        return id;
    },
 
    getCallbackId: function(callback, scope) {
        var idMap = this.callbackIdMap,
            dataMap = this.callbackDataMap,
            id, scopeId, callbackId, data;
 
        if (!scope) {
            scopeId = this.globalScopeId;
        }
        else if (scope.isIdentifiable) {
            scopeId = scope.getId();
        }
        else {
            scopeId = this.getId(scope);
        }
 
        callbackId = this.getId(callback);
 
        if (!idMap[scopeId]) {
            idMap[scopeId] = {};
        }
 
        if (!idMap[scopeId][callbackId]) {
            id = this.generateId();
            data = {
                callback: callback,
                scope: scope
            };
 
            idMap[scopeId][callbackId] = id;
            dataMap[id] = data;
        }
 
        return idMap[scopeId][callbackId];
    },
 
    getCallbackData: function(id) {
        return this.callbackDataMap[id];
    },
 
    invoke: function(id, args) {
        var data = this.getCallbackData(id);
 
        data.callback.apply(data.scope, args);
    },
 
    send: function(args) {
        var callbacks, scope, name, callback;
 
        if (!args) {
            args = {};
        }
        else if (args.callbacks) {
            callbacks = args.callbacks;
            scope = args.scope;
 
            delete args.callbacks;
            delete args.scope;
 
            for (name in callbacks) {
                if (callbacks.hasOwnProperty(name)) {
                    callback = callbacks[name];
 
                    if (typeof callback === 'function') {
                        args[name] = this.getCallbackId(callback, scope);
                    }
                }
            }
        }
 
        args.__source = document.location.href;
 
        var result = this.doSend(args);
 
        return (result && result.length > 0) ? JSON.parse(result) : null;
    },
 
    doSend: function(args) {
        var xhr = new XMLHttpRequest();
 
        xhr.open('GET', this.SERVER_URL + '?' + Ext.Object.toQueryString(args) + '&_dc=' + new Date().getTime(), false);
 
        // wrap the request in a try/catch block so we can check if any errors are thrown and attempt to call any
        // failure/callback functions if defined
        try {
            xhr.send(null);
 
            return xhr.responseText;
        }
        catch (e) {
            if (args.failure) {
                this.invoke(args.failure);
            }
            else if (args.callback) {
                this.invoke(args.callback);
            }
        }
    }
});