import { KeyboardManager } from "./tools.js";
import { AnnotationEditorType } from "../constants"
import { bindEvents } from "./ui_utils.js";
import { PDFAnnotationSticky } from "./pdf_annotation_sticky.js";
import { InkEditor } from "./pdf_measure_editor.js";

/**
 * Manage all the different editors on a page.
 */
class PDFAnnotationLayer {

  /**
   * @param {AnnotationEditorLayerOptions} options
   */
  constructor(options) {
    this._cancelled = false;

    this._allowClick = false;

    this._boundPointerup = this.pointerup.bind(this);
  
    this._boundPointerdown = this.pointerdown.bind(this);
  
    this._editors = new Map();
  
    this._hadPointerDown = false;
  
    this._isCleaningUp = false;

    this._uiManager = options.uiManager;

    this.pageDiv = options.pageDiv;
    this.div = null;
    this.scale = options.scale;
    this._accessibilityManager = options.accessibilityManager
    options.uiManager.registerEditorTypes([PDFAnnotationSticky, InkEditor]);
    this.annotationStorage = options.annotationStorage || null;
    this.pageIndex = options.pageIndex;
  }

  /**
   * Update the toolbar if it's required to reflect the tool currently used.
   * @param {number} mode
   */
  updateToolbar(mode) {
    this._uiManager.updateToolbar(mode);
  }

  /**
   * The mode has changed: it must be updated.
   * @param {number} mode
   */
  updateMode(mode = this._uiManager.getMode()) {
    this._cleanup();
    if (mode === AnnotationEditorType.INK) {
      // We always want to an ink editor ready to draw in.
      this.addInkEditorIfNeeded(false);
      this.disableClick();
    } else {
      this.enableClick();
    }
    this._uiManager.unselectAll();

    this.div.classList.toggle(
      "freeTextEditing",
      mode === AnnotationEditorType.FREETEXT
    );
    this.div.classList.toggle("inkEditing", mode === AnnotationEditorType.INK);
  }

  moveEditorInDOM(editor) {
    this._accessibilityManager?.moveElementInDOM(
      this.div,
      editor.div,
      editor.contentDiv,
      /* isRemovable = */ true
    );
  }

  addInkEditorIfNeeded(isCommitting) {
    if (
      !isCommitting &&
      this._uiManager.getMode() !== AnnotationEditorType.INK
    ) {
      return;
    }

    if (!isCommitting) {
      // We're removing an editor but an empty one can already exist so in this
      // case we don't need to create a new one.
      for (const editor of this._editors.values()) {
        if (editor.isEmpty()) {
          editor.setInBackground();
          return;
        }
      }
    }

    const editor = this._createAndAddNewEditor({ offsetX: 0, offsetY: 0 });
    editor.setInBackground();
  }

  /**
   * Set the editing state.
   * @param {boolean} isEditing
   */
  setEditingState(isEditing) {
    this._uiManager.setEditingState(isEditing);
  }

  /**
   * Add some commands into the CommandManager (undo/redo stuff).
   * @param {Object} params
   */
  addCommands(params) {
    this._uiManager.addCommands(params);
  }

  /**
   * Enable pointer events on the main div in order to enable
   * editor creation.
   */
  enable() {
    this.div.style.pointerEvents = "auto";
    for (const editor of this._editors.values()) {
      editor.enableEditing();
    }
  }

  /**
   * Disable editor creation.
   */
  disable() {
    this.div.style.pointerEvents = "none";
    for (const editor of this._editors.values()) {
      editor.disableEditing();
    }
  }

  /**
   * Set the current editor.
   * @param {AnnotationEditor} editor
   */
  setActiveEditor(editor) {
    const currentActive = this._uiManager.getActive();
    if (currentActive === editor) {
      return;
    }

    this._uiManager.setActiveEditor(editor);
  }

  enableClick() {
    this.div.addEventListener("pointerdown", this._boundPointerdown);
    this.div.addEventListener("pointerup", this._boundPointerup);
  }

  disableClick() {
    this.div.removeEventListener("pointerdown", this._boundPointerdown);
    this.div.removeEventListener("pointerup", this._boundPointerup);
  }

  attach(editor) {
    this._editors.set(editor.id, editor);
  }

  detach(editor) {
    this._editors.delete(editor.id);
  }

  /**
   * Remove an editor.
   * @param {AnnotationEditor} editor
   */
  remove(editor) {
    // Since we can undo a removal we need to keep the
    // parent property as it is, so don't null it!

    this._uiManager.removeEditor(editor);
    this.detach(editor);
    this.annotationStorage.remove(editor.id);
    editor.div.style.display = "none";
    setTimeout(() => {
      // When the div is removed from DOM the focus can move on the
      // document.body, so we just slightly postpone the removal in
      // order to let an element potentially grab the focus before
      // the body.
      editor.div.style.display = "";
      editor.div.remove();
      editor.isAttachedToDOM = false;
      if (document.activeElement === document.body) {
        this._uiManager.focusMainContainer();
      }
    }, 0);

    if (!this._isCleaningUp) {
      this.addInkEditorIfNeeded(/* isCommitting = */ false);
    }
  }

  /**
   * An editor can have a different parent, for example after having
   * being dragged and droped from a page to another.
   * @param {AnnotationEditor} editor
   */
  _changeParent(editor) {
    if (editor.parent === this) {
      return;
    }

    this.attach(editor);
    editor.pageIndex = this.pageIndex;
    editor.parent?.detach(editor);
    editor.parent = this;
    if (editor.div && editor.isAttachedToDOM) {
      editor.div.remove();
      this.div.append(editor.div);
    }
  }

  /**
   * Add a new editor in the current view.
   * @param {AnnotationEditor} editor
   */
  add(editor) {
    this._changeParent(editor);
    this._uiManager.addEditor(editor);
    this.attach(editor);

    if (!editor.isAttachedToDOM) {
      const div = editor.render();
      this.div.append(div);
      editor.isAttachedToDOM = true;
    }

    editor.onceAdded();
    this.addToAnnotationStorage(editor);
  }

  /**
   * Add an editor in the annotation storage.
   * @param {AnnotationEditor} editor
   */
  addToAnnotationStorage(editor) {
    if (!editor.isEmpty() && !this.annotationStorage.has(editor.id)) {
      this.annotationStorage.setValue(editor.id, editor);
    }
  }

  updateAnnotationStorage(key) {
    
  }

  /**
   * Add or rebuild depending if it has been removed or not.
   * @param {AnnotationEditor} editor
   */
  addOrRebuild(editor) {
    if (editor.needsToBeRebuilt()) {
      editor.rebuild();
    } else {
      this.add(editor);
    }
  }

  /**
   * Add a new editor and make this addition undoable.
   * @param {AnnotationEditor} editor
   */
  addUndoableEditor(editor) {
    this.addOrRebuild(editor);
  }

  /**
   * Get an id for an editor.
   * @returns {string}
   */
  getNextId() {
    return this._uiManager.getId();
  }

  /**
   * Create a new editor
   * @param {Object} params
   * @returns {AnnotationEditor}
   */
  _createNewEditor(params) {
    switch (this._uiManager.getMode()) {
      case AnnotationEditorType.FREETEXT:
        return new PDFAnnotationSticky(params);
      case AnnotationEditorType.INK:
        return new InkEditor(params);
    }
    return null;
  }

  /**
   * Create a new editor
   * @param {Object} data
   * @returns {AnnotationEditor}
   */
  deserialize(data) {
    switch (data.annotationType) {
      case AnnotationEditorType.FREETEXT:
        return PDFAnnotationSticky.deserialize(data, this);
      case AnnotationEditorType.INK:
        return InkEditor.deserialize(data, this);
    }
    return null;
  }

  /**
   * Create and add a new editor.
   * @param {PointerEvent} event
   * @returns {AnnotationEditor}
   */
  _createAndAddNewEditor(event) {
    const id = this.getNextId();
    const editor = this._createNewEditor({
      parent: this,
      id,
      x: event.offsetX,
      y: event.offsetY,
    });
    if (editor) {
      this.add(editor);
    }

    return editor;
  }

  /**
   * Set the last selected editor.
   * @param {AnnotationEditor} editor
   */
  setSelected(editor) {
    this._uiManager.setSelected(editor);
  }

  /**
   * Add or remove an editor the current selection.
   * @param {AnnotationEditor} editor
   */
  toggleSelected(editor) {
    this._uiManager.toggleSelected(editor);
  }

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

  /**
   * Unselect an editor.
   * @param {AnnotationEditor} editor
   */
  unselect(editor) {
    this._uiManager.unselect(editor);
  }

  /**
   * Pointerup callback.
   * @param {PointerEvent} event
   */
  pointerup(event) {
    const isMac = KeyboardManager.platform.isMac;
    if (event.button !== 0 || (event.ctrlKey && isMac)) {
      // Don't create an editor on right click.
      return;
    }

    if (event.target !== this.div) {
      return;
    }

    if (!this._hadPointerDown) {
      // It can happen when the user starts a drag inside a text editor
      // and then releases the mouse button outside of it. In such a case
      // we don't want to create a new editor, hence we check that a pointerdown
      // occured on this div previously.
      return;
    }
    this._hadPointerDown = false;

    if (!this._allowClick) {
      this._allowClick = true;
      return;
    }

    this._createAndAddNewEditor(event);
  }

  /**
   * Pointerdown callback.
   * @param {PointerEvent} event
   */
  pointerdown(event) {
    const isMac = KeyboardManager.platform.isMac;
    if (event.button !== 0 || (event.ctrlKey && isMac)) {
      // Do nothing on right click.
      return;
    }

    if (event.target !== this.div) {
      return;
    }

    this._hadPointerDown = true;

    const editor = this._uiManager.getActive();
    this._allowClick = !editor || editor.isEmpty();
  }

  /**
   * Drag callback.
   * @param {DragEvent} event
   */
  drop(event) {
    const id = event.dataTransfer.getData("text/plain");
    const editor = this._uiManager.getEditor(id);
    if (!editor) {
      return;
    }

    event.preventDefault();
    event.dataTransfer.dropEffect = "move";

    this._changeParent(editor);

    const rect = this.div.getBoundingClientRect();
    const endX = event.clientX - rect.x;
    const endY = event.clientY - rect.y;

    editor.translate(endX - editor.startX, endY - editor.startY);
    editor.div.focus();
  }

  /**
   * Dragover callback.
   * @param {DragEvent} event
   */
  dragover(event) {
    event.preventDefault();
  }

  _cleanup() {
    // When we're cleaning up, some editors are removed but we don't want
    // to add a new one which will induce an addition in this._editors, hence
    // an infinite loop.
    this._isCleaningUp = true;
    for (const editor of this._editors.values()) {
      if (editor.isEmpty()) {
        editor.remove();
      }
    }
    this._isCleaningUp = false;
  }

  _removeAll() {
    // When we're cleaning up, some editors are removed but we don't want
    // to add a new one which will induce an addition in this._editors, hence
    // an infinite loop.
    this._isCleaningUp = true;
    for (const editor of this._editors.values()) {
      editor.remove();
    }
    this._isCleaningUp = false;
  }

  /**
   * Render the main editor.
   * @param {Object} parameters
   */
  render(viewport) {
    if (this._cancelled) {
      return;
    }

    const clonedViewport = viewport.clone({ dontFlip: true });
    if (this.div) {
      this.update({ viewport: clonedViewport });
      this.show();
      return;
    }

    // Create an AnnotationEditor layer div
    this.div = document.createElement("div");
    this.div.className = "annotationLayer";
    this.pageDiv.append(this.div);
  }

  cancel() {
    this._cancelled = true;
    this.destroy();
  }

  hide() {
    if (!this.div) {
      return;
    }
    this.div.hidden = true;
  }

  show() {
    if (!this.div) {
      return;
    }
    this.div.hidden = false;
  }

  /**
   * Destroy the main editor.
   */
  destroy() {
    if (!this.div) {
      return;
    }
    this.pageDiv = null;
    if (this._uiManager.getActive()?.parent === this) {
      this._uiManager.setActiveEditor(null);
    }

    for (const editor of this._editors.values()) {
      editor.isAttachedToDOM = false;
      editor.div.remove();
      editor.parent = null;
    }
    this.div.remove();
    this.div = null;
    this._editors.clear();
    this._uiManager.removeLayer(this);
  }

  /**
   * Remove an editor.
   * @param {AnnotationEditor} editor
   */
   removeAllAnnotations() {
    // Since we can undo a removal we need to keep the
    // parent property as it is, so don't null it!
    
    if (this._uiManager.getActive()?.parent === this) {
      this._uiManager.setActiveEditor(null);
    }
    for (const editor of this._editors.values()) {
      this.remove(editor)
    }
  }


  /**
   * Update the main editor.
   * @param {Object} parameters
   */
  update(parameters) {
    // Editors have their dimensions/positions in percent so to avoid any
    // issues (see #15582), we must commit the current one before changing
    // the viewport.
    this._uiManager.commitOrRemove();

    this.viewport = parameters.viewport;
    this.setDimensions();
    this.updateMode();
  }

  /**
   * Get the scale factor from the viewport.
   * @returns {number}
   */
  get scaleFactor() {
    return this.viewport.scale;
  }

  /**
   * Get page dimensions.
   * @returns {Object} dimensions.
   */
  get pageDimensions() {
    const [pageLLx, pageLLy, pageURx, pageURy] = this.viewport.viewBox;
    const width = pageURx - pageLLx;
    const height = pageURy - pageLLy;

    return [width, height];
  }

  get viewportBaseDimensions() {
    const { width, height, rotation } = this.viewport;
    return rotation % 180 === 0 ? [width, height] : [height, width];
  }

  /**
   * Set the dimensions of the main div.
   */
  setDimensions() {
    const { width, height, rotation } = this.viewport;

    const flipOrientation = rotation % 180 !== 0,
      widthStr = Math.floor(width) + "px",
      heightStr = Math.floor(height) + "px";

    this.div.style.width = flipOrientation ? heightStr : widthStr;
    this.div.style.height = flipOrientation ? widthStr : heightStr;
  }
}

export { PDFAnnotationLayer };
