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


export enum Keys {
  'escape' = 'escape',
  'delete' = 'delete',
  'tab' = 'tab',
  'shift' = 'shift',
  'control' = 'control',
  'meta' = 'meta',
  'mod' = 'mod',
  'alt' = 'alt',
  't' = 'keyt',
  'h' = 'keyh',
  'v' = 'keyv',
  'f' = 'keyf',
  'r' = 'keyr',
  'y' = 'keyy',
  'z' = 'keyz',
  'c' = 'keyc',
  'mousedown' = 'mousedown',
}

type HotkeyCallback = () => void | Promise<void>;

interface RegisterHotkeyParams {
  keys: Keys[];
  callback: HotkeyCallback;
}

interface UnregisterHotkeyParams {
  keys: Keys[];
  hash?: string | null;
}

interface HotkeyRegistration {
  keys: Keys[];
  callback: HotkeyCallback;
}

export interface IHotkeys {
  register(params: RegisterHotkeyParams): string | null;
  unregister(params: UnregisterHotkeyParams): void;
}

// todo: возможно можно сделать синглтоном и использовать импортируя экземпляр класса,
//  там где это нужно вместо props drilling, как сейчас

export class Hotkeys implements IHotkeys {
  private registrations = new Map<string, HotkeyRegistration>();

  constructor() {
    document.addEventListener('keydown', this.handleKeyDown);
  }

  public register({ keys, callback }: RegisterHotkeyParams): string | null {
    if (keys.length === 0) {
      console.error('[Hotkeys] попытка задать пустой хоткей');

      return null;
    }

    const hash = `hotkeyCallback-${crypto.randomUUID()}`;

    this.registrations.set(hash, {
      keys,
      callback,
    });

    return hash;
  }

  public unregister({ hash }: UnregisterHotkeyParams): void {
    if (!hash) {
      return;
    }

    this.registrations.delete(hash);
  }

  public destroy(): void {
    document.removeEventListener('keydown', this.handleKeyDown);

    this.registrations.clear();
  }

  private handleKeyDown = async (event: KeyboardEvent): Promise<void> => {
    if (event.repeat || isEditableElement(event.target)) {
      return;
    }

    const registration = [...this.registrations.values()]
      .reverse()
      .find(({ keys }) => matchesHotkey(event, keys));

    if (!registration) {
      return;
    }

    event.preventDefault();

    await registration.callback();
  };
}

function matchesHotkey(event: KeyboardEvent, keys: Keys[]): boolean {
  const eventKey = normalizeCode(event.code);
  const primaryKeys = keys.filter((key) => !isModifier(key));

  if (primaryKeys.length > 1) {
    return false;
  }

  if (primaryKeys.length === 1 && primaryKeys[0] !== eventKey) {
    return false;
  }

  if (primaryKeys.length === 0 && !matchesModifierKey(keys, eventKey)) {
    return false;
  }

  if (keys.includes(Keys.mod)) {
    if (!event.ctrlKey && !event.metaKey) {
      return false;
    }
  } else if (event.ctrlKey !== keys.includes(Keys.control) || event.metaKey !== keys.includes(Keys.meta)) {
    return false;
  }

  return event.shiftKey === keys.includes(Keys.shift) && event.altKey === keys.includes(Keys.alt);
}

function matchesModifierKey(keys: Keys[], eventKey: Keys): boolean {
  return keys.includes(eventKey) || (keys.includes(Keys.mod) && (eventKey === Keys.control || eventKey === Keys.meta));
}

function isModifier(key: Keys): boolean {
  return key === Keys.mod || key === Keys.control || key === Keys.meta || key === Keys.shift || key === Keys.alt;
}

function normalizeCode(code: string): Keys {
  switch (code.toLowerCase()) {
    case 'controlleft':
    case 'controlright':
      return Keys.control;

    case 'metaleft':
    case 'metaright':
      return Keys.meta;

    case 'shiftleft':
    case 'shiftright':
      return Keys.shift;

    case 'altleft':
    case 'altright':
      return Keys.alt;

    default:
      return code.toLowerCase() as Keys;
  }
}

function isEditableElement(target: EventTarget | null): boolean {
  if (!(target instanceof HTMLElement)) {
    return false;
  }

  return target.isContentEditable || target.closest('input, textarea, select, [contenteditable]') !== null;
}



import { BehaviorSubject, Observable } from 'rxjs';

import { DrawingsNames } from '@src/constants';
import { ActiveDrawingTool, SettingsValues } from '@src/types';

import { Drawing } from './Drawings';
import { DrawingsManager } from './DrawingsManager';
import { Hotkeys, Keys } from './Hotkeys';
import { PaneManager } from './PaneManager';

// todo: нужно дописывать класс)
export class DrawingsManagerCollection {
  private managersMap: Map<number, DrawingsManager> = new Map();
  private paneCollection: PaneManager;
  private hotkeys: Hotkeys;

  private activeTool$ = new BehaviorSubject<ActiveDrawingTool>('crosshair');
  private endlessMode$ = new BehaviorSubject(false);
  private paneClickListenerUnsub = () => {};

  private hotkeyRegistrations: { keys: Keys[]; hash: string | null }[] = [];

  constructor({ paneCollection, hotkeys }: { paneCollection: PaneManager; hotkeys: Hotkeys }) {
    this.paneCollection = paneCollection;
    this.hotkeys = hotkeys;

    this.hotkeyRegistrations = [
      {
        keys: [Keys.escape],
        hash: this.hotkeys.register({
          keys: [Keys.escape],
          callback: this.activateCrosshair,
        }),
      },
      {
        keys: [Keys.alt, Keys.t],
        hash: this.hotkeys.register({
          keys: [Keys.alt, Keys.t],
          callback: () => {
            this.addDrawingForce(DrawingsNames.trendLine);
          },
        }),
      },
      {
        keys: [Keys.alt, Keys.h],
        hash: this.hotkeys.register({
          keys: [Keys.alt, Keys.h],
          callback: () => {
            this.addDrawingForce(DrawingsNames.horizontalLine);
          },
        }),
      },
      {
        keys: [Keys.alt, Keys.v],
        hash: this.hotkeys.register({
          keys: [Keys.alt, Keys.v],
          callback: () => {
            this.addDrawingForce(DrawingsNames.verticalLine);
          },
        }),
      },
      {
        keys: [Keys.alt, Keys.f],
        hash: this.hotkeys.register({
          keys: [Keys.alt, Keys.f],
          callback: () => {
            this.addDrawingForce(DrawingsNames.fibonacciRetracement);
          },
        }),
      },
      {
        keys: [Keys.alt, Keys.shift, Keys.r],
        hash: this.hotkeys.register({
          keys: [Keys.alt, Keys.shift, Keys.r],
          callback: () => {
            this.addDrawingForce(DrawingsNames.rectangle);
          },
        }),
      },
    ];
  }

  public getIsEndlessMode(): boolean {
    return this.endlessMode$.value;
  }

  public getActiveToolValue(): ActiveDrawingTool {
    return this.activeTool$.value;
  }

  public setActiveTool(next: ActiveDrawingTool): void {
    this.activeTool$.next(next);
  }

  public removeDrawingManager(paneId: number): void {
    this.managersMap.delete(paneId);
  }

  public addDrawingManager(manager: DrawingsManager, paneId: number): void {
    this.managersMap.set(paneId, manager);
  }

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

  public continueDrawing = (name: DrawingsNames): void => {
    this.paneClickListenerUnsub();
    this.setActiveTool(name);

    this.paneClickListenerUnsub = this.paneCollection.listenPanesToAddDrawing(name);
  };

  public setEndlessDrawingMode = (value: boolean): void => {
    if (this.endlessMode$.value === value) {
      return;
    }

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

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

  public activateCrosshair = (): void => {
    this.paneClickListenerUnsub();
    this.setActiveTool('crosshair');

    Array.from(this.managersMap.values()).forEach((manager) => {
      manager.activateCrosshair();
    });
  };

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

  public selectedDrawing(): Observable<Drawing | null> {
    return Array.from(this.managersMap.values())[0].selectedDrawing();
  }

  public updateSelectedDrawingSettings = (settings: SettingsValues): void => {
    Array.from(this.managersMap.values()).forEach((manager) => {
      manager.updateSelectedDrawingSettings(settings);
    });
  };

  public toggleSelectedDrawingLock = (): void => {
    Array.from(this.managersMap.values())[0].toggleSelectedDrawingLock();
  };

  public openSelectedDrawingSettings = (): void => {
    Array.from(this.managersMap.values())[0].openSelectedDrawingSettings();
  };

  public deleteSelectedDrawing = (): void => {
    Array.from(this.managersMap.values())[0].deleteSelectedDrawing();
  };

  public destroy(): void {
    this.hotkeyRegistrations.forEach(({ keys, hash }) => {
      this.hotkeys.unregister({
        keys,
        hash,
      });
    });

    this.hotkeyRegistrations = [];

    this.paneClickListenerUnsub();

    this.activeTool$.complete();
    this.endlessMode$.complete();
  }
}


import { IChartApi, ISeriesApi, MouseEventParams, 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 } 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 { Pane } from './Pane';

import type { DrawingInteraction } from '@src/core/Drawings/SeriesDrawingBase';
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;
  pane: Pane;
  setActiveTool: (name: ActiveDrawingTool) => void;
  getActiveTool: () => ActiveDrawingTool;
  getIsEndlessMode: () => boolean;
  continueDrawing: (name: DrawingsNames) => void;
}

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); // todo: переместить в DrawingsManagerCollection
  private pendingSnapshot: DrawingsManagerSnapshot | null = null;
  private selectedDrawingSnapshot: DrawingSnapshotItem | null = null;
  private continueDrawingScheduled = false;
  private pane: Pane;

  private copyPasteBuffer: DrawingSnapshotItem | null = null;
  private setActiveTool: (name: ActiveDrawingTool) => void;
  private getActiveTool: () => ActiveDrawingTool;
  private getIsEndlessMode: () => boolean;
  private continueDrawing: (name: DrawingsNames) => void;

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

    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;
          doAfterPromise(() => this.setSnapshot(snapshot), this.pane.isReady());
        }
      }),
    );

    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 => {
    if (!this.isEventInPane(event)) {
      return;
    }

    this.selectedDrawingSnapshot = null;

    if (event.button === 0) {
      const drawings = this.drawings$.value;
      const selectedDrawing = this.selectedDrawing$.value;
      const pendingDrawing = drawings.find((drawing) => drawing.isCreationPending());

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

      if (drawing && !drawing.isCreationPending()) {
        this.selectedDrawingSnapshot = this.createDrawingSnapshot(drawing);
      }

      (drawing ?? selectedDrawing)?.getSeriesDrawing().pointerDown(event);
    }

    this.DOM.refreshEntities();
  };

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

    this.selectedDrawingSnapshot = 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();
  };

  private handleDoubleClick = (event: MouseEvent): void => {
    if (!this.isEventInPane(event)) {
      return;
    }

    const pendingDrawing = this.drawings$.value.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.getSeriesDrawing().doubleClick(event);

    this.DOM.refreshEntities();
  };

  private handleContextMenu = (event: MouseEvent): void => {
    if (!this.isEventInPane(event)) {
      return;
    }

    const pendingDrawing = this.drawings$.value.find((drawing) => drawing.isCreationPending());
    const drawing = pendingDrawing ?? this.findTopDrawing(event);

    drawing?.getSeriesDrawing().contextMenu(event);
  };

  private handleClick = (): void => {
    this.DOM.refreshEntities();
  };

  private isEventInPane(event: MouseEvent): boolean {
    const paneElement = this.pane.getHTMLElement();

    return paneElement !== null && event.target instanceof Node && paneElement.contains(event.target);
  }

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

    for (const drawing of this.drawings$.value) {
      if (!drawing.getSeriesDrawing().isHit(event)) {
        continue;
      }

      if (!topDrawing || drawing.zIndex > topDrawing.zIndex) {
        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.setActiveTool('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({
      name: snapshot.drawingName,
      options: {
        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.getActiveTool();

    if (activeTool !== 'crosshair' && this.getIsEndlessMode()) {
      if (this.continueDrawingScheduled) {
        return;
      }

      this.continueDrawingScheduled = true;

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

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

        if (currentTool === 'crosshair' || !this.getIsEndlessMode() || hasPendingAfterTick) {
          return;
        }

        this.continueDrawing(currentTool);
      });

      return;
    }

    this.setActiveTool('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);
    }

    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,
    event?: MouseEventParams,
    shouldContinueDrawing = true,
  ): 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.setActiveTool(name);

    const drawing = this.createDrawing({
      name,
      event,
      shouldContinueDrawing,
    });

    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,
    options = {},
    event,
    shouldContinueDrawing = true,
  }: {
    name: DrawingsNames;
    options?: CreateDrawingOptions;
    event?: MouseEventParams;
    shouldContinueDrawing?: boolean;
  }): 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 = this.pane.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);
          }
        },
        initialEvent: event,
      });
    };

    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,
      });

    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);

        if (shouldContinueDrawing) {
          this.updateActiveTool();
        } else {
          this.setActiveTool('crosshair');
        }

        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.removeDrawings(this.drawings$.value, false);

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

      drawings.push(
        this.createDrawing({
          name: item.drawingName,
          options: {
            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.setActiveTool('crosshair');
    this.DOM.refreshEntities();
  }

  public activateCrosshair(): void {
    this.removePendingDrawings(false);
    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.drawings$.value.forEach((drawing) => drawing.hide());
    this.DOM.refreshEntities();
  }

  public destroy(): void {
    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.copyPasteBuffer = null;

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

async function doAfterPromise(cb: () => void, waiter: Promise<void>) {
  await waiter;
  cb();
}


private initClickListener(): void {
  const handler = (param: MouseEventParams) => {
    if (param.paneIndex === undefined) {
      return;
    }

    const clickedPane = this.getPaneByIndex(param.paneIndex);

    if (!clickedPane) {
      return;
    }

    const isQuickMeasure =
      param.sourceEvent?.shiftKey === true &&
      this.drawingsManagerCollection.getActiveToolValue() === 'crosshair';

    if (isQuickMeasure) {
      clickedPane.getDrawingManager().addDrawingForce(DrawingsNames.ruler, param, false);

      return;
    }

    clickedPane.fireClick(param);
  };

  this.lwcChart.subscribeClick(handler);
  this.subscriptions.add(() => this.lwcChart.unsubscribeClick(handler));
}

import classNames from 'classnames';

import { Button, Divider, Tooltip } from 'exchange-elements/v2';

import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';

import { Observable } from 'rxjs';

import { MenuList } from '@src/components/Menu';
import SplitDropdown from '@src/components/SplitDropdown';

import {
  gannAndFibonacciTools,
  geometricShapes,
  measurementTools,
  trendLines,
} from '@src/components/Toolbar/constants';

import { CHART_DRAWING_TOOLTIP, DrawingsNames } from '@src/constants';
import { t } from '@src/translations';
import { ActiveDrawingTool } from '@src/types';
import { ensureDefined, useObservable } from '@src/utils';

import {
  CrossIcon,
  EyeBrushIcon,
  HeartIcon,
  LayersIcon,
  MagnetIcon,
  PencilLockerIcon,
  RulerIcon,
  SplineCurveIcon,
  TrashIcon,
  TypeIcon,
  UnlockIcon,
  ZoomInIcon,
} from '../Icon';

import styles from './index.module.scss';

interface ToolbarProps {
  toggleDOM: () => void;
  addDrawing: (name: DrawingsNames) => Promise<void>;
  setEndlessDrawingsMode: (value: boolean) => void;
  isEndlessDrawingsMode$: Observable<boolean>;
  activateCrosshair: () => void;
  activeTool$: Observable<ActiveDrawingTool>;
}

const implemented = {
  crosshair: true,
  lines: true,
  fib: true,
  rectangle: true,
  text: true,
  XABCD: false,
  position: true,
  icons: false,
};

export default function Toolbar({
  toggleDOM,
  addDrawing,
  setEndlessDrawingsMode,
  isEndlessDrawingsMode$,
  activateCrosshair,
  activeTool$,
}: ToolbarProps) {
  const [selectedLineType, setSelectedLineType] = useState<DrawingsNames>(DrawingsNames.trendLine);
  const [selectedMeasurementTool, setSelectedMeasurementTool] = useState(DrawingsNames.fixedRangeProfile);
  const [selectedGeometricShape, setSelectedGeometricShape] = useState(DrawingsNames.rectangle);
  const [selectedGannAndFibonacci, setSelectedGannAndFibonacci] = useState(DrawingsNames.fibonacciRetracement);

  const isEndlessDrawingsMode = useObservable(isEndlessDrawingsMode$);

  const activeTool = useObservable(activeTool$, 'crosshair');

  const toolbarRef = useRef<HTMLDivElement | null>(null);

  const createDrawingHandler = (setter: Dispatch<SetStateAction<DrawingsNames>>) => async (value: DrawingsNames) => {
    setter(value);
    await addDrawing(value);
  };

  const findOptionByValue = <T extends { value: string }>(options: T[], selectedValue: T['value']): T | undefined =>
    options.find((item) => item.value === selectedValue);

  const selectedTrendLineOption = findOptionByValue(trendLines(), selectedLineType);
  const selectedMeasurementToolOption = findOptionByValue(measurementTools(), selectedMeasurementTool);
  const selectedGeometricShapeOption = findOptionByValue(geometricShapes(), selectedGeometricShape);
  const selectedGannAndFibonacciOption = findOptionByValue(gannAndFibonacciTools(), selectedGannAndFibonacci);

  const getButtonClassName = (tool: ActiveDrawingTool) =>
    classNames(styles.button, {
      [styles.pressed]: activeTool === tool,
    });

  const getTooltipClassName = () => classNames(styles.tooltipHint, CHART_DRAWING_TOOLTIP);

  useEffect(() => {
    if (activeTool === 'crosshair') {
      return;
    }

    const drawingTool = activeTool as DrawingsNames;

    if (trendLines().some(({ value }) => value === drawingTool)) {
      setSelectedLineType(drawingTool);

      return;
    }

    if (gannAndFibonacciTools().some(({ value }) => value === drawingTool)) {
      setSelectedGannAndFibonacci(drawingTool);

      return;
    }

    if (geometricShapes().some(({ value }) => value === drawingTool)) {
      setSelectedGeometricShape(drawingTool);

      return;
    }

    if (measurementTools().some(({ value }) => value === drawingTool)) {
      setSelectedMeasurementTool(drawingTool);
    }
  }, [activeTool]);

  return (
    <div
      ref={toolbarRef}
      className={styles.toolbar}
    >
      <div className={classNames(styles.group)}>
        {implemented.crosshair && (
          <Tooltip
            tooltipClassName={getTooltipClassName()}
            showMessageOnFocus
            label={t('Crosshair')}
            location="right"
          >
            <Button
              size="sm"
              onClick={activateCrosshair}
              className={getButtonClassName('crosshair')}
              label={<CrossIcon />}
            />
          </Tooltip>
        )}

        {implemented.lines && (
          <SplitDropdown
            anchorRef={toolbarRef}
            mainContent={ensureDefined(selectedTrendLineOption?.icon)}
            mainTooltip={ensureDefined(selectedTrendLineOption).label}
            onMainClick={() => addDrawing(selectedLineType)}
            menuTooltip={t('Trend line')}
            mainButtonClassName={getButtonClassName(selectedLineType)}
            tooltipClassName={getTooltipClassName()}
          >
            <MenuList
              mode="single"
              value={selectedLineType}
              options={trendLines()}
              onClick={createDrawingHandler(setSelectedLineType)}
            />
          </SplitDropdown>
        )}

        {implemented.fib && (
          <SplitDropdown
            anchorRef={toolbarRef}
            mainContent={ensureDefined(selectedGannAndFibonacciOption?.icon)}
            mainTooltip={ensureDefined(selectedGannAndFibonacciOption).label}
            onMainClick={() => addDrawing(selectedGannAndFibonacci)}
            menuTooltip={t('Gann and Fibonacci')}
            mainButtonClassName={getButtonClassName(selectedGannAndFibonacci)}
            tooltipClassName={getTooltipClassName()}
          >
            <MenuList
              mode="single"
              value={selectedGannAndFibonacci}
              options={gannAndFibonacciTools()}
              onClick={createDrawingHandler(setSelectedGannAndFibonacci)}
            />
          </SplitDropdown>
        )}

        {implemented.rectangle && (
          <SplitDropdown
            anchorRef={toolbarRef}
            mainContent={ensureDefined(selectedGeometricShapeOption?.icon)}
            mainTooltip={ensureDefined(selectedGeometricShapeOption).label}
            onMainClick={() => addDrawing(selectedGeometricShape)}
            menuTooltip={t('Geometric shapes')}
            mainButtonClassName={getButtonClassName(selectedGeometricShape)}
            tooltipClassName={getTooltipClassName()}
          >
            <MenuList
              mode="single"
              value={selectedGeometricShape}
              options={geometricShapes()}
              onClick={createDrawingHandler(setSelectedGeometricShape)}
            />
          </SplitDropdown>
        )}

        {implemented.text && (
          <Tooltip
            tooltipClassName={getTooltipClassName()}
            showMessageOnFocus
            label={t('Text')}
            location="right"
          >
            <Button
              size="sm"
              className={getButtonClassName(DrawingsNames.text)}
              onClick={() => addDrawing(DrawingsNames.text)}
              label={<TypeIcon />}
            />
          </Tooltip>
        )}

        {implemented.XABCD && (
          <Tooltip
            tooltipClassName={getTooltipClassName()}
            showMessageOnFocus
            label={t('XABCD template')}
            location="right"
          >
            <Button
              size="sm"
              className={styles.button}
              onClick={() => {}}
              label={<SplineCurveIcon />}
            />
          </Tooltip>
        )}

        {implemented.position && (
          <SplitDropdown
            anchorRef={toolbarRef}
            mainContent={ensureDefined(selectedMeasurementToolOption?.icon)}
            mainTooltip={ensureDefined(selectedMeasurementToolOption).label}
            menuTooltip={t('Measurement tools')}
            onMainClick={() => addDrawing(selectedMeasurementTool)}
            mainButtonClassName={getButtonClassName(selectedMeasurementTool)}
            tooltipClassName={getTooltipClassName()}
          >
            <MenuList
              mode="single"
              value={selectedMeasurementTool}
              options={measurementTools()}
              onClick={createDrawingHandler(setSelectedMeasurementTool)}
            />
          </SplitDropdown>
        )}

        {implemented.icons && (
          <Tooltip
            label={t('Pin')}
            location="right"
            tooltipClassName={getTooltipClassName()}
            showMessageOnFocus
          >
            <Button
              size="sm"
              className={styles.button}
              onClick={() => {}}
              label={<HeartIcon />}
            />
          </Tooltip>
        )}
      </div>

      <Divider
        direction="horizontal"
        pt={{ divider: { className: styles.divider } }}
      />

      <div className={classNames(styles.group)}>
        <Tooltip
          tooltipClassName={getTooltipClassName()}
          showMessageOnFocus
          label={t('Ruler')}
          location="right"
        >
          <Button
            size="sm"
            className={getButtonClassName(DrawingsNames.ruler)}
            onClick={() => addDrawing(DrawingsNames.ruler)}
            label={<RulerIcon />}
          />
        </Tooltip>

        <Tooltip
          tooltipClassName={getTooltipClassName()}
          showMessageOnFocus
          label={t('Upscale')}
          location="right"
          className={styles.notImplemented}
        >
          <Button
            size="sm"
            className={styles.button}
            onClick={() => {}}
            label={<ZoomInIcon />}
          />
        </Tooltip>
      </div>

      <Divider
        direction="horizontal"
        pt={{ divider: { className: classNames(styles.divider) } }}
      />

      <div className={classNames(styles.group)}>
        <Tooltip
          tooltipClassName={getTooltipClassName()}
          showMessageOnFocus
          label={t('Magnet allows you to attract objects points to the nearest bar prices')}
          location="right"
          className={styles.notImplemented}
        >
          <Button
            size="sm"
            className={styles.button}
            onClick={() => {}}
            label={<MagnetIcon />}
          />
        </Tooltip>

        <Tooltip
          tooltipClassName={getTooltipClassName()}
          showMessageOnFocus
          label={t('Endless drawing mode')}
          location="right"
        >
          <Button
            size="sm"
            className={`${styles.button} ${isEndlessDrawingsMode ? styles.pressed : ''}`}
            onClick={() => setEndlessDrawingsMode(!isEndlessDrawingsMode)}
            label={<PencilLockerIcon />}
          />
        </Tooltip>

        <Tooltip
          tooltipClassName={getTooltipClassName()}
          showMessageOnFocus
          label={t('Fix all objects')}
          location="right"
          className={styles.notImplemented}
        >
          <Button
            size="sm"
            className={styles.button}
            onClick={() => {}}
            label={<UnlockIcon />}
          />
        </Tooltip>

        <Tooltip
          tooltipClassName={getTooltipClassName()}
          showMessageOnFocus
          label={t('Hide all drawing objects')}
          location="right"
          className={styles.notImplemented}
        >
          <Button
            size="sm"
            className={styles.button}
            onClick={() => {}}
            label={<EyeBrushIcon />}
          />
        </Tooltip>
      </div>

      <Divider
        direction="horizontal"
        pt={{ divider: { className: classNames(styles.divider) } }}
      />

      <div className={classNames(styles.group)}>
        <Tooltip
          tooltipClassName={getTooltipClassName()}
          showMessageOnFocus
          label={t('Clear objects')}
          location="right"
          className={styles.notImplemented}
        >
          <Button
            size="sm"
            className={styles.button}
            onClick={() => {}}
            label={<TrashIcon />}
          />
        </Tooltip>

        <Tooltip
          tooltipClassName={getTooltipClassName()}
          showMessageOnFocus
          label={t('DOM tree')}
          location="right"
        >
          <Button
            size="sm"
            className={styles.button}
            onClick={() => toggleDOM()}
            label={<LayersIcon />}
          />
        </Tooltip>
      </div>
    </div>
  );
}