import { Node } from "prosemirror-model";
import { AllSelection, Selection, TextSelection } from "prosemirror-state";
import { EditorView, NodeView } from "prosemirror-view";
import React from "react";
import { BehaviorSubject, Observable, Subscription } from "rxjs";
import { map } from "rxjs/operators";
import { CssClassName } from "../constants";
import { EditorSchema } from "../schema";
import { CellSelection } from "../table/CellSelection";
import { el } from "../util/dom";
import { PortalProviderApi } from "../util/PortalProvider";
import { AttachmentService } from "./AttachmentService";
import * as operations from "./operations";
import { AttachmentPreview } from "./react/AttachmentPreview";
import { AttachmentAttrs } from "./schema";
import { decode } from "./util/attrs";

interface State {
  active: boolean;
  editable: boolean;
  node: Node;
  selected: boolean;
}

export class AttachmentView implements NodeView {
  public readonly dom = el("div", CssClassName.ATTACHMENT);

  private readonly editableSubscription: Subscription;
  private readonly state: BehaviorSubject<State>;
  private readonly selectionSubscription: Subscription;
  private readonly renderSubscription: Subscription;
  private readonly portalProviderApi: PortalProviderApi;

  constructor(
    node: Node,
    view: EditorView<EditorSchema>,
    getPos: () => number,
    portalProviderApi: PortalProviderApi,
    attachmentService: AttachmentService,
    selection$: Observable<Selection<EditorSchema> | null>,
    editable$: Observable<boolean>
  ) {
    this.portalProviderApi = portalProviderApi;
    this.state = new BehaviorSubject<State>({
      active: false,
      editable: false,
      node,
      selected: false
    });

    this.selectionSubscription = selection$
      .pipe(
        map(selection => {
          if (selection instanceof TextSelection || selection instanceof CellSelection) {
            const pos = getPos();
            return pos >= selection.from && pos <= selection.to;
          } else if (selection instanceof AllSelection) {
            return true;
          } else {
            return false;
          }
        })
      )
      .subscribe(inSelection => this.setState({ selected: inSelection }));

    this.editableSubscription = editable$.subscribe(newEditable => {
      if (this.state.value.editable !== newEditable) {
        this.setState({ editable: newEditable });
      }
    });

    this.renderSubscription = this.state.subscribe(({ editable, node, active, selected }) => {
      const attachment = decode(node.attrs as AttachmentAttrs);
      const style = active ? "active" : selected ? "selected" : undefined;
      this.portalProviderApi.render(
        <AttachmentPreview
          attachment={attachment}
          attachmentService={attachmentService}
          editable={editable}
          onHidePreview={() => {
            const tr = view.state.tr;
            operations.toggleHidePreview(tr, getPos());
            view.dispatch(tr);
          }}
          onRemove={() => {
            const tr = view.state.tr;
            operations.remove(tr, getPos());
            view.dispatch(tr);
          }}
          onShowPreview={() => {
            const tr = view.state.tr;
            operations.toggleHidePreview(tr, getPos());
            view.dispatch(tr);
          }}
          style={style}
        />,
        this.dom
      );
    });
  }

  public update(node: Node) {
    if (node.type !== this.state.value.node.type) {
      return false;
    }
    this.setState({ node });
    return true;
  }

  public selectNode() {
    this.setState({ active: true });
  }

  public deselectNode() {
    this.setState({ active: false });
  }

  public destroy() {
    this.portalProviderApi.destroy(this.dom);
    this.selectionSubscription.unsubscribe();
    this.editableSubscription.unsubscribe();
    this.renderSubscription.unsubscribe();
  }

  private setState(patch: Partial<State>): void {
    this.state.next({ ...this.state.value, ...patch });
  }
}
