// @flow
/* global getSelection, HTMLAnchorElement, HTMLInputElement, HTMLLinkElement */
import "../../common/RichTextEditorInlineReply.js";
import Caret from "./Caret.js";
import _ from "../../../util/i18n";
import { StackObject, UndoStack } from "../../../util/undoStack.js";
import type { CaretPos } from "./Caret.js";

const undoStack = new UndoStack();
const isMac = process.platform === "darwin";

function fireEvent(event: string, data?: string) {
  // Use console.log to notify wrapping container that change has occurred
  if (data) {
    console.log(`${event} ${data}`);
  } else {
    console.log(event);
  }
}

const stripInternalUrl = (url: string): string => {
  if (url.includes("richtexteditor.html#")) {
    return url.split("richtexteditor.html")[1];
  }
  return url;
};

const clearPopups = () => {
  [...document.getElementsByClassName("tooltip-text")].forEach(el => {
    el && el.parentElement && el.parentElement.classList.remove("tooltip");
    el.remove();
  });
};

const selectTextAndExec = (command: string, event: MouseEvent) => {
  const sel = getSelection();
  if (sel && sel.focusNode) {
    sel.setBaseAndExtent(
      sel.focusNode,
      0,
      sel.focusNode,
      sel.focusNode.nodeValue.length
    );
  }
  fireEvent(command);
  event.preventDefault();
  event.stopPropagation();
};

const onKeyDown = (event: KeyboardEvent) => {
  const key = event.key.toLowerCase();
  if (event.altKey && event.shiftKey && key === "s") {
    document.execCommand("strikeThrough", false, null);
  } else if (isMac ? event.metaKey : event.ctrlKey) {
    switch (key) {
      case "z":
        event.preventDefault();
        event.stopPropagation();
        if (event.shiftKey) {
          undoStack.redo();
        } else {
          undoStack.undo();
        }
        break;
      case "k":
        fireEvent("createLink");
        event.preventDefault();
        event.stopPropagation();
        break;
      case "s":
        // Prevent browser save dialog
        event.preventDefault();
        event.stopPropagation();
        break;
      default:
        break;
    }
  }
};

const preventDefault = (event: MouseEvent) => {
  event.preventDefault();
};

const renderLinkToolbar = (event: MouseEvent) => {
  clearPopups();
  // $FlowFixMe select all anchor text first to remove existing link
  const clickedLink = event.target.closest("a");
  if (!clickedLink) {
    return;
  }

  const tooltip = document.createElement("SPAN");
  tooltip.classList.add("tooltip-text");
  tooltip.setAttribute("contenteditable", "false");

  const tooltipLink = document.createElement("A");
  const href = stripInternalUrl(clickedLink.href);
  tooltipLink.setAttribute("href", href);
  if (!href.startsWith("#")) {
    tooltipLink.setAttribute("target", "_blank");
  }
  tooltipLink.appendChild(document.createTextNode(href));

  const tooltipCopyButton = document.createElement("INPUT");
  tooltipCopyButton.setAttribute("type", "button");
  tooltipCopyButton.setAttribute("value", _("link", "Copy"));
  tooltipCopyButton.setAttribute("title", _("Copy Link Address"));

  const tooltipEditButton = document.createElement("INPUT");
  tooltipEditButton.setAttribute("type", "button");
  tooltipEditButton.setAttribute("value", _("link", "Edit"));
  tooltipCopyButton.setAttribute("title", _("Edit Link Address"));

  const tooltipUnlinkButton = document.createElement("INPUT");
  tooltipUnlinkButton.setAttribute("type", "button");
  tooltipUnlinkButton.setAttribute("value", _("link", "Remove"));
  tooltipUnlinkButton.setAttribute("title", _("Remove Link"));

  tooltip.appendChild(tooltipLink);
  tooltip.appendChild(tooltipCopyButton);
  tooltip.appendChild(tooltipEditButton);
  tooltip.appendChild(tooltipUnlinkButton);
  clickedLink.classList.add("tooltip");
  clickedLink.appendChild(tooltip);

  tooltipCopyButton.addEventListener("mousedown", preventDefault);
  tooltipCopyButton.addEventListener("click", (event: MouseEvent) => {
    event.preventDefault();
    event.stopPropagation();
    clearPopups();
    const anchor = getAnchor();
    navigator.clipboard.writeText(anchor);
    fireEvent("copyLink", anchor);
  });
  tooltipEditButton.addEventListener("mousedown", preventDefault);
  tooltipEditButton.addEventListener("click", (event: MouseEvent) => {
    event.preventDefault();
    event.stopPropagation();
    clearPopups();
    selectTextAndExec("editLink", event);
  });
  tooltipUnlinkButton.addEventListener("mousedown", preventDefault);
  tooltipUnlinkButton.addEventListener("click", (event: MouseEvent) => {
    event.preventDefault();
    event.stopPropagation();
    clearPopups();
    selectTextAndExec("unlink", event);
  });
};

const getAnchor = (): string => {
  const sel = getSelection();
  if (sel?.focusNode?.parentElement) {
    const anchorElement: ?Element = sel.focusNode.parentElement.closest("a");
    if (anchorElement instanceof HTMLAnchorElement && anchorElement.href) {
      return stripInternalUrl(anchorElement.href);
    }
  }
  return "";
};

// Selection should fit in the Create Link popup. Very large selection is
// probably not a URL.
const MAX_SELECTION_LENGTH = 300;

const onSelectionChange = (event: Event) => {
  let paramType: "anchor" | "select";
  let param: string = getAnchor();
  if (param) {
    paramType = "anchor";
  } else {
    paramType = "select";
    const sel = getSelection();
    param = sel?.toString().substr(0, MAX_SELECTION_LENGTH) ?? "";
  }
  fireEvent("selectionchange", `${paramType} ${param}`);
};

const setStyle = (style: string, isNote: boolean) => {
  const styleTag = document.getElementById("richtextstyle");
  const linkTag = document.getElementById("stylesheet");
  if (!styleTag) {
    return;
  }
  styleTag.innerHTML = style;
  if (isNote && linkTag instanceof HTMLLinkElement) {
    linkTag.rel = "stylesheet";
    linkTag.href = "./md.css";
  }
};

/*
  VB-95001 - Before this change, we were letting the javascript Browser Object
  Model (BOM) create the html by assigning the content into innerText which
  produced a long list of <#TEXT> elements and <BR> elements with same parent.
  Then, when the user added a new line, the rendering would recalcuate styles
  of all those elements.
  For the mail composer and a message of 10,000 lines that would take more than
  20s to finish (see Jira for a sample message to import when testing).
  Enclosing each line in a <DIV>, the rendering would only have to recalculate
  style of that one element and this would be finished in an instant.
  Using content-visibility did not benefit this case despite some resources
  pointing to that as a solution.
*/
const textToHtml = (text: string): string => {
  return `<div>${text.replace(/\n/g, "<br></div><div>")}</div>`;
};

const setContent = (
  items: Array<{ content: string, isPlainText: boolean }>
): void => {
  if (!document.body) {
    return;
  }
  let innerContent = "";
  for (const { content, isPlainText } of items) {
    if (isPlainText) {
      innerContent += textToHtml(content);
    } else {
      innerContent += content;
    }
  }
  document.body.innerHTML = innerContent;
  currentContent = innerContent;
};

const selectSurrounding = () => {
  const sel = getSelection();
  sel &&
    sel.focusNode &&
    sel.setBaseAndExtent(
      sel.focusNode,
      0,
      sel.focusNode,
      sel.focusNode.nodeValue.length
    );
};

function execCommand(command: string, value: string) {
  switch (command) {
    case "undo":
      undoStack.undo();
      break;
    case "redo":
      undoStack.redo();
      break;
    default:
      document.execCommand(command, false, value);
  }
}

let currentContent: string = "";
let currentCaretPos: CaretPos = { selector: Caret.defaultPath, offset: 0 };

type ChangeObj = {
  content: string,
  caretPos: CaretPos,
};

function startObserving() {
  let skipObserve = false;

  const updateFunc = (changeObj: ChangeObj) => {
    if (!document.body) {
      return;
    }
    // Don't record changes when redoing/undoing
    skipObserve = true;

    // Change content to recorded state and set caret position
    document.body.innerHTML = changeObj.content;
    Caret.setPosition(changeObj.caretPos);
  };

  const observer = new MutationObserver(mutations => {
    if (!document.body) {
      return;
    }

    if (skipObserve) {
      skipObserve = false;
      return;
    }
    const newContent = document.body.innerHTML;
    if (newContent !== currentContent) {
      const newCaretPos = Caret.getPosition();
      undoStack.record(
        new StackObject<ChangeObj>(
          { content: currentContent, caretPos: currentCaretPos },
          { content: newContent, caretPos: newCaretPos },
          updateFunc
        )
      );
      currentContent = newContent;
      currentCaretPos = newCaretPos;
    }
  });

  if (document.body) {
    observer.observe(document.body, {
      attributes: true,
      childList: true,
      characterData: true,
      characterDataOldValue: false,
      subtree: true,
    });
  }
}

const onInput = (event: MessageEvent) => {
  // Set checkbox attribute in HTML to enable interactive md-todo support
  const input = event.target;
  if (input instanceof HTMLInputElement && input.type === "checkbox") {
    if (input.checked) {
      input.setAttribute("checked", "true");
    } else {
      input.removeAttribute("checked");
    }
  }
  fireEvent("input");
};

const onMessage = (event: MessageEvent) => {
  const message = {
    type: "",
    isDark: false,
    isNote: false,
    css: "",
    command: "",
    value: "",
    content: [],
    style: {},
    ...event.data,
  };

  switch (message.type) {
    case "EXEC_COMMAND":
      execCommand(message.command, message.value);
      break;
    case "SELECT_SURROUNDING":
      selectSurrounding();
      break;
    case "SET_STYLE":
      setStyle(message.style, message.isNote);
      break;
    case "SET_CONTENT":
      setContent(message.content);
      break;
    case "START_OBSERVING":
      startObserving();
      break;
  }
};

function init() {
  document.addEventListener(
    "DOMContentLoaded",
    () => document.body && document.body.focus()
  );
  document.addEventListener("input", onInput);
  document.addEventListener("selectionchange", onSelectionChange);
  document.addEventListener("keydown", onKeyDown);
  document.addEventListener("click", renderLinkToolbar);
  document.addEventListener("undoredo", () => fireEvent("input"));
  window.addEventListener("blur", clearPopups);
  window.addEventListener("message", onMessage);
  // for attachment dropping
  window.addEventListener("dragover", (e: DragEvent) => {
    fireEvent("dragover", e.dataTransfer?.types.join(","));
  });
}

init();
