import { FuzzyMatch, App, FuzzySuggestModal, Scope } from "obsidian";
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
import { Suggester } from "./Suggester";
import { t } from "src/lang/helpers";

export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
  items: T[] = [];
  suggestions: HTMLDivElement[];
  popper: WeakRef<PopperInstance>;
  scope: Scope = new Scope(this.app.scope);
  suggester: Suggester<FuzzyMatch<T>>;
  suggestEl: HTMLDivElement;
  promptEl: HTMLDivElement;
  emptyStateText: string = t("SUGGESTION_NOMATCH");
  limit: number = 100;
  shouldNotOpen: boolean;

  // Pre-bound event handlers
  private handleInput: () => void;
  private handleFocus: () => void;
  private handleBlur: () => void;
  private handleMouseDown: (event: MouseEvent) => void;
  public readonly collisionBoundary?: HTMLElement;

  constructor(
    app: App,
    inputEl: HTMLInputElement | HTMLTextAreaElement,
    items: T[],
    collisionBoundary?: HTMLElement,
  ) {
    super(app);
    this.inputEl = inputEl as HTMLInputElement;
    this.items = items;
    this.collisionBoundary = collisionBoundary;

    // Pre-bind event handlers
    this.handleInput = () => this.onInputChanged();
    this.handleFocus = () => this.onFocus();
    this.handleBlur = () => this.close();
    this.handleMouseDown = (event: MouseEvent) => {
      event.preventDefault();
    };

    this.suggestEl = createDiv("suggestion-container");
    this.contentEl = this.suggestEl.createDiv("suggestion");
    this.suggester = new Suggester(this, this.contentEl, this.scope);
    this.scope.register([], "Escape", () => this.onEscape());
    this.inputEl.addEventListener("input", this.handleInput);
    this.inputEl.addEventListener("focus", this.handleFocus);
    this.inputEl.addEventListener("blur", this.handleBlur);
    this.suggestEl.on(
      "mousedown",
      ".suggestion-container",
      this.handleMouseDown,
    );
  }

  empty() {
    this.suggester.empty();
  }

  onInputChanged(): void {
    if (this.shouldNotOpen) {
      return;
    }
    const inputStr = this.modifyInput(this.inputEl.value);
    const suggestions = this.getSuggestions(inputStr);
    if (suggestions.length > 0) {
      this.suggester.setSuggestions(suggestions.slice(0, this.limit));
    } else {
      this.onNoSuggestion();
    }
    this.open();
  }

  onFocus(): void {
    this.shouldNotOpen = false;
    this.onInputChanged();
  }

  modifyInput(input: string): string {
    return input;
  }

  onNoSuggestion() {
    this.empty();
    this.renderSuggestion(null, this.contentEl.createDiv("suggestion-item"));
  }

  open(): void {
    // TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
    this.app.keymap.pushScope(this.scope);
    //setTimeout(() => {
    const modal = this.inputEl.closest(".modal");
    const modalContainer =
      this.inputEl.closest(".modal-container") ??
      (modal?.parentElement?.matches(".modal-container")
        ? modal.parentElement
        : null);

    const host = modalContainer ?? this.inputEl.ownerDocument.body;

    host.appendChild(this.suggestEl);

    const boundary = this.collisionBoundary ?? host;

    this.popper = new WeakRef(
      createPopper(this.inputEl, this.suggestEl, {
        placement: "bottom-start",
        strategy: host.matches(".modal-container") ? "absolute" : "fixed",
        modifiers: [
          { name: "offset", options: { offset: [0, 10] } },
          { name: "flip", options: { fallbackPlacements: ["top"] } },
          {
            name: "preventOverflow",
            options: {
              boundary,
              altBoundary: true,
              padding: { left: 8, right: 8 },
            },
          },
        ],
      }),
    );
    //},50);
  }

  onEscape(): void {
    this.close();
    this.shouldNotOpen = true;
  }

  close(): void {
    // TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
    this.app.keymap.popScope(this.scope);
    this.suggester.setSuggestions([]);
    if (this.popper?.deref()) {
      this.popper.deref().destroy();
    }
    this.inputEl.removeEventListener("input", this.handleInput);
    this.inputEl.removeEventListener("focus", this.handleFocus);
    this.inputEl.removeEventListener("blur", this.handleBlur);
    this.suggestEl.detach();
  }

  createPrompt(prompts: HTMLSpanElement[]) {
    if (!this.promptEl) {
      this.promptEl = this.suggestEl.createDiv("prompt-instructions");
    }
    const prompt = this.promptEl.createDiv("prompt-instruction");
    for (const p of prompts) {
      prompt.appendChild(p);
    }
  }

  abstract onChooseItem(item: T, evt: MouseEvent | KeyboardEvent): void;
  abstract getItemText(arg: T): string;
  abstract getItems(): T[];
}
