mergeInto(LibraryManager.library, {
  $IDBFS__deps: ['$FS', '$MEMFS', '$PATH'],
  $IDBFS: {
    dbs: {},
    indexedDB: function() {
      return window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
    },
    DB_VERSION: 20,
    DB_STORE_NAME: 'FILE_DATA',
    // reuse all of the core MEMFS functionality
    mount: function(mount) {
      return MEMFS.mount.apply(null, arguments);
    },
    // the only custom function IDBFS implements is to handle
    // synchronizing the wrapped MEMFS with a backing IDB instance
    syncfs: function(mount, populate, callback) {
      IDBFS.getLocalSet(mount, function(err, local) {
        if (err) return callback(err);

        IDBFS.getRemoteSet(mount, function(err, remote) {
          if (err) return callback(err);

          var src = populate ? remote : local;
          var dst = populate ? local : remote;

          IDBFS.reconcile(src, dst, callback);
        });
      });
    },
    reconcile: function(src, dst, callback) {
      var total = 0;

      var create = {};
      for (var key in src.files) {
        if (!src.files.hasOwnProperty(key)) continue;
        var e = src.files[key];
        var e2 = dst.files[key];
        if (!e2 || e.timestamp > e2.timestamp) {
          create[key] = e;
          total++;
        }
      }

      var remove = {};
      for (var key in dst.files) {
        if (!dst.files.hasOwnProperty(key)) continue;
        var e = dst.files[key];
        var e2 = src.files[key];
        if (!e2) {
          remove[key] = e;
          total++;
        }
      }

      if (!total) {
        // early out
        return callback(null);
      }

      var completed = 0;
      function done(err) {
        if (err) return callback(err);
        if (++completed >= total) {
          return callback(null);
        }
      };

      // create a single transaction to handle and IDB reads / writes we'll need to do
      var db = src.type === 'remote' ? src.db : dst.db;
      var transaction = db.transaction([IDBFS.DB_STORE_NAME], 'readwrite');
      transaction.onerror = function transaction_onerror() { callback(this.error); };
      var store = transaction.objectStore(IDBFS.DB_STORE_NAME);

      for (var path in create) {
        if (!create.hasOwnProperty(path)) continue;
        var entry = create[path];

        if (dst.type === 'local') {
          // save file to local
          try {
            if (FS.isDir(entry.mode)) {
              FS.mkdir(path, entry.mode);
            } else if (FS.isFile(entry.mode)) {
              var stream = FS.open(path, 'w+', 0666);
              FS.write(stream, entry.contents, 0, entry.contents.length, 0, true /* canOwn */);
              FS.close(stream);
            }
            done(null);
          } catch (e) {
            return done(e);
          }
        } else {
          // save file to IDB
          var req = store.put(entry, path);
          req.onsuccess = function req_onsuccess() { done(null); };
          req.onerror = function req_onerror() { done(this.error); };
        }
      }

      for (var path in remove) {
        if (!remove.hasOwnProperty(path)) continue;
        var entry = remove[path];

        if (dst.type === 'local') {
          // delete file from local
          try {
            if (FS.isDir(entry.mode)) {
              // TODO recursive delete?
              FS.rmdir(path);
            } else if (FS.isFile(entry.mode)) {
              FS.unlink(path);
            }
            done(null);
          } catch (e) {
            return done(e);
          }
        } else {
          // delete file from IDB
          var req = store.delete(path);
          req.onsuccess = function req_onsuccess() { done(null); };
          req.onerror = function req_onerror() { done(this.error); };
        }
      }
    },
    getLocalSet: function(mount, callback) {
      var files = {};

      function isRealDir(p) {
        return p !== '.' && p !== '..';
      };
      function toAbsolute(root) {
        return function(p) {
          return PATH.join2(root, p);
        }
      };

      var check = FS.readdir(mount.mountpoint)
        .filter(isRealDir)
        .map(toAbsolute(mount.mountpoint));

      while (check.length) {
        var path = check.pop();
        var stat, node;

        try {
          var lookup = FS.lookupPath(path);
          node = lookup.node;
          stat = FS.stat(path);
        } catch (e) {
          return callback(e);
        }

        if (FS.isDir(stat.mode)) {
          check.push.apply(check, FS.readdir(path)
            .filter(isRealDir)
            .map(toAbsolute(path)));

          files[path] = { mode: stat.mode, timestamp: stat.mtime };
        } else if (FS.isFile(stat.mode)) {
          files[path] = { contents: node.contents, mode: stat.mode, timestamp: stat.mtime };
        } else {
          return callback(new Error('node type not supported'));
        }
      }

      return callback(null, { type: 'local', files: files });
    },
    getDB: function(name, callback) {
      // look it up in the cache
      var db = IDBFS.dbs[name];
      if (db) {
        return callback(null, db);
      }
      var req;
      try {
        req = IDBFS.indexedDB().open(name, IDBFS.DB_VERSION);
      } catch (e) {
        return onerror(e);
      }
      req.onupgradeneeded = function req_onupgradeneeded() {
        db = req.result;
        db.createObjectStore(IDBFS.DB_STORE_NAME);
      };
      req.onsuccess = function req_onsuccess() {
        db = req.result;
        // add to the cache
        IDBFS.dbs[name] = db;
        callback(null, db);
      };
      req.onerror = function req_onerror() {
        callback(this.error);
      };
    },
    getRemoteSet: function(mount, callback) {
      var files = {};

      IDBFS.getDB(mount.mountpoint, function(err, db) {
        if (err) return callback(err);

        var transaction = db.transaction([IDBFS.DB_STORE_NAME], 'readonly');
        transaction.onerror = function transaction_onerror() { callback(this.error); };

        var store = transaction.objectStore(IDBFS.DB_STORE_NAME);
        store.openCursor().onsuccess = function store_openCursor_onsuccess(event) {
          var cursor = event.target.result;
          if (!cursor) {
            return callback(null, { type: 'remote', db: db, files: files });
          }

          files[cursor.key] = cursor.value;
          cursor.continue();
        };
      });
    }
  }
});
