const GET_SW_CACHE = "GET_SW_CACHE";
const DEL_SW_CACHE = "DEL_SW_CACHE";

// ----------------------------------------------------------------------- //
// --------------------------- Event Listeners --------------------------- //
// ----------------------------------------------------------------------- //

/**
 * Initial load handler. Gets called on DOMContentLoaded.
 **/
async function handleLoad() {
  handleOnlineChange();

  // If SW Redirected
  if (document.location.pathname !== "/local")
    window.history.replaceState(null, null, "/local");

  // Create Upload Listener
  let uploadButton = document.querySelector("button");
  uploadButton.addEventListener("click", handleFileAdd);

  // Ensure Installed -> Get Cached Items
  let swCache = await SW.install()
    // Get Service Worker Cache Books
    .then(async () => {
      let swResponse = await SW.send({ type: GET_SW_CACHE });
      return Promise.all(
        // Normalize Cached Results
        swResponse.map(async (item) => {
          let localCache = await IDB.get("PROGRESS-" + item.id);
          if (localCache) {
            item.progress = localCache.progress;
            item.percentage = Math.round(localCache.percentage * 10000) / 100;
          }

          // Additional Values
          item.fileURL = "/documents/" + item.id + "/file";
          item.coverURL = "/documents/" + item.id + "/cover";
          item.type = "REMOTE";

          return item;
        })
      );
    })
    // Fail Nicely -> Allows Local Feature
    .catch((e) => {
      console.log("[loadContent] Service Worker Cache Error:", e);
      return [];
    });

  // Get & Normalize Local Books
  let localResponse = await IDB.find(/^FILE-.{32}$/, false);
  let localCache = await Promise.all(localResponse.map(getLocalProgress));

  // Populate DOM with Cache & Local Books
  populateDOMBooks([...swCache, ...localCache]);
}

/**
 * Update DOM to indicate online status. If no argument is passed, we attempt
 * to determine online status via `navigator.onLine`.
 **/
function handleOnlineChange(isOnline) {
  let onlineEl = document.querySelector("#online");
  isOnline = isOnline == undefined ? navigator.onLine : isOnline;
  onlineEl.hidden = !isOnline;
}

/**
 * Allow deleting local or remote cached files. Deleting remotely cached files
 * does not remove progress. Progress will still be flushed once online.
 **/
async function handleFileDelete(event, item) {
  let mainEl =
    event.target.parentElement.parentElement.parentElement.parentElement
      .parentElement;

  if (item.type == "LOCAL") {
    await IDB.del("FILE-" + item.id);
    await IDB.del("FILE-METADATA-" + item.id);
  } else if (item.type == "REMOTE") {
    let swResp = await SW.send({ type: DEL_SW_CACHE, id: item.id });
    if (swResp != "SUCCESS")
      throw new Error("[handleFileDelete] Service Worker Error");
  }

  console.log("[handleFileDelete] Item Deleted");

  mainEl.remove();
  updateMessage();
}

/**
 * Allow adding file to offline reader. Add to IndexedDB,
 * and later upload? Add style indicating external file?
 **/
async function handleFileAdd() {
  const fileInput = document.getElementById("document_file");
  const file = fileInput.files[0];

  if (!file) return console.log("[handleFileAdd] No File");

  function readFile(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();

      reader.onload = (event) => resolve(event.target.result);
      reader.onerror = (error) => reject(error);

      reader.readAsArrayBuffer(file);
    });
  }

  function randomID() {
    return "00000000000000000000000000000000".replace(/[018]/g, (c) =>
      (
        c ^
        (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
      ).toString(16)
    );
  }

  let newID = randomID();

  readFile(file)
    // Store Blob in IDB
    .then((fileData) => {
      if (!isEpubFile(fileData)) throw new Error("Invalid File Type");

      return IDB.set(
        "FILE-" + newID,
        new Blob([fileData], { type: "application/octet-binary" })
      );
    })
    // Process File
    .then(() => getLocalProgress("FILE-" + newID))
    // Populate in DOM
    .then((item) => populateDOMBooks([item]))
    // Hide Add File Button
    .then(() => {
      let addButtonEl = document.querySelector("#add-file-button");
      addButtonEl.checked = false;
    })
    // Logging
    .then(() => console.log("[handleFileAdd] File Add Successfully"))
    .catch((e) => console.log("[handleFileAdd] File Add Failed:", e));
}

// Add Event Listeners
window.addEventListener("DOMContentLoaded", handleLoad);
window.addEventListener("online", () => handleOnlineChange(true));
window.addEventListener("offline", () => handleOnlineChange(false));

// ----------------------------------------------------------------------- //
// ------------------------------- Helpers ------------------------------- //
// ----------------------------------------------------------------------- //

/**
 * Update the message element. Called after initial load, on item add or on
 * item delete.
 **/
function updateMessage() {
  // Update Loader / No Results Indicator
  let itemsEl = document.querySelector("#items");
  let messageEl = document.querySelector("#message");

  if (itemsEl.children.length == 0) {
    messageEl.innerText = "No Results";
    messageEl.hidden = false;
  } else messageEl.hidden = true;
}

/**
 * Populate DOM with cached documents.
 **/
function populateDOMBooks(data) {
  let allDocuments = document.querySelector("#items");

  // Create Document Items
  data.forEach((item) => {
    // Create Main Element
    let baseEl = document.querySelector("#item-template").cloneNode(true);
    baseEl.removeAttribute("id");

    // Get Elements
    let [titleEl, authorEl, percentageEl] = baseEl.querySelectorAll("p + p");
    let [svgDivEl, textEl] = baseEl.querySelector("strong").children;
    let coverEl = baseEl.querySelector("a img");
    let downloadEl = baseEl.querySelector("svg").parentElement;
    let deleteInputEl = baseEl.querySelector("#delete-button");
    let deleteLabelEl = deleteInputEl.previousElementSibling;
    let deleteTextEl = baseEl.querySelector("input + div span");

    // Set Download Attributes
    downloadEl.setAttribute("href", item.fileURL);
    downloadEl.setAttribute(
      "download",
      item.title + " - " + item.author + ".epub"
    );

    // Set Cover Attributes
    coverEl.setAttribute("src", item.coverURL);
    coverEl.parentElement.setAttribute(
      "href",
      "/reader#id=" + item.id + "&type=" + item.type
    );

    // Set Additional Metadata Attributes
    titleEl.textContent = item.title;
    authorEl.textContent = item.author;
    percentageEl.textContent = item.percentage + "%";

    // Set Remote / Local Indicator
    let newSvgEl =
      item.type == "LOCAL"
        ? document.querySelector("#local-svg-template").cloneNode(true)
        : document.querySelector("#remote-svg-template").cloneNode(true);
    svgDivEl.append(newSvgEl);
    textEl.textContent = item.type;

    // Delete Item
    deleteInputEl.setAttribute("id", "delete-button-" + item.id);
    deleteLabelEl.setAttribute("for", "delete-button-" + item.id);
    deleteTextEl.addEventListener("click", (e) => handleFileDelete(e, item));
    deleteTextEl.textContent =
      item.type == "LOCAL" ? "Delete Local" : "Delete Cache";

    allDocuments.append(baseEl);
  });

  updateMessage();
}

/**
 * Given an item id, generate expected item format from IDB data store.
 **/
async function getLocalProgress(id) {
  // Get Metadata (Cover Always Needed)
  let fileBlob = await IDB.get(id);
  let fileURL = URL.createObjectURL(fileBlob);
  let metadata = await getMetadata(fileURL);

  // Attempt Cache
  let documentID = id.replace("FILE-", "");
  let documentData = await IDB.get("FILE-METADATA-" + documentID);
  if (documentData)
    return { ...documentData, fileURL, coverURL: metadata.coverURL };

  // Create Starting Progress
  let newProgress = {
    id: documentID,
    title: metadata.title,
    author: metadata.author,
    type: "LOCAL",
    percentage: 0,
    progress: "",
    words: 0,
  };

  // Update Cache
  await IDB.set("FILE-METADATA-" + documentID, newProgress);

  // Return Cache + coverURL
  return { ...newProgress, fileURL, coverURL: metadata.coverURL };
}

/**
 * Retrieve the Title, Author, and CoverURL (blob) for a given file.
 **/
async function getMetadata(fileURL) {
  let book = ePub(fileURL, { openAs: "epub" });
  console.log({ book });
  let coverURL = (await book.coverUrl()) || "/assets/images/no-cover.jpg";
  let metadata = await book.loaded.metadata;

  let title =
    metadata.title && metadata.title != "" ? metadata.title : "Unknown";
  let author =
    metadata.creator && metadata.creator != "" ? metadata.creator : "Unknown";

  book.destroy();

  return { title, author, coverURL };
}

/**
 * Validate filetype. We check the headers and validate that they are ZIP.
 * After which we validate contents. This isn't 100% effective, but unless
 * someone is trying to trick it, it should be fine.
 **/
function isEpubFile(arrayBuffer) {
  const view = new DataView(arrayBuffer);

  // Too Small
  if (view.byteLength < 4) {
    return false;
  }

  // Check for the ZIP file signature (PK)
  const littleEndianSignature = view.getUint16(0, true);
  const bigEndianSignature = view.getUint16(0, false);

  if (littleEndianSignature !== 0x504b && bigEndianSignature !== 0x504b) {
    return false;
  }

  // Additional Checks (No FP on ZIP)
  const textDecoder = new TextDecoder();
  const zipContent = textDecoder.decode(new Uint8Array(arrayBuffer));

  if (
    zipContent.includes("mimetype") &&
    zipContent.includes("META-INF/container.xml")
  ) {
    return true;
  }

  return false;
}
