Загрузка данных


import {
  AutoscaleInfo,
  CrosshairMode,
  IChartApi,
  IPrimitivePaneView,
  ISeriesApi,
  ISeriesPrimitive,
  ISeriesPrimitiveAxisView,
  Logical,
  PrimitiveHoveredItem,
  SeriesAttachedParameter,
  SeriesOptionsMap,
  SeriesType,
  Time,
} from 'lightweight-charts';
import { Observable, Subject, Subscription } from 'rxjs';

import { getPointerPoint as getPointerPointFromEvent } from '@core/Drawings/helpers';
import { AxisLabel, AxisSegment, Point, SeriesApi } from '@core/Drawings/types';

import { SettingsTab, SettingsValues } from '@src/types';

export interface DrawingInteraction {
  selected$: Observable<boolean>;
  locked$: Observable<boolean>;

  isSelected(): boolean;
  isLocked(): boolean;

  select(): void;
  deselect(): void;
}

export interface ISeriesDrawing extends ISeriesPrimitive<Time> {
  show(): void;
  hide(): void;
  rebind(series: ISeriesApi<SeriesType>): void;
  destroy(): void;

  waitTillReady(): Promise<void>;
  isCreationPending(): boolean;
  shouldShowInObjectTree(): boolean;

  getState(): unknown;
  setState(state: unknown): void;

  getSettings(): SettingsValues;
  getSettingsTabs(): SettingsTab[];
  updateSettings(settings: SettingsValues): void;

  subscribeSettings(callback: (settings: SettingsValues) => void): Subscription;

  isHit(event: MouseEvent): boolean;
  pointerDown(event: PointerEvent): void;
  pointerMove(event: PointerEvent): void;
  pointerUp(event: PointerEvent): void;
  doubleClick(event: MouseEvent): void;
  contextMenu(event: MouseEvent): void;

  getRenderData(): unknown;
}

interface SeriesDrawingBaseParams {
  container: HTMLElement;
  chart: IChartApi;
  series: SeriesApi;
  interaction: DrawingInteraction;
}

export abstract class SeriesDrawingBase<TSettings extends SettingsValues = SettingsValues> implements ISeriesDrawing {
  protected hidden = false;
  protected chart: IChartApi;
  protected series: SeriesApi;
  protected subscriptions = new Subscription();
  protected abstract mode: unknown; // todo: хочется иметь единый mode
  protected abstract settings: TSettings;
  protected readonly container: HTMLElement;
  protected isBound = false;

  private readonly interaction: DrawingInteraction;
  private readonly settingsSubject = new Subject<SettingsValues>();
  private isInteractionBound = false;

  protected readyPromise: Promise<void> | null = null;
  protected resolveReady: (() => void) | null = null;
  protected requestUpdate: (() => void) | null = null;

  constructor({ chart, series, container, interaction }: SeriesDrawingBaseParams) {
    this.chart = chart;
    this.series = series;
    this.container = container;
    this.interaction = interaction;
  }

  public subscribeSettings(callback: (settings: SettingsValues) => void): Subscription {
    callback(this.getSettings());

    return this.settingsSubject.subscribe(callback);
  }

  public show(): void {
    this.hidden = false;
    this.render();
  }

  public hide(): void {
    this.hidden = true;
    this.showCrosshair();
    this.render();
  }

  public rebind(series: SeriesApi): void {
    if (this.series === series) {
      return;
    }

    this.showCrosshair();
    this.unbindEvents();
    this.series.detachPrimitive(this as unknown as ISeriesPrimitive<Time>);

    this.series = series;
    this.requestUpdate = null;

    this.series.attachPrimitive(this as unknown as ISeriesPrimitive<Time>);
    this.render();
  }

  public destroy(): void {
    this.showCrosshair();
    this.unbindEvents();
    this.subscriptions.unsubscribe();
    this.settingsSubject.complete();
    this.series.detachPrimitive(this as unknown as ISeriesPrimitive<Time>);
    this.requestUpdate = null;
    this.resolveReady?.();
  }

  public waitTillReady(): Promise<void> {
    if (this.mode === 'ready') {
      return Promise.resolve();
    }

    if (!this.readyPromise) {
      this.readyPromise = new Promise((resolve) => {
        this.resolveReady = resolve;
      });
    }

    return this.readyPromise;
  }

  public shouldShowInObjectTree(): boolean {
    return this.mode !== 'idle';
  }

  public getSettings(): SettingsValues {
    return { ...this.settings };
  }

  public updateSettings(settings: SettingsValues): void {
    this.settings = {
      ...this.settings,
      ...settings,
    };

    this.settingsSubject.next(this.getSettings());
    this.render();
  }

  public attached(param: SeriesAttachedParameter<Time, keyof SeriesOptionsMap>): void {
    this.requestUpdate = param.requestUpdate;
    this.bindInteraction();
    this.bindEvents();
  }

  public detached(): void {
    this.showCrosshair();
    this.unbindEvents();
    this.requestUpdate = null;
  }

  public autoscaleInfo(_start: Logical, _end: Logical): AutoscaleInfo | null {
    return null;
  }

  public hitTest(x: number, y: number): PrimitiveHoveredItem | null {
    const hoveredItem = this.getHoveredItem(x, y);

    if (!hoveredItem || !this.isLocked()) {
      return hoveredItem;
    }

    return {
      ...hoveredItem,
      cursorStyle: 'pointer',
    };
  }

  public isHit(event: MouseEvent): boolean {
    if (!this.isEventInside(event)) {
      return false;
    }

    const point = this.getEventPoint(event as PointerEvent);

    return this.getHoveredItem(point.x, point.y) !== null;
  }

  public pointerDown(event: PointerEvent): void {
    if (!this.isEventInside(event)) {
      return;
    }

    if (!this.isLocked() || this.isCreationPending() || event.button !== 0) {
      this.handlePointerDown(event);

      return;
    }

    const point = this.getEventPoint(event);
    const isDrawingHit = this.getHoveredItem(point.x, point.y) !== null;

    if (isDrawingHit) {
      this.select();

      return;
    }

    if (this.isSelected()) {
      this.deselect();
    }
  }

  public pointerMove(event: PointerEvent): void {
    this.handlePointerMove(event);
  }

  public pointerUp(event: PointerEvent): void {
    this.handlePointerUp(event);
  }

  public doubleClick(event: MouseEvent): void {
    if (!this.isEventInside(event)) {
      return;
    }

    this.handleDoubleClick(event);
  }

  public contextMenu(event: MouseEvent): void {
    if (!this.isEventInside(event)) {
      return;
    }

    this.handleContextMenu(event);
  }

  public abstract getRenderData(): unknown; // todo: make proper type
  public abstract getState(): unknown;
  public abstract getSettingsTabs(): SettingsTab[];
  public abstract isCreationPending(): boolean;
  public abstract setState(state: unknown): void;
  public abstract updateAllViews(): void;
  public abstract paneViews(): readonly IPrimitivePaneView[];
  public abstract priceAxisPaneViews(): readonly IPrimitivePaneView[];
  public abstract priceAxisViews(): readonly ISeriesPrimitiveAxisView[];
  public abstract timeAxisPaneViews(): readonly IPrimitivePaneView[];
  public abstract timeAxisViews(): readonly ISeriesPrimitiveAxisView[];

  protected isSelected(): boolean {
    return this.interaction.isSelected();
  }

  protected isLocked(): boolean {
    return this.interaction.isLocked();
  }

  protected select(): void {
    this.interaction.select();
  }

  protected deselect(): void {
    this.interaction.deselect();
  }

  protected shouldShowHandles(): boolean {
    return !this.isLocked() && (this.isSelected() || this.isCreationPending());
  }

  protected render(): void {
    this.updateAllViews();
    this.requestUpdate?.();
  }

  protected hideCrosshair(): void {
    this.chart.applyOptions({
      crosshair: {
        mode: CrosshairMode.Hidden,
      },
    });
  }

  protected showCrosshair(): void {
    this.chart.applyOptions({
      crosshair: {
        mode: CrosshairMode.Normal,
      },
    });
  }

  protected getEventPoint(event: PointerEvent): Point {
    return getPointerPointFromEvent(this.container, event);
  }

  protected bindEvents(): void {
    if (this.isBound) {
      return;
    }

    this.isBound = true;
  }

  protected unbindEvents(): void {
    if (!this.isBound) {
      return;
    }

    this.isBound = false;
  }

  // todo: хочется общую реализацию для каждой кнопки
  protected handleContextMenu(event: MouseEvent): void {}
  protected handleDoubleClick(event: MouseEvent): void {}
  protected handlePointerDown(event: PointerEvent): void {}
  protected handlePointerMove(event: PointerEvent): void {}
  protected handlePointerUp(event: PointerEvent): void {}

  protected abstract getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null;
  protected abstract getGeometry(): unknown; // todo: make proper type
  protected abstract getTimeAxisSegments(): AxisSegment[];
  protected abstract getPriceAxisSegments(): AxisSegment[];
  protected abstract getTimeAxisLabel(kind: string): AxisLabel | null;
  protected abstract getPriceAxisLabel(kind: string): AxisLabel | null;

  private bindInteraction(): void {
    if (this.isInteractionBound) {
      return;
    }

    this.isInteractionBound = true;

    this.subscriptions.add(
      this.interaction.selected$.subscribe(() => {
        this.render();
      }),
    );

    this.subscriptions.add(
      this.interaction.locked$.subscribe((isLocked) => {
        if (isLocked) {
          this.showCrosshair();
        }

        this.render();
      }),
    );
  }

  private isEventInside(event: MouseEvent): boolean {
    return event.target instanceof Node && this.container.contains(event.target);
  }
}




import { IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';
import { cloneDeep, isEqual } from 'lodash-es';
import { BehaviorSubject, distinctUntilChanged, map, Observable, Subscription } from 'rxjs';

import { EventManager } from '@core';
import { DOMModel } from '@core/DOMModel';
import { Drawing } from '@core/Drawings';
import { Hotkeys, Keys } from '@core/Hotkeys';

import { EntitySettingsModal } from '@src/components/EntitySettingsModal';
import { drawingLabelById, drawingsMap, DrawingsNames } from '@src/constants';
import { ModalRenderer } from '@src/core/ModalRenderer';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { ActiveDrawingTool, DOMObjectSnapshot } from '@src/types';

import type { DrawingInteraction } from '@src/core/Drawings/common';
import type { SettingsValues } from '@src/types/settings';

interface DrawingsManagerParams {
  eventManager: EventManager;
  mainSeries$: Observable<SeriesStrategies | null>;
  lwcChart: IChartApi;
  DOM: DOMModel;
  container: HTMLElement;
  modalRenderer: ModalRenderer;
  paneId: number;
  hotkeys: Hotkeys;
}

export interface DrawingSnapshotItem extends Partial<DOMObjectSnapshot> {
  id: string;
  drawingName: DrawingsNames;
  state: unknown;
  isLocked?: boolean;
  zIndex?: number;
}

interface CreateDrawingOptions {
  id?: string;
  state?: unknown;
  isLocked?: boolean;
  zIndex?: number;
  shouldUpdateDrawingsList?: boolean;
}

export type DrawingsManagerSnapshot = DrawingSnapshotItem[];

export class DrawingsManager {
  private eventManager: EventManager;
  private lwcChart: IChartApi;
  private DOM: DOMModel;
  private container: HTMLElement;
  private modalRenderer: ModalRenderer;
  private paneId: number;
  private hotkeys: Hotkeys;

  private mainSeries: SeriesStrategies | null = null;
  private subscriptions = new Subscription();
  private drawings$ = new BehaviorSubject<Drawing[]>([]);
  private selectedDrawing$ = new BehaviorSubject<Drawing | null>(null);
  private activeTool$ = new BehaviorSubject<ActiveDrawingTool>('crosshair');
  private endlessMode$ = new BehaviorSubject(false);
  private pendingSnapshot: DrawingsManagerSnapshot | null = null;
  private selectedDrawingSnapshot: DrawingSnapshotItem | null = null;
  private pointerDrawing: Drawing | null = null;
  private recreateScheduled = false;

  private copyPasteBuffer: DrawingSnapshotItem | null = null;
  private escapeUnregisterHash: string | null = null;

  constructor({
    eventManager,
    mainSeries$,
    lwcChart,
    DOM,
    container,
    modalRenderer,
    paneId,
    hotkeys,
  }: DrawingsManagerParams) {
    this.DOM = DOM;
    this.eventManager = eventManager;
    this.paneId = paneId;
    this.lwcChart = lwcChart;
    this.container = container;
    this.modalRenderer = modalRenderer;
    this.hotkeys = hotkeys;

    this.subscriptions.add(
      mainSeries$.subscribe((series) => {
        if (!series) {
          return;
        }

        this.mainSeries = series;
        this.drawings$.value.forEach((drawing) => drawing.rebind(series));

        if (this.pendingSnapshot) {
          const snapshot = this.pendingSnapshot;
          this.pendingSnapshot = null;
          this.setSnapshot(snapshot);
        }
      }),
    );

    window.addEventListener('pointermove', this.handlePointerMove);
    window.addEventListener('pointerup', this.handlePointerUp);
    window.addEventListener('pointercancel', this.handlePointerUp);
    this.container.addEventListener('click', this.handleClick);
    this.container.addEventListener('pointerdown', this.handlePointerDown);
    this.container.addEventListener('dblclick', this.handleDoubleClick);
    this.container.addEventListener('contextmenu', this.handleContextMenu);
    // todo: implement ctrl+v
    // hotkeys.register({
    //   keys: [Keys.control, Keys.v],
    //   callback: () => {
    //     const bufferWithAppliedPosition = {
    //       ...this.copyPasteBuffer,
    //       state: {
    //         ...this.copyPasteBuffer?.state,
    //         startAnchor: {
    //           price: 73.36210252637723,
    //           time: 1783679170
    //         }
    //       }
    //     }
    //
    //     this.setSnapshot([
    //       ...this.getSnapshot(),
    //       bufferWithAppliedPosition
    //     ])
    //     // hotkeys.unregister({ // todo: unregister all else ctrl+c's
    //     //   keys: [Keys.control, Keys.c]
    //     // })
    //   }
    // })
  }

  private handlePointerDown = (event: PointerEvent): void => {
    this.selectedDrawingSnapshot = null;

    if (event.button === 0) {
      const drawings = this.drawings$.value;
      const selectedDrawing = this.selectedDrawing$.value;

      const pendingDrawing =
        this.pointerDrawing?.isCreationPending() === true
          ? this.pointerDrawing
          : drawings.find((drawing) => drawing.isCreationPending());

      const drawing =
        pendingDrawing ??
        (selectedDrawing?.getLwcDrawing().isHit(event) ? selectedDrawing : null) ??
        this.findTopDrawing(event) ??
        selectedDrawing;

      this.pointerDrawing = drawing;
      drawing?.getLwcDrawing().pointerDown(event);
    }

    queueMicrotask(() => {
      const drawing = this.selectedDrawing$.value;

      if (!drawing || drawing.isCreationPending()) {
        return;
      }

      this.selectedDrawingSnapshot = this.createDrawingSnapshot(drawing);
    });

    this.DOM.refreshEntities();
  };

  private handlePointerMove = (event: PointerEvent): void => {
    this.pointerDrawing?.getLwcDrawing().pointerMove(event);
  };

  private handlePointerUp = (event: PointerEvent): void => {
    const previousSnapshot = this.selectedDrawingSnapshot;
    const pointerDrawing = this.pointerDrawing;

    this.selectedDrawingSnapshot = null;

    pointerDrawing?.getLwcDrawing().pointerUp(event);

    if (pointerDrawing && !pointerDrawing.isCreationPending()) {
      this.pointerDrawing = null;
    }

    queueMicrotask(() => {
      if (!previousSnapshot) {
        return;
      }

      const drawing = this.findDrawing(previousSnapshot.id);

      if (!drawing || drawing.isCreationPending()) {
        return;
      }

      this.pushDrawingChange(previousSnapshot, this.createDrawingSnapshot(drawing));
    });

    this.DOM.refreshEntities();
    this.updateActiveTool();
  };

  private handleDoubleClick = (event: MouseEvent): void => {
    const drawings = this.drawings$.value;

    const pendingDrawing =
      this.pointerDrawing?.isCreationPending() === true
        ? this.pointerDrawing
        : drawings.find((drawing) => drawing.isCreationPending());

    const drawing = pendingDrawing ?? this.findTopDrawing(event);

    if (!drawing) {
      return;
    }

    if (!drawing.isCreationPending() && drawing !== this.selectedDrawing$.value) {
      this.selectedDrawing$.next(drawing);
    }

    drawing.getLwcDrawing().doubleClick(event);

    if (this.pointerDrawing && !this.pointerDrawing.isCreationPending()) {
      this.pointerDrawing = null;
    }

    this.DOM.refreshEntities();
    this.updateActiveTool();
  };

  private handleContextMenu = (event: MouseEvent): void => {
    const drawings = this.drawings$.value;

    const pendingDrawing =
      this.pointerDrawing?.isCreationPending() === true
        ? this.pointerDrawing
        : drawings.find((drawing) => drawing.isCreationPending());

    const drawing = pendingDrawing ?? this.findTopDrawing(event);

    drawing?.getLwcDrawing().contextMenu(event);

    if (this.pointerDrawing && !this.pointerDrawing.isCreationPending()) {
      this.pointerDrawing = null;
    }
  };

  private handleClick = (): void => {
    if (this.pointerDrawing && !this.pointerDrawing.isCreationPending()) {
      this.pointerDrawing = null;
    }

    this.DOM.refreshEntities();
    this.updateActiveTool();
  };

  private findTopDrawing(event: MouseEvent): Drawing | null {
    let topDrawing: Drawing | null = null;

    for (const drawing of this.drawings$.value) {
      if (topDrawing && drawing.zIndex <= topDrawing.zIndex) {
        continue;
      }

      if (drawing.getLwcDrawing().isHit(event)) {
        topDrawing = drawing;
      }
    }

    return topDrawing;
  }

  private findDrawing(id: string): Drawing | undefined {
    return this.drawings$.value.find((drawing) => drawing.id === id);
  }

  private createDrawingSnapshot(drawing: Drawing): DrawingSnapshotItem {
    return {
      ...drawing.getSnapshot(),
      drawingName: drawing.getDrawingName(),
      state: cloneDeep(drawing.getState()),
      isLocked: drawing.isLocked(),
    };
  }

  private updateDrawing(drawing: Drawing, update: () => void): void {
    if (drawing.isCreationPending()) {
      return;
    }

    const previousSnapshot = this.createDrawingSnapshot(drawing);

    update();

    this.pushDrawingChange(previousSnapshot, this.createDrawingSnapshot(drawing));
  }

  private pushDrawingChange(
    previousSnapshot: DrawingSnapshotItem | null,
    nextSnapshot: DrawingSnapshotItem | null,
  ): void {
    if (isEqual(previousSnapshot, nextSnapshot)) {
      return;
    }

    const previous = cloneDeep(previousSnapshot);
    const next = cloneDeep(nextSnapshot);

    // todo: объединять последовательные изменения одного дровинга в одну запись истории
    this.eventManager.getUndoRedo().pushCommand({
      undo: () => {
        this.replaceDrawingSnapshot(next, previous);
      },
      redo: () => {
        this.replaceDrawingSnapshot(previous, next);
      },
    });
  }

  private replaceDrawingSnapshot(
    currentSnapshot: DrawingSnapshotItem | null,
    nextSnapshot: DrawingSnapshotItem | null,
  ): void {
    if (currentSnapshot && nextSnapshot && currentSnapshot.id === nextSnapshot.id) {
      const drawing = this.findDrawing(nextSnapshot.id);

      if (drawing) {
        drawing.setState(cloneDeep(nextSnapshot.state));
        drawing.setLocked(nextSnapshot.isLocked ?? false);
        this.DOM.refreshEntities();

        return;
      }
    }

    if (currentSnapshot) {
      this.removeDrawingInternal(currentSnapshot.id, false);
    }

    if (nextSnapshot) {
      this.restoreDrawing(nextSnapshot);
    }

    this.activeTool$.next('crosshair');
  }

  private restoreDrawing(snapshot: DrawingSnapshotItem): Drawing {
    const existingDrawing = this.findDrawing(snapshot.id);

    if (existingDrawing) {
      existingDrawing.setState(cloneDeep(snapshot.state));
      existingDrawing.setLocked(snapshot.isLocked ?? false);

      if (snapshot.zIndex !== undefined) {
        existingDrawing.setZIndex(snapshot.zIndex);
      }

      this.drawings$.next([...this.drawings$.value].sort((left, right) => left.zIndex - right.zIndex));

      this.DOM.refreshEntities();

      return existingDrawing;
    }

    return this.createDrawing(snapshot.drawingName, {
      id: snapshot.id,
      state: cloneDeep(snapshot.state),
      isLocked: snapshot.isLocked,
      zIndex: snapshot.zIndex,
    });
  }

  private updateActiveTool = (): void => {
    const hasPendingDrawing = this.drawings$.value.some((drawing) => drawing.isCreationPending());

    if (hasPendingDrawing) {
      return;
    }

    const activeTool = this.activeTool$.value;
    const isSingleInstanceTool = activeTool !== 'crosshair' && drawingsMap[activeTool]?.singleInstance;

    if (activeTool !== 'crosshair' && this.endlessMode$.value && !isSingleInstanceTool) {
      if (this.recreateScheduled) {
        return;
      }

      this.recreateScheduled = true;

      queueMicrotask(() => {
        this.recreateScheduled = false;

        const currentTool = this.activeTool$.value;
        const hasPendingAfterTick = this.drawings$.value.some((drawing) => drawing.isCreationPending());

        if (currentTool === 'crosshair') {
          return;
        }

        if (!this.endlessMode$.value) {
          return;
        }

        if (drawingsMap[currentTool]?.singleInstance) {
          return;
        }

        if (hasPendingAfterTick) {
          return;
        }

        this.addDrawingForce(currentTool);
      });

      return;
    }

    this.activeTool$.next('crosshair');
  };

  private removeDrawing = (id: string): void => {
    const drawing = this.findDrawing(id);

    if (!drawing) {
      return;
    }

    if (drawing.isCreationPending()) {
      this.removeDrawingInternal(id);

      return;
    }

    const snapshot = this.createDrawingSnapshot(drawing);

    this.removeDrawingInternal(id);
    this.pushDrawingChange(snapshot, null);
  };

  private removeDrawingInternal(id: string, shouldUpdateTool = true): void {
    const drawing = this.findDrawing(id);

    if (!drawing) {
      return;
    }

    this.removeDrawings([drawing], shouldUpdateTool);
  }

  private removePendingDrawings(shouldUpdateTool = true): void {
    const drawingsToRemove = this.drawings$.value.filter((drawing) => drawing.isCreationPending());

    this.removeDrawings(drawingsToRemove, shouldUpdateTool);
  }

  private removeDrawings(drawingsToRemove: Drawing[], shouldUpdateTool = true): void {
    if (!drawingsToRemove.length) {
      return;
    }

    const selectedDrawing = this.selectedDrawing$.value;

    if (selectedDrawing && drawingsToRemove.includes(selectedDrawing)) {
      this.selectedDrawing$.next(null);
    }

    if (this.pointerDrawing && drawingsToRemove.includes(this.pointerDrawing)) {
      this.pointerDrawing = null;
    }

    drawingsToRemove.forEach((drawing) => {
      drawing.destroy();
      this.DOM.removeEntity(drawing);
    });

    this.drawings$.next(this.drawings$.value.filter((drawing) => !drawingsToRemove.includes(drawing)));

    if (shouldUpdateTool) {
      this.updateActiveTool();
    }

    this.DOM.refreshEntities();
  }

  public addDrawingForce = async (name: DrawingsNames): Promise<void> => {
    this.removePendingDrawings(false);

    const previousDrawing = drawingsMap[name].singleInstance
      ? this.drawings$.value.find((drawing) => drawing.getDrawingName() === name)
      : undefined;

    const previousSnapshot = previousDrawing ? this.createDrawingSnapshot(previousDrawing) : null;

    if (previousDrawing) {
      this.removeDrawingInternal(previousDrawing.id, false);
    }

    this.activeTool$.next(name);
    const drawing = this.createDrawing(name);
    this.DOM.refreshEntities();

    await drawing.waitForCreation();

    if (!this.findDrawing(drawing.id)) {
      if (previousSnapshot) {
        this.restoreDrawing(previousSnapshot);
      }

      return;
    }

    this.pushDrawingChange(previousSnapshot, this.createDrawingSnapshot(drawing));
  };

  private createDrawing(name: DrawingsNames, options: CreateDrawingOptions = {}): Drawing {
    const { mainSeries } = this;
    if (!mainSeries) {
      throw new Error('[Drawings] main series is not defined');
    }

    const { id, state, isLocked = false, zIndex, shouldUpdateDrawingsList = true } = options;
    const shouldSelectAfterCreation = state === undefined;

    if (shouldSelectAfterCreation && this.selectedDrawing$.value) {
      this.selectedDrawing$.next(null);
    }

    const config = drawingsMap[name];
    const drawingId = id ?? crypto.randomUUID();

    let createdDrawing: Drawing | null = null;

    const selected$ = this.selectedDrawing$.pipe(
      map((drawing) => drawing?.id === drawingId),
      distinctUntilChanged(),
    );

    const construct = (chart: IChartApi, series: ISeriesApi<SeriesType>, interaction: DrawingInteraction) => {
      const paneElement = series.getPane().getHTMLElement();

      if (!paneElement) {
        throw new Error('[Drawing Manager]: cannot place drawing, there is no pane');
      }

      const cells = paneElement.querySelectorAll<HTMLTableCellElement>(':scope > td');
      const canvasElement = cells.item(1);

      return config.construct({
        chart,
        series,
        eventManager: this.eventManager,
        container: canvasElement,
        interaction,
        removeSelf: () => this.removeDrawing(drawingId),
        openSettings: () => {
          if (createdDrawing) {
            this.openSettings(createdDrawing);
          }
        },
      });
    };

    const drawingFactory = (entityZIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) =>
      new Drawing({
        lwcChart: this.lwcChart,
        mainSeries,
        id: drawingId,
        drawingName: name,
        name: drawingLabelById()[name],
        onDelete: this.removeDrawing,
        onCopy: () => {
          if (createdDrawing) {
            this.copyPasteBuffer = this.createDrawingSnapshot(createdDrawing);
          }
        },
        zIndex: entityZIndex,
        moveDown,
        moveUp,
        construct,
        selected$,
        isSelected: () => this.selectedDrawing$.value?.id === drawingId,
        select: () => {
          if (!createdDrawing || createdDrawing.isCreationPending() || this.selectedDrawing$.value === createdDrawing) {
            return;
          }

          this.selectedDrawing$.next(createdDrawing);
        },
        deselect: () => {
          if (!createdDrawing || this.selectedDrawing$.value !== createdDrawing) {
            return;
          }

          this.selectedDrawing$.next(null);
        },
        isLocked,
        paneId: this.paneId,
        hotkeys: this.hotkeys,
        resetActiveTool: () => {
          this.activeTool$.next('crosshair');
        },
      });

    const entity = this.DOM.setEntity<Drawing>(drawingFactory, zIndex);

    createdDrawing = entity;

    if (state !== undefined) {
      entity.setState(cloneDeep(state));
    }

    if (shouldUpdateDrawingsList) {
      this.drawings$.next([...this.drawings$.value, entity].sort((left, right) => left.zIndex - right.zIndex));
    }

    if (shouldSelectAfterCreation) {
      entity.waitForCreation().then(() => {
        if (!this.drawings$.value.includes(entity)) {
          return;
        }

        this.selectedDrawing$.next(entity);
        this.updateActiveTool();
        this.DOM.refreshEntities();
      });
    }

    return entity;
  }

  public getSnapshot(): DrawingsManagerSnapshot {
    return this.drawings$.value
      .filter((drawing) => !drawing.isCreationPending())
      .map((drawing) => this.createDrawingSnapshot(drawing));
  }

  public setSnapshot(snapshot: DrawingsManagerSnapshot): void {
    if (!Array.isArray(snapshot)) {
      return;
    }

    if (!this.mainSeries) {
      this.pendingSnapshot = cloneDeep(snapshot);

      return;
    }

    this.selectedDrawingSnapshot = null;
    this.pointerDrawing = null;
    this.removeDrawings(this.drawings$.value, false);

    const restoredDrawings = snapshot.reduce<Drawing[]>((drawings, item) => {
      if (!drawingsMap[item.drawingName]) {
        return drawings;
      }

      drawings.push(
        this.createDrawing(item.drawingName, {
          id: item.id,
          state: cloneDeep(item.state),
          isLocked: item.isLocked,
          zIndex: item.zIndex,
          shouldUpdateDrawingsList: false,
        }),
      );

      return drawings;
    }, []);

    this.drawings$.next(restoredDrawings.sort((left, right) => left.zIndex - right.zIndex));

    this.activeTool$.next('crosshair');
    this.DOM.refreshEntities();
  }

  public setEndlessDrawingMode = (value: boolean): void => {
    if (value) {
      this.escapeUnregisterHash = this.hotkeys.register({
        keys: [Keys.escape],
        callback: () => {
          this.setEndlessDrawingMode(false);
        },
      });
    } else {
      this.hotkeys.unregister({
        keys: [Keys.escape],
        hash: this.escapeUnregisterHash,
      });

      this.escapeUnregisterHash = null;
    }

    this.endlessMode$.next(value);
  };

  public isEndlessDrawingsMode(): Observable<boolean> {
    return this.endlessMode$.asObservable();
  }

  public getActiveTool(): Observable<ActiveDrawingTool> {
    return this.activeTool$.asObservable();
  }

  public activateCrosshair(): void {
    this.removePendingDrawings(false);
    this.pointerDrawing = null;
    this.activeTool$.next('crosshair');
    this.DOM.refreshEntities();
  }

  public entities(): Observable<Drawing[]> {
    return this.drawings$.asObservable();
  }

  public selectedDrawing(): Observable<Drawing | null> {
    return this.selectedDrawing$.asObservable();
  }

  public updateSelectedDrawingSettings = (settings: SettingsValues): void => {
    const drawing = this.selectedDrawing$.value;

    if (!drawing) {
      return;
    }

    this.updateDrawing(drawing, () => {
      drawing.updateSettings(settings);
    });
  };

  public openSelectedDrawingSettings(): void {
    const drawing = this.selectedDrawing$.value;

    if (!drawing) {
      return;
    }

    this.openSettings(drawing);
  }

  public deleteSelectedDrawing(): void {
    const drawing = this.selectedDrawing$.value;

    if (!drawing) {
      return;
    }

    this.removeDrawing(drawing.id);
  }

  public toggleSelectedDrawingLock(): void {
    const drawing = this.selectedDrawing$.value;

    if (!drawing) {
      return;
    }

    this.updateDrawing(drawing, () => {
      drawing.toggleLock();
    });
  }

  private openSettings = (drawing: Drawing): void => {
    const tabs = drawing.getSettingsTabs();

    if (!tabs.length || tabs.every((tab) => tab.fields.length === 0)) {
      return;
    }

    let settings = drawing.getSettings();

    this.modalRenderer.renderComponent(
      <EntitySettingsModal
        tabs={tabs}
        values={settings}
        onChange={(nextSettings) => {
          settings = nextSettings;
        }}
        initialTabKey={tabs[0]?.key}
      />,
      {
        size: 'sm',
        title: drawing.name,
        onSave: () => {
          if (!this.findDrawing(drawing.id)) {
            return;
          }

          this.updateDrawing(drawing, () => {
            drawing.updateSettings(settings);
          });
        },
      },
    );
  };

  public getDrawings(): Drawing[] {
    return this.drawings$.value;
  }

  public hideAll(): void {
    if (this.selectedDrawing$.value) {
      this.selectedDrawing$.next(null);
    }

    this.pointerDrawing = null;
    this.drawings$.value.forEach((drawing) => drawing.hide());
    this.DOM.refreshEntities();
  }

  public destroy(): void {
    this.hotkeys.unregister({
      keys: [Keys.escape],
      hash: this.escapeUnregisterHash,
    });

    window.removeEventListener('pointermove', this.handlePointerMove);
    window.removeEventListener('pointerup', this.handlePointerUp);
    window.removeEventListener('pointercancel', this.handlePointerUp);
    this.container.removeEventListener('click', this.handleClick);
    this.container.removeEventListener('pointerdown', this.handlePointerDown);
    this.container.removeEventListener('dblclick', this.handleDoubleClick);
    this.container.removeEventListener('contextmenu', this.handleContextMenu);

    this.drawings$.value.forEach((drawing) => drawing.destroy());

    this.selectedDrawingSnapshot = null;
    this.pointerDrawing = null;
    this.copyPasteBuffer = null;

    this.subscriptions.unsubscribe();
    this.drawings$.complete();
    this.selectedDrawing$.complete();
    this.activeTool$.complete();
    this.endlessMode$.complete();
  }
}