import { fsm2 } from "@heydovetail/ui-components";
import { Node } from "prosemirror-model";
import { Plugin, Transaction, EditorState } from "prosemirror-state";
import React from "react";
import * as pquery from "../pquery";
import { DocPos, DocPosRange } from "../types";
import { el } from "../util/dom";
import { PluginDescriptor } from "../util/PluginDescriptor";
import { PortalProviderApi } from "../util/PortalProvider";
import { safeUrl } from "../util/safeUrl";
import { LinkEditor } from "./react/LinkEditor";
import { EditorSchema } from "../schema";

export enum LinkPluginStateType {
  IDLE = "idle",
  SHOW_DETAIL = "showDetail",
  SHOW_EDITOR = "showEditor",
  SHOW_COMPOSER = "showComposer"
}

export interface Link {
  readonly text: string;
  readonly url: string;
  readonly range: DocPosRange;
}

export interface LinkDraft {
  readonly markRange: DocPosRange;
  // The text and range are not necessarily the same as the mark range for the
  // compose case. If a selection spans multiple blocks, we *could*
  // concatenate the text from all the blocks together and display that, but
  // we wouldn't be able to map edits back onto the document.
  //
  // The approach we use instead follows Google's Inbox editor, where the text
  // is taken from the last block in the selection, and edits only apply to
  // that block.
  readonly text: string;
  readonly textRange: DocPosRange;
}

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

export interface ShowDetailState {
  readonly type: LinkPluginStateType.SHOW_DETAIL;
  readonly link: Link;
}

export interface ShowEditorState {
  readonly type: LinkPluginStateType.SHOW_EDITOR;
  readonly link: Link;
}

export interface ShowComposerState {
  readonly type: LinkPluginStateType.SHOW_COMPOSER;
  readonly draft: LinkDraft;
}

export type PluginState = IdleState | ShowDetailState | ShowEditorState | ShowComposerState;

function pluginStateEq(a: PluginState, b: PluginState) {
  return JSON.stringify(a) === JSON.stringify(b);
}

export const transition = fsm2.createTransition<PluginState>()({
  idle: {
    showDetail: (_prev, link: Link) => ({ type: LinkPluginStateType.SHOW_DETAIL, link }),
    showEditor: (_prev, link: Link) => ({ type: LinkPluginStateType.SHOW_EDITOR, link }),
    showComposer: (_prev, draft: LinkDraft) => ({ type: LinkPluginStateType.SHOW_COMPOSER, draft }),
    tr: (_prev, tr: Transaction) => pickIdleOrDetail(tr)
  },
  showDetail: {
    idle: () => ({ type: LinkPluginStateType.IDLE }),
    showDetail: (_prev, link: Link) => ({ type: LinkPluginStateType.SHOW_DETAIL, link }),
    showEditor: (_prev, link: Link) => ({ type: LinkPluginStateType.SHOW_EDITOR, link }),
    tr: (_prev, tr: Transaction) => pickIdleOrDetail(tr)
  },
  showEditor: {
    idle: () => ({ type: LinkPluginStateType.IDLE }),
    showEditor: (_prev, link: Link) => ({ type: LinkPluginStateType.SHOW_EDITOR, link }),
    tr: (_prev, tr: Transaction) => {
      const link = getAdjacentLinkFromSelection(tr);
      return link === null ? { type: LinkPluginStateType.IDLE } : { type: LinkPluginStateType.SHOW_EDITOR, link };
    }
  },
  showComposer: {
    idle: () => ({ type: LinkPluginStateType.IDLE }),
    showComposer: (_prev, draft: LinkDraft) => ({ type: LinkPluginStateType.SHOW_COMPOSER, draft }),
    tr: (prev, tr: Transaction) => {
      const {
        draft: {
          markRange: { 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) {
        const draft = tryComposeAttemptOnRange(tr.doc, { from: newMarkRangeFrom as DocPos, to: newMarkRangeTo as DocPos });
        if (draft !== null) {
          return { type: LinkPluginStateType.SHOW_COMPOSER, draft };
        }
      }

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

export function tryComposeAttemptOnRange(doc: Node, range: DocPosRange): LinkDraft | null {
  const textRange = pquery.contiguousTextRange(doc.resolve(range.from), doc.resolve(range.to));
  return textRange === null
    ? null
    : {
        markRange: {
          from: range.from,
          to: range.to
        },
        text: doc.textBetween(textRange.from, textRange.to),
        textRange: {
          from: textRange.from as DocPos,
          to: textRange.to as DocPos
        }
      };
}

export function pickIdleOrDetail({
  selection,
  doc
}: Pick<EditorState<EditorSchema>, "selection" | "doc">): IdleState | ShowDetailState {
  const link = getAdjacentLinkFromSelection({ selection, doc });
  return link === null ? { type: LinkPluginStateType.IDLE } : { type: LinkPluginStateType.SHOW_DETAIL, link };
}

export function getAdjacentLinkFromSelection({
  selection,
  doc
}: Pick<EditorState<EditorSchema>, "selection" | "doc">): Link | null {
  const searchResult = pquery.adjacentMarkBounds(selection.$from, doc.type.schema.marks["l"]);
  if (searchResult !== null) {
    const { mark, from, to } = searchResult;
    const text = doc.textBetween(from, to);
    const url = safeUrl(mark.attrs["u"]);
    return { range: { from: from as DocPos, to: to as DocPos }, text, url };
  }
  return null;
}

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

export class LinkPlugin extends Plugin {
  constructor(options: { portalProviderApi: PortalProviderApi; initialState?: PluginState }) {
    const { initialState = { type: LinkPluginStateType.IDLE }, portalProviderApi } = options;
    super({
      key,

      state: {
        init() {
          return initialState;
        },

        apply(tr, cur: PluginState): PluginState {
          const nextPluginState = getPluginState(tr);
          if (nextPluginState !== null) {
            return nextPluginState;
          }

          if (tr.docChanged || tr.selectionSet) {
            return transition(cur).tr(tr);
          }

          return cur;
        }
      },

      view: view => {
        const container = view.dom.appendChild(el("div"));
        portalProviderApi.render(<LinkEditor view={view} />, container);

        return {
          update: (view, prevState) => {
            const prevPluginState = getPluginStateOrThrow(prevState);
            const newPluginState = getPluginStateOrThrow(view.state);
            if (!pluginStateEq(prevPluginState, newPluginState)) {
              portalProviderApi.render(<LinkEditor view={view} />, container);
            }
          },
          destroy: () => {
            if (container.parentElement !== null) {
              container.parentElement.removeChild(container);
            }
            portalProviderApi.destroy(container);
          }
        };
      }
    });
  }
}
