import { fsm2 } from "@heydovetail/ui-components";
import { Plugin, Transaction, TextSelection } from "prosemirror-state";
import { Decoration, DecorationSet } from "prosemirror-view";
import React from "react";
import { Observable } from "rxjs";
import { asap } from "rxjs/internal/scheduler/asap";
import { observeOn } from "rxjs/operators";
import { style } from "typestyle-react";
import { CssClassName, DEFAULT_HIGHLIGHT_COLOR } from "../constants";
import { getPluginStateOrThrow as getRangePluginStateOrThrow, PluginState as RangePluginState } from "../range/RangePlugin";
import { getPluginStateOrThrow as getEditablePluginStateOrThrow } from "../editable/EditablePlugin";
import { HighlightRange, RangeType } from "../range/types";
import { EditorSchema } from "../schema";
import { el, isKeyboardEvent } from "../util/dom";
import { PluginDescriptor } from "../util/PluginDescriptor";
import { PortalProviderApi } from "../util/PortalProvider";
import { removeTagFromHighlight, upsertTagToHighlight } from "./operations";
import { HighlightWidget } from "./react/HighlightWidget";

export interface TagGroup {
  id: string;
  title: string;
  color: string;
  tags: Tag[];
}

export interface Tag {
  id: string;
  title: string;
  color: string;
  count: number | null;
}

export enum WidgetPluginStateType {
  DISABLED = "disabled",
  IDLE = "idle",
  SELECTING = "selecting",
  TAGGING = "tagging"
}

export interface DisabledState {
  readonly type: WidgetPluginStateType.DISABLED;
}

export interface IdleState {
  readonly type: WidgetPluginStateType.IDLE;
}

export interface SelectingState {
  readonly type: WidgetPluginStateType.SELECTING;
  readonly selections: Set<"touch" | "mouse" | "keyboard">;
}

export interface TaggingState {
  readonly type: WidgetPluginStateType.TAGGING;
  readonly selection: { from: number; to: number };
}

export type WidgetPluginState = DisabledState | IdleState | SelectingState | TaggingState;

export const transitionWidget = fsm2.createTransition<WidgetPluginState>()({
  disabled: {
    toggleActive: (_prev, selection: TaggingState["selection"] | null) =>
      selection === null
        ? {
            type: WidgetPluginStateType.IDLE
          }
        : { type: WidgetPluginStateType.TAGGING, selection },
    tr: (_prev, _: { tr: Transaction; rangePluginState: RangePluginState }) => _prev
  },
  idle: {
    updateSelection: (_prev, opts: { trigger: "touch" | "mouse" | "keyboard"; action: "add" | "remove" }) => ({
      type: WidgetPluginStateType.SELECTING,
      selections: opts.action === "add" ? new Set([opts.trigger]) : new Set()
    }),
    idle: _prev => _prev,
    tagging: (_prev, selection: { from: number; to: number }) => ({ type: WidgetPluginStateType.TAGGING, selection }),
    toggleActive: (_prev, _: TaggingState["selection"] | null) => ({
      type: WidgetPluginStateType.DISABLED
    }),
    tr: (_prev, { tr, rangePluginState }: { tr: Transaction; rangePluginState: RangePluginState }) => {
      if (tr.selection.empty && !tr.docChanged) {
        const cursorPos = tr.selection.from;
        const range = rangePluginState.sortedRanges.find(r => r.from < cursorPos && r.to > cursorPos);
        if (range !== undefined) {
          return { type: WidgetPluginStateType.TAGGING, selection: { from: range.from, to: range.to } };
        }
      }
      return _prev;
    }
  },
  selecting: {
    idle: () => ({ type: WidgetPluginStateType.IDLE }),
    updateSelection: (_prev, opts: { trigger: "touch" | "mouse" | "keyboard"; action: "add" | "remove" }) => {
      const selections = new Set(_prev.selections);
      if (opts.action === "add") {
        selections.add(opts.trigger);
      } else {
        selections.delete(opts.trigger);
      }
      return {
        type: WidgetPluginStateType.SELECTING,
        selections
      };
    },
    tagging: (_prev, selection: { from: number; to: number }) => ({ type: WidgetPluginStateType.TAGGING, selection }),
    toggleActive: (_prev, _: TaggingState["selection"] | null) => ({
      type: WidgetPluginStateType.DISABLED
    }),
    tr: (_prev, _: { tr: Transaction; rangePluginState: RangePluginState }) => _prev
  },
  tagging: {
    idle: () => ({ type: WidgetPluginStateType.IDLE }),
    updateSelection: (_prev, opts: { trigger: "touch" | "mouse" | "keyboard"; action: "add" | "remove" }) => ({
      type: WidgetPluginStateType.SELECTING,
      selections: opts.action === "add" ? new Set([opts.trigger]) : new Set()
    }),
    toggleActive: (_prev, _: TaggingState["selection"] | null) => ({
      type: WidgetPluginStateType.DISABLED
    }),
    tr: (_prev, { tr, rangePluginState }: { tr: Transaction; rangePluginState: RangePluginState }) => {
      const {
        selection: { from: prevMarkRangeFrom, to: prevMarkRangeTo }
      } = _prev;
      const { deleted: markRangeFromDeleted, pos: newMarkRangeFrom } = tr.mapping.mapResult(prevMarkRangeFrom, 1);
      const { deleted: markRangeToDeleted, pos: newMarkRangeTo } = tr.mapping.mapResult(prevMarkRangeTo, -1);

      if (!markRangeFromDeleted && !markRangeToDeleted) {
        if (
          !tr.selection.empty &&
          tr.selection instanceof TextSelection &&
          tr.selection.from === newMarkRangeFrom &&
          tr.selection.to === newMarkRangeTo
        ) {
          return { type: WidgetPluginStateType.TAGGING, selection: { from: newMarkRangeFrom, to: newMarkRangeTo } };
        }
      }

      if (tr.selection.empty && !tr.docChanged) {
        const cursorPos = tr.selection.from;
        const range = rangePluginState.sortedRanges.find(r => r.from < cursorPos && r.to > cursorPos);
        if (range !== undefined) {
          return { type: WidgetPluginStateType.TAGGING, selection: { from: range.from, to: range.to } };
        }
      }

      return { type: WidgetPluginStateType.IDLE };
    }
  }
});

export interface PluginState {
  widgetState: WidgetPluginState;
  tagGroups: TagGroup[];
  tagLookup: Map<Tag["id"], Tag>;
}

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

export { getPluginStateOrThrow, getPluginState, setPluginState };

const highlightClass = style({
  $nest: {
    ".ProseMirror:not(.ProseMirror-focused) &": {
      backgroundColor: DEFAULT_HIGHLIGHT_COLOR,
      padding: "5px 0"
    }
  }
});

export class HighlightWidgetPlugin extends Plugin<EditorSchema> {
  constructor(
    portalProviderApi: PortalProviderApi,
    getTagGroups: () => Observable<TagGroup[]>,
    createTag: (args: { groupId: string | null; title: string }) => Promise<Tag>
  ) {
    super({
      key,

      state: {
        init: (_, state): PluginState => {
          const { editable } = getEditablePluginStateOrThrow(state);
          return {
            widgetState: {
              type: editable ? WidgetPluginStateType.IDLE : WidgetPluginStateType.DISABLED
            } as DisabledState | IdleState,
            tagGroups: [],
            tagLookup: new Map<Tag["id"], Tag>()
          };
        },
        apply: (tr, curr: PluginState, _oldState, newState): PluginState => {
          const nextPluginState = getPluginState(tr);
          const rangePluginState = getRangePluginStateOrThrow(newState);

          return nextPluginState !== null
            ? nextPluginState
            : {
                ...curr,
                widgetState: transitionWidget(curr.widgetState).tr({ tr, rangePluginState })
              };
        }
      },

      view: view => {
        const container = el("div");
        const $tagGroups = getTagGroups()
          .pipe(observeOn(asap))
          .subscribe(tagGroups => {
            const tagLookup = new Map(
              tagGroups.reduce<Tag[]>((accum, tg) => [...accum, ...tg.tags], []).map(tag => [tag.id, tag] as [string, Tag])
            );
            setPluginState(view, prevPluginState => ({
              ...prevPluginState,
              tagLookup,
              tagGroups
            }));
          });

        if (view.dom.parentElement !== null) {
          view.dom.parentElement.insertBefore(container, view.dom.parentElement.firstChild);
        }

        return {
          update: view => {
            const state = view.state;
            const { widgetState, tagGroups, tagLookup } = getPluginStateOrThrow(state);
            const rangePluginState = getRangePluginStateOrThrow(state);

            if (view.dom.parentElement !== null && widgetState.type === WidgetPluginStateType.TAGGING) {
              // Otherwise, reposition it and update its content
              const { from, to } = widgetState.selection;
              // These are in screen coordinates
              const start = view.coordsAtPos(from);
              const end = view.coordsAtPos(to);
              const left = Math.max((start.left + end.left) / 2, start.left + 3);

              const highlightedRanges = rangePluginState.sortedRanges.filter<HighlightRange>(
                (range): range is HighlightRange => from === range.from && to === range.to && range.type === RangeType.HIGHLIGHT
              );

              const currentTags = highlightedRanges
                .reduce<string[]>((accum, r) => [...accum, ...r.attrs.tagIds], [])
                .map(id => tagLookup.get(id))
                .filter((tag): tag is Tag => tag !== undefined);

              portalProviderApi.render(
                <HighlightWidget
                  anchorPosition={{
                    x: left - view.dom.parentElement.offsetLeft,
                    y: start.top - view.dom.parentElement.getBoundingClientRect().top
                  }}
                  anchorHeight={end.bottom - start.top}
                  currentTags={currentTags}
                  createTag={options => createTag(options)}
                  tagGroups={tagGroups}
                  selection={{ from, to }}
                  onDismiss={() => {
                    setPluginState(view, existing => ({
                      ...existing,
                      widgetState: transitionWidget(widgetState).idle()
                    }));
                    view.focus();
                  }}
                  onAddTag={({ id, from, to }) => view.dispatch(upsertTagToHighlight(view.state, { from, to }, id))}
                  onRemoveTag={({ id, from, to }) => view.dispatch(removeTagFromHighlight(view.state, { from, to }, id))}
                />,
                container
              );
            } else {
              portalProviderApi.destroy(container);
            }
          },
          destroy: () => {
            $tagGroups.unsubscribe();

            if (container.parentElement !== null) {
              container.parentElement.removeChild(container);
            }
          }
        };
      },

      props: {
        attributes(state) {
          const { widgetState } = getPluginStateOrThrow(state);
          return widgetState.type !== WidgetPluginStateType.DISABLED ? { class: CssClassName.TAGGING_SELECTION_COLOR } : null;
        },
        decorations(state) {
          const { widgetState } = getPluginStateOrThrow(state);
          const rangePluginState = getRangePluginStateOrThrow(state);

          if (widgetState.type === WidgetPluginStateType.TAGGING) {
            const range = rangePluginState.sortedRanges.find(
              r => r.from === widgetState.selection.from && r.to === widgetState.selection.to && r.type === RangeType.HIGHLIGHT
            );
            if (range === undefined) {
              return DecorationSet.create(state.doc, [
                Decoration.inline(widgetState.selection.from, widgetState.selection.to, {
                  class: highlightClass
                })
              ]);
            }
          }
          return null;
        },
        handleDOMEvents: {
          mousedown: view => {
            const { widgetState: prevWidgetState } = getPluginStateOrThrow(view.state);
            if (prevWidgetState.type !== WidgetPluginStateType.DISABLED) {
              setPluginState(view, existing => ({
                ...existing,
                widgetState: transitionWidget(prevWidgetState).updateSelection({ trigger: "mouse", action: "add" })
              }));

              // Add window event listener for up event
              const root = view.root as DocumentFragment;
              root.addEventListener("mouseup", function mouseup() {
                setPluginState(view, existing => {
                  let updated: WidgetPluginState = transitionWidget(prevWidgetState).updateSelection({
                    trigger: "mouse",
                    action: "remove"
                  });
                  if (
                    updated.selections.size === 0 &&
                    !view.state.selection.empty &&
                    view.state.selection instanceof TextSelection
                  ) {
                    updated = transitionWidget(updated).tagging(view.state.selection);
                  } else if (updated.selections.size === 0 && view.state.selection.empty) {
                    updated = transitionWidget(updated).idle();
                  }
                  return {
                    ...existing,
                    widgetState: updated
                  };
                });
                root.removeEventListener("mouseup", mouseup);
              });
            }

            return false;
          },
          touchstart: view => {
            const { widgetState: prevWidgetState } = getPluginStateOrThrow(view.state);
            if (prevWidgetState.type !== WidgetPluginStateType.DISABLED) {
              setPluginState(view, existing => ({
                ...existing,
                widgetState: transitionWidget(prevWidgetState).updateSelection({ trigger: "touch", action: "add" })
              }));

              // Add window event listener for end event
              const root = view.root as DocumentFragment;
              root.addEventListener("touchend", function touchend() {
                setPluginState(view, existing => {
                  let updated: WidgetPluginState = transitionWidget(prevWidgetState).updateSelection({
                    trigger: "touch",
                    action: "remove"
                  });
                  if (
                    updated.selections.size === 0 &&
                    !view.state.selection.empty &&
                    view.state.selection instanceof TextSelection
                  ) {
                    updated = transitionWidget(updated).tagging(view.state.selection);
                  } else if (updated.selections.size === 0 && view.state.selection.empty) {
                    updated = transitionWidget(updated).idle();
                  }
                  return {
                    ...existing,
                    widgetState: updated
                  };
                });
                root.removeEventListener("touchend", touchend);
              });
            }

            return false;
          },
          keydown: (view, event) => {
            const { widgetState: prevWidgetState } = getPluginStateOrThrow(view.state);
            if (prevWidgetState.type !== WidgetPluginStateType.DISABLED) {
              if (isKeyboardEvent(event) && event.key === "Shift") {
                setPluginState(view, existing => ({
                  ...existing,
                  widgetState: transitionWidget(prevWidgetState).updateSelection({ trigger: "keyboard", action: "add" })
                }));
              }

              // Add window event listener for up state
              const root = view.root as DocumentFragment;
              root.addEventListener("keyup", function keyup(event) {
                if (isKeyboardEvent(event) && event.key === "Shift") {
                  setPluginState(view, existing => {
                    let updated: WidgetPluginState = transitionWidget(prevWidgetState).updateSelection({
                      trigger: "keyboard",
                      action: "remove"
                    });
                    if (
                      updated.selections.size === 0 &&
                      !view.state.selection.empty &&
                      view.state.selection instanceof TextSelection
                    ) {
                      updated = transitionWidget(updated).tagging(view.state.selection);
                    } else if (updated.selections.size === 0 && view.state.selection.empty) {
                      updated = transitionWidget(updated).idle();
                    }
                    return {
                      ...existing,
                      widgetState: updated
                    };
                  });
                  root.removeEventListener("keyup", keyup);
                }
              });
            }

            return false;
          }
        }
      }
    });
  }
}
