import * as React from "react";
import PropTypes from "prop-types";
import { useEffect, useRef, useCallback } from "react";
import { debounce, isEqual } from "radash";
import {
  Tldraw,
  DefaultColorStyle,
  DefaultDashStyle,
  DefaultFillStyle,
  DefaultFontStyle,
  DefaultSizeStyle,
  InstancePresenceRecordType,
  setDefaultUiAssetUrls,
  setDefaultEditorAssetUrls,
} from "@bigbluebutton/tldraw";
import "@bigbluebutton/tldraw/tldraw.css";
import { compressToBase64, decompressFromBase64 } from 'lz-string';
import SlideCalcUtil from "/imports/utils/slideCalcUtils";
import { HUNDRED_PERCENT } from "/imports/utils/slideCalcUtils";
// eslint-disable-next-line import/no-extraneous-dependencies
import Settings from '/imports/ui/services/settings';
import meetingClientSettingsInitialValues from '/imports/ui/core/initial-values/meetingClientSettings';
import getFromUserSettings from '/imports/ui/services/users-settings';
import KEY_CODES from "/imports/utils/keyCodes";
import Styled from "./styles";
import {
  mapLanguage,
  isValidShapeType,
} from "./utils";
import { useMouseEvents, useCursor } from "./hooks";
import { notifyShapeNumberExceeded, getCustomEditorAssetUrls, getCustomAssetUrls } from "./service";

import NoopTool from './custom-tools/noop-tool/component';

import { PollShapeUtil } from './custom-shapes/poll/component';

const CAMERA_TYPE = "camera";

// Helper functions
const deleteLocalStorageItemsWithPrefix = (prefix) => {
  const keysToRemove = Object.keys(localStorage).filter((key) =>
    key.startsWith(prefix)
  );
  keysToRemove.forEach((key) => localStorage.removeItem(key));
};

// Example of typical LocalStorage entry tldraw creates:
// `{ TLDRAW_USER_DATA_v3: '{"version":2,"user":{"id":"epDk1 ...`
const clearTldrawCache = () => {
  deleteLocalStorageItemsWithPrefix("TLDRAW");
};

const calculateEffectiveZoom = (initViewboxWidth, curViewboxWidth, initViewboxHeight, curViewboxHeight) => {
  // Calculate the effective zoom level based on the change in viewBox dimensions
  const widthZoomValue = (initViewboxWidth * 100) / curViewboxWidth;
  const heightZoomValue = (initViewboxHeight * 100) / curViewboxHeight;
  
  // Take the smaller zoom value to ensure the entire content fits in the viewbox
  const effectiveZoomValue = Math.min(widthZoomValue, heightZoomValue);
  return effectiveZoomValue;
};

const determineViewerFitToWidth = (currentPresentationPage) => {
  return (
    currentPresentationPage?.scaledViewBoxWidth ===
      currentPresentationPage?.scaledWidth &&
    currentPresentationPage?.scaledViewBoxHeight !==
      currentPresentationPage?.scaledHeight
  );
};

const createCamera = (pageId, zoomLevel) => ({
  id: `camera:page:${pageId}`,
  meta: {},
  typeName: CAMERA_TYPE,
  x: 0,
  y: 0,
  z: zoomLevel,
});

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

const defaultUser = {
  userId: '',
};

const Whiteboard = React.memo(function Whiteboard(props) {
  const {
    isPresenter = false,
    removeShapes,
    persistShapeWrapper,
    shapes,
    assets,
    currentUser = defaultUser,
    whiteboardId = undefined,
    zoomSlide,
    curPageId,
    zoomChanger,
    isMultiUserActive,
    isRTL,
    fitToWidth,
    zoomValue,
    colorStyle,
    dashStyle,
    fillStyle,
    fontStyle,
    sizeStyle,
    presentationAreaHeight,
    presentationAreaWidth,
    setTldrawIsMounting,
    setTldrawAPI,
    whiteboardToolbarAutoHide,
    toggleToolsAnimations,
    animations,
    isToolbarVisible,
    isModerator,
    currentPresentationPage,
    presentationId = undefined,
    hasWBAccess,
    bgShape,
    publishCursorUpdate,
    otherCursors,
    hideViewersCursor,
    presentationHeight,
    presentationWidth,
    skipToSlide,
    intl,
    maxNumberOfAnnotations,
    notifyNotAllowedChange,
    locale,
    darkTheme,
    selectedLayout,
    isInfiniteWhiteboard,
    whiteboardWriters,
  } = props;

  clearTldrawCache();

  const [tlEditor, setTlEditor] = React.useState(null);
  const [isMounting, setIsMounting] = React.useState(true);
  const [initialViewBoxWidth, setInitialViewBoxWidth] = React.useState(null);
  const [initialViewBoxHeight, setInitialViewBoxHeight] = React.useState(null);

  if (isMounting) {
    setDefaultEditorAssetUrls(getCustomEditorAssetUrls());
    setDefaultUiAssetUrls(getCustomAssetUrls());
  }

  const whiteboardRef = React.useRef(null);
  const zoomValueRef = React.useRef(null);
  const prevShapesRef = React.useRef(shapes);
  const tlEditorRef = React.useRef(tlEditor);
  const slideChanged = React.useRef(false);
  const slideNext = React.useRef(null);
  const prevZoomValueRef = React.useRef(null);
  const initialZoomRef = useRef(null);
  const isMouseDownRef = useRef(false);
  const isMountedRef = useRef(false);
  const isWheelZoomRef = useRef(false);
  const isPresenterRef = useRef(isPresenter);
  const whiteboardIdRef = React.useRef(whiteboardId);
  const curPageIdRef = React.useRef(curPageId);
  const hasWBAccessRef = React.useRef(hasWBAccess);
  const isModeratorRef = React.useRef(isModerator);
  const currentPresentationPageRef = React.useRef(currentPresentationPage);
  const initialViewBoxWidthRef = React.useRef(null);
  const initialViewBoxHeightRef = React.useRef(null);
  const previousTool = React.useRef(null);

  const THRESHOLD = 0.1;
  const CAMERA_UPDATE_DELAY = 650;
  const lastKnownHeight = React.useRef(presentationAreaHeight);
  const lastKnownWidth = React.useRef(presentationAreaWidth);

  const [shapesVersion, setShapesVersion] = React.useState(0);

  const customShapeUtils = [PollShapeUtil];
  const customTools = [NoopTool];

  const presenterChanged = usePrevious(isPresenter) !== isPresenter;

  let clipboardContent = null;
  let isPasting = false;
  let pasteTimeout = null;

  const setIsMouseDown = (val) => {
    isMouseDownRef.current = val;
  };

  const setIsWheelZoom = (val) => {
    isWheelZoomRef.current = val;
  };

  const setWheelZoomTimeout = () => {
    isWheelZoomRef.currentTimeout = setTimeout(() => {
      setIsWheelZoom(false);
    }, 300);
  };

  React.useEffect(() => {
    currentPresentationPageRef.current = currentPresentationPage;
  }, [currentPresentationPage]);

  React.useEffect(() => {
    curPageIdRef.current = curPageId;
  }, [curPageId]);

  React.useEffect(() => {
    isModeratorRef.current = isModerator;
  }, [isModerator]);

  React.useEffect(() => {
    whiteboardIdRef.current = whiteboardId;
  }, [whiteboardId]);

  React.useEffect(() => {
    hasWBAccessRef.current = hasWBAccess;

    if (!hasWBAccess && !isPresenter) {
      tlEditorRef?.current?.setCurrentTool('noop');
    } else if (hasWBAccess && !isPresenter) {
      tlEditorRef?.current?.setCurrentTool('draw');
    }
  }, [hasWBAccess]);

  React.useEffect(() => {
      isPresenterRef.current = isPresenter;

      if (!hasWBAccessRef.current && !isPresenter) {
        tlEditorRef?.current?.setCurrentTool('noop');
      }
  }, [isPresenter]);

  React.useEffect(() => {
    if (!isEqual(prevShapesRef.current, shapes)) {
      prevShapesRef.current = shapes;
      setShapesVersion((v) => v + 1);
    }
  }, [shapes]);

  React.useEffect(() => {
    if (whiteboardRef.current) {
      whiteboardRef.current.addEventListener("keydown", handleKeyDown, {
        capture: true,
      });
    }

    return () => {
      whiteboardRef.current?.removeEventListener("keydown", handleKeyDown);
    };
  }, [whiteboardRef.current]);

  const language = React.useMemo(() => {
    return mapLanguage(locale?.toLowerCase() || "en");
  }, [locale]);

  const [cursorPosition, updateCursorPosition] = useCursor(
    publishCursorUpdate,
    whiteboardIdRef.current
  );

  const pasteTldrawContent = (editor, clipboard, point) => {
    const p = point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : undefined);
    editor.mark('paste');
    editor.putContentOntoCurrentPage(clipboard, {
      point: p,
      select: true,
    });
  };

  const handleCut = useCallback((shouldCopy) => {
    const selectedShapes = tlEditorRef.current?.getSelectedShapes();
    if (!selectedShapes || selectedShapes.length === 0) {
      return;
    }
    if (shouldCopy) {
      handleCopy();
    }
    tlEditorRef.current?.deleteShapes(selectedShapes.map(shape => shape.id));
  }, [tlEditorRef]);

  const handleCopy = useCallback(() => {
    const selectedShapes = tlEditorRef.current?.getSelectedShapes();
    if (!selectedShapes || selectedShapes.length === 0) {
      return;
    }
    const content = tlEditorRef.current?.getContentFromCurrentPage(selectedShapes.map(shape => shape.id));
    if (content) {
      clipboardContent = content;
      const stringifiedClipboard = compressToBase64(
        JSON.stringify({
          type: 'application/tldraw',
          kind: 'content',
          data: content,
        })
      );

      if (navigator.clipboard?.write) {
        const htmlBlob = new Blob([`<tldraw>${stringifiedClipboard}</tldraw>`], {
          type: 'text/html',
        });

        navigator.clipboard.write([
          new ClipboardItem({
            'text/html': htmlBlob,
            'text/plain': new Blob([''], { type: 'text/plain' }),
          }),
        ]);
      } else if (navigator.clipboard.writeText) {
        navigator.clipboard.writeText(`<tldraw>${stringifiedClipboard}</tldraw>`);
      }
    }
  }, [tlEditorRef]);

  const handlePaste = useCallback(() => {
    if (isPasting) {
      return;
    }
    isPasting = true;

    clearTimeout(pasteTimeout);
    pasteTimeout = setTimeout(() => {
      if (clipboardContent) {
        pasteTldrawContent(tlEditorRef.current, clipboardContent);
        isPasting = false;
      } else {
        navigator.clipboard.readText().then((text) => {
          const match = text.match(/<tldraw>(.*)<\/tldraw>/);
          if (match && match[1]) {
            const content = JSON.parse(decompressFromBase64(match[1]));
            pasteTldrawContent(tlEditorRef.current, content);
          }
          isPasting = false;
        }).catch((error) => {
          isPasting = false;
        });
      }
    }, 100);
  }, [tlEditorRef]);

  const handleKeyDown = useCallback((event) => {
    const debouncedUndo = debounce({ delay: 150 }, () => {
      tlEditorRef.current?.undo();
    });

    if (event.key === 'Escape' || event.keyCode === 27) {
      tlEditorRef.current?.deselect(...tlEditorRef.current?.getSelectedShapes());
      return;
    }

    const editingShape = tlEditorRef.current?.getEditingShape();
    if (editingShape && (isPresenterRef.current || hasWBAccessRef.current)) {
      return;
    }

    if (event.key === 'Delete') {
      handleCut(false);
      return;
    }

    if (event.key === ' ' && tlEditorRef.current?.getCurrentToolId() !== 'hand' && isPresenterRef.current) {
      previousTool.current = tlEditorRef.current?.getCurrentToolId();
      tlEditorRef.current?.setCurrentTool("hand");
      return;
    }

    // Mapping of simple key shortcuts to tldraw functions
    const simpleKeyMap = {
      'v': () => tlEditorRef.current?.setCurrentTool("select"),
      'd': () => tlEditorRef.current?.setCurrentTool("draw"),
      'e': () => tlEditorRef.current?.setCurrentTool("eraser"),
      'h': () => tlEditorRef.current?.setCurrentTool("hand"),
      'r': () => tlEditorRef.current?.setCurrentTool("rectangle"),
      'o': () => tlEditorRef.current?.setCurrentTool("ellipse"),
      'a': () => tlEditorRef.current?.setCurrentTool("arrow"),
      'l': () => tlEditorRef.current?.setCurrentTool("line"),
      't': () => tlEditorRef.current?.setCurrentTool("text"),
      'f': () => tlEditorRef.current?.setCurrentTool("frame"),
      'n': () => tlEditorRef.current?.setCurrentTool("note"),
    };

    if (event.ctrlKey || event.metaKey) {
      const ctrlKeyMap = {
        'a': () => {
          tlEditorRef.current?.selectAll();
          tlEditorRef.current?.setCurrentTool("select");
        },
        'd': () => {
          tlEditorRef.current?.duplicateShapes(tlEditorRef.current?.getSelectedShapes(), { x: 35, y: 35 });
          tlEditorRef.current?.selectNone();
        },
        'x': () => {
          handleCut(true);
        },
        'c': () => {
          handleCopy();
        },
        'v': () => {
          if (!isPasting) {
            handlePaste();
          }
        },
        'z': debouncedUndo,
      };
      if (ctrlKeyMap[event.key]) {
        event.preventDefault();
        event.stopPropagation();
        ctrlKeyMap[event.key]();
        return;
      }
    }

    if (simpleKeyMap[event.key]) {
      event.preventDefault();
      event.stopPropagation();
      simpleKeyMap[event.key]();
      return;
    }

    const moveDistance = 10;
    const selectedShapes = tlEditorRef.current?.getSelectedShapes().map(shape => shape.id);

    const arrowKeyMap = {
      'ArrowUp': { x: 0, y: -moveDistance },
      'ArrowDown': { x: 0, y: moveDistance },
      'ArrowLeft': { x: -moveDistance, y: 0 },
      'ArrowRight': { x: moveDistance, y: 0 },
    };

    if (arrowKeyMap[event.key]) {
      event.preventDefault();
      event.stopPropagation();
      tlEditorRef.current?.nudgeShapes(selectedShapes, arrowKeyMap[event.key], { squashing: true });
    }
  }, [tlEditorRef, isPresenterRef, hasWBAccessRef, previousTool, handleCut, handleCopy, handlePaste]);

  const handleTldrawMount = (editor) => {
    setTlEditor(editor);
    setTldrawAPI(editor);

    editor?.user?.updateUserPreferences({ locale: language });

    const debouncePersistShape = debounce({ delay: 0 }, persistShapeWrapper);

    const colorStyles = [
      "black",
      "blue",
      "green",
      "grey",
      "light-blue",
      "light-green",
      "light-red",
      "light-violet",
      "orange",
      "red",
      "violet",
      "yellow",
    ];
    const dashStyles = ["dashed", "dotted", "draw", "solid"];
    const fillStyles = ["none", "pattern", "semi", "solid"];
    const fontStyles = ["draw", "mono", "sans", "serif"];
    const sizeStyles = ["l", "m", "s", "xl"];

    if (colorStyles.includes(colorStyle)) {
      editor.setStyleForNextShapes(DefaultColorStyle, colorStyle);
    }
    if (dashStyles.includes(dashStyle)) {
      editor.setStyleForNextShapes(DefaultDashStyle, dashStyle);
    }
    if (fillStyles.includes(fillStyle)) {
      editor.setStyleForNextShapes(DefaultFillStyle, fillStyle);
    }
    if (fontStyles.includes(fontStyle)) {
      editor.setStyleForNextShapes(DefaultFontStyle, fontStyle);
    }
    if (sizeStyles.includes(sizeStyle)) {
      editor.setStyleForNextShapes(DefaultSizeStyle, sizeStyle);
    }

    editor.store.listen(
      (entry) => {
        const { changes } = entry;
        const { added, updated, removed } = changes;

        const addedCount = Object.keys(added).length;
        const shapeNumberExceeded = Object.keys(prevShapesRef.current).length + addedCount > maxNumberOfAnnotations;
        const invalidShapeType = Object.keys(added).find((id) => !isValidShapeType(added[id]));

        if (shapeNumberExceeded || invalidShapeType) {
          // notify and undo last command without persisting to not generate the onUndo/onRedo callback
          if (shapeNumberExceeded) {
            notifyShapeNumberExceeded(intl, maxNumberOfAnnotations);
          } else {
            notifyNotAllowedChange(intl);
          }
          editor.history.undo({ persist: false });
        } else {
          Object.values(added).forEach((record) => {
            const updatedRecord = {
              ...record,
              meta: {
                ...record.meta,
                createdBy: currentUser?.userId,
              },
            };

            persistShapeWrapper(
              updatedRecord,
              whiteboardIdRef.current,
              isModeratorRef.current
            );
          });
        }

        Object.values(updated).forEach(([_, record]) => {
          const createdBy =
            prevShapesRef.current[record?.id]?.meta?.createdBy ||
            currentUser?.userId;
          const updatedRecord = {
            ...record,
            meta: {
              createdBy,
              updatedBy: currentUser?.userId,
            },
          };

          persistShapeWrapper(
            updatedRecord,
            whiteboardIdRef.current,
            isModeratorRef.current
          );
        });

        Object.values(removed).forEach((record) => {
          removeShapes([record?.id]);
        });
      },
      { source: "user", scope: "document" }
    );

    editor.store.listen(
      (entry) => {
        const { changes, source } = entry;
        const { updated } = changes;
        const { "pointer:pointer": pointers } = updated;

        if ((isPresenterRef.current || hasWBAccessRef.current) && pointers) {
          const [prevPointer, nextPointer] = pointers;
          updateCursorPosition(nextPointer?.x, nextPointer?.y);
        }

        const camKey = `camera:page:${curPageIdRef.current}`;
        const { [camKey]: cameras } = updated;

        if (cameras) {
          const [prevCam, nextCam] = cameras;
          const panned = prevCam.x !== nextCam.x || prevCam.y !== nextCam.y;

          if (panned && isPresenterRef.current) {
            let viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(
              editor?.getViewportPageBounds()?.w,
              currentPresentationPageRef.current?.scaledWidth
            );
            let viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(
              editor?.getViewportPageBounds()?.h,
              currentPresentationPageRef.current?.scaledHeight
            );

            zoomSlide(viewedRegionW, viewedRegionH, nextCam.x, nextCam.y);
          }
        }
      },
      { source: "user" }
    );

    if (editor && curPageIdRef.current) {
      const pages = [
        {
          meta: {},
          id: `page:${curPageIdRef.current}`,
          name: `Slide ${curPageIdRef.current}`,
          index: `a1`,
          typeName: "page",
        },
      ];

      editor.store.mergeRemoteChanges(() => {
        editor.batch(() => {
          editor.store.put(pages);
          editor.store.put(assets);
          editor.setCurrentPage(`page:${curPageIdRef.current}`);
          editor.store.put(bgShape);
          editor.history.clear();
        });
      });

      const remoteShapes = shapes;
      const localShapes = editor.store.allRecords();
      const filteredShapes =
        localShapes.filter((item) => item.typeName === "shape") || [];

      const localShapesObj = {};
      filteredShapes.forEach((shape) => {
        localShapesObj[shape.id] = shape;
      });

      const shapesToAdd = [];
      for (let id in remoteShapes) {
        if (
          !localShapesObj[id] ||
          JSON.stringify(remoteShapes[id]) !==
            JSON.stringify(localShapesObj[id])
        ) {
          shapesToAdd.push(remoteShapes[id]);
        }
      }

      editor.store.mergeRemoteChanges(() => {
        if (shapesToAdd && shapesToAdd.length) {
          shapesToAdd.forEach((shape) => {
            delete shape.isModerator;
            delete shape.questionType;
          });
          editor.store.put(shapesToAdd);
        }
      });

      editor.store.onBeforeChange = (prev, next, source) => {
        if (next?.typeName === "instance_page_state") {
          if (isPresenterRef.current || isModeratorRef.current) return next;

          // Filter selectedShapeIds based on shape owner
          if (next.selectedShapeIds.length > 0 && !isEqual(prev.selectedShapeIds, next.selectedShapeIds)) {
            next.selectedShapeIds = next.selectedShapeIds.filter((shapeId) => {
              const shapeOwner = prevShapesRef.current[shapeId]?.meta?.createdBy;
              return !shapeOwner || shapeOwner === currentUser?.userId;
            });
          }

          if (!isEqual(prev.hoveredShapeId, next.hoveredShapeId)) {
            const hoveredShapeOwner = prevShapesRef.current[next.hoveredShapeId]?.meta?.createdBy;
            if (hoveredShapeOwner !== currentUser?.userId) {
              next.hoveredShapeId = null;
            }
          }

          return next;
        }

        // Get viewport dimensions and bounds
        const viewportPageBounds = editor.getViewportPageBounds();
        const { w: viewportWidth, h: viewportHeight } = viewportPageBounds;

        const presentationWidth = currentPresentationPage?.scaledWidth || 0;
        const presentationHeight = currentPresentationPage?.scaledHeight || 0;

        // Adjust camera position to ensure it stays within bounds
        const panned = next?.id?.includes("camera") && (prev.x !== next.x || prev.y !== next.y);
        if (panned && !currentPresentationPageRef.current?.infiniteWhiteboard) {
          // Horizontal bounds check
          if (next.x > 0) {
            next.x = 0;
          } else if (next.x < -(presentationWidth - viewportWidth)) {
            next.x = -(presentationWidth - viewportWidth);
          }

          // Vertical bounds check
          if (next.y > 0) {
            next.y = 0;
          } else if (next.y < -(presentationHeight - viewportHeight)) {
            next.y = -(presentationHeight - viewportHeight);
          }
        }

        return next;
      };

      if (!isPresenterRef.current && !hasWBAccessRef.current) {
        editor.setCurrentTool('noop');
      }
    }

    isMountedRef.current = true;
  };

  const { shapesToAdd, shapesToUpdate, shapesToRemove } = React.useMemo(() => {
    const localShapes = tlEditorRef.current?.getCurrentPageShapes();
    const filteredShapes =
      localShapes?.filter((item) => item?.index !== "a0") || [];
    const localLookup = new Map(
      filteredShapes.map((shape) => [shape.id, shape])
    );
    const remoteShapeIds = Object.keys(prevShapesRef.current);
    const toAdd = [];
    const toUpdate = [];
    const toRemove = [];

    Object.values(prevShapesRef.current).forEach((remoteShape) => {
      if (!remoteShape.id) return;
      const localShape = localLookup.get(remoteShape.id);
      const prevShape = prevShapesRef.current[remoteShape.id];

      if (!localShape) {
        delete remoteShape.isModerator;
        delete remoteShape.questionType;
        toAdd.push(remoteShape);
      } else {
        const remoteShapeMeta = remoteShape?.meta;
        const isCreatedByCurrentUser = remoteShapeMeta?.createdBy === currentUser?.userId;
        const isUpdatedByCurrentUser = remoteShapeMeta?.updatedBy === currentUser?.userId;

        // System-level shapes (background image) lack createdBy and updatedBy metadata, which can cause false positives.
        // These cases expect an early return and shouldn't be updated.
        if (
          remoteShapeMeta && (
            (isCreatedByCurrentUser && isUpdatedByCurrentUser)
            || (!isCreatedByCurrentUser && isUpdatedByCurrentUser)
          )
        ) {
          return;
        }

        const diff = remoteShape;
        delete diff.isModerator;
        delete diff.questionType;
        toUpdate.push(diff);
      }
    });

    filteredShapes.forEach((localShape) => {
      if (!remoteShapeIds.includes(localShape.id)) {
        toRemove.push(localShape.id);
      }
    });

    return {
      shapesToAdd: toAdd,
      shapesToUpdate: toUpdate,
      shapesToRemove: toRemove,
    };
  }, [prevShapesRef.current, curPageId]);

  const setCamera = (zoom, x = 0, y = 0) => {
    if (tlEditorRef.current) {
      tlEditorRef.current.setCamera({ x, y, z: zoom }, false);
    }
  };

  const calculateZoomValue = (localWidth, localHeight, isViewer = false) => {
    let calcedZoom;

    calcedZoom = fitToWidth
      ? presentationAreaWidth / localWidth
      : Math.min(
          presentationAreaWidth / localWidth,
          presentationAreaHeight / localHeight
        );

    return calcedZoom === 0 || calcedZoom === Infinity
      ? HUNDRED_PERCENT
      : calcedZoom;
  };

  const calculateZoomWithGapValue = (
    localWidth,
    localHeight,
    isViewer = false,
    widthAdjustment = 0
  ) => {
    let presentationWidth = presentationAreaWidth - widthAdjustment;
    let calcedZoom = (baseZoom = fitToWidth
      ? presentationWidth / localWidth
      : Math.min(
          presentationWidth / localWidth,
          presentationAreaHeight / localHeight
        ));
    return calcedZoom === 0 || calcedZoom === Infinity
      ? HUNDRED_PERCENT
      : calcedZoom;
  };

  useMouseEvents(
    { whiteboardRef, tlEditorRef, isWheelZoomRef, initialZoomRef },
    {
      isPresenter,
      hasWBAccess: hasWBAccessRef.current,
      whiteboardToolbarAutoHide,
      animations,
      publishCursorUpdate,
      whiteboardId: whiteboardIdRef.current,
      cursorPosition,
      updateCursorPosition,
      toggleToolsAnimations,
      currentPresentationPage,
      zoomChanger,
      setIsMouseDown,
      setIsWheelZoom,
      setWheelZoomTimeout,
    }
  );

  React.useEffect(() => {
    tlEditorRef.current = tlEditor;
  }, [tlEditor]);

  React.useEffect(() => {
    let undoRedoIntervalId = null;

    const undo = () => {
      tlEditorRef?.current?.history?.undo();
    };

    const redo = () => {
      tlEditorRef?.current?.history?.redo();
    };

    const handleArrowPress = (event) => {
      const currPageNum = parseInt(curPageIdRef.current);
      const shapeSelected = tlEditorRef.current.getSelectedShapes()?.length > 0;
      const changeSlide = (direction) => {
        if (!currentPresentationPage) return;
        let newSlideNum = currPageNum + direction;
        const outOfBounds =
          direction > 0
            ? newSlideNum > currentPresentationPage?.totalPages
            : newSlideNum < 1;

        if (outOfBounds) return;

        skipToSlide(newSlideNum);
        zoomChanger(HUNDRED_PERCENT);
        zoomSlide(HUNDRED_PERCENT, HUNDRED_PERCENT, 0, 0);
      };

      if (!shapeSelected) {
        if (event.keyCode === KEY_CODES.ARROW_RIGHT) {
          changeSlide(1); // Move to the next slide
        } else if (event.keyCode === KEY_CODES.ARROW_LEFT) {
          changeSlide(-1); // Move to the previous slide
        }
      }
    };

    const handleUndoRedoOnCondition = (condition, action) => {
      if (condition) {
        action();
      }
    };

    const handleKeyDown = (event) => {
      const isUndo =
        (event.ctrlKey || event.metaKey) && event.key === "z" && !event.shiftKey;
      const isRedo =
        (event.ctrlKey || event.metaKey) && event.shiftKey && event.key === "Z";

      if (
        (isUndo || isRedo) &&
        (isPresenterRef.current || hasWBAccessRef.current)
      ) {
        event.preventDefault();
        event.stopPropagation();

        if (!undoRedoIntervalId) {
          undoRedoIntervalId = setInterval(() => {
            handleUndoRedoOnCondition(isUndo, undo);
            handleUndoRedoOnCondition(isRedo, redo);
          }, 300);
        }
      }

      if (
        (event.keyCode === KEY_CODES.ARROW_RIGHT ||
          event.keyCode === KEY_CODES.ARROW_LEFT) &&
        isPresenterRef.current
      ) {
        handleArrowPress(event);
      }
    };

    const handleKeyUp = (event) => {
      if ((event.key === "z" || event.key === "Z") && undoRedoIntervalId) {
        clearInterval(undoRedoIntervalId);
        undoRedoIntervalId = null;
      }

      if (event.key === ' ') {
        if (previousTool.current) {
          tlEditorRef.current?.setCurrentTool(previousTool.current);
          previousTool.current = null;
        }
      }
    };

    whiteboardRef.current?.addEventListener("keydown", handleKeyDown, {
      capture: true,
    });
    whiteboardRef.current?.addEventListener("keyup", handleKeyUp, {
      capture: true,
    });

    return () => {
      whiteboardRef.current?.removeEventListener("keydown", handleKeyDown);
      whiteboardRef.current?.removeEventListener("keyup", handleKeyUp);
      if (undoRedoIntervalId) {
        clearInterval(undoRedoIntervalId);
      }
    };
  }, [whiteboardRef.current]);

  React.useEffect(() => {
    zoomValueRef.current = zoomValue;
    let timeoutId = null;

    if (
      tlEditor &&
      curPageIdRef.current &&
      currentPresentationPage &&
      isPresenter &&
      isWheelZoomRef.current === false
    ) {
      const zoomLevelForReset =
        initialZoomRef.current ||
        calculateZoomValue(
          currentPresentationPage.scaledWidth,
          currentPresentationPage.scaledHeight
        );

      const zoomCamera =
        zoomValue === HUNDRED_PERCENT
          ? zoomLevelForReset
          : (zoomLevelForReset * zoomValue) / HUNDRED_PERCENT;
      const camera = tlEditorRef.current.getCamera();

      const nextCamera = {
        x:
          zoomValue === HUNDRED_PERCENT
            ? 0
            : camera.x +
              ((camera.x + tlEditorRef.current.getViewportPageBounds().w / 2) /
                zoomCamera -
                camera.x),
        y:
          zoomValue === HUNDRED_PERCENT
            ? 0
            : camera.y +
              ((camera.y + tlEditorRef.current.getViewportPageBounds().h / 2) /
                zoomCamera -
                camera.y),
        z: zoomCamera,
      };

      if (
        zoomValue !== prevZoomValueRef.current ||
        zoomValue === HUNDRED_PERCENT
      ) {
        tlEditor.setCamera(nextCamera, false);

        timeoutId = setTimeout(() => {
          if (zoomValue === HUNDRED_PERCENT) {
            zoomChanger(HUNDRED_PERCENT);
            zoomSlide(HUNDRED_PERCENT, HUNDRED_PERCENT, 0, 0);
          } else {
            // Recalculate viewed region width and height for zoomSlide call
            let viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(
              tlEditorRef.current.getViewportPageBounds().w,
              currentPresentationPage.scaledWidth
            );
            let viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(
              tlEditorRef.current.getViewportPageBounds().h,
              currentPresentationPage.scaledHeight
            );

            zoomSlide(viewedRegionW, viewedRegionH, nextCamera.x, nextCamera.y);
          }
        }, 500);
      }
    }

    // Update the previous zoom value ref with the current zoom value
    prevZoomValueRef.current = zoomValue;
    return () => clearTimeout(timeoutId);
  }, [zoomValue, tlEditor, curPageId, isWheelZoomRef.current]);

  React.useEffect(() => {
    // A slight delay to ensure the canvas has rendered
    const timeoutId = setTimeout(() => {
      if (
        currentPresentationPage.scaledWidth > 0 &&
        currentPresentationPage.scaledHeight > 0
      ) {
        // Subtract the toolbar height from the presentation area height for the presenter
        const adjustedPresentationAreaHeight = isPresenter
          ? presentationAreaHeight - 40
          : presentationAreaHeight;
        const slideAspectRatio =
          currentPresentationPage.scaledWidth /
          currentPresentationPage.scaledHeight;
        const presentationAreaAspectRatio =
          presentationAreaWidth / adjustedPresentationAreaHeight;

        let initialZoom;

        if (slideAspectRatio > presentationAreaAspectRatio) {
          initialZoom =
            presentationAreaWidth / currentPresentationPage.scaledWidth;
        } else {
          initialZoom =
            adjustedPresentationAreaHeight /
            currentPresentationPage.scaledHeight;
        }

        const tldrawZoom = initialZoom;
        initialZoomRef.current = initialZoom;
        prevZoomValueRef.current = zoomValue;
      }
    }, CAMERA_UPDATE_DELAY);

    return () => clearTimeout(timeoutId);
  }, [
    currentPresentationPage.scaledHeight,
    currentPresentationPage.scaledWidth,
    presentationAreaWidth,
    presentationAreaHeight,
    isPresenter,
    presentationId,
  ]);

  React.useEffect(() => {
    const handleResize = () => {
      if (!initialViewBoxWidthRef.current) {
        initialViewBoxWidthRef.current = currentPresentationPageRef.current?.scaledViewBoxWidth;
      }
      if (!initialViewBoxHeightRef.current) {
        initialViewBoxHeightRef.current = currentPresentationPageRef.current?.scaledViewBoxHeight;
      }
      // Calculate the absolute difference
      const heightDifference = Math.abs(
        presentationAreaHeight - lastKnownHeight.current
      );
      const widthDifference = Math.abs(
        presentationAreaWidth - lastKnownWidth.current
      );

      // Check if the difference is greater than the threshold
      if (heightDifference > THRESHOLD || widthDifference > THRESHOLD) {
        // Update the last known dimensions
        lastKnownHeight.current = presentationAreaHeight;
        lastKnownWidth.current = presentationAreaWidth;

        if (
          presentationAreaHeight > 0 &&
          presentationAreaWidth > 0 &&
          tlEditor &&
          currentPresentationPage &&
          currentPresentationPage.scaledWidth > 0 &&
          currentPresentationPage.scaledHeight > 0
        ) {
          const currentZoom = zoomValueRef.current || HUNDRED_PERCENT;
          const baseZoom = calculateZoomValue(
            currentPresentationPage.scaledWidth,
            currentPresentationPage.scaledHeight
          );
          let adjustedZoom = baseZoom * (currentZoom / HUNDRED_PERCENT);
          if (isPresenter) {
            const container = document.querySelector(
              '[data-test="presentationContainer"]'
            );
            const innerWrapper = document.getElementById(
              "presentationInnerWrapper"
            );
            const containerWidth = container ? container.offsetWidth : 0;
            const innerWrapperWidth = innerWrapper
              ? innerWrapper.offsetWidth
              : 0;
            const widthGap = Math.max(containerWidth - innerWrapperWidth, 0);
            const camera = tlEditorRef.current.getCamera();

            let adjustedZoom;
            if (widthGap > 0) {
              adjustedZoom = calculateZoomWithGapValue(
                currentPresentationPage.scaledWidth,
                currentPresentationPage.scaledHeight,
                false,
                widthGap
              );

              adjustedZoom *= currentZoom / HUNDRED_PERCENT;
            } else {
              adjustedZoom = baseZoom * (currentZoom / HUNDRED_PERCENT);
            }

            const zoomToApply =
              widthGap > 0
                ? adjustedZoom
                : baseZoom * (currentZoom / HUNDRED_PERCENT);

            const formattedPageId = Number(curPageIdRef?.current);
            const cameras = [
              createCamera(formattedPageId - 1, zoomToApply),
              createCamera(formattedPageId, zoomToApply),
              createCamera(formattedPageId + 1, zoomToApply),
            ];
            tlEditorRef.current.store.put(cameras);
            setCamera(zoomToApply, camera.x, camera.y);
          } else {
            // Viewer logic
            const effectiveZoom = calculateEffectiveZoom(
              initialViewBoxWidthRef.current,
              currentPresentationPage.scaledViewBoxWidth,
              initialViewBoxHeightRef.current,
              currentPresentationPage.scaledViewBoxHeight
            );
            adjustedZoom = baseZoom * (effectiveZoom / HUNDRED_PERCENT);

            const camera = tlEditorRef.current.getCamera();
            const formattedPageId = Number(curPageIdRef?.current);
            const cameras = [
              createCamera(formattedPageId - 1, adjustedZoom),
              createCamera(formattedPageId, adjustedZoom),
              createCamera(formattedPageId + 1, adjustedZoom),
            ];
            tlEditorRef.current.store.put(cameras);
            setCamera(adjustedZoom, camera.x, camera.y);
          }
        }
      }
    };

    const timeoutId = setTimeout(handleResize, CAMERA_UPDATE_DELAY);

    return () => clearTimeout(timeoutId);
  }, [presentationAreaHeight, presentationAreaWidth, curPageId, presentationId]);

  React.useEffect(() => {
    if (!fitToWidth && isPresenter) {
      zoomChanger(HUNDRED_PERCENT);
      zoomSlide(HUNDRED_PERCENT, HUNDRED_PERCENT, 0, 0);
    }
  }, [fitToWidth, isPresenter]);

  React.useEffect(() => {
    if (!isPresenter && tlEditorRef.current && initialViewBoxWidthRef.current && initialViewBoxHeightRef.current && currentPresentationPage) {
      const effectiveZoom = calculateEffectiveZoom(
        initialViewBoxWidthRef.current,
        currentPresentationPage.scaledViewBoxWidth,
        initialViewBoxHeightRef.current,
        currentPresentationPage.scaledViewBoxHeight
      );
  
      let adjustedZoom = calculateZoomValue(
        currentPresentationPage.scaledWidth,
        currentPresentationPage.scaledHeight
      ) * (effectiveZoom / HUNDRED_PERCENT);

      const adjustedXPos = currentPresentationPage.xOffset;
      const adjustedYPos = currentPresentationPage.yOffset;

      setCamera(
        adjustedZoom,
        adjustedXPos,
        adjustedYPos
      );
    }
  }, [currentPresentationPage]);

  React.useEffect(() => {
    if (shapesToAdd.length || shapesToUpdate.length || shapesToRemove.length) {
      const tlStoreUpdateTimeoutId = setTimeout(() => {
        tlEditor?.store?.mergeRemoteChanges(() => {
          if (shapesToRemove.length > 0) {
            tlEditor?.store?.remove(shapesToRemove);
          }
          if (shapesToAdd.length) {
            tlEditor?.store?.put(shapesToAdd);
          }
          if (shapesToUpdate.length) {
            const updatedShapes = shapesToUpdate.map(shape => {
              const currentShape = tlEditor?.getShape(shape.id);
              if (currentShape) {
                return { ...currentShape, ...shape };
              }
              return null;
            }).filter(Boolean);

            if (updatedShapes.length) {
              tlEditor?.store?.put(updatedShapes);
            }
          }
        });
      }, 300);

      return () => clearTimeout(tlStoreUpdateTimeoutId);
    }
  }, [shapesToAdd, shapesToUpdate, shapesToRemove]);

  // Updating presences in tldraw store based on changes in cursors
  React.useEffect(() => {
    if (tlEditorRef.current) {
      const useElement = document.querySelector(".tl-cursor use");
      if (useElement && !isMultiUserActive && !isPresenter) {
        useElement.setAttribute("href", "#redPointer");
      } else if (useElement) {
        useElement.setAttribute("href", "#cursor");
      }

      const idsToRemove = [];

      // Get all presence records from the store
      const allRecords = tlEditorRef.current.store.allRecords();
      const presenceRecords = allRecords.filter(record => record.id.startsWith('instance_presence:'));

      // Check if any presence records correspond to users not in whiteboardWriters
      presenceRecords.forEach(record => {
        const userId = record.userId.split('instance_presence:')[1];
        const hasAccessToWhiteboard = whiteboardWriters.some(writer => writer.userId === userId);

        if (!hasAccessToWhiteboard) {
          idsToRemove.push(record.id);
        }
      });

      const updatedPresences = otherCursors
        .map(({ userId, user, xPercent, yPercent }) => {
          const { presenter, name } = user;
          const id = InstancePresenceRecordType.createId(userId);
          const active = xPercent !== -1 && yPercent !== -1;
          // if cursor is not active remove it from tldraw store
          if (
            !active ||
            (hideViewersCursor &&
              user.role === "VIEWER" &&
              !currentUser?.presenter) ||
            (!presenter && !isMultiUserActive)
          ) {
            idsToRemove.push(id);
            return null;
          }

          const cursor = {
            x: xPercent,
            y: yPercent,
            type: "default",
            rotation: 0,
          };
          const color = presenter ? "#FF0000" : "#70DB70";
          const c = {
            ...InstancePresenceRecordType.create({
              id,
              currentPageId: `page:${curPageIdRef.current}`,
              userId,
              userName: name,
              cursor,
              color,
            }),
            lastActivityTimestamp: Date.now(),
          };

          return c;
        })
        .filter((cursor) => cursor && cursor.userId !== currentUser?.userId);

      if (idsToRemove.length) {
        tlEditorRef.current?.store.remove(idsToRemove);
      }

      // If there are any updated presences, put them all in the store
      if (updatedPresences.length) {
        tlEditorRef.current?.store.put(updatedPresences);
      }
    }
  }, [otherCursors, whiteboardWriters]);

  // set current tldraw page when presentation id updates
  React.useEffect(() => {
    if (tlEditorRef.current && curPageIdRef.current !== "0") {
      const pages = [
        {
          meta: {},
          id: `page:${curPageIdRef.current}`,
          name: `Slide ${curPageIdRef.current}`,
          index: `a1`,
          typeName: "page",
        }
      ];

      tlEditorRef.current.store.mergeRemoteChanges(() => {
        tlEditorRef.current.batch(() => {
          tlEditorRef.current.store.put(pages);
          const tlZ = tlEditorRef.current.getCamera()?.z;
          const formattedPageId = Number(curPageIdRef?.current);
          const cameras = [
            createCamera(formattedPageId - 1, tlZ),
            createCamera(formattedPageId, tlZ),
            createCamera(formattedPageId + 1, tlZ),
          ];
          tlEditorRef.current.store.put(cameras);
          tlEditorRef.current.setCurrentPage(`page:${curPageIdRef.current}`);
          tlEditorRef.current.store.put(assets);
          tlEditorRef.current.store.put(bgShape);
          tlEditorRef.current.history.clear();
        });
      });

      whiteboardToolbarAutoHide &&
        toggleToolsAnimations(
          "fade-in",
          "fade-out",
          "0s",
          hasWBAccessRef.current || isPresenter
        );
      slideChanged.current = false;
      slideNext.current = null;
    }
  }, [curPageId]);

  const adjustCameraOnMount = (includeViewerLogic = true) => {
    if (presenterChanged) {
      localStorage.removeItem('initialViewBoxWidth');
      localStorage.removeItem('initialViewBoxHeight');
    }

    const storedWidth = localStorage.getItem('initialViewBoxWidth');
    const storedHeight = localStorage.getItem('initialViewBoxHeight');
    if (storedWidth && storedHeight) {
      initialViewBoxWidthRef.current = parseFloat(storedWidth);
      initialViewBoxHeightRef.current = parseFloat(storedHeight);
    } else {
      const currentZoomLevel = currentPresentationPageRef.current.scaledWidth / currentPresentationPageRef.current.scaledViewBoxWidth;
      const calculatedWidth = currentZoomLevel !== 1
        ? currentPresentationPageRef.current.scaledWidth / currentZoomLevel
        : currentPresentationPageRef.current.scaledWidth;
      const calculatedHeight = currentZoomLevel !== 1
        ? currentPresentationPageRef.current.scaledHeight / currentZoomLevel
        : currentPresentationPageRef.current.scaledHeight;

      initialViewBoxWidthRef.current = calculatedWidth;
      initialViewBoxHeightRef.current = calculatedHeight;
      localStorage.setItem('initialViewBoxWidth', calculatedWidth.toString());
      localStorage.setItem('initialViewBoxHeight', calculatedHeight.toString());
    }

    setTimeout(() => {
      if (
        presentationAreaHeight > 0 &&
        presentationAreaWidth > 0 &&
        currentPresentationPageRef.current &&
        currentPresentationPageRef.current.scaledWidth > 0 &&
        currentPresentationPageRef.current.scaledHeight > 0
      ) {
        const adjustedPresentationAreaHeight = isPresenter
          ? presentationAreaHeight - 40
          : presentationAreaHeight;

        let effectiveZoom = calculateEffectiveZoom(
          initialViewBoxWidthRef.current,
          currentPresentationPageRef.current.scaledViewBoxWidth,
          initialViewBoxHeightRef.current,
          currentPresentationPageRef.current.scaledViewBoxHeight
        );

        let baseZoom;
        if (isPresenter) {
          baseZoom = fitToWidth
            ? presentationAreaWidth / currentPresentationPageRef.current.scaledWidth
            : Math.min(
                presentationAreaWidth / currentPresentationPageRef.current.scaledWidth,
                adjustedPresentationAreaHeight / currentPresentationPageRef.current.scaledHeight
              );

          const zoomAdjustmentFactor = currentPresentationPageRef.current.scaledWidth / currentPresentationPageRef.current.scaledViewBoxWidth;
          baseZoom *= zoomAdjustmentFactor;

          const adjustedXPos = currentPresentationPageRef.current.xOffset;
          const adjustedYPos = currentPresentationPageRef.current.yOffset;

          setCamera(baseZoom, adjustedXPos, adjustedYPos);
        } else if (includeViewerLogic) {
          baseZoom = Math.min(
            presentationAreaWidth / currentPresentationPageRef.current.scaledWidth,
            adjustedPresentationAreaHeight / currentPresentationPageRef.current.scaledHeight
          );

          const zoomAdjustmentFactor = currentPresentationPageRef.current.scaledWidth / currentPresentationPageRef.current.scaledViewBoxWidth;
          baseZoom *= zoomAdjustmentFactor;

          const presenterXOffset = currentPresentationPageRef.current.xOffset;
          const presenterYOffset = currentPresentationPageRef.current.yOffset;

          const adjustedXPos = isInfiniteWhiteboard ? presenterXOffset : presenterXOffset * effectiveZoom;
          const adjustedYPos = isInfiniteWhiteboard ? presenterYOffset : presenterYOffset * effectiveZoom;

          setCamera(baseZoom, adjustedXPos, adjustedYPos);
        }
      }
    }, CAMERA_UPDATE_DELAY);
  };

  React.useEffect(() => {
    if (isMountedRef.current) {
      adjustCameraOnMount(true);
    }
  }, [
    isMountedRef.current,
    presentationId,
    curPageId,
    isMultiUserActive,
    isPresenter,
    animations,
    locale,
    whiteboardToolbarAutoHide,
    darkTheme,
    isInfiniteWhiteboard,
  ]);

  React.useEffect(() => {
    if (isMountedRef.current) {
      adjustCameraOnMount(false);
    }
  }, [
    isMountedRef.current,
    selectedLayout,
    isInfiniteWhiteboard,
  ]);

  React.useEffect(() => {
    setTldrawIsMounting(true);
    return () => {
      isMountedRef.current = false;
      localStorage.removeItem('initialViewBoxWidth');
      localStorage.removeItem('initialViewBoxHeight');
    };
  }, []);

  React.useEffect(() => {
    if (isMounting) {
      setIsMounting(false);
      /// brings presentation toolbar back
      setTldrawIsMounting(false);
    }
  }, [
    tlEditorRef?.current?.camera,
    presentationAreaWidth,
    presentationAreaHeight,
    presentationId,
  ]);

  React.useEffect (() => {
    const bbb_multi_user_tools = getFromUserSettings(
      'bbb_multi_user_tools',
      meetingClientSettingsInitialValues.public.whiteboard.toolbar.multiUserTools,
    );
      const allElements = document.querySelectorAll('[data-testid^="tools."]');

      if (bbb_multi_user_tools.length >= 1 && !isModerator) {
        allElements.forEach((element) => {
          const toolName = element.getAttribute('data-testid').split('.')[1];

          if (!bbb_multi_user_tools.includes(toolName)) {
            element.style.display = 'none';
          }
        });
      }
    }),[]

    React.useEffect (() => {
      const bbb_presenter_tools = getFromUserSettings(
        'bbb_presenter_tools',
        meetingClientSettingsInitialValues.public.whiteboard.toolbar.presenterTools,
      );
        const allElements = document.querySelectorAll('[data-testid^="tools."]');

        if (bbb_presenter_tools.length >= 1 && isPresenter) {
          allElements.forEach((element) => {
            const toolName = element.getAttribute('data-testid').split('.')[1];

            if (!bbb_presenter_tools.includes(toolName)) {
              element.style.display = 'none';
            }
          });
        }
      }),[]

      React.useEffect (() => {
        const bbb_multi_user_pen_only = getFromUserSettings(
          'bbb_multi_user_pen_only',
          false,
        );
          const allElements = document.querySelectorAll('[data-testid^="tools."]');

          if (bbb_multi_user_pen_only && !isModerator && !isPresenter) {
            allElements.forEach((element) => {
              const toolName = element.getAttribute('data-testid').split('.')[1];

              if (toolName != 'draw') {
                element.style.display = 'none';
              } else {
                element.style.display = 'flex';
              }
            });
          }
        }),[]

  return (
    <div
      ref={whiteboardRef}
      id={"whiteboard-element"}
      key={`animations=-${animations}-${whiteboardToolbarAutoHide}-${language}-${presentationId}`}
    >
      <Tldraw
        autoFocus={false}
        key={`tldrawv2-${presentationId}-${animations}-${isInfiniteWhiteboard}`}
        forceMobile={true}
        hideUi={hasWBAccessRef.current || isPresenter ? false : true}
        onMount={handleTldrawMount}
        shapeUtils={customShapeUtils}
        tools={customTools}
      />
      <Styled.TldrawV2GlobalStyle
        {...{
          hasWBAccess: hasWBAccessRef.current,
          isPresenter,
          isRTL,
          isMultiUserActive,
          isToolbarVisible,
          presentationHeight,
        }}
      />
    </div>
  );
});

export default Whiteboard;

Whiteboard.propTypes = {
  isPresenter: PropTypes.bool,
  isIphone: PropTypes.bool.isRequired,
  removeShapes: PropTypes.func.isRequired,
  persistShapeWrapper: PropTypes.func.isRequired,
  notifyNotAllowedChange: PropTypes.func.isRequired,
  shapes: PropTypes.objectOf(PropTypes.shape).isRequired,
  assets: PropTypes.arrayOf(PropTypes.shape).isRequired,
  currentUser: PropTypes.shape({
    userId: PropTypes.string.isRequired,
  }),
  whiteboardId: PropTypes.string,
  zoomSlide: PropTypes.func.isRequired,
  curPageId: PropTypes.string.isRequired,
  presentationWidth: PropTypes.number.isRequired,
  presentationHeight: PropTypes.number.isRequired,
  zoomChanger: PropTypes.func.isRequired,
  isRTL: PropTypes.bool.isRequired,
  fitToWidth: PropTypes.bool.isRequired,
  zoomValue: PropTypes.number.isRequired,
  intl: PropTypes.shape({
    formatMessage: PropTypes.func.isRequired,
  }).isRequired,
  maxStickyNoteLength: PropTypes.number.isRequired,
  fontFamily: PropTypes.string.isRequired,
  colorStyle: PropTypes.string.isRequired,
  dashStyle: PropTypes.string.isRequired,
  fillStyle: PropTypes.string.isRequired,
  fontStyle: PropTypes.string.isRequired,
  sizeStyle: PropTypes.string.isRequired,
  presentationAreaHeight: PropTypes.number.isRequired,
  presentationAreaWidth: PropTypes.number.isRequired,
  maxNumberOfAnnotations: PropTypes.number.isRequired,
  notifyShapeNumberExceeded: PropTypes.func.isRequired,
  darkTheme: PropTypes.bool.isRequired,
  setTldrawIsMounting: PropTypes.func.isRequired,
  width: PropTypes.number.isRequired,
  height: PropTypes.number.isRequired,
  fullscreenElementId: PropTypes.string.isRequired,
  isFullscreen: PropTypes.bool.isRequired,
  layoutContextDispatch: PropTypes.func.isRequired,
  fullscreenAction: PropTypes.string.isRequired,
  handleToggleFullScreen: PropTypes.func.isRequired,
  presentationId: PropTypes.string,
};
