(function(){
 
/**
 * Promise-based API for uploading files via HTTP POST to a form handler URL.
 *
 * TODO fix the docs here
 * To upload a file:
 *
 *      // list some details when the upload completes
 *      function onComplete(file, upload) {
 *          Ext.space.Logger.log("Upload finished: " + upload.url);
 *          Ext.space.Logger.log("Upload size: " + upload.totalBytes);
 *          Ext.space.Logger.log("Saved to: " + upload.fileName);
 *          Ext.space.Logger.log("File path: " + file.path);
 *          Ext.space.Logger.log("File name: " + file.name);
 *          Ext.space.Logger.log("File size on disk: " + file.size);
 *      }
 *
 *      Ext.space.Uploads.upload({ url: "http://www.sencha.com/" }).then(onComplete);
 *
 * The `upload` objects involved here are instances of Ext.space.files.Upload;
 * the `file` object is an Ext.space.files.File.
 *
 * To get a list of all uploads currently in progress, plus up to the ten most
 * recently completed uploads:
 *
 *      Ext.space.Uploads.getUploads().then(function(uploads) {
 *          uploads.forEach(function(upload) {
 *              Ext.space.Logger.log(upload.fileName);
 *          });
 *      });
 *
 * If you have a upload object and want to fetch the latest information about it,
 * you can get the progress of a single upload at a time:
 *
 *      upload.getProgress().then(function(updatedUpload) {
 *          Ext.space.Logger.log(updatedUpload.bytesUploaded + " bytes uploaded");
 *      });
 *
 * Alternatively, you can wire up a progress event callback on the upload:
 *
 *      upload.on("progress", function(updatedUpload) {
 *          // gets called every time new progress is available
 *      });
 *
 * To cancel a upload in progress:
 *
 *      upload.cancel().then(function() {
 *          Ext.space.Logger.log("Canceled!");
 *      });
 *
 * @aside guide file_locker
 *
 */
Ext.define("Ext.space.Uploads", {
    singleton: true,
 
    /**
     * @private
     * Cache of the upload information returned by the native bridge
     */
    uploads: null,
 
    /**
     * @private
     * Whether or not the upload manager has registered callbacks with the native bridge UploadManager#watchUploads
     */
    watching: false,
 
    /**
     * @private
     */
    constructor: function() {
        this.uploads = {};
    },
 
    /**
     * Upload a file.
     *
     * @param {Object} args Upload parameters
     * @param {String} args.url The URL where the file will be posted.
     * @param {String} args.fileFieldName The post field name to assign the file to.
     * @param {Object} args.params Optional POST parameters to be sent along with the file.
     * @param {Object} args.headers Optional HTTP headers to be sent along with the file.
     * @return {Ext.space.Promise} Promise that will resolve with the upload response
     */
    upload: function(args) {
        var upload = new Ext.space.files.Upload();
        var promise = upload.done;
 
        if (!args) {
            promise.reject("Missing upload arguments definition");
 
        } else if (!args.key) {
            promise.reject("Missing file key");
 
        } else if (!args.url) {
            promise.reject("Missing URL");
 
        } else if (!args.fileFieldName) {
            promise.reject("Missing fileFieldName");
 
        } else {
            var cmd = {
                command: "Files#uploadFile",
                key: args.key,
                postUrl: args.url,
                fileFieldName: args.fileFieldName,
                callbacks: {
                    // TODO: when UploadManger is done, switch to this onStart method, remove onSuccess 
                    /*
                    onStart: function(id) {
                        if (id) {
                            // cache a reference to the Upload object, so we can
                            // continue to update it over time
                            manager.uploads[id] = upload;
                        }
 
                        manager.watchUploads();
                    },
                    // no onSuccess callback because we'll let watchUploads do
                    // the necessary notification
                    // onSuccess: function(id) {},
                    */
                    onSuccess: function(response) {
                        Ext.space.Logger.warn("Upload complete", response);
                        if (response && !upload.response) {
                            upload.response = response;
                        }
                        promise.fulfill(args.file, upload);
                    },
                    onError: function(error) {
                        promise.reject(error);
                    }
                }
            };
 
            if (args.params) {
                cmd.params = args.params;
            }
            if (args.headers) {
                cmd.headers = args.headers;
            }
 
            Ext.space.Communicator.send(cmd);
        }
 
        // TODO when UploadManger is done, just return upload 
        return upload.done;
    },
 
    /**
     * Retrieve the current status of all active uploads, plus the most recently
     * completed uploads.
     *
     * @return {Ext.space.Promise} Promise which will receive an array of
     *                       Ext.space.files.Upload objects
     */
    getUploads: function(args) {
        var promise = new Ext.space.Promise();
        var manager = this;
 
        function makeUpload(item) {
            var id = item.uploadId;
            if (manager.uploads[id]) {
                return manager.uploads[id]._updateWith(item);
            } else {
                manager.uploads[id] = new Ext.space.files.Upload(item);
                return manager.uploads[id];
            }
        }
 
        Ext.space.Communicator.send({
            command: "UploadManager#getUploads",
            callbacks: {
                onSuccess: function(responses) {
                    if (Object.prototype.toString.call(responses) === "[object Array]") {
                        // resolve with an array of Upload objects 
                        promise.fulfill(responses.map(makeUpload));
                        manager.watchUploads();
 
                    } else {
                        // what happened? 
                        promise.reject("Malformed (non-Array) response from the native bridge");
                    }
                },
                onError: function(error) {
                    promise.reject(error);
                }
            }
        });
 
        return promise;
    },
 
    /**
     * Check a upload's progress (normally done via upload.getProgress()).
     *
     * @private
     * @param {Object} upload Upload to check
     * @return {Ext.space.Promise} Promise which will receive an up-to-date copy of the
     *                       Ext.space.files.Upload
     */
    getProgress: function(upload) {
        var id, match, manager = this;
        var promise = new Ext.space.Promise();
 
        if (!upload) {
            promise.reject("Missing upload");
 
        } else {
            id = upload.uploadId;
 
            if (id && manager.uploads[id]) {
                if (manager.uploads[id].isComplete) {
                    // if it's cached and complete, return it 
                    promise.fulfill(manager.uploads[id]);
 
                } else {
                    // if it's cached and incomplete, get it from getUploads 
                    this.getUploads().then(function(uploads) {
                        uploads.some(function(ul) {
                            if (ul.uploadId === id) {
                                match = ul;
                                return true;
                            }
                        });
 
                        if (match) {
                            promise.fulfill(match);
                        } else {
                            promise.reject("Upload " + id + " not found");
                        }
 
                    }, function(error) {
                        promise.reject(error);
                    });
                }
            } else {
                promise.reject("Upload " + id + " not found");
            }
        }
 
        return promise;
    },
 
    /**
     * Cancel a upload (normally done via upload.cancel()).
     *
     * @private
     * @param {Object} upload Upload to check
     * @return {Ext.space.Promise} Promise which will resolve when the upload is canceled. If
     *                       the upload is already done or canceled, it will reject.
     */
    cancel: function(upload) {
        var promise = new Ext.space.Promise(), manager = this;
 
        if (!upload || !upload.uploadId) {
            promise.reject("Missing upload ID");
 
        } else {
            Ext.space.Communicator.send({
                command: "UploadManager#cancelUpload",
                uploadId: upload.uploadId,
                callbacks: {
                    onSuccess: function() {
                        manager.uploads[upload.uploadId].done.reject("Canceled");
                        promise.fulfill(true);
                    },
                    onError: function(error) {
                        promise.reject(error);
                    }
                }
            });
        }
 
        return promise;
    },
 
    /**
     * Watch for updates coming in from the native bridge, to keep the internal
     * cache up to date
     *
     * @private
     */
    watchUploads: function() {
        var manager = this,
            cache = this.uploads,
            activeCount = 0;
 
        function processItem(item) {
            var id = item.uploadId,
                alreadyComplete = !(id in cache) || cache[id].isComplete,
                justCompleted = !alreadyComplete && item.isComplete;
 
            // count the uploads still in progress to we know when to unwatch 
            if (!item.isComplete) {
                activeCount++;
            }
 
            // create or update the cached upload object 
            if (cache[id]) {
                cache[id]._updateWith(item);
            } else {
                cache[id] = new Ext.space.files.Upload(item);
            }
 
            // resolve the original promise with the final data 
            if (justCompleted) {
                Ext.space.SecureFiles.getFile(cache[id].fileKey).then(function(file) {
                    cache[id].done.fulfill(file, cache[id]);
                });
            }
        }
 
        if (!manager.watching) {
            manager.watching = true;
            Ext.space.Communicator.send({
                command: "UploadManager#watchUploads",
                callbacks: {
                    onSuccess: function(responses) {
                        activeCount = 0;
                        if (Object.prototype.toString.call(responses) === "[object Array]") {
                            responses.forEach(processItem);
                            if (!activeCount) {
                                manager.unwatchUploads();
                            }
                        }
                    },
                    onError: function(error) {
                        manager.unwatchUploads();
                    }
                }
            });
        }
    },
 
    /**
     * Discontinue watching for upload updates from the native bridge
     *
     * @private
     */
    unwatchUploads: function() {
        if (this.watching) {
            Ext.space.Communicator.send({
                command: "UploadManager#unwatchUploads"
            });
            this.watching = false;
        }
    }
});
 
}());