///////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2002-2025, Open Design Alliance (the "Alliance").
// All rights reserved.
//
// This software and its documentation and related materials are owned by
// the Alliance. The software may only be incorporated into application
// programs owned by members of the Alliance, subject to a signed
// Membership Agreement and Supplemental Software License Agreement with the
// Alliance. The structure and organization of this software are the valuable
// trade secrets of the Alliance and its suppliers. The software is also
// protected by copyright law and international treaty provisions. Application
// programs incorporating this software must include the following statement
// with their copyright notices:
//
//   This application incorporates Open Design Alliance software pursuant to a
//   license agreement with Open Design Alliance.
//   Open Design Alliance Copyright (C) 2002-2025 by Open Design Alliance.
//   All rights reserved.
//
// By use of this software, its documentation or related materials, you
// acknowledge and accept the above terms.
///////////////////////////////////////////////////////////////////////////////

import Konva from "konva";
import { IEventEmitter } from "@inweb/eventemitter2";
import {
  ChangeActiveDraggerEvent,
  IArrow,
  ICloud,
  IEllipse,
  IImage,
  ILine,
  IRectangle,
  IText,
  IViewpoint,
  PanEvent,
  ZoomAtEvent,
} from "@inweb/viewer-core";

import { IMarkup, MarkupMode } from "../IMarkup";
import { IWorldTransform } from "../IWorldTransform";
import { WorldTransform } from "../WorldTransform";
import { IMarkupObject } from "../IMarkupObject";
import { MarkupLineType } from "../IMarkupLine";
import { MarkupColor } from "./MarkupColor";
import { KonvaLine } from "./KonvaLine";
import { KonvaText } from "./KonvaText";
import { KonvaRectangle } from "./KonvaRectangle";
import { KonvaEllipse } from "./KonvaEllipse";
import { KonvaArrow } from "./KonvaArrow";
import { KonvaImage } from "./KonvaImage";
import { KonvaCloud } from "./KonvaCloud";

const MarkupMode2Konva = {
  SelectMarkup: {
    name: "SelectMarkup",
    initializer: null,
  },
  Line: {
    name: "Line",
    initializer: (ref: any, params = null) => new KonvaLine(params, ref),
  },
  Text: {
    name: "Text",
    initializer: (ref: any, params = null) => new KonvaText(params, ref),
  },
  Rectangle: {
    name: "Rect",
    initializer: (ref: any, params = null) => new KonvaRectangle(params, ref),
  },
  Ellipse: {
    name: "Ellipse",
    initializer: (ref: any, params = null) => new KonvaEllipse(params, ref),
  },
  Arrow: {
    name: "Arrow",
    initializer: (ref: any, params = null) => new KonvaArrow(params, ref),
  },
  Image: {
    name: "Image",
    initializer: (ref: any, params = null) => new KonvaImage(params, ref),
  },
  Cloud: {
    name: "Cloud",
    initializer: (ref: any, params = null) => new KonvaCloud(params, ref),
  },
};

/**
 * 2D markup core.
 */
export class KonvaMarkup implements IMarkup {
  private _viewer: IEventEmitter;
  private _worldTransformer: IWorldTransform;
  private _container: HTMLElement;
  private _containerEvents: string[] = [];
  private _markupIsActive = false;
  private _markupMode: MarkupMode;
  private _markupColor = new MarkupColor(255, 0, 0);
  private _konvaStage: Konva.Stage;
  private _konvaLayer: Konva.Layer;
  private _konvaTransformer: Konva.Transformer;

  private _textInputRef: HTMLTextAreaElement;
  private _textInputPos: Konva.Vector2d;
  private _textInputAngle: number;
  private _imageInputRef: HTMLInputElement;
  private _imageInputPos: Konva.Vector2d;
  private _markupContainer: HTMLDivElement;
  private _resizeObserver: ResizeObserver;
  private _groupImages: Konva.Group;
  private _groupGeometry: Konva.Group;
  private _groupTexts: Konva.Group;

  public lineWidth = 4;
  public lineType: MarkupLineType = "solid";
  public fontSize = 34;

  initialize(
    container: HTMLElement,
    containerEvents?: string[],
    viewer?: IEventEmitter,
    worldTransformer?: IWorldTransform
  ): void {
    if (!Konva)
      throw new Error(
        'Markup error: Konva is not initialized. Forgot to add <script src="https://unpkg.com/konva@9/konva.min.js"></script> to your page?'
      );

    this._viewer = viewer;
    this._worldTransformer = worldTransformer ?? new WorldTransform();
    this._container = container;
    this._containerEvents = containerEvents ?? [];

    this._markupContainer = document.createElement("div");
    this._markupContainer.id = "markup-container";
    this._markupContainer.style.position = "absolute";
    this._markupContainer.style.top = "0px";
    this._markupContainer.style.left = "0px";
    this._markupContainer.style.outline = "0px"; // <- to eliminate grey box during delete elements
    this._markupContainer.style.pointerEvents = "none";

    const parentDiv = this._container.parentElement;
    parentDiv.appendChild(this._markupContainer);

    this._resizeObserver = new ResizeObserver(this.resizeContainer);
    this._resizeObserver.observe(parentDiv);

    this._markupColor.setColor(255, 0, 0);

    this.initializeKonva();

    if (this._viewer) {
      // this._containerEvents.forEach((x) => this._markupContainer.addEventListener(x, this.redirectToViewer));
      this._viewer.addEventListener("changeactivedragger", this.changeActiveDragger);
      this._viewer.addEventListener("pan", this.pan);
      this._viewer.addEventListener("zoomat", this.zoomAt);
    }
  }

  dispose(): void {
    if (this._viewer) {
      this._viewer.removeEventListener("zoomat", this.zoomAt);
      this._viewer.removeEventListener("pan", this.pan);
      this._viewer.removeEventListener("changeactivedragger", this.changeActiveDragger);
      // this._containerEvents.forEach((x) => this._markupContainer.removeEventListener(x, this.redirectToViewer));
    }

    this.destroyKonva();

    this._resizeObserver?.disconnect();
    this._resizeObserver = undefined;

    this._markupContainer?.remove();
    this._markupContainer = undefined;

    this._container = undefined;
    this._viewer = undefined;
    this._worldTransformer = undefined;

    this._markupIsActive = false;
  }

  changeActiveDragger = (event: ChangeActiveDraggerEvent) => {
    const draggerName = event.data;

    this._markupContainer.className = this._container.className
      .split(" ")
      .filter((x) => !x.startsWith("oda-cursor-"))
      .filter((x) => x)
      .concat(`oda-cursor-${draggerName.toLowerCase()}`)
      .join(" ");

    this.removeTextInput();
    this.removeImageInput();

    this.enableEditMode(draggerName as MarkupMode);
  };

  resizeContainer = (entries: ResizeObserverEntry[]) => {
    const { width, height } = entries[0].contentRect;

    if (!width || !height) return; // <- invisible container, or container with parent removed
    if (!this._konvaStage) return;

    this._konvaStage.width(width);
    this._konvaStage.height(height);
  };

  pan = (event: PanEvent) => {
    const newPos = {
      x: this._konvaStage.x() + event.dX,
      y: this._konvaStage.y() + event.dY,
    };
    this._konvaStage.position(newPos);
  };

  zoomAt = (event: ZoomAtEvent) => {
    const newScale = this._konvaStage.scaleX() * event.data;
    this._konvaStage.scale({ x: newScale, y: newScale });

    const newPos = {
      x: event.x - (event.x - this._konvaStage.x()) * event.data,
      y: event.y - (event.y - this._konvaStage.y()) * event.data,
    };
    this._konvaStage.position(newPos);
  };

  redirectToViewer = (event: any) => {
    if (this._viewer) this._viewer.emit(event);
  };

  syncOverlay(): void {}

  clearOverlay(): void {
    this.removeTextInput();
    this.removeImageInput();
    this.clearSelected();
    this.getObjects().forEach((obj) => obj.delete());
  }

  getMarkupColor(): { r: number; g: number; b: number } {
    return this._markupColor.asRGB();
  }

  setMarkupColor(r: number, g: number, b: number): void {
    this._markupColor.setColor(r, g, b);
    this.redirectToViewer({ type: "changemarkupcolor", data: { r, g, b } });
  }

  colorizeAllMarkup(r: number, g: number, b: number): void {
    const hexColor = new MarkupColor(r, g, b).asHex();
    this.getObjects().filter((obj: any) => obj.setColor?.(hexColor));
  }

  colorizeSelectedMarkups(r: number, g: number, b: number): void {
    const hexColor = new MarkupColor(r, g, b).asHex();
    this.getSelectedObjects().filter((obj: any) => obj.setColor?.(hexColor));
  }

  setViewpoint(viewpoint: IViewpoint): void {
    this.clearSelected();
    this.removeTextInput();
    this.removeImageInput();
    this._konvaStage.scale({ x: 1, y: 1 });
    this._konvaStage.position({ x: 0, y: 0 });

    const markupColor = viewpoint.custom_fields?.markup_color || { r: 255, g: 0, b: 0 };
    this.setMarkupColor(markupColor.r, markupColor.g, markupColor.b);

    viewpoint.lines?.forEach((line: ILine) => {
      const linePoints = [];
      line.points.forEach((point) => {
        const screenPoint = this._worldTransformer.worldToScreen(point);
        linePoints.push(screenPoint.x);
        linePoints.push(screenPoint.y);
      });
      this.addLine(linePoints, line.color, line.type as MarkupLineType, line.width, line.id);
    });

    viewpoint.texts?.forEach((text: IText) => {
      const screenPoint = this._worldTransformer.worldToScreen(text.position);
      this.addText(text.text, screenPoint, text.angle, text.color, text.text_size, text.font_size, text.id);
    });

    viewpoint.rectangles?.forEach((rect: IRectangle) => {
      const screenPoint = this._worldTransformer.worldToScreen(rect.position);
      this.addRectangle(screenPoint, rect.width, rect.height, rect.line_width, rect.color, rect.id);
    });

    viewpoint.ellipses?.forEach((ellipse: IEllipse) => {
      const screenPoint = this._worldTransformer.worldToScreen(ellipse.position);
      this.addEllipse(screenPoint, ellipse.radius, ellipse.line_width, ellipse.color, ellipse.id);
    });

    viewpoint.arrows?.forEach((arrow: IArrow) => {
      const startPoint = this._worldTransformer.worldToScreen(arrow.start);
      const endPoint = this._worldTransformer.worldToScreen(arrow.end);
      this.addArrow(startPoint, endPoint, arrow.color, arrow.id);
    });

    viewpoint.clouds?.forEach((cloud: ICloud) => {
      const screenPoint = this._worldTransformer.worldToScreen(cloud.position);
      this.addCloud(screenPoint, cloud.width, cloud.height, cloud.line_width, cloud.color, cloud.id);
    });

    viewpoint.images?.forEach((image: IImage) => {
      const screenPoint = this._worldTransformer.worldToScreen(image.position);
      this.addImage(screenPoint, image.src, image.width, image.height, image.id);
    });
  }

  getViewpoint(viewpoint: IViewpoint): IViewpoint {
    if (!viewpoint) viewpoint = {};

    viewpoint.lines = this.getMarkupLines();
    viewpoint.texts = this.getMarkupTexts();
    viewpoint.arrows = this.getMarkupArrows();
    viewpoint.clouds = this.getMarkupClouds();
    viewpoint.ellipses = this.getMarkupEllipses();
    viewpoint.images = this.getMarkupImages();
    viewpoint.rectangles = this.getMarkupRectangles();

    viewpoint.custom_fields = { markup_color: this.getMarkupColor() };
    viewpoint.snapshot = { data: this.combineMarkupWithDrawing() };

    return viewpoint;
  }

  enableEditMode(mode: MarkupMode | false): this {
    if (!mode || !MarkupMode2Konva[mode]) {
      this.clearSelected();
      this.removeTextInput();
      this.removeImageInput();
      this._markupContainer.style.pointerEvents = "none";
      this._markupIsActive = false;
    } else {
      this._markupMode = mode;
      this._markupContainer.style.pointerEvents = "all";
      this._markupIsActive = true;
    }
    return this;
  }

  createObject(type: string, params: any): IMarkupObject {
    const konvaShape = MarkupMode2Konva[type];
    if (!konvaShape || !konvaShape.initializer)
      throw new Error(`Markup CreateObject - unsupported markup type ${type}`);

    const object = konvaShape.initializer(null, params);
    this.addObject(object);

    return object;
  }

  getObjects(): IMarkupObject[] {
    const objects = [];
    Object.keys(MarkupMode2Konva).forEach((type) => {
      const konvaShape = MarkupMode2Konva[type];
      this.konvaLayerFind(type).forEach((ref) => objects.push(konvaShape.initializer(ref)));
    });
    return objects;
  }

  getSelectedObjects(): IMarkupObject[] {
    if (!this._konvaTransformer) return [];

    return this._konvaTransformer
      .nodes()
      .map((ref) => {
        const name = ref.className;
        const konvaShape = Object.values(MarkupMode2Konva).find((shape) => shape.name === name);
        return konvaShape ? konvaShape.initializer(ref) : null;
      })
      .filter((x) => x);
  }

  selectObjects(objects: IMarkupObject[]) {
    if (!this._konvaTransformer) return;

    const selectedObjs = this._konvaTransformer.nodes().concat(objects.map((x) => x.ref()));
    this._konvaTransformer.nodes(selectedObjs);
  }

  clearSelected(): void {
    if (this._konvaTransformer) this._konvaTransformer.nodes([]);
  }

  private addObject(object: IMarkupObject): void {
    if (object.type() === "Image") this._groupImages.add(object.ref());
    else if (object.type() === "Text") this._groupTexts.add(object.ref());
    else this._groupGeometry.add(object.ref());
  }

  private konvaLayerFind(type: string): any {
    if (!this._konvaLayer) return [];

    const konvaShape = MarkupMode2Konva[type];
    if (!konvaShape || !konvaShape.initializer) return [];

    // for "draggable" Konva uses Rectangles in Transformer. We need only Shapes from layer.
    return this._konvaLayer
      .find(konvaShape.name)
      .filter(
        (ref) =>
          ref.parent === this._konvaLayer ||
          ref.parent === this._groupImages ||
          ref.parent === this._groupGeometry ||
          ref.parent === this._groupTexts
      );
  }

  private getRelativePointPosition = (point, node) => {
    // the function will return pointer position relative to the passed node
    const transform = node.getAbsoluteTransform().copy();
    // to detect relative position we need to invert transform
    transform.invert();
    // get pointer (say mouse or touch) position
    // now we find relative point
    return transform.point(point);
  };

  private getRelativePointerPosition = (node) => {
    return this.getRelativePointPosition(node.getStage().getPointerPosition(), node);
  };

  private initializeKonva(): any {
    // first we need Konva core things: stage and layer
    const stage = new Konva.Stage({
      container: this._markupContainer,
      width: this._container.clientWidth,
      height: this._container.clientHeight,
    });
    this._konvaStage = stage;

    const layer = new Konva.Layer({ pixelRation: window.devicePixelRatio });
    stage.add(layer);

    this._groupImages = new Konva.Group();
    layer.add(this._groupImages);
    this._groupGeometry = new Konva.Group();
    layer.add(this._groupGeometry);
    this._groupTexts = new Konva.Group();
    layer.add(this._groupTexts);

    this._konvaLayer = layer;

    const transformer = new Konva.Transformer({
      shouldOverdrawWholeArea: false,
      keepRatio: false,
      flipEnabled: false,
    });
    layer.add(transformer);
    this._konvaTransformer = transformer;

    let isPaint = false;
    let lastLine;
    let mouseDownPos;
    let lastObj;

    stage.on("mousedown touchstart", (e) => {
      // do nothing if we mousedown on any shape
      if (!this._markupIsActive || e.target !== stage || this._markupMode === "Text" || this._markupMode === "Image")
        return;

      if (e.target === stage && transformer.nodes().length > 0) {
        transformer.nodes([]);
        return;
      }

      const pos = this.getRelativePointerPosition(stage);
      mouseDownPos = pos;

      isPaint = ["Arrow", "Cloud", "Ellipse", "Line", "Rectangle"].some((m) => m === this._markupMode);
      if (this._markupMode === "Line") {
        // add point twice, so we have some drawings even on a simple click
        lastLine = this.addLine([pos.x, pos.y, pos.x, pos.y]);
      }
    });

    stage.on("mouseup touchend", (e) => {
      if (!this._markupIsActive) return;

      if (isPaint) {
        const pos = this.getRelativePointerPosition(stage);
        const defParams = mouseDownPos && pos.x === mouseDownPos.x && pos.y === mouseDownPos.y;
        const startX = defParams ? mouseDownPos.x : Math.min(mouseDownPos.x, pos.x);
        const startY = defParams ? mouseDownPos.y : Math.min(mouseDownPos.y, pos.y);
        const dX = defParams ? 200 : Math.abs(mouseDownPos.x - pos.x);
        const dY = defParams ? 200 : Math.abs(mouseDownPos.y - pos.y);
        if (defParams) {
          if (this._markupMode === "Rectangle") {
            this.addRectangle({ x: startX, y: startY }, dX, dY);
          } else if (this._markupMode === "Ellipse") {
            this.addEllipse({ x: startX, y: startY }, { x: dX / 2, y: dY / 2 });
          } else if (this._markupMode === "Arrow") {
            this.addArrow(
              { x: mouseDownPos.x, y: mouseDownPos.y },
              { x: defParams ? mouseDownPos.x + 200 : pos.x, y: defParams ? startY : pos.y }
            );
          } else if (this._markupMode === "Cloud") {
            this.addCloud({ x: startX, y: startY }, Math.max(100, dX), Math.max(100, dY));
          }
        }
      }

      lastObj = undefined;
      isPaint = false;
    });

    stage.on("mousemove touchmove", (e) => {
      if (!this._markupIsActive) return;
      if (!isPaint) {
        return;
      }

      // prevent scrolling on touch devices
      //e.evt.preventDefault();

      const pos = this.getRelativePointerPosition(stage);
      const defParams = mouseDownPos && pos.x === mouseDownPos.x && pos.y === mouseDownPos.y;
      const startX = defParams ? mouseDownPos.x : Math.min(mouseDownPos.x, pos.x);
      const startY = defParams ? mouseDownPos.y : Math.min(mouseDownPos.y, pos.y);
      const dX = defParams ? 200 : Math.abs(mouseDownPos.x - pos.x);
      const dY = defParams ? 200 : Math.abs(mouseDownPos.y - pos.y);

      if (this._markupMode === "Line") {
        lastLine.addPoints([{ x: pos.x, y: pos.y }]);
      } else if (this._markupMode === "Arrow") {
        if (lastObj) lastObj.setEndPoint(pos.x, pos.y);
        else lastObj = this.addArrow({ x: mouseDownPos.x, y: mouseDownPos.y }, { x: pos.x, y: pos.y });
      } else if (this._markupMode === "Rectangle") {
        if (lastObj) {
          lastObj.setPosition(startX, startY);
          lastObj.setWidth(dX);
          lastObj.setHeight(dY);
        } else lastObj = this.addRectangle({ x: startX, y: startY }, dX, dY);
      } else if (this._markupMode === "Ellipse") {
        if (lastObj) {
          lastObj.setPosition(startX, startY);
          lastObj.setRadiusX(dX);
          lastObj.setRadiusY(dY);
        } else lastObj = this.addEllipse({ x: startX, y: startY }, { x: dX, y: dY });
      } else if (this._markupMode === "Cloud") {
        if (lastObj) {
          lastObj.setPosition(startX, startY);
          lastObj.setWidth(Math.max(100, dX));
          lastObj.setHeight(Math.max(100, dY));
        } else lastObj = this.addCloud({ x: startX, y: startY }, dX, dY);
      }
    });

    // clicks should select/deselect shapes
    stage.on("click tap", (e) => {
      if (!this._markupIsActive) return;

      // if click on empty area - remove all selections
      if (e.target === stage) {
        if (this._markupMode === "Text") {
          if (this._textInputRef && this._textInputRef.value)
            this.addText(this._textInputRef.value, this._textInputPos, this._textInputAngle);
          else if (transformer.nodes().length === 0) {
            const pos = this.getRelativePointerPosition(stage);
            this.createTextInput(pos, e.evt.pageX, e.evt.pageY, 0, null);
          }
        } else if (this._markupMode === "Image") {
          if (this._imageInputRef && this._imageInputRef.value)
            this.addImage(
              { x: this._imageInputPos.x, y: this._imageInputPos.y },
              this._imageInputRef.value,
              0,
              0,
              this._imageInputRef.value
            );
          else if (transformer.nodes().length === 0) {
            const pos = this.getRelativePointerPosition(stage);
            this.createImageInput(pos);
          }
        }
        transformer.nodes([]);
        return;
      }

      if (this._markupMode === "Text" || this._markupMode === "SelectMarkup") {
        if (e.target.className === "Text" && transformer.nodes().length === 1 && transformer.nodes()[0] === e.target) {
          if (this._textInputRef && this._textInputRef.value)
            this.addText(this._textInputRef.value, this._textInputPos, this._textInputAngle);
          else
            this.createTextInput(
              { x: e.target.attrs.x, y: e.target.attrs.y },
              e.evt.pageX,
              e.evt.pageY,
              e.target.attrs.rotation,
              e.target.attrs.text
            );
          return;
        } else {
          this.removeTextInput();
        }
      }

      if (this._markupMode === "Image" || this._markupMode === "SelectMarkup") {
        if (e.target.className === "Image" && transformer.nodes().length === 1 && transformer.nodes()[0] === e.target) {
          if (this._imageInputRef && this._imageInputRef.value)
            this.addImage(this._imageInputPos, this._imageInputRef.value, 0, 0);
          else this.createImageInput({ x: e.target.attrs.x, y: e.target.attrs.y });
          return;
        } else {
          this.removeImageInput();
        }
      }

      if (
        transformer.nodes().filter((x) => x.className === "Cloud" || x.className === "Image").length > 0 ||
        e.target.className === "Cloud" ||
        e.target.className === "Image"
      ) {
        transformer.rotateEnabled(false);
      } else {
        transformer.rotateEnabled(true);
      }

      // do we pressed shift or ctrl?
      const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey;
      const isSelected = transformer.nodes().indexOf(e.target) >= 0;

      if (!metaPressed && !isSelected) {
        // if no key pressed and the node is not selected
        // select just one
        transformer.nodes([e.target]);
      } else if (metaPressed && isSelected) {
        // if we pressed keys and node was selected
        // we need to remove it from selection:
        const nodes = transformer.nodes().slice(); // use slice to have new copy of array
        // remove node from array
        nodes.splice(nodes.indexOf(e.target), 1);
        transformer.nodes(nodes);
      } else if (metaPressed && !isSelected) {
        // add the node into selection
        const nodes = transformer.nodes().concat([e.target]);
        transformer.nodes(nodes);
      }
    });

    const container = stage.container();
    container.tabIndex = 1;
    // focus it
    // also stage will be in focus on its click
    container.focus();

    container.addEventListener("keydown", (e) => {
      if (!this._markupIsActive) return;
      if (e.code === "Delete") {
        this.getSelectedObjects().forEach((obj: IMarkupObject) => obj.delete());
        this.clearSelected();
        return;
      }
      e.preventDefault();
    });
  }

  private destroyKonva() {
    this.removeTextInput();
    this.removeImageInput();
    this.clearOverlay();

    this._konvaStage?.destroy();

    this._groupImages = undefined;
    this._groupGeometry = undefined;
    this._groupTexts = undefined;
    this._konvaLayer = undefined;
    this._konvaTransformer = undefined;
    this._konvaStage = undefined;
  }

  private getMarkupLines(): Array<ILine> {
    const lines = [];

    this.konvaLayerFind("Line").forEach((ref) => {
      const linePoints = ref.points();
      if (!linePoints) return;

      const worldPoints = [];
      const absoluteTransform = ref.getAbsoluteTransform();
      for (let i = 0; i < linePoints.length; i += 2) {
        // we need getAbsoluteTransform because inside Konva position starts from {0, 0}
        // https://stackoverflow.com/a/57641487 - check answer's comments
        const atPoint = absoluteTransform.point({
          x: linePoints[i],
          y: linePoints[i + 1],
        });
        const worldPoint = this._worldTransformer.screenToWorld(atPoint);

        worldPoints.push(worldPoint);
      }

      const konvaLine = new KonvaLine(null, ref);
      const line: ILine = {
        id: konvaLine.id(),
        points: worldPoints,
        color: konvaLine.getColor() || "#ff0000",
        type: konvaLine.getLineType() || this.lineType,
        width: konvaLine.getLineWidth() || this.lineWidth,
      };

      lines.push(line);
    });

    return lines;
  }

  private getMarkupTexts(): Array<IText> {
    const texts = [];

    this.konvaLayerFind("Text").forEach((ref) => {
      const textSize = 0.02;
      const textScale = this._worldTransformer.getScale();

      const position = ref.position();
      const stageAbsoluteTransform = this._konvaStage.getAbsoluteTransform();
      const atPoint = stageAbsoluteTransform.point({ x: position.x, y: position.y });

      const worldPoint = this._worldTransformer.screenToWorld(atPoint);

      const shape = new KonvaText(null, ref);
      const text: IText = {
        id: shape.id(),
        position: worldPoint,
        text: shape.getText(),
        text_size: textSize * textScale.y,
        angle: shape.getRotation(),
        color: shape.getColor(),
        font_size: shape.getFontSize() * stageAbsoluteTransform.getMatrix()[0],
      };

      texts.push(text);
    });

    return texts;
  }

  private getMarkupRectangles(): Array<IRectangle> {
    const rectangles = [];

    this.konvaLayerFind("Rectangle").forEach((ref) => {
      const position = ref.position();
      const stageAbsoluteTransform = this._konvaStage.getAbsoluteTransform();
      const atPoint = stageAbsoluteTransform.point({ x: position.x, y: position.y });
      const worldPoint = this._worldTransformer.screenToWorld(atPoint);
      const scale = stageAbsoluteTransform.getMatrix()[0];

      const shape = new KonvaRectangle(null, ref);
      const rectangle: IRectangle = {
        id: shape.id(),
        position: worldPoint,
        width: shape.getWidth() * scale,
        height: shape.getHeigth() * scale,
        line_width: shape.getLineWidth(),
        color: shape.getColor(),
      };

      rectangles.push(rectangle);
    });

    return rectangles;
  }

  private getMarkupEllipses(): Array<IEllipse> {
    const ellipses = [];

    this.konvaLayerFind("Ellipse").forEach((ref) => {
      const position = ref.position();
      const stageAbsoluteTransform = this._konvaStage.getAbsoluteTransform();
      const atPoint = stageAbsoluteTransform.point({ x: position.x, y: position.y });
      const worldPoint = this._worldTransformer.screenToWorld(atPoint);
      const scale = stageAbsoluteTransform.getMatrix()[0];

      const shape = new KonvaEllipse(null, ref);
      const ellipse: IEllipse = {
        id: shape.id(),
        position: worldPoint,
        radius: {
          x: ref.getRadiusX() * scale,
          y: ref.getRadiusY() * scale,
        },
        line_width: shape.getLineWidth(),
        color: shape.getColor(),
      };

      ellipses.push(ellipse);
    });

    return ellipses;
  }

  private getMarkupArrows(): Array<IArrow> {
    const arrows = [];

    this.konvaLayerFind("Arrow").forEach((ref) => {
      // we need getAbsoluteTransform because inside Konva position starts from {0, 0}
      const absoluteTransform = ref.getAbsoluteTransform();

      const atStartPoint = absoluteTransform.point({
        x: ref.points()[0],
        y: ref.points()[1],
      });
      const worldStartPoint = this._worldTransformer.screenToWorld(atStartPoint);

      const atEndPoint = absoluteTransform.point({
        x: ref.points()[2],
        y: ref.points()[3],
      });
      const worldEndPoint = this._worldTransformer.screenToWorld(atEndPoint);

      const shape = new KonvaArrow(null, ref);
      const arrow: IArrow = {
        id: shape.id(),
        start: worldStartPoint,
        end: worldEndPoint,
        color: shape.getColor(),
      };

      arrows.push(arrow);
    });

    return arrows;
  }

  private getMarkupImages(): Array<IImage> {
    const images = [];

    this.konvaLayerFind("Image").forEach((ref) => {
      const position = ref.position();
      const stageAbsoluteTransform = this._konvaStage.getAbsoluteTransform();
      const atPoint = stageAbsoluteTransform.point({ x: position.x, y: position.y });
      const worldPoint = this._worldTransformer.screenToWorld(atPoint);
      const scale = stageAbsoluteTransform.getMatrix()[0];

      const shape = new KonvaImage(null, ref);
      const image: IImage = {
        id: shape.id(),
        position: worldPoint,
        src: shape.getSrc(),
        width: shape.getWidth() * scale,
        height: shape.getHeight() * scale,
      };

      images.push(image);
    });

    return images;
  }

  private getMarkupClouds(): Array<ICloud> {
    const clouds = [];

    this.konvaLayerFind("Cloud").forEach((ref) => {
      const position = ref.position();
      const stageAbsoluteTransform = this._konvaStage.getAbsoluteTransform();
      const atPoint = stageAbsoluteTransform.point({ x: position.x, y: position.y });
      const worldPoint = this._worldTransformer.screenToWorld(atPoint);
      const scale = stageAbsoluteTransform.getMatrix()[0];

      const shape = new KonvaCloud(null, ref);
      const cloud: ICloud = {
        id: shape.id(),
        position: worldPoint,
        width: shape.getWidth() * scale,
        height: shape.getHeigth() * scale,
        line_width: shape.getLineWidth(),
        color: shape.getColor(),
      };

      clouds.push(cloud);
    });

    return clouds;
  }

  private combineMarkupWithDrawing() {
    this.clearSelected();

    const tempCanvas = document.createElement("canvas");
    if (this._konvaStage) {
      tempCanvas.width = this._konvaStage.width();
      tempCanvas.height = this._konvaStage.height();

      const ctx = tempCanvas.getContext("2d");
      if (this._container instanceof HTMLCanvasElement) ctx.drawImage(this._container, 0, 0);
      ctx.drawImage(this._konvaStage.toCanvas({ pixelRatio: window.devicePixelRatio }), 0, 0);
    }

    return tempCanvas.toDataURL("image/jpeg", 0.25);
  }

  private addLine(
    linePoints: number[],
    color?: string,
    type?: MarkupLineType,
    width?: number,
    id?: string
  ): KonvaLine | void {
    if (!linePoints || linePoints.length === 0) return;

    const points: { x: number; y: number }[] = [];
    for (let i = 0; i < linePoints.length; i += 2) {
      points.push({ x: linePoints[i], y: linePoints[i + 1] });
    }

    const konvaLine = new KonvaLine({
      points,
      type: type || this.lineType,
      width: width || this.lineWidth,
      color: color || this._markupColor.asHex(),
      id,
    });

    this.addObject(konvaLine);
    return konvaLine;
  }

  private createTextInput(pos: Konva.Vector2d, inputX: number, inputY: number, angle: number, text: string): void {
    if (!this._textInputRef) {
      this._textInputPos = pos;
      this._textInputAngle = angle;
      this._textInputRef = document.createElement("textarea");
      this._textInputRef.style.zIndex = "9999";
      this._textInputRef.style.position = "absolute";
      this._textInputRef.style.display = "block";
      this._textInputRef.style.top = inputY + "px";
      this._textInputRef.style.left = inputX + "px";
      this._textInputRef.style.fontSize = `${this.fontSize}px`;
      this._textInputRef.style.color = `${this._markupColor.asHex()}`;
      this._textInputRef.style.fontFamily = "Calibri";

      this._textInputRef.onkeydown = (event) => {
        if (event.key === "Enter" && !event.shiftKey) {
          event.preventDefault();
          this.addText(this._textInputRef.value, this._textInputPos, this._textInputAngle);
        }
        if (event.key === "Escape") {
          event.preventDefault();
          this.removeTextInput();
        }
      };
      if (text) this._textInputRef.value = text;
      document.body.appendChild(this._textInputRef);

      setTimeout(() => {
        this._textInputRef.focus();
      }, 50);
    } else {
      this.removeTextInput();
    }
  }

  private removeTextInput(): void {
    this._textInputRef?.remove();
    this._textInputRef = null;
    this._textInputPos = null;
    this._textInputAngle = 0;
  }

  private createImageInput(pos: Konva.Vector2d): void {
    if (!this._imageInputRef) {
      const convertBase64 = (file) => {
        return new Promise<string | ArrayBuffer>((resolve, reject) => {
          const fileReader = new FileReader();
          fileReader.readAsDataURL(file);

          fileReader.onload = () => {
            resolve(fileReader.result);
          };

          fileReader.onerror = (error) => {
            reject(error);
          };
        });
      };

      this._imageInputPos = pos;
      this._imageInputRef = document.createElement("input");
      this._imageInputRef.style.display = "none";
      this._imageInputRef.type = "file";
      this._imageInputRef.accept = "image/png, image/jpeg";
      this._imageInputRef.onchange = async (event) => {
        const file = (event.target as HTMLInputElement).files[0];
        const base64 = await convertBase64(file);
        this.addImage({ x: this._imageInputPos.x, y: this._imageInputPos.y }, base64.toString(), 0, 0);
      };
      this._imageInputRef.oncancel = (event) => {
        this.removeImageInput();
      };
      document.body.appendChild(this._imageInputRef);

      setTimeout(() => {
        this._imageInputRef.click();
      }, 50);
    } else {
      this.removeImageInput();
    }
  }

  private removeImageInput(): void {
    this._imageInputRef?.remove();
    this._imageInputRef = null;
    this._imageInputPos = null;
  }

  private addText(
    text: string,
    position: Konva.Vector2d,
    angle?: number,
    color?: string,
    textSize?: number,
    fontSize?: number,
    id?: string
  ): KonvaText | void {
    if (!text) return;

    // in case of edit - remove old Konva.Text object
    this.getSelectedObjects().at(0)?.delete();

    this.clearSelected();
    this.removeTextInput();

    // in case we have old viewpoint without font_size
    const tolerance = 1.0e-6;
    if (textSize && textSize > tolerance && (!fontSize || fontSize < tolerance)) {
      const size = 0.02;
      const scale = this._worldTransformer.getScale();
      fontSize = textSize / (scale.y / size) / 34;
    }

    const konvaText = new KonvaText({
      position: { x: position.x, y: position.y },
      text,
      rotation: angle,
      fontSize: fontSize || this.fontSize,
      color: color || this._markupColor.asHex(),
      id,
    });

    this.addObject(konvaText);
    return konvaText;
  }

  private addRectangle(
    position: Konva.Vector2d,
    width: number,
    height: number,
    lineWidth?: number,
    color?: string,
    id?: string
  ): KonvaRectangle | void {
    if (!position) return;

    const konvaRectangle = new KonvaRectangle({
      position,
      width,
      height,
      lineWidth: lineWidth || this.lineWidth,
      color: color || this._markupColor.asHex(),
      id,
    });

    this.addObject(konvaRectangle);
    return konvaRectangle;
  }

  private addEllipse(
    position: { x: number; y: number },
    radius: { x: number; y: number },
    lineWidth?: number,
    color?: string,
    id?: string
  ): KonvaEllipse | void {
    if (!position) return;

    const konvaEllipse = new KonvaEllipse({
      position,
      radius,
      lineWidth,
      color: color || this._markupColor.asHex(),
      id,
    });

    this.addObject(konvaEllipse);
    return konvaEllipse;
  }

  private addArrow(
    start: { x: number; y: number },
    end: { x: number; y: number },
    color?: string,
    id?: string
  ): KonvaArrow | void {
    if (!start || !end) return;

    const konvaArrow = new KonvaArrow({
      start,
      end,
      color: color || this._markupColor.asHex(),
      id,
    });

    this.addObject(konvaArrow);
    return konvaArrow;
  }

  private addCloud(
    position: { x: number; y: number },
    width: number,
    height: number,
    lineWidth?: number,
    color?: string,
    id?: string
  ): KonvaCloud | void {
    if (!position || !width || !height) return;

    const konvaCloud = new KonvaCloud({
      position,
      width,
      height,
      color: color || this._markupColor.asHex(),
      lineWidth: lineWidth || this.lineWidth,
      id,
    });

    this.addObject(konvaCloud);
    return konvaCloud;
  }

  private addImage(
    position: { x: number; y: number },
    src: string,
    width?: number,
    height?: number,
    id?: string
  ): KonvaImage | void {
    if (!position || !src) return;

    // in case of edit - remove old Image placeholder object
    this.getSelectedObjects().at(0)?.delete();

    this.clearSelected();
    this.removeImageInput();

    const konvaImage = new KonvaImage({
      position,
      src,
      width,
      height,
      maxWidth: this._konvaStage.width() - position.x,
      maxHeight: this._konvaStage.height() - position.y,
      id,
    });

    this.addObject(konvaImage);
    return konvaImage;
  }
}
