import * as React from "react";

import { Paper, PaperTransform } from "./paper";
import { PaperAreaContextTypes, PaperAreaContextWrapper } from "./paperArea";
import { Element, Cell } from "./elements";
import { ElementContextWrapper, ElementContextTypes } from "./elementLayer";
import { EventObserver } from "../viewUtils/events";

import { Vector, Rect } from "./geometry";
import { getContentFittingBox } from "./paperArea";

export interface State {
  paperWidth?: number;
  paperHeight?: number;
  offsetX?: number;
  offsetY?: number;
}

export class EmbeddedLayer extends React.Component<{}, State> {
  static contextTypes = { ...ElementContextTypes, ...PaperAreaContextTypes };

  context: ElementContextWrapper & PaperAreaContextWrapper;

  private readonly listener = new EventObserver();
  private nestedElementListener = new EventObserver();

  private layerOffsetLeft = 0;
  private layerOffsetTop = 0;

  private isApplyingParentMove = false;
  private isNestedElementMoving = false;
  private previousPositions: { id: string; position: Vector }[] = [];

  constructor(props: {}) {
    super(props);
    this.state = { paperWidth: 0, paperHeight: 0, offsetX: 0, offsetY: 0 };
  }

  componentDidMount() {
    const { element } = this.context.graphExplorerElement;
    const { paperArea, view } = this.context.paperArea;

    this.listener.listen(
      view.model.events,
      "changeGroupContent",
      ({ group }) => {
        if (group === element.id) {
          this.listenNestedElements(this.getNestedElements());
          const { offsetX, offsetY } = this.getOffset();
          this.moveNestedElements(offsetX, offsetY);
        }
      }
    );

    this.listener.listen(element.events, "changePosition", () => {
      if (this.isNestedElementMoving) {
        return;
      }

      const { offsetX, offsetY } = this.getOffset();
      const { x, y } = this.getContentFittingBox();

      const diffX = offsetX - x;
      const diffY = offsetY - y;
      this.moveNestedElements(diffX, diffY);

      this.setState({ offsetX, offsetY });
    });

    this.listener.listen(paperArea.events, "pointerUp", (_e) => {
      this.isNestedElementMoving = false;
    });

    const nestedElements = this.getNestedElements();
    this.listenNestedElements(nestedElements);

    if (nestedElements.length > 0) {
      const {
        x: offsetX,
        y: offsetY,
        width: paperWidth,
        height: paperHeight,
      } = this.getContentFittingBox();
      this.setState({ offsetX, offsetY, paperWidth, paperHeight }, () =>
        element.redraw()
      );
    } else {
      element.requestGroupContent();
    }
  }

  private listenNestedElements(nestedElements: readonly Element[]) {
    const listener = new EventObserver();
    for (const nestedElement of nestedElements) {
      listener.listen(
        nestedElement.events,
        "changePosition",
        this.recomputeSelfBounds
      );
      listener.listen(
        nestedElement.events,
        "changeSize",
        this.recomputeSelfBounds
      );
    }
    this.nestedElementListener.stopListening();
    this.nestedElementListener = listener;
  }

  componentWillUnmount() {
    this.listener.stopListening();
    this.nestedElementListener.stopListening();
    this.removeElements();
    this.setState({ paperWidth: 0, paperHeight: 0, offsetX: 0, offsetY: 0 });
  }

  private getNestedElements() {
    const { element } = this.context.graphExplorerElement;
    const { view } = this.context.paperArea;
    return view.model.elements.filter((el) => el.group === element.id);
  }

  private getContentFittingBox(): Rect {
    const nestedElements = this.getNestedElements();
    return getContentFittingBox(nestedElements, []);
  }

  private removeElements() {
    const { view } = this.context.paperArea;
    const batch = view.model.history.startBatch();
    for (const element of this.getNestedElements()) {
      view.model.removeElement(element.id);
    }
    batch.discard();
  }

  private getOffset(): { offsetX: number; offsetY: number } {
    const { element } = this.context.graphExplorerElement;
    const { x: elementX, y: elementY } = element.position;

    const offsetX = elementX + this.layerOffsetLeft;
    const offsetY = elementY + this.layerOffsetTop;

    return { offsetX, offsetY };
  }

  private moveNestedElements = (offsetX: number, offsetY: number) => {
    this.isApplyingParentMove = true;
    try {
      for (const element of this.getNestedElements()) {
        const { x, y } = element.position;
        const newPosition = { x: x + offsetX, y: y + offsetY };
        element.setPosition(newPosition);
      }
    } finally {
      this.isApplyingParentMove = false;
      this.recomputeSelfBounds();
    }
  };

  private recomputeSelfBounds = () => {
    if (this.isApplyingParentMove) {
      return;
    }

    const { element } = this.context.graphExplorerElement;
    const {
      x: offsetX,
      y: offsetY,
      width: paperWidth,
      height: paperHeight,
    } = this.getContentFittingBox();

    if (this.isNestedElementMoving) {
      const position = {
        x: offsetX - this.layerOffsetLeft,
        y: offsetY - this.layerOffsetTop,
      };
      element.setPosition(position);
    }

    this.setState({ offsetX, offsetY, paperWidth, paperHeight }, () =>
      element.redraw()
    );
  };

  private onPaperPointerDown = (
    e: React.MouseEvent<HTMLElement>,
    cell: Cell | undefined
  ) => {
    if (e.button !== 0 /* left mouse button */) {
      return;
    }

    if (cell && cell instanceof Element) {
      e.preventDefault();
      this.isNestedElementMoving = true;
    }
  };

  private calculateOffset(layer: HTMLElement): { left: number; top: number } {
    const { paperArea } = this.context.paperArea;
    const scale = paperArea.getScale();
    const parent = findParentElement(layer);
    const { left, top } = layer.getBoundingClientRect();
    const { left: parentLeft, top: parentTop } = parent.getBoundingClientRect();

    return {
      left: (left - parentLeft) / scale,
      top: (top - parentTop) / scale,
    };
  }

  private onLayerInit = (layer: HTMLElement) => {
    if (!layer) {
      return;
    }

    const { left, top } = this.calculateOffset(layer);

    this.layerOffsetLeft = left;
    this.layerOffsetTop = top;
  };

  render() {
    const { element } = this.context.graphExplorerElement;
    const { view } = this.context.paperArea;
    const { paperWidth, paperHeight, offsetX, offsetY } = this.state;

    const paperTransform: PaperTransform = {
      width: paperWidth,
      height: paperHeight,
      originX: -offsetX,
      originY: -offsetY,
      scale: 1,
      paddingX: 0,
      paddingY: 0,
    };

    return (
      <div className="graph-explorer-embedded-layer" ref={this.onLayerInit}>
        <Paper
          view={view}
          paperTransform={paperTransform}
          onPointerDown={this.onPaperPointerDown}
          group={element.id}
        ></Paper>
      </div>
    );
  }
}

function findParentElement(layer: HTMLElement): HTMLElement {
  const parent = layer.parentElement;
  if (!parent) {
    throw new Error("Cannot find parent diagram element for EmbeddedLayer");
  } else if (parent.hasAttribute("data-element-id")) {
    return parent;
  } else {
    return findParentElement(parent);
  }
}
