import { shadow } from "./ui_utils.js";
import { AnnotationEditorType } from "../constants"
/**
 * Class to handle the different keyboards shortcuts we can have on mac or
 * non-mac OSes.
 */
class KeyboardManager {
  /**
   * Create a new keyboard manager class.
   * @param {Array<Array>} callbacks - an array containing an array of shortcuts
   * and a callback to call.
   * A shortcut is a string like `ctrl+c` or `mac+ctrl+c` for mac OS.
   */
  constructor(callbacks) {
    this.buffer = [];
    this.callbacks = new Map();
    this.allKeys = new Set();

    const isMac = KeyboardManager.platform.isMac;
    for (const [keys, callback] of callbacks) {
      for (const key of keys) {
        const isMacKey = key.startsWith("mac+");
        if (isMac && isMacKey) {
          this.callbacks.set(key.slice(4), callback);
          this.allKeys.add(key.split("+").at(-1));
        } else if (!isMac && !isMacKey) {
          this.callbacks.set(key, callback);
          this.allKeys.add(key.split("+").at(-1));
        }
      }
    }
  }

  static get platform() {
    const platform = typeof navigator !== "undefined" ? navigator.platform : "";

    return shadow(this, "platform", {
      isWin: platform.includes("Win"),
      isMac: platform.includes("Mac"),
    });
  }

  /**
   * Serialize an event into a string in order to match a
   * potential key for a callback.
   * @param {KeyboardEvent} event
   * @returns {string}
   */
  _serialize(event) {
    if (event.altKey) {
      this.buffer.push("alt");
    }
    if (event.ctrlKey) {
      this.buffer.push("ctrl");
    }
    if (event.metaKey) {
      this.buffer.push("meta");
    }
    if (event.shiftKey) {
      this.buffer.push("shift");
    }
    this.buffer.push(event.key);
    const str = this.buffer.join("+");
    this.buffer.length = 0;

    return str;
  }

  /**
   * Execute a callback, if any, for a given keyboard event.
   * The self is used as `this` in the callback.
   * @param {Object} self.
   * @param {KeyboardEvent} event
   * @returns
   */
  exec(self, event) {
    if (!this.allKeys.has(event.key)) {
      return;
    }
    const callback = this.callbacks.get(this._serialize(event));
    if (!callback) {
      return;
    }
    callback.bind(self)();
    event.stopPropagation();
    event.preventDefault();
  }
}

/**
 * Convert a number between 0 and 100 into an hex number between 0 and 255.
 * @param {number} opacity
 * @return {string}
 */
 function opacityToHex(opacity) {
  return Math.round(Math.min(255, Math.max(1, 255 * opacity)))
    .toString(16)
    .padStart(2, "0");
}

const AnnotationEditorPrefix = "pdfjs_internal_editor_";
/**
 * Class to create some unique ids for the different editors.
 */
class IdManager {
  #id = 0;

  /**
   * Get a unique id.
   * @returns {string}
   */
  getId() {
    return `${AnnotationEditorPrefix}${this.#id++}`;
  }
}

/**
 * A pdf has several pages and each of them when it will rendered
 * will have an AnnotationEditorLayer which will contain the some
 * new Annotations associated to an editor in order to modify them.
 *
 * This class is used to manage all the different layers, editors and
 * some action like copy/paste, undo/redo, ...
 */
class AnnotationManager {

  constructor(container, eventBus) {
    
    this._activeEditor = null;

    this._allEditors = new Map();

    this._allLayers = new Map();

    this._currentPageIndex = 0;

    this._editorTypes = null;

    this._isEnabled = false;

    this._mode = AnnotationEditorType.NONE;

    this._idManager = new IdManager();
    this._selectedEditors = new Set();

    this._boundCopy = this.copy.bind(this);

    this._boundCut = this.cut.bind(this);

    this._boundPaste = this.paste.bind(this);

    this._boundKeydown = this.keydown.bind(this);

    this._boundOnEditingAction = this.onEditingAction.bind(this);

    this._boundOnPageChanging = this.onPageChanging.bind(this);

    this._previousStates = {
      isEditing: false,
      isEmpty: true,
      hasSomethingToUndo: false,
      hasSomethingToRedo: false,
      hasSelectedEditor: false,
    };

    this._keyboardManager = new KeyboardManager([
      [
        [
          "Backspace",
          "alt+Backspace",
          "ctrl+Backspace",
          "shift+Backspace",
          "mac+Backspace",
          "mac+alt+Backspace",
          "mac+ctrl+Backspace",
          "Delete",
          "ctrl+Delete",
          "shift+Delete",
        ],
        AnnotationManager.prototype.delete,
      ]
    ]);

    this._container = container;
    this._eventBus = eventBus;
    this._eventBus._on("editingaction", this._boundOnEditingAction);
    this._eventBus._on("onPageNumberUpdated", this._boundOnPageChanging);
  }

  destroy() {
    this._removeKeyboardManager();
    this._eventBus._off("editingaction", this._boundOnEditingAction);
    this._eventBus._off("onPageNumberUpdated", this._boundOnPageChanging);
    for (const layer of this._allLayers.values()) {
      layer.destroy();
    }
    this._allLayers.clear();
    this._allEditors.clear();
    this._activeEditor = null;
    this._selectedEditors.clear();
  }

  removeAllAnnotations() {
    for (const layer of this._allLayers.values()) {
      layer.removeAllAnnotations();
    }
    this._allEditors.clear();
    this._activeEditor = null;
    this._selectedEditors.clear();
  }

  onPageChanging({ pageNumber }) {
    this._currentPageIndex = pageNumber - 1;
  }

  focusMainContainer() {
    this._container.focus();
  }

  _addKeyboardManager() {
    // The keyboard events are caught at the container level in order to be able
    // to execute some callbacks even if the current page doesn't have focus.
    // this._container.addEventListener("keydown", this._boundKeydown);
  }

  _removeKeyboardManager() {
    this._container.removeEventListener("keydown", this._boundKeydown);
  }

  _addCopyPasteListeners() {
    document.addEventListener("copy", this._boundCopy);
    document.addEventListener("cut", this._boundCut);
    document.addEventListener("paste", this._boundPaste);
  }

  _removeCopyPasteListeners() {
    document.removeEventListener("copy", this._boundCopy);
    document.removeEventListener("cut", this._boundCut);
    document.removeEventListener("paste", this._boundPaste);
  }

  /**
   * Copy callback.
   * @param {ClipboardEvent} event
   */
  copy(event) {
    event.preventDefault();

    if (this._activeEditor) {
      // An editor is being edited so just commit it.
      this._activeEditor.commitOrRemove();
    }

    if (!this.hasSelection) {
      return;
    }

    const editors = [];
    for (const editor of this._selectedEditors) {
      if (!editor.isEmpty()) {
        editors.push(editor.serialize());
      }
    }
    if (editors.length === 0) {
      return;
    }

    event.clipboardData.setData("application/pdfjs", JSON.stringify(editors));
  }

  /**
   * Cut callback.
   * @param {ClipboardEvent} event
   */
  cut(event) {
    this.copy(event);
    this.delete();
  }

  /**
   * Paste callback.
   * @param {ClipboardEvent} event
   */
  paste(event) {
    event.preventDefault();

    let data = event.clipboardData.getData("application/pdfjs");
    if (!data) {
      return;
    }

    try {
      data = JSON.parse(data);
    } catch (ex) {
      console.warn(`paste: "${ex.message}".`);
      return;
    }

    if (!Array.isArray(data)) {
      return;
    }

    this.unselectAll();
    const layer = this._allLayers.get(this._currentPageIndex);

    try {
      const newEditors = [];
      for (const editor of data) {
        const deserializedEditor = layer.deserialize(editor);
        if (!deserializedEditor) {
          return;
        }
        newEditors.push(deserializedEditor);
      }

      for (const editor of newEditors) {
        this._addEditorToLayer(editor);
      }
      this._selectEditors(newEditors);
    } catch (ex) {
      console.warn(`paste: "${ex.message}".`);
    }
  }

  /**
   * Keydown callback.
   * @param {KeyboardEvent} event
   */
  keydown(event) {
    if (!this.getActive()?.shouldGetKeyboardEvents()) {
      this._keyboardManager.exec(this, event);
    }
  }

  /**
   * Execute an action for a given name.
   * For example, the user can click on the "Undo" entry in the context menu
   * and it'll trigger the undo action.
   * @param {Object} details
   */
  onEditingAction(details) {
    if (["delete"].includes(details.name)) {
      this[details.name]();
    }
  }

  /**
   * Update the different possible states of this manager, e.g. is there
   * something to undo, redo, ...
   * @param {Object} details
   */
  _dispatchUpdateStates(details) {
    const hasChanged = Object.entries(details).some(
      ([key, value]) => this._previousStates[key] !== value
    );

    if (hasChanged) {
      this._eventBus.dispatch("annotationeditorstateschanged", {
        source: this,
        details: Object.assign(this._previousStates, details),
      });
    }
  }

  _dispatchUpdateUI(details) {
    this._eventBus.dispatch("annotationeditorparamschanged", {
      source: this,
      details,
    });
  }

  /**
   * Set the editing state.
   * It can be useful to temporarily disable it when the user is editing a
   * FreeText annotation.
   * @param {boolean} isEditing
   */
  setEditingState(isEditing) {
    if (isEditing) {
      this._addKeyboardManager();
      this._addCopyPasteListeners();
      this._dispatchUpdateStates({
        isEditing: this._mode !== AnnotationEditorType.NONE,
        isEmpty: this._isEmpty(),
        hasSelectedEditor: false,
      });
    } else {
      this._removeKeyboardManager();
      this._removeCopyPasteListeners();
      this._dispatchUpdateStates({
        isEditing: false,
      });
    }
  }

  registerEditorTypes(types) {
    if (this._editorTypes) {
      return;
    }
    this._editorTypes = types;
    for (const editorType of this._editorTypes) {
      this._dispatchUpdateUI(editorType.defaultPropertiesToUpdate);
    }
  }

  /**
   * Get an id.
   * @returns {string}
   */
  getId() {
    return this._idManager.getId();
  }

  /**
   * Add a new layer for a page which will contains the editors.
   * @param {AnnotationEditorLayer} layer
   */
  addLayer(layer) {
    this._allLayers.set(layer.pageIndex, layer);
    if (this._isEnabled) {
      layer.enable();
    } else {
      layer.disable();
    }
  }

  /**
   * Remove a layer.
   * @param {AnnotationEditorLayer} layer
   */
  removeLayer(layer) {
    this._allLayers.delete(layer.pageIndex);
  }

  /**
   * Change the editor mode (None, FreeText, Ink, ...)
   * @param {number} mode
   */
  updateMode(mode) {
    this._mode = mode;
    if (mode === AnnotationEditorType.NONE) {
      this.setEditingState(false);
      this._disableAll();
    } else {
      this.setEditingState(true);
      this._enableAll();
      for (const layer of this._allLayers.values()) {
        layer.updateMode(mode);
      }
    }
  }

  /**
   * Update the toolbar if it's required to reflect the tool currently used.
   * @param {number} mode
   * @returns {undefined}
   */
  updateToolbar(mode) {
    if (mode === this._mode) {
      return;
    }
    this._eventBus.dispatch("switchannotationeditormode", {
      source: this,
      mode,
    });
  }

  /**
   * Update a parameter in the current editor or globally.
   * @param {number} type
   * @param {*} value
   */
  updateParams(type, value) {
    if (!this._editorTypes) {
      return;
    }

    for (const editor of this._selectedEditors) {
      editor.updateParams(type, value);
    }

    for (const editorType of this._editorTypes) {
      editorType.updateDefaultParams(type, value);
    }
  }

  /**
   * Enable all the layers.
   */
   _enableAll() {
    if (!this._isEnabled) {
      this._isEnabled = true;
      for (const layer of this._allLayers.values()) {
        layer.enable();
      }
    }
  }

  /**
   * Disable all the layers.
   */
  _disableAll() {
    this.unselectAll();
    if (this._isEnabled) {
      this._isEnabled = false;
      for (const layer of this._allLayers.values()) {
        layer.disable();
      }
    }
  }

  /**
   * Get all the editors belonging to a give page.
   * @param {number} pageIndex
   * @returns {Array<AnnotationEditor>}
   */
  getEditors(pageIndex) {
    const editors = [];
    for (const editor of this._allEditors.values()) {
      if (editor.pageIndex === pageIndex) {
        editors.push(editor);
      }
    }
    return editors;
  }

  /**
   * Get an editor with the given id.
   * @param {string} id
   * @returns {AnnotationEditor}
   */
  getEditor(id) {
    return this._allEditors.get(id);
  }

  /**
   * Add a new editor.
   * @param {AnnotationEditor} editor
   */
  addEditor(editor) {
    this._allEditors.set(editor.id, editor);
  }

  /**
   * Remove an editor.
   * @param {AnnotationEditor} editor
   */
  removeEditor(editor) {
    this._allEditors.delete(editor.id);
    this.unselect(editor);
  }

  /**
   * Add an editor to the layer it belongs to or add it to the global map.
   * @param {AnnotationEditor} editor
   */
  _addEditorToLayer(editor) {
    const layer = this._allLayers.get(editor.pageIndex);
    if (layer) {
      layer.addOrRebuild(editor);
    } else {
      this.addEditor(editor);
    }
  }

  /**
   * Set the given editor as the active one.
   * @param {AnnotationEditor} editor
   */
  setActiveEditor(editor) {
    if (this._activeEditor === editor) {
      return;
    }

    this._activeEditor = editor;
    if (editor) {
      this._dispatchUpdateUI(editor.propertiesToUpdate);
    }
  }

  /**
   * Add or remove an editor the current selection.
   * @param {AnnotationEditor} editor
   */
  toggleSelected(editor) {
    if (this._selectedEditors.has(editor)) {
      this._selectedEditors.delete(editor);
      editor.unselect();
      this._dispatchUpdateStates({
        hasSelectedEditor: this.hasSelection,
      });
      return;
    }
    this._selectedEditors.add(editor);
    editor.select();
    this._dispatchUpdateUI(editor.propertiesToUpdate);
    this._dispatchUpdateStates({
      hasSelectedEditor: true,
    });
  }

  /**
   * Set the last selected editor.
   * @param {AnnotationEditor} editor
   */
  setSelected(editor) {
    for (const ed of this._selectedEditors) {
      if (ed !== editor) {
        ed.unselect();
      }
    }
    this._selectedEditors.clear();

    this._selectedEditors.add(editor);
    editor.select();
    this._dispatchUpdateUI(editor.propertiesToUpdate);
    this._dispatchUpdateStates({
      hasSelectedEditor: true,
    });
  }

  /**
   * Check if the editor is selected.
   * @param {AnnotationEditor} editor
   */
  isSelected(editor) {
    return this._selectedEditors.has(editor);
  }

  /**
   * Unselect an editor.
   * @param {AnnotationEditor} editor
   */
  unselect(editor) {
    editor.unselect();
    this._selectedEditors.delete(editor);
    this._dispatchUpdateStates({
      hasSelectedEditor: this.hasSelection,
    });
  }

  get hasSelection() {
    return this._selectedEditors.size !== 0;
  }

  _isEmpty() {
    if (this._allEditors.size === 0) {
      return true;
    }

    if (this._allEditors.size === 1) {
      for (const editor of this._allEditors.values()) {
        return editor.isEmpty();
      }
    }

    return false;
  }

  /**
   * Delete the current editor or all.
   */
  delete() {
    this.commitOrRemove();
    const editors = [...this._selectedEditors];
    for (const editor of editors) {
      editor.remove();
    }
  }

  commitOrRemove() {
    // An editor is being edited so just commit it.
    this._activeEditor?.commitOrRemove();
  }

  /**
   * Select the editors.
   * @param {Array<AnnotationEditor>} editors
   */
  _selectEditors(editors) {
    this._selectedEditors.clear();
    for (const editor of editors) {
      if (editor.isEmpty()) {
        continue;
      }
      this._selectedEditors.add(editor);
      editor.select();
    }
    this._dispatchUpdateStates({ hasSelectedEditor: true });
  }

  /**
   * Select all the editors.
   */
  selectAll() {
    for (const editor of this._selectedEditors) {
      editor.commit();
    }
    this._selectEditors(this._allEditors.values());
  }

  /**
   * Unselect all the selected editors.
   */
  unselectAll() {
    if (this._activeEditor) {
      // An editor is being edited so just commit it.
      this._activeEditor.commitOrRemove();
      return;
    }

    if (this._selectedEditors.size === 0) {
      return;
    }
    for (const editor of this._selectedEditors) {
      editor.unselect();
    }
    this._selectedEditors.clear();
    this._dispatchUpdateStates({
      hasSelectedEditor: false,
    });
  }

  /**
   * Is the current editor the one passed as argument?
   * @param {AnnotationEditor} editor
   * @returns
   */
  isActive(editor) {
    return this._activeEditor === editor;
  }

  /**
   * Get the current active editor.
   * @returns {AnnotationEditor|null}
   */
  getActive() {
    return this._activeEditor;
  }

  /**
   * Get the current editor mode.
   * @returns {number}
   */
  getMode() {
    return this._mode;
  }
}

export {
  AnnotationManager,
  KeyboardManager,
  opacityToHex
};
