(function () {
  const DB_NAME = "md-hub-cache";
  const DB_VERSION = 3;
  const STORE_DOCUMENTS = "documents";
  const STORE_CONTENT = "content";
  const STORE_PAGES = "pages";
  const STORE_PENDING_EDITS = "pending_edits";
  const CACHE_ENTRY_LIMIT = 100;

  function openDB() {
    return new Promise(function (resolve, reject) {
      const request = indexedDB.open(DB_NAME, DB_VERSION);
      request.onerror = function () {
        reject(request.error);
      };
      request.onsuccess = function () {
        resolve(request.result);
      };
      request.onupgradeneeded = function (event) {
        const db = event.target.result;
        if (!db.objectStoreNames.contains(STORE_DOCUMENTS)) {
          db.createObjectStore(STORE_DOCUMENTS, { keyPath: "path" });
        }
        if (!db.objectStoreNames.contains(STORE_CONTENT)) {
          db.createObjectStore(STORE_CONTENT, { keyPath: "path" });
        }
        if (!db.objectStoreNames.contains(STORE_PAGES)) {
          db.createObjectStore(STORE_PAGES, { keyPath: "path" });
        }
        if (!db.objectStoreNames.contains(STORE_PENDING_EDITS)) {
          db.createObjectStore(STORE_PENDING_EDITS, { keyPath: "path" });
        }
      };
    });
  }

  function normalizePath(path) {
    if (!path) return "/";
    if (path === "/") return path;
    return path.replace(/\/$/, "") || "/";
  }

  function pruneStore(store, limit) {
    const request = store.getAll();
    request.onsuccess = function () {
      const records = request.result || [];
      if (records.length <= limit) {
        return;
      }

      records
        .sort(function (a, b) {
          return (a.cachedAt || 0) - (b.cachedAt || 0);
        })
        .slice(0, records.length - limit)
        .forEach(function (record) {
          if (record && record.path) {
            store.delete(record.path);
          }
        });
    };
  }

  function cacheDocuments(documents) {
    return openDB().then(function (db) {
      return new Promise(function (resolve, reject) {
        const tx = db.transaction(STORE_DOCUMENTS, "readwrite");
        const store = tx.objectStore(STORE_DOCUMENTS);
        documents.forEach(function (doc) {
          store.put(doc);
        });
        tx.oncomplete = function () {
          resolve();
        };
        tx.onerror = function () {
          reject(tx.error);
        };
      });
    });
  }

  function cacheContent(path, html, title) {
    return openDB().then(function (db) {
      return new Promise(function (resolve, reject) {
        const tx = db.transaction(STORE_CONTENT, "readwrite");
        const store = tx.objectStore(STORE_CONTENT);
        store.put({ path: path, html: html, title: title, cachedAt: Date.now() });
        pruneStore(store, CACHE_ENTRY_LIMIT);
        tx.oncomplete = function () {
          resolve();
        };
        tx.onerror = function () {
          reject(tx.error);
        };
      });
    });
  }

  function getCachedDocuments() {
    return openDB().then(function (db) {
      return new Promise(function (resolve, reject) {
        const tx = db.transaction(STORE_DOCUMENTS, "readonly");
        const store = tx.objectStore(STORE_DOCUMENTS);
        const request = store.getAll();
        request.onsuccess = function () {
          resolve(request.result);
        };
        request.onerror = function () {
          reject(request.error);
        };
      });
    });
  }

  function getCachedContent(path) {
    return openDB().then(function (db) {
      return new Promise(function (resolve, reject) {
        const tx = db.transaction(STORE_CONTENT, "readonly");
        const store = tx.objectStore(STORE_CONTENT);
        const request = store.get(path);
        request.onsuccess = function () {
          resolve(request.result);
        };
        request.onerror = function () {
          reject(request.error);
        };
      });
    });
  }

  function cachePage(path, html, title) {
    const normalizedPath = normalizePath(path);
    return openDB().then(function (db) {
      return new Promise(function (resolve, reject) {
        const tx = db.transaction(STORE_PAGES, "readwrite");
        const store = tx.objectStore(STORE_PAGES);
        store.put({
          path: normalizedPath,
          html: html,
          title: title,
          cachedAt: Date.now(),
        });
        pruneStore(store, CACHE_ENTRY_LIMIT);
        tx.oncomplete = function () {
          resolve();
        };
        tx.onerror = function () {
          reject(tx.error);
        };
      });
    });
  }

  function getCachedPage(path) {
    const normalizedPath = normalizePath(path);
    return openDB().then(function (db) {
      return new Promise(function (resolve, reject) {
        const tx = db.transaction(STORE_PAGES, "readonly");
        const store = tx.objectStore(STORE_PAGES);
        const request = store.get(normalizedPath);
        request.onsuccess = function () {
          resolve(request.result);
        };
        request.onerror = function () {
          reject(request.error);
        };
      });
    });
  }

  function putPendingEdit(path, content, baseHash) {
    const normalizedPath = normalizePath(path);
    return openDB().then(function (db) {
      return new Promise(function (resolve, reject) {
        const tx = db.transaction(STORE_PENDING_EDITS, "readwrite");
        const store = tx.objectStore(STORE_PENDING_EDITS);
        store.put({
          path: normalizedPath,
          content: content,
          baseHash: baseHash || "",
          conflict: null,
          retries: 0,
          updatedAt: Date.now(),
        });
        tx.oncomplete = function () {
          resolve();
        };
        tx.onerror = function () {
          reject(tx.error);
        };
      });
    });
  }

  function markPendingEditConflict(path, conflict) {
    const normalizedPath = normalizePath(path);
    return openDB().then(function (db) {
      return new Promise(function (resolve, reject) {
        const tx = db.transaction(STORE_PENDING_EDITS, "readwrite");
        const store = tx.objectStore(STORE_PENDING_EDITS);
        const request = store.get(normalizedPath);
        request.onsuccess = function () {
          const record = request.result;
          if (!record) {
            resolve();
            return;
          }
          record.conflict = conflict;
          record.updatedAt = Date.now();
          store.put(record);
        };
        tx.oncomplete = function () {
          resolve();
        };
        tx.onerror = function () {
          reject(tx.error);
        };
      });
    });
  }

  function getPendingEdits() {
    return openDB().then(function (db) {
      return new Promise(function (resolve, reject) {
        const tx = db.transaction(STORE_PENDING_EDITS, "readonly");
        const store = tx.objectStore(STORE_PENDING_EDITS);
        const request = store.getAll();
        request.onsuccess = function () {
          resolve(request.result || []);
        };
        request.onerror = function () {
          reject(request.error);
        };
      });
    });
  }

  function getPendingEdit(path) {
    const normalizedPath = normalizePath(path);
    return openDB().then(function (db) {
      return new Promise(function (resolve, reject) {
        const tx = db.transaction(STORE_PENDING_EDITS, "readonly");
        const store = tx.objectStore(STORE_PENDING_EDITS);
        const request = store.get(normalizedPath);
        request.onsuccess = function () {
          resolve(request.result);
        };
        request.onerror = function () {
          reject(request.error);
        };
      });
    });
  }

  function deletePendingEdit(path) {
    const normalizedPath = normalizePath(path);
    return openDB().then(function (db) {
      return new Promise(function (resolve, reject) {
        const tx = db.transaction(STORE_PENDING_EDITS, "readwrite");
        const store = tx.objectStore(STORE_PENDING_EDITS);
        store.delete(normalizedPath);
        tx.oncomplete = function () {
          resolve();
        };
        tx.onerror = function () {
          reject(tx.error);
        };
      });
    });
  }

  window.MDHubCache = {
    cacheDocuments: cacheDocuments,
    cacheContent: cacheContent,
    cachePage: cachePage,
    putPendingEdit: putPendingEdit,
    markPendingEditConflict: markPendingEditConflict,
    getPendingEdits: getPendingEdits,
    getPendingEdit: getPendingEdit,
    deletePendingEdit: deletePendingEdit,
    getCachedDocuments: getCachedDocuments,
    getCachedContent: getCachedContent,
    getCachedPage: getCachedPage,
    normalizePath: normalizePath,
  };
})();
