﻿/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 */

/* global IDBKeyRange */

var LocalFileSystem = require('./LocalFileSystem'),
    FileSystem = require('./FileSystem'),
    FileEntry = require('./FileEntry'),
    FileError = require('./FileError'),
    DirectoryEntry = require('./DirectoryEntry'),
    File = require('./File');

/*
QUIRKS:
    Does not fail when removing non-empty directories
    Does not support metadata for directories
    Does not support requestAllFileSystems
    Does not support resolveLocalFileSystemURI
    Methods copyTo and moveTo do not support directories

    Heavily based on https://github.com/ebidel/idb.filesystem.js
 */


(function(exports, global) {
    var indexedDB = global.indexedDB || global.mozIndexedDB;
    if (!indexedDB) {
        throw "Firefox OS File plugin: indexedDB not supported";
    }

    var fs_ = null;

    var idb_ = {};
    idb_.db = null;
    var FILE_STORE_ = 'entries';

    var DIR_SEPARATOR = '/';
    var DIR_OPEN_BOUND = String.fromCharCode(DIR_SEPARATOR.charCodeAt(0) + 1);

    var pathsPrefix = {
        // Read-only directory where the application is installed.
        applicationDirectory: location.origin + "/",
        // Where to put app-specific data files.
        dataDirectory: 'file:///persistent/',
        // Cached files that should survive app restarts.
        // Apps should not rely on the OS to delete files in here.
        cacheDirectory: 'file:///temporary/',
    };

/*** Exported functionality ***/

    exports.requestFileSystem = function(successCallback, errorCallback, args) {
        var type = args[0];
        //var size = args[1];

        if (type !== LocalFileSystem.TEMPORARY && type !== LocalFileSystem.PERSISTENT) {
            if (errorCallback) {
                errorCallback(FileError.INVALID_MODIFICATION_ERR);
            }
            return;
        }

        var name = type === LocalFileSystem.TEMPORARY ? 'temporary' : 'persistent';
        var storageName = (location.protocol + location.host).replace(/:/g, '_');

        var root = new DirectoryEntry('', DIR_SEPARATOR);
        fs_ = new FileSystem(name, root);

        idb_.open(storageName, function() {
            successCallback(fs_);
        }, errorCallback);
    };

    require('./fileSystems').getFs = function(name, callback) {
        callback(new FileSystem(name, fs_.root));
    };

    // list a directory's contents (files and folders).
    exports.readEntries = function(successCallback, errorCallback, args) {
        var fullPath = args[0];

        if (!successCallback) {
            throw Error('Expected successCallback argument.');
        }

        var path = resolveToFullPath_(fullPath);

        idb_.getAllEntries(path.fullPath, path.storagePath, function(entries) {
            successCallback(entries);
        }, errorCallback);
    };

    exports.getFile = function(successCallback, errorCallback, args) {
        var fullPath = args[0];
        var path = args[1];
        var options = args[2] || {};

        // Create an absolute path if we were handed a relative one.
        path = resolveToFullPath_(fullPath, path);

        idb_.get(path.storagePath, function(fileEntry) {
            if (options.create === true && options.exclusive === true && fileEntry) {
                // If create and exclusive are both true, and the path already exists,
                // getFile must fail.

                if (errorCallback) {
                    errorCallback(FileError.PATH_EXISTS_ERR);
                }
            } else if (options.create === true && !fileEntry) {
                // If create is true, the path doesn't exist, and no other error occurs,
                // getFile must create it as a zero-length file and return a corresponding
                // FileEntry.
                var newFileEntry = new FileEntry(path.fileName, path.fullPath, new FileSystem(path.fsName, fs_.root));

                newFileEntry.file_ = new MyFile({
                    size: 0,
                    name: newFileEntry.name,
                    lastModifiedDate: new Date(),
                    storagePath: path.storagePath
                });

                idb_.put(newFileEntry, path.storagePath, successCallback, errorCallback);
            } else if (options.create === true && fileEntry) {
                if (fileEntry.isFile) {
                    // Overwrite file, delete then create new.
                    idb_['delete'](path.storagePath, function() {
                        var newFileEntry = new FileEntry(path.fileName, path.fullPath, new FileSystem(path.fsName, fs_.root));

                        newFileEntry.file_ = new MyFile({
                            size: 0,
                            name: newFileEntry.name,
                            lastModifiedDate: new Date(),
                            storagePath: path.storagePath
                        });

                        idb_.put(newFileEntry, path.storagePath, successCallback, errorCallback);
                    }, errorCallback);
                } else {
                    if (errorCallback) {
                        errorCallback(FileError.INVALID_MODIFICATION_ERR);
                    }
                }
            } else if ((!options.create || options.create === false) && !fileEntry) {
                // If create is not true and the path doesn't exist, getFile must fail.
                if (errorCallback) {
                    errorCallback(FileError.NOT_FOUND_ERR);
                }
            } else if ((!options.create || options.create === false) && fileEntry &&
                fileEntry.isDirectory) {
                // If create is not true and the path exists, but is a directory, getFile
                // must fail.
                if (errorCallback) {
                    errorCallback(FileError.INVALID_MODIFICATION_ERR);
                }
            } else {
                // Otherwise, if no other error occurs, getFile must return a FileEntry
                // corresponding to path.

                successCallback(fileEntryFromIdbEntry(fileEntry));
            }
        }, errorCallback);
    };

    exports.getFileMetadata = function(successCallback, errorCallback, args) {
        var fullPath = args[0];

        exports.getFile(function(fileEntry) {
            successCallback(new File(fileEntry.file_.name, fileEntry.fullPath, '', fileEntry.file_.lastModifiedDate,
                fileEntry.file_.size));
        }, errorCallback, [fullPath, null]);
    };

    exports.getMetadata = function(successCallback, errorCallback, args) {
        exports.getFile(function (fileEntry) {
            successCallback(
                {
                    modificationTime: fileEntry.file_.lastModifiedDate,
                    size: fileEntry.file_.lastModifiedDate
                });
        }, errorCallback, args);
    };

    exports.setMetadata = function(successCallback, errorCallback, args) {
        var fullPath = args[0];
        var metadataObject = args[1];

        exports.getFile(function (fileEntry) {
              fileEntry.file_.lastModifiedDate = metadataObject.modificationTime;
        }, errorCallback, [fullPath, null]);
    };

    exports.write = function(successCallback, errorCallback, args) {
        var fileName = args[0],
            data = args[1],
            position = args[2];
            //isBinary = args[3];

        if (!data) {
            if (errorCallback) {
                errorCallback(FileError.INVALID_MODIFICATION_ERR);
            }
            return;
        }

        exports.getFile(function(fileEntry) {
            var blob_ = fileEntry.file_.blob_;

            if (!blob_) {
                blob_ = new Blob([data], {type: data.type});
            } else {
                // Calc the head and tail fragments
                var head = blob_.slice(0, position);
                var tail = blob_.slice(position + data.byteLength);

                // Calc the padding
                var padding = position - head.size;
                if (padding < 0) {
                    padding = 0;
                }

                // Do the "write". In fact, a full overwrite of the Blob.
                blob_ = new Blob([head, new Uint8Array(padding), data, tail],
                    {type: data.type});
            }

            // Set the blob we're writing on this file entry so we can recall it later.
            fileEntry.file_.blob_ = blob_;
            fileEntry.file_.lastModifiedDate = data.lastModifiedDate || null;
            fileEntry.file_.size = blob_.size;
            fileEntry.file_.name = blob_.name;
            fileEntry.file_.type = blob_.type;

            idb_.put(fileEntry, fileEntry.file_.storagePath, function() {
                successCallback(data.byteLength);
            }, errorCallback);
        }, errorCallback, [fileName, null]);
    };

    exports.readAsText = function(successCallback, errorCallback, args) {
        var fileName = args[0],
            enc = args[1],
            startPos = args[2],
            endPos = args[3];

        readAs('text', fileName, enc, startPos, endPos, successCallback, errorCallback);
    };

    exports.readAsDataURL = function(successCallback, errorCallback, args) {
        var fileName = args[0],
            startPos = args[1],
            endPos = args[2];

        readAs('dataURL', fileName, null, startPos, endPos, successCallback, errorCallback);
    };

    exports.readAsBinaryString = function(successCallback, errorCallback, args) {
        var fileName = args[0],
            startPos = args[1],
            endPos = args[2];

        readAs('binaryString', fileName, null, startPos, endPos, successCallback, errorCallback);
    };

    exports.readAsArrayBuffer = function(successCallback, errorCallback, args) {
        var fileName = args[0],
            startPos = args[1],
            endPos = args[2];

        readAs('arrayBuffer', fileName, null, startPos, endPos, successCallback, errorCallback);
    };

    exports.removeRecursively = exports.remove = function(successCallback, errorCallback, args) {
        var fullPath = args[0];

        // TODO: This doesn't protect against directories that have content in it.
        // Should throw an error instead if the dirEntry is not empty.
        idb_['delete'](fullPath, function() {
            successCallback();
        }, errorCallback);
    };

    exports.getDirectory = function(successCallback, errorCallback, args) {
        var fullPath = args[0];
        var path = args[1];
        var options = args[2];

        // Create an absolute path if we were handed a relative one.
        path = resolveToFullPath_(fullPath, path);

        idb_.get(path.storagePath, function(folderEntry) {
            if (!options) {
                options = {};
            }

            if (options.create === true && options.exclusive === true && folderEntry) {
                // If create and exclusive are both true, and the path already exists,
                // getDirectory must fail.
                if (errorCallback) {
                    errorCallback(FileError.INVALID_MODIFICATION_ERR);
                }
            } else if (options.create === true && !folderEntry) {
                // If create is true, the path doesn't exist, and no other error occurs,
                // getDirectory must create it as a zero-length file and return a corresponding
                // MyDirectoryEntry.
                var dirEntry = new DirectoryEntry(path.fileName, path.fullPath, new FileSystem(path.fsName, fs_.root));

                idb_.put(dirEntry, path.storagePath, successCallback, errorCallback);
            } else if (options.create === true && folderEntry) {

                if (folderEntry.isDirectory) {
                    // IDB won't save methods, so we need re-create the MyDirectoryEntry.
                    successCallback(new DirectoryEntry(folderEntry.name, folderEntry.fullPath, folderEntry.fileSystem));
                } else {
                    if (errorCallback) {
                        errorCallback(FileError.INVALID_MODIFICATION_ERR);
                    }
                }
            } else if ((!options.create || options.create === false) && !folderEntry) {
                // Handle root special. It should always exist.
                if (path.fullPath === DIR_SEPARATOR) {
                    successCallback(fs_.root);
                    return;
                }

                // If create is not true and the path doesn't exist, getDirectory must fail.
                if (errorCallback) {
                    errorCallback(FileError.NOT_FOUND_ERR);
                }
            } else if ((!options.create || options.create === false) && folderEntry &&
                folderEntry.isFile) {
                // If create is not true and the path exists, but is a file, getDirectory
                // must fail.
                if (errorCallback) {
                    errorCallback(FileError.INVALID_MODIFICATION_ERR);
                }
            } else {
                // Otherwise, if no other error occurs, getDirectory must return a
                // MyDirectoryEntry corresponding to path.

                // IDB won't' save methods, so we need re-create MyDirectoryEntry.
                successCallback(new DirectoryEntry(folderEntry.name, folderEntry.fullPath, folderEntry.fileSystem));
            }
        }, errorCallback);
    };

    exports.getParent = function(successCallback, errorCallback, args) {
        var fullPath = args[0];

        if (fullPath === DIR_SEPARATOR) {
            successCallback(fs_.root);
            return;
        }

        var pathArr = fullPath.split(DIR_SEPARATOR);
        pathArr.pop();
        var namesa = pathArr.pop();
        var path = pathArr.join(DIR_SEPARATOR);

        exports.getDirectory(successCallback, errorCallback, [path, namesa, {create: false}]);
    };

    exports.copyTo = function(successCallback, errorCallback, args) {
        var srcPath = args[0];
        var parentFullPath = args[1];
        var name = args[2];

        // Read src file
        exports.getFile(function(srcFileEntry) {

            // Create dest file
            exports.getFile(function(dstFileEntry) {

                exports.write(function() {
                    successCallback(dstFileEntry);
                }, errorCallback, [dstFileEntry.file_.storagePath, srcFileEntry.file_.blob_, 0]);

            }, errorCallback, [parentFullPath, name, {create: true}]);

        }, errorCallback, [srcPath, null]);
    };

    exports.moveTo = function(successCallback, errorCallback, args) {
        var srcPath = args[0];
        //var parentFullPath = args[1];
        //var name = args[2];

        exports.copyTo(function (fileEntry) {

            exports.remove(function () {
                successCallback(fileEntry);
            }, errorCallback, [srcPath]);

        }, errorCallback, args);
    };

    exports.resolveLocalFileSystemURI = function(successCallback, errorCallback, args) {
        var path = args[0];

        // Ignore parameters
        if (path.indexOf('?') !== -1) {
            path = String(path).split("?")[0];
        }

        // support for encodeURI
        if (/\%5/g.test(path)) {
            path = decodeURI(path);
        }

        if (path.indexOf(pathsPrefix.dataDirectory) === 0) {
            path = path.substring(pathsPrefix.dataDirectory.length - 1);

            exports.requestFileSystem(function(fs) {
                fs.root.getFile(path, {create: false}, successCallback, function() {
                    fs.root.getDirectory(path, {create: false}, successCallback, errorCallback);
                });
            }, errorCallback, [LocalFileSystem.PERSISTENT]);
        } else if (path.indexOf(pathsPrefix.cacheDirectory) === 0) {
            path = path.substring(pathsPrefix.cacheDirectory.length - 1);

            exports.requestFileSystem(function(fs) {
                fs.root.getFile(path, {create: false}, successCallback, function() {
                    fs.root.getDirectory(path, {create: false}, successCallback, errorCallback);
                });
            }, errorCallback, [LocalFileSystem.TEMPORARY]);
        } else if (path.indexOf(pathsPrefix.applicationDirectory) === 0) {
            path = path.substring(pathsPrefix.applicationDirectory.length);

            var xhr = new XMLHttpRequest();
            xhr.open("GET", path, true);
            xhr.onreadystatechange = function () {
                if (xhr.status === 200 && xhr.readyState === 4) {
                    exports.requestFileSystem(function(fs) {
                        fs.name = location.hostname;
                        fs.root.getFile(path, {create: true}, writeFile, errorCallback);
                    }, errorCallback, [LocalFileSystem.PERSISTENT]);
                }
            };

            xhr.onerror = function () {
                if (errorCallback) {
                    errorCallback(FileError.NOT_READABLE_ERR);
                }
            };

            xhr.send();
        } else {
            if (errorCallback) {
                errorCallback(FileError.NOT_FOUND_ERR);
            }
        }

        function writeFile(entry) {
            entry.createWriter(function (fileWriter) {
                fileWriter.onwriteend = function (evt) {
                    if (!evt.target.error) {
                        entry.filesystemName = location.hostname;
                        successCallback(entry);
                    }
                };
                fileWriter.onerror = function () {
                    if (errorCallback) {
                        errorCallback(FileError.NOT_READABLE_ERR);
                    }
                };
                fileWriter.write(new Blob([xhr.response]));
            }, errorCallback);
        }
    };

    exports.requestAllPaths = function(successCallback) {
        successCallback(pathsPrefix);
    };

/*** Helpers ***/

    /**
     * Interface to wrap the native File interface.
     *
     * This interface is necessary for creating zero-length (empty) files,
     * something the Filesystem API allows you to do. Unfortunately, File's
     * constructor cannot be called directly, making it impossible to instantiate
     * an empty File in JS.
     *
     * @param {Object} opts Initial values.
     * @constructor
     */
    function MyFile(opts) {
        var blob_ = new Blob();

        this.size = opts.size || 0;
        this.name = opts.name || '';
        this.type = opts.type || '';
        this.lastModifiedDate = opts.lastModifiedDate || null;
        this.storagePath = opts.storagePath || '';

        // Need some black magic to correct the object's size/name/type based on the
        // blob that is saved.
        Object.defineProperty(this, 'blob_', {
            enumerable: true,
            get: function() {
                return blob_;
            },
            set: function(val) {
                blob_ = val;
                this.size = blob_.size;
                this.name = blob_.name;
                this.type = blob_.type;
                this.lastModifiedDate = blob_.lastModifiedDate;
            }.bind(this)
        });
    }

    MyFile.prototype.constructor = MyFile;

    // When saving an entry, the fullPath should always lead with a slash and never
    // end with one (e.g. a directory). Also, resolve '.' and '..' to an absolute
    // one. This method ensures path is legit!
    function resolveToFullPath_(cwdFullPath, path) {
        path = path || '';
        var fullPath = path;
        var prefix = '';

        cwdFullPath = cwdFullPath || DIR_SEPARATOR;
        if (cwdFullPath.indexOf(FILESYSTEM_PREFIX) === 0) {
            prefix = cwdFullPath.substring(0, cwdFullPath.indexOf(DIR_SEPARATOR, FILESYSTEM_PREFIX.length));
            cwdFullPath = cwdFullPath.substring(cwdFullPath.indexOf(DIR_SEPARATOR, FILESYSTEM_PREFIX.length));
        }

        var relativePath = path[0] !== DIR_SEPARATOR;
        if (relativePath) {
            fullPath = cwdFullPath;
            if (cwdFullPath != DIR_SEPARATOR) {
                fullPath += DIR_SEPARATOR + path;
            } else {
                fullPath += path;
            }
        }

        // Adjust '..'s by removing parent directories when '..' flows in path.
        var parts = fullPath.split(DIR_SEPARATOR);
        for (var i = 0; i < parts.length; ++i) {
            var part = parts[i];
            if (part == '..') {
                parts[i - 1] = '';
                parts[i] = '';
            }
        }
        fullPath = parts.filter(function(el) {
            return el;
        }).join(DIR_SEPARATOR);

        // Add back in leading slash.
        if (fullPath[0] !== DIR_SEPARATOR) {
            fullPath = DIR_SEPARATOR + fullPath;
        }

        // Replace './' by current dir. ('./one/./two' -> one/two)
        fullPath = fullPath.replace(/\.\//g, DIR_SEPARATOR);

        // Replace '//' with '/'.
        fullPath = fullPath.replace(/\/\//g, DIR_SEPARATOR);

        // Replace '/.' with '/'.
        fullPath = fullPath.replace(/\/\./g, DIR_SEPARATOR);

        // Remove '/' if it appears on the end.
        if (fullPath[fullPath.length - 1] == DIR_SEPARATOR &&
            fullPath != DIR_SEPARATOR) {
            fullPath = fullPath.substring(0, fullPath.length - 1);
        }

        return {
            storagePath: prefix + fullPath,
            fullPath: fullPath,
            fileName: fullPath.split(DIR_SEPARATOR).pop(),
            fsName: prefix.split(DIR_SEPARATOR).pop()
        };
    }

    function fileEntryFromIdbEntry(fileEntry) {
        // IDB won't save methods, so we need re-create the FileEntry.
        var clonedFileEntry = new FileEntry(fileEntry.name, fileEntry.fullPath, fileEntry.fileSystem);
        clonedFileEntry.file_ = fileEntry.file_;

        return clonedFileEntry;
    }

    function readAs(what, fullPath, encoding, startPos, endPos, successCallback, errorCallback) {
        exports.getFile(function(fileEntry) {
            var fileReader = new FileReader(),
                blob = fileEntry.file_.blob_.slice(startPos, endPos);

            fileReader.onload = function(e) {
                successCallback(e.target.result);
            };

            fileReader.onerror = errorCallback;

            switch (what) {
                case 'text':
                    fileReader.readAsText(blob, encoding);
                    break;
                case 'dataURL':
                    fileReader.readAsDataURL(blob);
                    break;
                case 'arrayBuffer':
                    fileReader.readAsArrayBuffer(blob);
                    break;
                case 'binaryString':
                    fileReader.readAsBinaryString(blob);
                    break;
            }

        }, errorCallback, [fullPath, null]);
    }

/*** Core logic to handle IDB operations ***/

    idb_.open = function(dbName, successCallback, errorCallback) {
        var self = this;

        // TODO: FF 12.0a1 isn't liking a db name with : in it.
        var request = indexedDB.open(dbName.replace(':', '_')/*, 1 /*version*/);

        request.onerror = errorCallback || onError;

        request.onupgradeneeded = function(e) {
            // First open was called or higher db version was used.

            // console.log('onupgradeneeded: oldVersion:' + e.oldVersion,
            //           'newVersion:' + e.newVersion);

            self.db = e.target.result;
            self.db.onerror = onError;

            if (!self.db.objectStoreNames.contains(FILE_STORE_)) {
                self.db.createObjectStore(FILE_STORE_/*,{keyPath: 'id', autoIncrement: true}*/);
            }
        };

        request.onsuccess = function(e) {
            self.db = e.target.result;
            self.db.onerror = onError;
            successCallback(e);
        };

        request.onblocked = errorCallback || onError;
    };

    idb_.close = function() {
        this.db.close();
        this.db = null;
    };

    idb_.get = function(fullPath, successCallback, errorCallback) {
        if (!this.db) {
            if (errorCallback) {
                errorCallback(FileError.INVALID_MODIFICATION_ERR);
            }
            return;
        }

        var tx = this.db.transaction([FILE_STORE_], 'readonly');

        //var request = tx.objectStore(FILE_STORE_).get(fullPath);
        var range = IDBKeyRange.bound(fullPath, fullPath + DIR_OPEN_BOUND,
            false, true);
        var request = tx.objectStore(FILE_STORE_).get(range);

        tx.onabort = errorCallback || onError;
        tx.oncomplete = function(e) {
            successCallback(request.result);
        };
    };

    idb_.getAllEntries = function(fullPath, storagePath, successCallback, errorCallback) {
        if (!this.db) {
            if (errorCallback) {
                errorCallback(FileError.INVALID_MODIFICATION_ERR);
            }
            return;
        }

        var results = [];

        if (storagePath[storagePath.length - 1] === DIR_SEPARATOR) {
            storagePath = storagePath.substring(0, storagePath.length - 1);
        }

        var range = IDBKeyRange.bound(
                storagePath + DIR_SEPARATOR, storagePath + DIR_OPEN_BOUND, false, true);

        var tx = this.db.transaction([FILE_STORE_], 'readonly');
        tx.onabort = errorCallback || onError;
        tx.oncomplete = function(e) {
            results = results.filter(function(val) {
                var valPartsLen = val.fullPath.split(DIR_SEPARATOR).length;
                var fullPathPartsLen = fullPath.split(DIR_SEPARATOR).length;

                if (fullPath === DIR_SEPARATOR && valPartsLen < fullPathPartsLen + 1) {
                    // Hack to filter out entries in the root folder. This is inefficient
                    // because reading the entires of fs.root (e.g. '/') returns ALL
                    // results in the database, then filters out the entries not in '/'.
                    return val;
                } else if (fullPath !== DIR_SEPARATOR &&
                    valPartsLen === fullPathPartsLen + 1) {
                    // If this a subfolder and entry is a direct child, include it in
                    // the results. Otherwise, it's not an entry of this folder.
                    return val;
                }
            });

            successCallback(results);
        };

        var request = tx.objectStore(FILE_STORE_).openCursor(range);

        request.onsuccess = function(e) {
            var cursor = e.target.result;
            if (cursor) {
                var val = cursor.value;

                results.push(val.isFile ? fileEntryFromIdbEntry(val) : new DirectoryEntry(val.name, val.fullPath, val.fileSystem));
                cursor['continue']();
            }
        };
    };

    idb_['delete'] = function(fullPath, successCallback, errorCallback) {
        if (!this.db) {
            if (errorCallback) {
                errorCallback(FileError.INVALID_MODIFICATION_ERR);
            }
            return;
        }

        var tx = this.db.transaction([FILE_STORE_], 'readwrite');
        tx.oncomplete = successCallback;
        tx.onabort = errorCallback || onError;

        //var request = tx.objectStore(FILE_STORE_).delete(fullPath);
        var range = IDBKeyRange.bound(
            fullPath, fullPath + DIR_OPEN_BOUND, false, true);
        tx.objectStore(FILE_STORE_)['delete'](range);
    };

    idb_.put = function(entry, storagePath, successCallback, errorCallback) {
        if (!this.db) {
            if (errorCallback) {
                errorCallback(FileError.INVALID_MODIFICATION_ERR);
            }
            return;
        }

        var tx = this.db.transaction([FILE_STORE_], 'readwrite');
        tx.onabort = errorCallback || onError;
        tx.oncomplete = function(e) {
            // TODO: Error is thrown if we pass the request event back instead.
            successCallback(entry);
        };

        tx.objectStore(FILE_STORE_).put(entry, storagePath);
    };

    // Global error handler. Errors bubble from request, to transaction, to db.
    function onError(e) {
        switch (e.target.errorCode) {
            case 12:
                console.log('Error - Attempt to open db with a lower version than the ' +
                    'current one.');
                break;
            default:
                console.log('errorCode: ' + e.target.errorCode);
        }

        console.log(e, e.code, e.message);
    }

// Clean up.
// TODO: Is there a place for this?
//    global.addEventListener('beforeunload', function(e) {
//        idb_.db && idb_.db.close();
//    }, false);

})(module.exports, window);

require("cordova/exec/proxy").add("File", module.exports);
