import { LocationDescriptor, MeatballMenuApi } from "@heydovetail/ui-components";
import { Plugin } from "prosemirror-state";
import React from "react";
import { style } from "typestyle-react";
import { getPluginStateOrThrow as getRangePluginStateOrThrow } from "../range/RangePlugin";
import { HighlightRange, RangeType } from "../range/types";
import { EditorSchema } from "../schema";
import { el } from "../util/dom";
import { PluginDescriptor } from "../util/PluginDescriptor";
import { PortalProviderApi } from "../util/PortalProvider";
import { focusRange } from "./commands";
import { getPluginStateOrThrow as getHighlightWidgetPluginStateOrThrow } from "./HighlightWidgetPlugin";
import { removeTagFromHighlight } from "./operations";
import { HighlightSidebar } from "./react/HighlightSidebar";
import { getPluginStateOrThrow as getEditablePluginStateOrThrow } from "../editable/EditablePlugin";

export interface PluginState {
  readonly container: HTMLDivElement;
  readonly visible: boolean;
  readonly width: number;
}

function pluginStateEq(a: PluginState, b: PluginState) {
  return a.visible === b.visible && a.width === b.width;
}

const { key, getPluginState, getPluginStateOrThrow, setPluginState } = new PluginDescriptor<PluginState>(
  "HighlightSidebarPlugin"
);
export { getPluginStateOrThrow, setPluginState };

export class HighlightSidebarPlugin extends Plugin<EditorSchema> {
  constructor(
    portalProviderApi: PortalProviderApi,
    generateTagURL: (tagId: string) => LocationDescriptor,
    onCopyHighlightURL?: (rangeId: string) => void,
    tagQuickEditRenderer?: (tagId: string, menuApi: MeatballMenuApi) => React.ReactNode
  ) {
    super({
      key,

      state: {
        init: (): PluginState => ({
          container: el(
            "div",
            style({
              position: "absolute",
              top: 0,
              left: "calc(100% + 16px)",
              height: "100%"
            })
          ),
          visible: true,
          width: 0
        }),
        apply(tr, cur: PluginState): PluginState {
          const nextPluginState = getPluginState(tr);
          return nextPluginState !== null ? nextPluginState : cur;
        }
      },

      view: view => {
        const initialPluginState = getPluginStateOrThrow(view.state);
        const initialRangePluginState = getRangePluginStateOrThrow(view.state);
        const { tagLookup: initialTagLookup } = getHighlightWidgetPluginStateOrThrow(view.state);
        const { editable: initialEditable } = getEditablePluginStateOrThrow(view.state);

        let raf: number | null = null;

        const recalculateWidth = () => {
          const prev = getPluginStateOrThrow(view.state);
          if (prev.width !== window!.innerWidth - prev.container.getBoundingClientRect().left - 16) {
            setPluginState(view, prev => ({
              ...prev,
              width: window!.innerWidth - prev.container.getBoundingClientRect().left - 16
            }));
          }
          raf = window!.requestAnimationFrame(recalculateWidth);
        };

        raf = window!.requestAnimationFrame(recalculateWidth);

        const container = initialPluginState.container;
        let parentTopOffset = 0;
        let initialWidth = 0;

        if (view.dom.parentElement !== null) {
          view.dom.parentElement.insertBefore(container, view.dom.parentElement.firstChild);
          initialWidth = window!.innerWidth - container.getBoundingClientRect().left - 16;
          parentTopOffset = view.dom.parentElement.getBoundingClientRect().top;
        }

        const highlightedRanges = initialRangePluginState.sortedRanges
          .filter<HighlightRange>((range): range is HighlightRange => range.type === RangeType.HIGHLIGHT)
          .map(range => ({ ...range, topOffset: view.coordsAtPos(range.from).top - parentTopOffset }));

        const onRemoveTag = ({ from, to, tagId }: { from: number; to: number; tagId: string }) =>
          view.dispatch(removeTagFromHighlight(view.state, { from, to }, tagId));
        const onFocusRange = (rangeId: string | null) => focusRange(rangeId)(view.state, view.dispatch);

        portalProviderApi.render(
          <HighlightSidebar
            highlights={highlightedRanges}
            width={initialWidth}
            tagLookup={initialTagLookup}
            onRemoveTag={initialEditable ? onRemoveTag : undefined}
            onCopyHighlightURL={onCopyHighlightURL}
            onFocusRange={onFocusRange}
            tagQuickEditRenderer={initialEditable ? tagQuickEditRenderer : undefined}
            generateTagURL={generateTagURL}
          />,
          container
        );

        return {
          update: (view, prevState) => {
            const prevPluginState = getPluginStateOrThrow(prevState);
            const newPluginState = getPluginStateOrThrow(view.state);
            const prevRangePluginState = getRangePluginStateOrThrow(prevState);
            const newRangePluginState = getRangePluginStateOrThrow(view.state);
            const { tagLookup } = getHighlightWidgetPluginStateOrThrow(view.state);
            const { editable } = getEditablePluginStateOrThrow(view.state);
            const onRemoveTag = ({ from, to, tagId }: { from: number; to: number; tagId: string }) =>
              view.dispatch(removeTagFromHighlight(view.state, { from, to }, tagId));
            const onFocusRange = (rangeId: string | null) => focusRange(rangeId)(view.state, view.dispatch);

            if (
              !pluginStateEq(prevPluginState, newPluginState) ||
              prevState.doc !== view.state.doc ||
              JSON.stringify(prevRangePluginState.sortedRanges) !== JSON.stringify(newRangePluginState)
            ) {
              if (view.dom.parentElement !== null) {
                parentTopOffset = view.dom.parentElement.getBoundingClientRect().top;
              }

              const highlightedRanges = newRangePluginState.sortedRanges
                .filter<HighlightRange>((range): range is HighlightRange => range.type === RangeType.HIGHLIGHT)
                .map(range => ({ ...range, topOffset: view.coordsAtPos(range.from).top - parentTopOffset }));

              portalProviderApi.render(
                <HighlightSidebar
                  highlights={highlightedRanges}
                  width={prevPluginState.width}
                  tagLookup={tagLookup}
                  onRemoveTag={editable ? onRemoveTag : undefined}
                  onCopyHighlightURL={onCopyHighlightURL}
                  onFocusRange={onFocusRange}
                  tagQuickEditRenderer={editable ? tagQuickEditRenderer : undefined}
                  generateTagURL={generateTagURL}
                />,
                container
              );
            }
          },
          destroy: () => {
            if (container.parentElement !== null) {
              container.parentElement.removeChild(container);
            }
            if (raf !== null) {
              window!.cancelAnimationFrame(raf);
            }

            portalProviderApi.destroy(container);
          }
        };
      }
    });
  }
}
