import React from "react";
import { PID } from "../persist";
import { Point, Range, Selection } from "../editor";
import { AbstractCmdController } from "./abstractCmdController";
import { throttle } from "lodash-es";
import {
  calcSelectionStyles,
  getCaretPosition,
  getEffectKeys,
  getOrderedRange,
  maxPoint,
  minPoint,
} from "../utils/editor";
import type { Controller } from "./index";

export type GetEditorHandler = () => {
  contentDOM: HTMLDivElement | null;
};

export class SelectionController extends AbstractCmdController {
  pendingPoint: Point | undefined;

  displayingParagraphs: Map<PID, GetEditorHandler> = new Map();

  selectionStyles: Record<PID, React.CSSProperties[]> = {};

  timeout: ReturnType<typeof setTimeout> | undefined = undefined;

  renderingPids = new Map<PID, boolean>();

  mergedSelectionRect: DOMRect | undefined;

  mergeSelectionAt(selection: Selection, point: Point) {
    const { controller } = this;
    const { start, end } = selection.range;
    const newSelection = Selection.create(
      Range.create({
        start: minPoint(controller, start, point),
        end: maxPoint(controller, end, point),
      })
    );
    controller.setCurrentSelection(newSelection);
  }

  isAllRendered() {
    return this.renderingPids.size === 0;
  }

  finishRender(pid: PID) {
    this.renderingPids.delete(pid);
    this.controller.emit("finishRenderParagraph", { pid });
    if (this.isAllRendered()) {
      this.calcSelectionStyles();
    }
  }

  constructor(controller: Controller) {
    super(controller);
    this.listen();
  }

  listen() {
    this.controller.on("selectionChange", ({ selection }) => {
      if (this.pendingPoint) {
        return;
      }
      this.clearSelectionStyles();

      if (!selection) return;
      if (
        this.isAllRendered() ||
        !getEffectKeys(this.controller.getPids(), selection.range).some(pid =>
          this.renderingPids.has(pid)
        )
      ) {
        this.calcSelectionStyles();
      }
    });

    this.controller.on(
      "bookParagraphChangeSpecific",
      ({ addParagraphPids, updateParagraphPids, removeParagraphPids }) => {
        const effectPids = addParagraphPids.concat(updateParagraphPids);
        effectPids.forEach(pid => {
          this.renderingPids.set(pid, true);
          this.controller.emit("renderingParagraph", { pid });
        });

        removeParagraphPids.forEach(pid => {
          this.renderingPids.delete(pid);
        });
      }
    );
  }

  startSelectionAt(point: Point) {
    this.pendingPoint = point;
    const selection = Selection.create(
      Range.create({
        start: point,
        end: point,
      })
    );
    this.controller.setCurrentSelection(selection);
  }

  pendingSelectionAt(point: Point) {
    if (!this.pendingPoint) {
      return;
    }

    const selection = Selection.create(
      getOrderedRange(
        this.controller,
        Range.create({
          start: this.pendingPoint,
          end: point,
        })
      )
    );

    this.controller.setCurrentSelection(selection);
    this.calcSelectionStyles();
  }

  endSelectionAt(point?: Point) {
    if (point) {
      this.pendingSelectionAt(point);
    }
    this.pendingPoint = undefined;
  }

  isSelectionPending(isUnfix: boolean) {
    const { pid } = this.pendingPoint || {};
    const unfixPid = this.controller.getUnfixPid();

    return !!this.pendingPoint && (isUnfix ? pid === unfixPid : pid !== unfixPid);
  }

  clearPendingPoint() {
    this.pendingPoint = undefined;
  }

  registerTextEditor(pid: PID, handler: GetEditorHandler) {
    this.displayingParagraphs.set(pid, handler);
    this.calcSelectionStyles();

    return () => {
      this.unregisterTextEditor(pid);
    };
  }

  unregisterTextEditor(pid: PID) {
    this.displayingParagraphs.delete(pid);
  }

  getParagraphDOM(pid: PID) {
    const handler = this.displayingParagraphs.get(pid);
    if (!handler) {
      return null;
    }
    return handler().contentDOM;
  }

  // eslint-disable-next-line @typescript-eslint/member-ordering
  calcSelectionStyles = throttle(() => {
    // const s = Date.now();
    this.selectionStyles =
      calcSelectionStyles(this.controller, {
        getParagraphDOM: (pid: PID) => this.getParagraphDOM(pid),
      }) || {};
    // console.log('debug calc selection using:', Date.now() - s);

    this.calcMergedSelectionRect();

    // console.log('debug merged selection using:', Date.now() - s);

    if (Object.keys(this.selectionStyles).length) {
      this.controller.emit("caretStyleChange", {
        value: undefined,
      });
    }
    this.controller.emit("selectionStylesChange", { styles: this.selectionStyles });
  }, 16.6);

  calcMergedSelectionRect() {
    const styles = this.selectionStyles;

    let minTop: number | undefined;
    let minLeft: number | undefined;
    let maxRight: number | undefined;
    let maxBottom: number | undefined;

    Object.keys(styles).forEach(pid => {
      const dom = this.getParagraphDOM(pid);
      if (!dom) {
        return;
      }
      const domRect = dom.getBoundingClientRect();

      styles[pid].forEach(style => {
        const { top: _top, left: _left, height, width } = style;

        const deltaX = domRect.x;
        const deltaY = domRect.y;

        const top = deltaY + Number(_top);
        const left = deltaX + Number(_left);
        const right = left + Number(width);
        const bottom = top + Number(height);

        minTop = minTop ? Math.min(minTop, top) : top;
        minLeft = minLeft ? Math.min(minLeft, left) : left;
        maxRight = maxRight ? Math.max(maxRight, right) : right;
        maxBottom = maxBottom ? Math.max(maxBottom, bottom) : bottom;
      });
    });

    if (minTop != null && minLeft != null && maxRight != null && maxBottom != null) {
      this.mergedSelectionRect = new DOMRect(
        minLeft,
        minTop,
        Math.max(maxRight - minLeft, 0),
        Math.max(maxBottom - minTop, 0)
      );
    } else {
      this.mergedSelectionRect = undefined;
    }
  }

  getMergedSelectionRect() {
    return this.mergedSelectionRect;
  }

  clearSelectionStyles() {
    this.selectionStyles = {};
    this.controller.emit("selectionStylesChange", { styles: this.selectionStyles });
  }

  getSelectionStyles(pid: PID) {
    return this.selectionStyles[pid] || [];
  }

  calcCaretStyles = throttle(() => {
    const { controller } = this;
    const selection = controller.getCurrentSelection();
    if (selection && selection.isCaret()) {
      const { pid } = selection.range.start;

      const dom = this.getParagraphDOM(pid);
      if (dom) {
        const caretStyle = getCaretPosition(this.controller, selection, dom);
        const editorRect = dom.getBoundingClientRect();
        const value = caretStyle
          ? {
              pid,
              editorRect,
              style: caretStyle!,
            }
          : undefined;

        this.clearSelectionStyles();
        controller.emit("caretStyleChange", { value });
        return value;
      }
    }

    controller.emit("caretStyleChange", {
      value: undefined,
    });
    return undefined;
  }, 16.6);
}
