import ProseMirrorPlugin from "../plugin.mjs";
import {Plugin} from "prosemirror-state";
import {parseHTMLString, serializeHTMLString} from "../util.mjs";
import {ReplaceStep} from "prosemirror-transform";

/**
 * A plugin for the chat message editor which handles interactivity.
 * @extends {ProseMirrorPlugin}
 */
export default class ChatInputPlugin extends ProseMirrorPlugin {
  /**
   * @param {Schema} schema  The ProseMirror schema to build the plugin against.
   * @param {ChatLog} chat   The ChatLog instance this plugin belongs to.
   */
  constructor(schema, chat) {
    super(schema);
    this.#chat = chat;
  }

  /* -------------------------------------------- */
  /*  Properties                                  */
  /* -------------------------------------------- */

  /**
   * The ChatLog instance this plugin belongs to.
   * @type {ChatLog}
   */
  get chat() {
    return this.#chat;
  }

  #chat;

  /* -------------------------------------------- */
  /*  Factory Methods                             */
  /* -------------------------------------------- */

  /**
   * Build the plugin.
   * @param {Schema} schema           The ProseMirror schema to build the plugin against.
   * @param {object} [options]
   * @param {ChatLog} [options.chat]  The ChatLog instance this plugin belongs to.
   * @returns {Plugin}
   */
  static build(schema, { chat }={}) {
    const plugin = new this(schema, chat);
    return new Plugin({
      appendTransaction: plugin._inspectTransactions.bind(plugin),
      instance: plugin,
      key: this.key,
      props: {
        handleKeyDown: plugin._onKeyDown.bind(plugin)
      }
    });
  }

  /* -------------------------------------------- */
  /*  Methods                                     */
  /* -------------------------------------------- */

  /**
   * Inspect transactions and update pending state if they involve insertions or deletions.
   * @param {Transaction[]} transactions  The transactions.
   * @param {EditorState} oldState        The editor state before.
   * @param {EditorState} newState        The editor state after.
   * @protected
   */
  _inspectTransactions(transactions, oldState, newState) {
    let modifiedPending = false;
    for ( const transaction of transactions ) {
      const { noHistory } = transaction.getMeta(this.constructor.key) ?? {};
      if ( noHistory ) return;
      if ( transaction.steps.some(s => s instanceof ReplaceStep) ) {
        modifiedPending = true;
        break;
      }
    }
    if ( modifiedPending ) this.chat.history.pending = serializeHTMLString(newState.doc.content);
  }

  /* -------------------------------------------- */

  /**
   * Handle keydown events.
   * @param {EditorView} view      The editor view.
   * @param {KeyboardEvent} event  The keyboard event.
   * @returns {boolean|void}
   * @protected
   */
  _onKeyDown(view, event) {
    if ( event.isComposing ) return; // Ignore IME composition.

    const inputOptions = { recordPending: true };
    if ( Hooks.call("chatInput", event, inputOptions) === false ) {
      if ( event.defaultPrevented || (event.key === "Enter") ) return true;
      if ( !event.key.startsWith("Arrow") && !inputOptions.recordPending ) return true;
      return;
    }

    if ( event.shiftKey || !["ArrowDown", "ArrowUp", "Enter"].includes(event.key) ) return;

    switch ( event.key ) {
      case "ArrowUp": case "ArrowDown":
        return this.#recallMessage(view, event);

      case "Enter":
        this.sendMessage(view);
        return true;
    }
  }

  /* -------------------------------------------- */

  /**
   * Handle navigating the chat history.
   * @param {EditorView} view      The editor view.
   * @param {KeyboardEvent} event  The keyboard event.
   * @returns {boolean|void}
   */
  #recallMessage(view, event) {
    let { index } = this.chat.history;
    const { pending, queue } = this.chat.history;
    if ( pending || !queue.length ) return;
    const dir = event.key === "ArrowUp" ? 1 : -1;
    index = Math.clamp(index + dir, -1, queue.length - 1);
    this.chat.history.index = index;
    this.setMessage(view, queue[index] || "", { noHistory: true });
    return true;
  }

  /* -------------------------------------------- */

  /**
   * Record a message in the history.
   * @param {string} message  The message.
   */
  #recordMessage(message) {
    const { queue } = this.chat.history;
    if ( queue.length >= this.chat.constructor.MAX_MESSAGE_HISTORY ) queue.pop();
    queue.unshift(message);
    this.chat.history.index = -1;
  }

  /* -------------------------------------------- */

  /**
   * Handle sending a chat message.
   * @param {EditorView} view  The editor view.
   */
  async sendMessage(view) {
    const message = serializeHTMLString(view.state.doc.content);
    if ( !message ) return;
    try {
      await this.chat.processMessage(message);
      this.#recordMessage(message);
      this.setMessage(view, "");
    } catch(e) {
      Hooks.onError("ChatLog#processMessage", e, { notify: "error", log: "error", message });
    }
  }

  /* -------------------------------------------- */

  /**
   * Set the contents of the chat input to the given value.
   * @param {EditorView} view  The editor view.
   * @param {string} message   The message to set.
   * @param {object} meta      Any metadata to append to the transaction.
   */
  setMessage(view, message, meta) {
    const { state } = view;
    let tr = state.tr.replaceWith(0, state.doc.content.size, parseHTMLString(message).content);
    if ( meta ) tr = tr.setMeta(this.constructor.key, meta);
    view.dispatch(tr);
  }
}
