(function(){
 
// utility function for returning a particular field from the item parameter, or if 
// item is a string, the item itself; if neither is valid, return undefined 
function extract(item, field) {
    if (item[field]) {
        return item[field];
    } else if (typeof item == "string" || typeof item == "number") {
        return item;
    }
    // intentionally don't return anything 
}
 
 
// utility function for creating a promise and wiring up callbacks if they were 
// provided in the args object (i.e., "callback style invocation"). 
function promisize(args) {
    var promise = new Ext.space.Promise();
 
    if (args && (args.onComplete || args.onError)) {
        promise.then(
            typeof args.onComplete == "function" ? args.onComplete : undefined,
            typeof args.onError == "function" ? args.onError : undefined
        );
    }
 
    return promise;
}
 
 
// utility function to filter out the weird empty items that sometimes come back from 
// the native side, that look like they're still in progress, but they're invalid; 
function downloadIsReal(item) {
    return item.isComplete || (!!item.totalBytes && !!item.fileName);
}
 
 
/**
 * Promise-based API for downloading files via URL.
 *
 * To download a file:
 *
 *      // list some details when the download completes
 *      function onComplete(file, download) {
 *          Ext.space.Logger.log("Download finished: " + download.url);
 *          Ext.space.Logger.log("Download size: " + download.totalBytes);
 *          Ext.space.Logger.log("Saved to: " + download.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.Downloads.download({ url: "http://www.sencha.com/" }).then(onComplete);
 *
 * To send custom request headers, add a `headers` property to the invocation:
 *
 *      // specify custom request headers
 *      Ext.space.Downloads.download({
 *          url: "http://www.sencha.com/",
 *          headers: {"x-example-app": "testing123"}
 *      }).then(...);
 *
 * The `download` objects involved here are instances of Ext.space.files.Download;
 * the `file` object is an Ext.space.files.File.
 *
 * To get a list of all downloads currently in progress, plus up to the ten most
 * recently completed downloads:
 *
 *      Ext.space.Downloads.getDownloads().then(function(downloads) {
 *          downloads.forEach(function(download) {
 *              Ext.space.Logger.log(download.fileName);
 *          });
 *      });
 *
 * If you have a download object and want to fetch the latest information about it,
 * you can get the progress of a single download at a time:
 *
 *      download.getProgress().then(function(updatedDownload) {
 *          Ext.space.Logger.log(updatedDownload.bytesDownloaded + " bytes downloaded");
 *      });
 *
 * Alternatively, you can wire up a progress event callback on the download:
 *
 *      download.on("progress", function(updatedDownload) {
 *          // gets called every time new progress is available
 *      });
 *
 * To cancel a download in progress:
 *
 *      download.cancel().then(function() {
 *          Ext.space.Logger.log("Canceled!");
 *      });
 *
 * @aside guide file_locker
 *
 */
Ext.define("Ext.space.Downloads", {
    singleton: true,
 
    /**
     * @private
     * Cache of the download information returned by the native bridge
     */
    downloads: null,
 
    /**
     * @private
     * Whether or not the download manager has registered callbacks with the native bridge DownloadManager#watchDownloads
     */
    watching: false,
 
    /**
     * @private
     */
    constructor: function() {
        this.downloads = {};
    },
 
    /**
     * Download a file.
     *
     * @param {Object} args Upload parameters
     * @param {String} args.url The URL of the download.
     * @param {Object} args.headers Optional HTTP headers to be sent with the request.
     * @return {Ext.space.files.Download} Download object that will be filled with data as it becomes available
     */
    download: function(args) {
        var url, manager = this, download = new Ext.space.files.Download();
 
        // backwards compatibility with old versions of this interface, which took 
        // event callbacks in the main args object 
        if (args && (args.onComplete || args.onError)) {
            download.then(
                typeof args.onComplete == "function" ? args.onComplete : undefined,
                typeof args.onError == "function" ? args.onError: undefined
            );
        }
 
        if (args) {
            url = extract(args, "url");
 
            if (url) {
                if (args.onProgress) {
                    download.on("progress", args.onProgress);
                }
 
                var cmd = {
                    command: "DownloadManager#download",
                    url: url,
                    callbacks: {
                        onStart: function(id) {
                            Ext.space.Logger.debug("download start: ", id, url);
                            if (id) {
                                // cache a reference to the Download object, so we can 
                                // continue to update it over time 
                                manager.downloads[id] = download;
                            }
 
                            manager.watchDownloads();
                        },
                        onSuccess: function(id) {
                            Ext.space.Logger.debug("download success: ", id);
                            manager.getProgress(id).then(function(obj) {
                                Ext.space.SecureFiles.getFile(obj.fileKey).then(function(file) {
                                    Ext.space.Logger.debug("Resolving successful download: ", id, file, obj);
                                    obj.done.fulfill(file, obj);
                                });
                            });
                        },
                        onError: function(error) {
                            download.done.reject(error);
                        }
                    }
                };
 
                if (args.headers) {
                    cmd.headers = args.headers;
                }
 
                Ext.space.Communicator.send(cmd);
 
            }
        }
 
        if (!args || !url) {
            download.done.reject("Missing URL");
        }
 
        return download;
    },
 
    /**
     * Retrieve the current status of all active downloads, plus the most recently
     * completed downloads.
     *
     * @param {Object} args (optional) Object with .onComplete and/or .onError
     *                      callback(s) to run when the download finishes
     * @return {Ext.space.Promise} Promise which will receive an array of
     *                       Ext.space.files.Download objects
     */
    getDownloads: function(args) {
        var promise = promisize(args);
 
        var manager = this;
 
        function makeDownload(item) {
            var id = item.downloadId;
            if (manager.downloads[id]) {
                return manager.downloads[id]._updateWith(item);
            } else {
                manager.downloads[id] = new Ext.space.files.Download(item);
                return manager.downloads[id];
            }
        }
 
        Ext.space.Communicator.send({
            command: "DownloadManager#getDownloads",
            callbacks: {
                onSuccess: function(responses) {
                    Ext.space.Logger.debug("getDownloads: ", responses);
                    if (Object.prototype.toString.call(responses) === "[object Array]") {
                        // resolve with an array of Download objects 
                        promise.fulfill(responses.filter(downloadIsReal).map(makeDownload));
                        manager.watchDownloads();
 
                    } else {
                        // what happened? 
                        promise.reject("Malformed (non-Array) response from the native bridge");
                    }
                },
                onError: function(error) {
                    promise.reject(error);
                }
            }
        });
 
        return promise;
    },
 
    /**
     * Check a download's progress (normally done via download.getProgress()).
     *
     * @private
     * @param {String|Object} args Download ID of the download to check, or an object
     *                             containing a .downloadId property containing such.
     * @return {Ext.space.Promise} Promise which will receive an up-to-date copy of the
     *                       Ext.space.files.Download
     */
    getProgress: function(args) {
        var id, promise, match, manager = this;
 
        if (args) {
            promise = promisize(args);
            id = typeof args == "number" ? args : extract(args, "downloadId");
 
            if (id && manager.downloads[id]) {
                if (manager.downloads[id].isComplete) {
                    // if it's cached and complete, return it 
                    promise.fulfill(manager.downloads[id]);
 
                } else {
                    // if it's cached and incomplete, get it from getDownloads 
                    this.getDownloads().then(function(downloads) {
                        downloads.some(function(download) {
                            if (download.downloadId === id) {
                                match = download;
                                return true;
                            }
                        });
 
                        if (match) {
                            promise.fulfill(match);
                        } else {
                            promise.reject("Download " + id + " not found");
                        }
 
                    }, function(error) {
                        promise.reject(error);
                    });
                }
            }
 
 
        }
 
        if (!args || !id) {
            if (!promise) {
                promise = new Ext.space.Promise();
            }
            promise.reject("Missing download ID");
        } else if (!manager.downloads[id]) {
            promise.reject("Download " + id + " not found");
        }
 
        return promise;
    },
 
    /**
     * Cancel a download (normally done via download.cancel()).
     *
     * @private
     * @param {String|Object} args Download ID of the download to check, or an object
     *                             containing a .downloadId property containing such.
     * @return {Ext.space.Promise} Promise which will resolve when the download is canceled. If
     *                       the download is already done or canceled, it will reject.
     */
    cancel: function(args) {
        var id, promise = new Ext.space.Promise(), manager = this;
 
        if (args) {
            promise = promisize(args);
            id = extract(args, "downloadId");
 
            if (id) {
                Ext.space.Communicator.send({
                    command: "DownloadManager#cancel",
                    downloadId: id,
                    callbacks: {
                        onSuccess: function() {
                            manager.downloads[id].done.reject("Canceled");
                            promise.fulfill(true);
                        },
                        onError: function(error) {
                            promise.reject(error);
                        }
                    }
                });
            }
        }
 
        if (!args || !id) {
            promise.reject("Missing download ID");
        }
 
        return promise;
    },
 
    /**
     * Watch for updates coming in from the native bridge, to keep the internal
     * cache up to date
     *
     * @private
     */
    watchDownloads: function() {
        var manager = this,
            cache = this.downloads,
            activeCount = 0;
 
        function processItem(item) {
            var id = item.downloadId,
                alreadyComplete = !(id in cache) || (cache[id].isComplete && !cache[id].isVolatile),
                justCompleted = !alreadyComplete && item.isComplete && !item.isVolatile;
 
            // count the downloads still in progress so we know when to unwatch 
            if (item.isVolatile) {
                activeCount++;
            }
 
            // create or update the cached download object 
            if (cache[id]) {
                cache[id]._updateWith(item);
            } else {
                cache[id] = new Ext.space.files.Download(item);
            }
 
            // resolve the original promise with the final data 
            if (justCompleted) {
                Ext.space.SecureFiles.getFile(cache[id].fileKey).then(function(file) {
                    Ext.space.Logger.debug("Resolving watched download: ", id, file, cache[id]);
                    cache[id].done.fulfill(file, cache[id]);
                });
            }
        }
 
        if (!manager.watching) {
            manager.watching = true;
            Ext.space.Communicator.send({
                command: "DownloadManager#watchDownloads",
                callbacks: {
                    onSuccess: function(responses) {
                        Ext.space.Logger.debug("watchDownloads: ", responses.length, responses);
                        activeCount = 0;
                        if (Object.prototype.toString.call(responses) === "[object Array]") {
                            responses.forEach(processItem);
                            if (!activeCount) {
                                manager.unwatchDownloads();
                            }
                        }
                        Ext.space.Logger.debug("watchDownloads activeCount: ", activeCount);
                    },
                    onError: function(error) {
                        Ext.space.Logger.debug("watchDownloads encountered an error; unwatching. Error: ", error);
                        manager.unwatchDownloads();
                    }
                }
            });
        }
    },
 
    /**
     * Discontinue watching for download updates from the native bridge
     *
     * @private
     */
    unwatchDownloads: function() {
        if (this.watching) {
            Ext.space.Communicator.send({
                command: "DownloadManager#unwatchDownloads"
            });
            this.watching = false;
        }
    }
});
 
}());