import { getAttributeOrThrow } from "../lib/attribute";
import LiveEditor from "./live_editor";
import Markdown from "./markdown";
import { globalPubSub } from "../lib/pub_sub";
import { md5Base64, smoothlyScrollToElement } from "../lib/utils";
import scrollIntoView from "scroll-into-view-if-needed";

/**
 * A hook managing a single cell.
 *
 * Mounts and manages the collaborative editor,
 * takes care of markdown rendering and focusing the editor when applicable.
 *
 * Configuration:
 *
 *   * `data-cell-id` - id of the cell being edited
 *   * `data-type` - type of the cell
 */
const Cell = {
  mounted() {
    this.props = getProps(this);
    this.state = {
      isFocused: false,
      insertMode: false,
      // For text cells (markdown or elixir)
      liveEditor: null,
      evaluationDigest: null,
    };

    if (["markdown", "elixir"].includes(this.props.type)) {
      this.pushEvent("cell_init", { cell_id: this.props.cellId }, (payload) => {
        const { source, revision, evaluation_digest } = payload;

        const editorContainer = this.el.querySelector(
          `[data-element="editor-container"]`
        );
        // Remove the content placeholder.
        editorContainer.firstElementChild.remove();
        // Create an empty container for the editor to be mounted in.
        const editorElement = document.createElement("div");
        editorContainer.appendChild(editorElement);
        // Setup the editor instance.
        this.state.liveEditor = new LiveEditor(
          this,
          editorElement,
          this.props.cellId,
          this.props.type,
          source,
          revision
        );

        // Setup action handlers
        if (this.props.type === "elixir") {
          const amplifyButton = this.el.querySelector(
            `[data-element="amplify-outputs-button"]`
          );
          amplifyButton.addEventListener("click", (event) => {
            this.el.toggleAttribute("data-js-amplified");
          });
        }

        // Setup change indicator
        if (this.props.type === "elixir") {
          this.state.evaluationDigest = evaluation_digest;

          const updateChangeIndicator = () => {
            const indicator = this.el.querySelector(
              `[data-element="change-indicator"]`
            );

            if (indicator) {
              const source = this.state.liveEditor.getSource();
              const digest = md5Base64(source);
              const changed = this.state.evaluationDigest !== digest;
              indicator.toggleAttribute("data-js-shown", changed);
            }
          };

          updateChangeIndicator();

          this.handleEvent(
            `evaluation_started:${this.props.cellId}`,
            ({ evaluation_digest }) => {
              this.state.evaluationDigest = evaluation_digest;
              updateChangeIndicator();
            }
          );

          this.state.liveEditor.onChange((newSource) => {
            updateChangeIndicator();
          });
        }

        // Setup markdown rendering
        if (this.props.type === "markdown") {
          const markdownContainer = this.el.querySelector(
            `[data-element="markdown-container"]`
          );
          const baseUrl = this.props.sessionPath;
          const markdown = new Markdown(markdownContainer, source, {
            baseUrl,
            emptyText: "Empty markdown cell",
          });

          this.state.liveEditor.onChange((newSource) => {
            markdown.setContent(newSource);
          });
        }

        // Once the editor is created, reflect the current state.
        if (this.state.isFocused && this.state.insertMode) {
          this.state.liveEditor.focus();
          // If the element is being scrolled to, focus interrupts it,
          // so ensure the scrolling continues.
          smoothlyScrollToElement(this.el);

          broadcastSelection(this);
        }

        this.state.liveEditor.onBlur(() => {
          // Prevent from blurring unless the state changes.
          // For example when we move cell using buttons
          // the editor should keep focus.
          if (this.state.isFocused && this.state.insertMode) {
            this.state.liveEditor.focus();
          }
        });

        this.state.liveEditor.onCursorSelectionChange((selection) => {
          broadcastSelection(this, selection);
        });
      });
    }

    if (this.props.type === "input") {
      const input = getInput(this);

      input.addEventListener("blur", (event) => {
        if (this.state.isFocused && this.state.insertMode) {
          // We are still in the insert mode, so focus the input
          // back once other handlers complete
          setTimeout(() => {
            input.focus();
          }, 0);
        }
      });
    }

    this._unsubscribeFromCellsEvents = globalPubSub.subscribe(
      "cells",
      (event) => {
        handleCellsEvent(this, event);
      }
    );
  },

  destroyed() {
    this._unsubscribeFromCellsEvents();

    if (this.state.liveEditor) {
      this.state.liveEditor.dispose();
    }
  },

  updated() {
    this.props = getProps(this);
  },
};

function getProps(hook) {
  return {
    cellId: getAttributeOrThrow(hook.el, "data-cell-id"),
    type: getAttributeOrThrow(hook.el, "data-type"),
    sessionPath: getAttributeOrThrow(hook.el, "data-session-path"),
  };
}

function getInput(hook) {
  if (hook.props.type === "input") {
    return hook.el.querySelector(`[data-element="input"]`);
  } else {
    return null;
  }
}

/**
 * Handles client-side cells event.
 */
function handleCellsEvent(hook, event) {
  if (event.type === "cell_focused") {
    handleCellFocused(hook, event.cellId, event.scroll);
  } else if (event.type === "insert_mode_changed") {
    handleInsertModeChanged(hook, event.enabled);
  } else if (event.type === "cell_moved") {
    handleCellMoved(hook, event.cellId);
  } else if (event.type === "cell_upload") {
    handleCellUpload(hook, event.cellId, event.url);
  } else if (event.type === "location_report") {
    handleLocationReport(hook, event.client, event.report);
  }
}

function handleCellFocused(hook, cellId, scroll) {
  if (hook.props.cellId === cellId) {
    hook.state.isFocused = true;
    hook.el.setAttribute("data-js-focused", "true");
    if (scroll) {
      smoothlyScrollToElement(hook.el);
    }
  } else if (hook.state.isFocused) {
    hook.state.isFocused = false;
    hook.el.removeAttribute("data-js-focused");
  }
}

function handleInsertModeChanged(hook, insertMode) {
  if (hook.state.isFocused) {
    hook.state.insertMode = insertMode;

    if (hook.state.liveEditor) {
      if (hook.state.insertMode) {
        hook.state.liveEditor.focus();
        // The insert mode may be enabled as a result of clicking the editor,
        // in which case we want to wait until editor handles the click
        // and sets new cursor position.
        // To achieve this, we simply put this task at the end of event loop,
        // ensuring all click handlers are executed first.
        setTimeout(() => {
          scrollIntoView(document.activeElement, {
            scrollMode: "if-needed",
            behavior: "smooth",
            block: "center",
          });
        }, 0);

        broadcastSelection(hook);
      } else {
        hook.state.liveEditor.blur();
      }
    }

    const input = getInput(hook);

    if (input) {
      if (hook.state.insertMode) {
        input.focus();
        // selectionStart is only supported on text based input
        if (input.selectionStart !== null) {
          input.selectionStart = input.selectionEnd = input.value.length;
        }
      } else {
        input.blur();
      }
    }
  }
}

function handleCellMoved(hook, cellId) {
  if (hook.state.isFocused && cellId === hook.props.cellId) {
    smoothlyScrollToElement(hook.el);
  }
}

function handleCellUpload(hook, cellId, url) {
  if (!hook.state.liveEditor) {
    return;
  }

  if (hook.props.cellId === cellId) {
    const markdown = `![](${url})`;
    hook.state.liveEditor.insert(markdown);
  }
}

function handleLocationReport(hook, client, report) {
  if (!hook.state.liveEditor) {
    return;
  }

  if (hook.props.cellId === report.cellId && report.selection) {
    hook.state.liveEditor.updateUserSelection(client, report.selection);
  } else {
    hook.state.liveEditor.removeUserSelection(client);
  }
}

function broadcastSelection(hook, selection = null) {
  selection = selection || hook.state.liveEditor.editor.getSelection();

  // Report new selection only if this cell is in insert mode
  if (hook.state.isFocused && hook.state.insertMode) {
    globalPubSub.broadcast("session", {
      type: "cursor_selection_changed",
      cellId: hook.props.cellId,
      selection,
    });
  }
}

export default Cell;
