(function() {
 
// utility function to reject the given promise with the first parameter passed to 
// the callback; intended as a shorthand for creating bridge call error handlers that 
// simply bail out when something goes wrong 
function reject(promise) {
    return function(error) {
        promise.reject(error);
    };
}
 
// utility function to add a wildcard before a filename's extension, e.g., transform 
// 'index.html' => 'index*.html', 'index.foo.whatever.html' => 'index.foo.whatever*.html' 
function wildcardify(name) {
    var parts = name.split(".");
    if (parts.length == 1) {
        parts[0] += "*";
    } else {
        parts[parts.length-2] += "*";
    }
    return parts.join(".");
}
 
// utility function that takes a name like foo.something.html and inserts `idx` 
// before the extension, returning, e.g., 'foo.something.2.html' 
function indexifyName(name, idx) {
    var parts = name.split(".");
    return parts.slice(0, parts.length-1)
                .concat(idx)
                .concat(parts[parts.length-1])
                .join(".");
}
 
/**
 * Key/Value store for files. A collection represents a flat grouping of files in an
 * application's file system, and it allows you to do basic CRUD operations on the
 * files contained therein. Typically you don't instantiate a collection yourself;
 * use Ext.space.SecureFiles.get() to create one.
 *
 * The `file` objects used in this API are instances of Ext.space.files.File.
 *
 * To create a collection:
 *
 *       var myCollection = Ext.space.SecureFiles.get("secrets");
 *
 * To retrieve file contents:
 *
 *       myCollection.get("someFile.txt").then(function(contents) {
 *           // got 'em
 *       });
 *
 * To write file contents:
 *
 *       myCollection.set("someFile.txt", "The new contents").then(function(file) {
 *           // `file` is the Ext.space.files.File that was written
 *       });
 *
 * ...and more. See the individual methods for full documentation.
 *
 */
Ext.define("Ext.space.files.Collection", {
    /**
     * @private
     * @type {String}
     * Root virtual path for the files in this collection
     */
    name: null,
 
    /**
     * @private
     * @type {Object}
     * Hash of files descriptors in this collection, by file key
     */
    files: null,
 
    /**
     * @private
     */
    constructor: function(name) {
        // store the collection name and create the file cache 
        this.name = name;
        this.files = {};
    },
 
    /**
     * Cache a file descriptor object in our local catalog.
     *
     * @private
     * @param {Object} obj File descriptor object (as a property bag)
     * @return {Ext.space.files.File} File object
     */
    _cache: function(obj) {
        var file = this._makeFile(obj);
        var id = file.key;
        if (this.files[id]) {
            // update the cached file 
            this.files[id]._updateWith(obj);
        } else {
            // cache a new one 
            this.files[id] = file;
        }
        return this.files[id];
    },
 
    /**
     * Bulk load an array of file descriptors loaded from the bridge into our cache.
     *
     * @private
     * @param {Array} results Results of a bridge query
     * @return {Array} Array of the Ext.space.files.File objects that got cached
     */
    _cacheResults: function(results) {
        return results.map(this._cache.bind(this));
    },
 
    /**
     * Transform a property bag file descriptor object into a real Ext.space.files.File.
     *
     * @private
     * @param {Object} obj File descriptor object (as a property bag)
     * @return {Ext.space.files.File} File object
     */
    _makeFile: function(obj) {
        var file = new Ext.space.files.File(obj);
        file.collection = this;
        return file;
    },
 
    /**
     * Retrieve an item from the local catalog, by name or File object.
     *
     * @private
     * @param {String} fileOrName File name as a string, or Ext.space.files.File object
     * @return {Ext.space.files.File} File descriptor object
     */
    _getItemByName: function(fileOrName) {
        var match;
        Object.keys(this.files).some(function(id) {
            if (this.files[id].name === (typeof fileOrName === "string" ? fileOrName : fileOrName.name)) {
                match = this.files[id];
                return true;
            }
        }, this);
        return match;
    },
 
    /**
     * Query the collection for files matching the given criteria. See the main
     * Ext.space.SecureFiles.query() documentation for query definitions.
     *
     * @private
     * @param {Object} query (optional) Query object
     * @param {Object} options (optional) Query options
     * @return {Ext.space.Promise} Promise that resolves with the Ext.space.files.File
     *                       objects that match the criteria.
     */
    _query: function(query, options) {
        var qry = {path: this.name};
 
        if (query) {
            // copy the query as is 
            if (query.name) { qry.name = query.name; }
            if (query.type) { qry.type = query.type; }
            if (query.createdBefore) { qry.createdBefore = query.createdBefore; }
            if (query.createdAfter) { qry.createdAfter = query.createdAfter; }
            if (query.modifiedBefore) { qry.modifiedBefore = query.modifiedBefore; }
            if (query.modifiedAfter) { qry.modifiedAfter = query.modifiedAfter; }
        }
 
        if (options) {
            options.collection = this;
        } else {
            options = {collection: this};
        }
 
        return Ext.space.SecureFiles.query(qry, options);
    },
 
    /**
     * Load a file descriptor from the filesystem, by name.
     *
     * @private
     * @param {String} name File name
     * @return {Ext.space.Promise} Promise that resolves with the file's cached catalog object;
     *                       if the file isn't found, the promise rejects.
     */
    _loadFile: function(name) {
        // Note that this method exhibits behavior slightly different than the native 
        // bridge's #queryFiles operation being used here. The bridge considers a 
        // zero-length result to still be a success as long as the operation doesn't 
        // encounter an error of some sort. Since this method is simply attempting 
        // to load metadata for a file, if the query itself succeeds but the file 
        // doesn't exist, we reject the promise. That allows consuming code to, e.g., 
        // go back and create a file, then retry whatever it was doing. 
        var result = new Ext.space.Promise();
        var collection = this;
 
        this.query({ name: name }).then(function(items) {
            // there really only should be a single match here; what 
            // should we do if there's more than one? 
            if (items.length) {
                result.fulfill(items[0]);
            } else {
                result.reject("File not found: " + collection.name + " :: " + name);
            }
        });
        return result;
    },
 
    /**
     * Retrieve the contents of a file by key.
     *
     * @private
     * @param {String} key File key
     * @param {Ext.space.files.File} file (optional) File object, to pass file descriptor data through into the promise
     * @return {Ext.space.Promise} Promise that resolves with the file's contents, plus possibly the file descriptor data
     */
    loadContents: function(key, file) {
        var result = new Ext.space.Promise();
        Ext.space.Communicator.send({
            command: "Files#getFileContents",
            key: key,
            callbacks: {
                onSuccess: function(contents) {
                    result.fulfill(contents, file);
                },
                onError: reject(result)
            }
        });
        return result;
    },
 
    /**
     * Create a file by name, with optional type, path, and contents.
     *
     * @private
     * @param {String} name File name
     * @param {Object} props (optional) Hash with extra data in .type  and/or .contents
     * @return {Ext.space.Promise} Promise that resolves with the Ext.space.files.File object created
     */
    _createFile: function(name, props) {
        var result = new Ext.space.Promise();
        var collection = this;
 
        var args = {
            command: "Files#createFile",
            path: this.name,
            name: name,
            callbacks: {
                onSuccess: function(fileData) {
                    result.fulfill(collection._cache(fileData));
                },
                onError: reject(result)
            }
        };
 
        // add the optional parameters 
        if (props) {
            if (props.type) { args.type = props.type; }
            if (props.contents) { args.fileData = props.contents; }
        }
 
        Ext.space.Communicator.send(args);
 
        return result;
    },
 
    /**
     * Remove a file's cached object from this collection (the actual file on disk
     * remains untouched).
     *
     * @private
     * @param {Ext.space.files.File} file File object
     */
    _disownFile: function(file) {
        // we don't need the cached object to be the same instance as what got passed 
        // in here as a parameter, so we just check that there's a cached item with 
        // the correct file key 
        if (file.collection === this && this.files[file.key]) {
            delete this.files[file.key];
        }
    },
 
    /**
     * Move the given file into this collection, renaming it if a name is provided.
     *
     * @private
     * @param {Ext.space.files.File} file File object
     * @param {String} name (optional) New name for the file
     * @return {Ext.space.Promise} Promise that resolves when the file is done moving
     */
    _ownFile: function(file, name) {
        var result = new Ext.space.Promise();
 
        var args = {
            command: "Files#renameFile",
            key: file.key,
            newPath: this.name,
            overwrite: true,
            callbacks: {
                onSuccess: function(overwrittenKey) {
                    // remove from the original collection object 
                    file.collection._disownFile(file);
 
                    // check whether we need to unwire any loaded file objects in 
                    // any collections due to a file disappearing 
                    if (overwrittenKey) {
                        Ext.space.SecureFiles._removeFileFromLoadedCollections(overwrittenKey);
                    }
 
                    // wire up the new associations/properties 
                    file.collection = this;
                    file.path = this.path;
                    if (name) { file.name = name; }
                    this.files[file.key] = file;
 
                    result.fulfill(file);
                }.bind(this),
                onError: reject(result)
            }
        };
 
        if (name) { args.newName = name; }
 
        Ext.space.Communicator.send(args);
 
        return result;
    },
 
    /**
     * Move the given file into this collection, renaming the file if the collection
     * already contains a file with the same name.
     *
     * @private
     * @param {Ext.space.files.File} file File object
     * @return {Ext.space.Promise} Promise that resolves when the file is done moving
     */
    _ownFileSafe: function(file) {
        var result = new Ext.space.Promise();
 
        // look for an existing file with the same name in the collection 
        this.query({ name: wildcardify(file.name) }).then(function(files) {
            var fileNames, testName, chosenName, done = false, idx = 0;
 
            if (!files) {
                // didn't get even an empty array of files, which shouldn't happen 
                result.reject("Error moving the downloaded file into the collection.");
 
            } else {
                // can we use the original name as is? 
                fileNames = files.map(function(f) { return f.name; });
                if (fileNames.indexOf(file.name) == -1) {
                    chosenName = file.name;
                    done = true;
                }
                // generate names and check them against the queried results 
                // to avoid conflicts 
                while (!done) {
                    testName = indexifyName(file.name, ++idx);
                    if (fileNames.indexOf(testName) == -1) {
                        chosenName = testName;
                        done = true;
                    }
                }
 
                // take ownership of the file and pass it into our returned promise 
                this._ownFile(file, chosenName).connect(result);
            }
 
        }.bind(this), function(error) {
            result.reject("Error querying the collection for conflicting files.");
        });
 
        return result;
    },
 
    /**
     * Launch the native viewer for a file by key.
     *
     * @private
     * @param {String} key File key
     * @return {Ext.space.Promise} Promise that resolves when the viewer is launched
     */
    viewFile: function(key) {
        var result = new Ext.space.Promise();
        Ext.space.Communicator.send({
            command: "Files#viewFile",
            key: key,
            callbacks: {
                onSuccess: function() {
                    result.fulfill();
                },
                onError: reject(result)
            }
        });
        return result;
    },
 
    /**
     * Remove a file from disk.
     *
     * @private
     * @param {String} key File key
     * @return {Ext.space.Promise} Promise that resolves when the file is removed
     */
    removeFile: function(key) {
        var result = new Ext.space.Promise();
        var collection = this;
        var file = this.files[key];
 
        Ext.space.Communicator.send({
            command: "Files#removeFile",
            key: key,
            callbacks: {
                onSuccess: function() {
                    // remove the cached item from the collection's internal catalog 
                    Object.keys(collection.files).some(function(id) {
                        if (id.toString() === file.key.toString()) {
                            delete collection.files[id];
                            Ext.space.SecureFiles._removeFileFromLoadedCollections(id);
                            return true;
                        }
                    });
                    result.fulfill(true);
                },
                onError: reject(result)
            }
        });
 
        return result;
    },
 
    /**
     * Write the contents of a file by key.
     *
     * @private
     * @param {String} key File key
     * @param {String} contents Contents to write
     * @return {Ext.space.Promise} Promise that resolves with the File object having been written
     */
    writeContents: function(key, contents) {
        // note: only call this method if you're sure the file already exists; 
        //       otherwise the bridge will invoke the onError handler 
        var result = new Ext.space.Promise();
        var collection = this;
 
        Ext.space.Communicator.send({
            command: "Files#setFileContents",
            key: key,
            fileData: contents,
            callbacks: {
                onSuccess: function() {
                    result.fulfill(collection.files[key]);
                },
                onError: reject(result)
            }
        });
 
        return result;
    },
 
 
    /**
     * Get the file contents for a name.
     *
     *      var files = Ext.space.SecureFiles.get('secrets');
     *
     *      files.get('myFile').then(function(contents){
     *          // do something with the contents of the file.
     *      });
     *
     * @param {String} name File name for which to retrieve contents
     * @return {Ext.space.Promise} Promise that resolves when the contents are available
     *
     */
    get: function(name) {
        var result, item = this._getItemByName(name);
        var collection = this;
 
        function loadContents(catalogItem) {
            return collection.loadContents(catalogItem.key, catalogItem);
        }
 
        if (item && item.key) {
            // we have the key, let's go straight to loading the contents 
            result = loadContents(item);
        } else {
            // couldn't find the key in cache (weird, why?), so re-query for the file 
            result = this._loadFile(name).then(loadContents, function(error) { result.reject(error); });
        }
        return result;
    },
 
    /**
     * Write the given contents to a file.
     *
     *      var files = Ext.space.SecureFiles.get('secrets');
     *
     *      files.set('myFile', 'the contents go here').then(function(file) {
     *          // can do something with `file` here
     *      });
     *
     * @param {String|Object} name File name to which to write contents, or an object
     *                             with properties specifying the name and MIME type
     *                             of the file, e.g., `{name: "foo", type: "text/plain"}`.
     *                             Note that the type will only be stored if the file
     *                             is being created; if the file already exists, any
     *                             provided type will be ignored
     * @param {String} contents Contents to write
     * @return {Ext.space.Promise} Promise that resolves when the file is written
     */
    set: function(name, contents) {
        var result, item, type, collection = this;
 
        if (typeof name === "object") {
            type = name.type;
            name = name.name;
        }
 
        item = this._getItemByName(name);
 
        function writeContents(catalogItem) {
            return collection.writeContents(catalogItem.key, contents);
        }
        function createWithContents() {
            // if the file doesn't exist, create it with the given contents 
            collection._createFile(name, {contents: contents, type: type}).then(function(file) {
                result.fulfill(file);
            });
        }
 
        if (item && item.key) {
            // we have the key, let's go straight to writing 
            result = writeContents(item);
        } else {
            // couldn't find the key in cache (weird, why?), so re-query for the file 
            this._loadFile(name).then(writeContents, createWithContents);
            result = new Ext.space.Promise();
        }
        return result;
    },
 
    /**
     * Query the collection for files matching the given criteria. See the main
     * Ext.space.SecureFiles.query() documentation for query definitions.
     *
     * @param {Object} query (optional) Query object
     * @param {String} query.name (optional) exactName.txt", "*.txt", etc...
     * @param {String} query.type MIME type ("text/plain", etc...)
     * @param {Date} query.createdBefore (optional) Date object
     * @param {Date} query.modifiedBefore (optional) Date object
     * @param {Date} query.modifiedAfter (optional) Date object
     * @param {String} options (optional) modifies how  the results will be returned
     * @param {String} options.fetch  (optional) "count" or "data" (the default), to return a simple count, or the complete results, respectively
     * @param {String} options.sortField  (optional)  name of the field on which to sort
     * @param {String} options.sortDirection  (optional)  "asc" or "desc"
     * @return {Ext.space.Promise} Promise that resolves with the Ext.space.files.File
     *                       objects that match the criteria.
     */
    query: function(query, options) {
        return this._query(query, options).then(this._cacheResults.bind(this));
    },
 
    /**
     * Delete all of the files in this collection.
     *
     * @return {Ext.space.Promise} Promise that resolves when the files are deleted
     */
    clear: function() {
        var collection = this;
 
        function removeFile(key) { return collection.removeFile(key); }
 
        return this.query().then(function(items) {
            return Ext.space.Promise.whenComplete(Object.keys(this.files).map(removeFile)).then(function() {
                return;
            });
        }.bind(this));
    },
 
    /**
     * Returns a count of the total number of files in the collection.
     *
     *      var secrets = Ext.space.SecureFiles.get('secrets');
     *
     *      secrets.count().then(function(count) {
     *          // done
     *      });
     *
     * @return {Ext.space.Promise} Promise that resolves with the number of files in the collection
     */
    count: function() {
        return this._query(null, { fetch: "count" });
    },
 
 
    /**
     * Checks to see if the given file exists.
     *
     *      var secrets = Ext.space.SecureFiles.get('secrets');
     *
     *      secrets.has('myFile').then(function(hasKey) {
     *          // check hasKey
     *      });
     *
     * @param {String} name Name of the file for which to search
     * @return {Ext.space.Promise} Promise that resolves with a boolean indicating presence of the file
     */
    has: function(name) {
        var result = new Ext.space.Promise();
        this._loadFile(name).then(function() {
            result.fulfill(true);
        }, function(error) {
            result.fulfill(false);
        });
        return result;
    },
 
    /**
     * Deletes the file (if present).
     *
     *      var secrets = Ext.space.SecureFiles.get('secrets');
     *
     *      secrets.remove('myFile').then(function(done) {
     *          // done
     *      });
     *
     * @param {String} name Name of the file to delete
     * @return {Ext.space.Promise} Promise that resolves when the file is deleted
     *
     */
    remove: function(name) {
        var result, item = this._getItemByName(name);
        var collection = this;
 
        function removeFile(catalogItem) {
            return collection.removeFile(catalogItem.key);
        }
 
        if (item && item.key) {
            // we have the key, let's go straight to removing it 
            result = removeFile(item);
        } else {
            // load it to get the key; if it's not found, act as though we succeeded 
            result = this._loadFile(name).then(removeFile, function(error) { result.fulfill(true); });
        }
 
        return result;
    },
 
    /**
     * Launches the viewer for a file.
     *
     *      var secrets = Ext.space.SecureFiles.get('secrets');
     *
     *      secrets.view('myFile').then(function() {
     *          // launched
     *      });
     *
     * @param {String} name Name of the file to view
     * @return {Ext.space.Promise} Promise that resolves when the file viewer is launched
     *
     */
    view: function(name) {
        var result, item = this._getItemByName(name);
        var collection = this;
 
        function viewFile(catalogItem) {
            return collection.viewFile(catalogItem.key);
        }
 
        if (item && item.key) {
            // we have the key, let's go straight to removing it 
            result = viewFile(item);
        } else {
            // load it to get the key 
            result = this._loadFile(name).then(viewFile, reject(result));
        }
 
        return result;
    },
 
 
    /**
     * Generate a list of all the names of the files in the collection, in no
     * particular order.
     *
     *      var secrets = Ext.space.SecureFiles.get('secrets');
     *
     *      secrets.keys().then(function(keys) {
     *          // array of file names
     *      });
     *
     * @return {Ext.space.Promise} Promise that will resolve when all of the keys have been collected.
     *
     */
    keys: function() {
        return this.query().then(function(items) {
            return Object.keys(this.files).map(function(id) { return this.files[id].name; }, this);
        }.bind(this));
    },
 
 
    /**
     * Iterates over all the files in a collection
     *
     *      var secrets = Ext.space.SecureFiles.get('secrets');
     *
     *      secrets.forEach(function(file) {...}).then(function() {
     *          // done
     *      });
     *
     * @param {Function}  callback Function to call once for each file in the collection.
     *                             As with Array.prototype.forEach, it receives three
     *                             parameters: an Ext.space.files.File object, its index
     *                             in the array being iterated, and the array of files
     *                             itself. Note however that the order of elements in
     *                             this array are NOT guaranteed in any way.
     * @param {Object} thisArg (optional) Value to use for `this` when executing the callback.
     * @return {Ext.space.Promise} Promise that resolves with an array of the File objects
     *                       operated on, after the callback has been run across the
     *                       entire collection.
     */
    forEach: function(callback, thisArg) {
        var args = arguments;
        return this.query().then(function(items) {
            items.forEach.apply(items, args);
            return items;
        });
    },
 
 
    /**
     * Downloads a file from the given URL into this collection.
     *
     * If you pass overwrite: true in the args parameter, the file will be overwritten
     * if the name conflicts with a file that already exists.
     *
     *      var secrets = Ext.space.SecureFiles.get('secrets');
     *
     *      // saves the file as 'file.html' from the URL
     *      secrets.download({ url: 'http://example.com/some/file.html' }).then(function(file) {
     *          // do something with the file
     *      });
     *
     *      // overwites file.html
     *      secrets.download({
     *          url: 'http://example.com/some/file.html',
     *          overwrite: true
     *      }).then(function(file) {
     *          // do something with the file
     *      });
     *
     * @param {Object} args Download parameters
     * @param {String} args.url The URL of the file to be downloaded
     * @param {Boolean} args.overwrite true/false (default false) to determine what to do in the case of a name collision.
     * @return {Ext.space.Promise} Promise that resolves with the File object for the file
     *                       downloaded
     */
    download: function(args) {
        var result = new Ext.space.Promise();
        var collection = this;
        var overwrite = !!(args && args.overwrite);
 
        if (!(args && args.url)) {
            result.reject("Missing URL");
        } else {
            Ext.space.Downloads.download({ url: args.url }).then(function(file, download) {
                collection[overwrite ? "_ownFile" : "_ownFileSafe"](file).connect(result);
            }, function(error) {
                result.reject("Download error: " + error);
            });
        }
 
        return result;
    },
 
    /**
     * Compress the entire collection into an archive.
     *
     *      var collection = Ext.space.SecureFiles.get("somepath");
     *      collection.compress({ archiveName: "somefiles.zip" }).then(function(file) {
     *          // do something with the archive file
     *      });
     *
     *      // or specify more options:
     *      collection.compress({
     *          archiveName: "somefiles.blob",
     *          path: "myArchivePath",
     *          type: "zip"
     *      }).then(function(file) {
     *          // do something with the archive file
     *      });
     *
     * @param {Object} args Options object
     * @param {String} args.archiveName Name of the archive file to create
     * @param {String} args.path (optional) Path into which to save the archive; defaults to ""
     * @param {String} args.type (optional) Compression type ("zip", "zip", "bzip2", "7zip");
     *                           if this is omitted, the system will attempt to determine
     *                           the compression type from the archiveName, and if it
     *                           cannot be determined, defaults to "zip".
     * @return {Ext.space.Promise} Promise that resolves with the Ext.space.files.File
     *                       object for the new archive.
     */
    compress: function(args) {
        var result = new Ext.space.Promise();
 
        if (!args) {
            result.reject("Missing compression arguments");
 
        } else if (!args.archiveName) {
            result.reject("Missing compressed archive name");
 
        } else {
            this.query().then(function(items) {
                var opts = {
                    files: items,
                    archiveName: args.archiveName,
                    path: args.path,
                    type: args.type
                };
 
                Ext.space.SecureFiles.compress(opts).connect(result);
            });
        }
 
        return result;
    }
 
});
 
})();