import * as React from "react";
import PropTypes from "prop-types";
import { useRef } from "react";
import { debounce, isEqual } from "radash";
import {
  Tldraw,
  DefaultColorStyle,
  DefaultDashStyle,
  DefaultFillStyle,
  DefaultFontStyle,
  DefaultSizeStyle,
  InstancePresenceRecordType,
  setDefaultUiAssetUrls,
  setDefaultEditorAssetUrls,
} from "@tldraw/tldraw";
import "@tldraw/tldraw/tldraw.css";
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 KEY_CODES from "/imports/utils/keyCodes";
import Styled from "./styles";
import {
  mapLanguage,
  isValidShapeType,
} from "./utils";
import { useMouseEvents, useCursor } from "./hooks";
import { notifyShapeNumberExceeded, customEditorAssetUrls, customAssetUrls } from "./service";

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

// 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) => {
  // Calculate the effective zoom level based on the change in viewBoxWidth
  const effectiveZoomValue = (initViewboxWidth * 100) / curViewboxWidth;
  return effectiveZoomValue;
};

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

setDefaultEditorAssetUrls(customEditorAssetUrls);

setDefaultUiAssetUrls(customAssetUrls);

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

  clearTldrawCache();

  const [tlEditor, setTlEditor] = React.useState(null);
  const [isMounting, setIsMounting] = React.useState(true);
  const [initialZoomSet, setInitialZoomSet] = React.useState(false);
  const [initialViewBoxWidth, setInitialViewBoxWidth] = React.useState(null);

  const whiteboardRef = React.useRef(null);
  const zoomValueRef = React.useRef(null);
  const prevShapesRef = React.useRef(shapes);
  const prevOtherCursorsRef = useRef(otherCursors);
  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 THRESHOLD = 0.1;
  const lastKnownHeight = React.useRef(presentationAreaHeight);
  const lastKnownWidth = React.useRef(presentationAreaWidth);

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

  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");
    }
  }, [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 (!isEqual(prevOtherCursorsRef.current, otherCursors)) {
      prevOtherCursorsRef.current = otherCursors;
    }
  }, [otherCursors]);

  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(Settings?.application?.locale?.toLowerCase() || "en");
  }, [Settings?.application?.locale]);

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

  const handleKeyDown = (event) => {
    if (!isPresenterRef.current) {
      if (
        !hasWBAccessRef.current ||
        (hasWBAccessRef.current && !tlEditorRef.current?.getEditingShape())
      ) {
        event.preventDefault();
        event.stopPropagation();
        return;
      }
    }
  };

  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 (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 && isPresenter) {
            let viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(
              editor?.getViewportPageBounds()?.w,
              currentPresentationPage?.scaledWidth
            );
            let viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(
              editor?.getViewportPageBounds()?.h,
              currentPresentationPage?.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.deletePage(editor.currentPageId);
          editor.setCurrentPage(`page:${curPageIdRef.current}`);
          editor.store.put(assets);
          editor.createShapes(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 (isPresenter || 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;
        }

        const camera = editor?.getCamera();
        const { maxX, maxY, minX, minY } = editor.getViewportPageBounds();
        const panned =
          next?.id?.includes("camera") &&
          (prev.x !== next.x || prev.y !== next.y);
        const zoomed = next?.id?.includes("camera") && prev.z !== next.z;
        if (panned) {
          // // limit bounds
          if (maxX > currentPresentationPage?.scaledWidth) {
            next.x += maxX - currentPresentationPage?.scaledWidth;
          }
          if (maxY > currentPresentationPage?.scaledHeight) {
            next.y += maxY - currentPresentationPage?.scaledHeight;
          }
          if (next.x > 0 || minX < 0) {
            next.x = 0;
          }
          if (next.y > 0 || minY < 0) {
            next.y = 0;
          }
        }
        return next;
      };
    }

    isMountedRef.current = true;
  };

  const { shapesToAdd, shapesToUpdate, shapesToRemove } = React.useMemo(() => {
    const selectedShapeIds = tlEditorRef.current?.getSelectedShapeIds() || [];
    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 = [];

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

    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 if (!isEqual(localShape, remoteShape) && prevShape) {
        const diff = {
          id: remoteShape.id,
          type: remoteShape.type,
          typeName: remoteShape.typeName,
        };

        Object.keys(remoteShape).forEach((key) => {
          if (
            key !== "isModerator" &&
            !isEqual(remoteShape[key], localShape[key])
          ) {
            diff[key] = remoteShape[key];
          }
        });

        if (remoteShape.props) {
          Object.keys(remoteShape.props).forEach((key) => {
            if (!isEqual(remoteShape.props[key], localShape.props[key])) {
              diff.props = diff.props || {};
              diff.props[key] = remoteShape.props[key];
            }
          });
        }

        delete diff.isModerator;
        delete diff.questionType;
        toUpdate.push(diff);
      }
    });

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

  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;
    if (isViewer) {
      // Logic originally in calculateViewerZoom
      calcedZoom = fitToWidth
        ? presentationAreaWidth / localWidth
        : Math.min(
            presentationAreaWidth / localWidth,
            presentationAreaHeight / localHeight
          );
    } else {
      // Logic originally in calculateZoom
      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.selectedShapes.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 undoCondition =
        event.ctrlKey && event.key === "z" && !event.shiftKey;
      const redoCondition =
        event.ctrlKey && event.shiftKey && event.key === "Z";

      if (
        (undoCondition || redoCondition) &&
        (isPresenter || hasWBAccessRef.current)
      ) {
        event.preventDefault();
        event.stopPropagation();

        if (!undoRedoIntervalId) {
          undoRedoIntervalId = setInterval(() => {
            handleUndoRedoOnCondition(undoCondition, undo);
            handleUndoRedoOnCondition(redoCondition, redo);
          }, 150);
        }
      }

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

    const handleKeyUp = (event) => {
      if ((event.key === "z" || event.key === "Z") && undoRedoIntervalId) {
        clearInterval(undoRedoIntervalId);
        undoRedoIntervalId = 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);
          }
        }, 300);
      }
    }

    // Update the previous zoom value ref with the current zoom value
    prevZoomValueRef.current = zoomValue;
    return () => clearTimeout(timeoutId);
  }, [zoomValue, tlEditor, curPageIdRef.current, 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;
        setInitialZoomSet(true);
        prevZoomValueRef.current = zoomValue;
      }
    }, 1000);

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

  React.useEffect(() => {
    const handleResize = () => {
      if (!initialViewBoxWidthRef.current) {
        initialViewBoxWidthRef.current = currentPresentationPageRef.current?.scaledViewBoxWidth;
      }
      // 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) {
            setTimeout(() => {
              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);

              setCamera(zoomToApply, camera.x, camera.y);
            }, 50);
          } else {
            // Viewer logic
            const effectiveZoom = calculateEffectiveZoom(
              initialViewBoxWidthRef.current,
              currentPresentationPage.scaledViewBoxWidth
            );
            adjustedZoom = baseZoom * (effectiveZoom / HUNDRED_PERCENT);

            const camera = tlEditorRef.current.getCamera();
            setCamera(adjustedZoom, camera.x, camera.y);
          }
        }
      }
    };

    const timeoutId = setTimeout(handleResize, 300);

    return () => clearTimeout(timeoutId);
  }, [presentationAreaHeight, presentationAreaWidth, curPageIdRef.current]);

  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 && currentPresentationPage) {
      const currentZoomRatio = currentPresentationPage.scaledViewBoxWidth / currentPresentationPage.scaledWidth;
      const initialZoomRatio = initialViewBoxWidthRef.current / currentPresentationPage.scaledWidth;
      const effectiveZoom = initialZoomRatio / currentZoomRatio;

      let adjustedZoom = calculateZoomValue(
        currentPresentationPage.scaledWidth,
        currentPresentationPage.scaledHeight
      ) * effectiveZoom;

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

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

  React.useEffect(() => {
    // Check if there are any changes to be made
    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) {
            tlEditor?.updateShapes(shapesToUpdate);
          }
        });
      }, 150);

      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 updatedPresences = prevOtherCursorsRef.current
        .map(({ userId, user, xPercent, yPercent }) => {
          const { presenter, name } = user;
          const id = InstancePresenceRecordType.createId(userId);
          const active = yPercent !== -1 && yPercent !== -1;
          // if cursor is not active remove it from tldraw store
          if (
            !active ||
            (hideViewersCursor &&
              user.role === "VIEWER" &&
              !currentUser?.presenter) ||
            (!presenter && !isMultiUserActive)
          ) {
            tlEditorRef.current?.store.remove([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 there are any updated presences, put them all in the store
      if (updatedPresences.length) {
        tlEditorRef.current?.store.put(updatedPresences);
      }
    }
  }, [prevOtherCursorsRef.current]);

  // 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);
          // tlEditorRef.current.deletePage(tlEditorRef.current.currentPageId);
          tlEditorRef.current.setCurrentPage(`page:${curPageIdRef.current}`);
          tlEditorRef.current.store.put(assets);
          tlEditorRef.current.createShapes(bgShape);
          tlEditorRef.current.history.clear();
        });
      });

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

  React.useEffect(() => {
    if (isMountedRef.current) {
      const storedWidth = localStorage.getItem('initialViewBoxWidth');
      if (storedWidth) {
        initialViewBoxWidthRef.current = parseFloat(storedWidth);
      } else {
        const currentZoomLevel = currentPresentationPageRef.current.scaledWidth / currentPresentationPageRef.current.scaledViewBoxWidth;
        const calculatedWidth = currentZoomLevel !== 1
          ? currentPresentationPageRef.current.scaledWidth / currentZoomLevel
          : currentPresentationPageRef.current.scaledWidth;
        initialViewBoxWidthRef.current = calculatedWidth;
        localStorage.setItem('initialViewBoxWidth', calculatedWidth.toString());
      }

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

          let baseZoom;
          if (isPresenter) {
            // For presenters, use the full area minus any UI components like toolbars
            baseZoom = fitToWidth
              ? presentationAreaWidth / currentPresentationPageRef.current.scaledWidth
              : Math.min(
                  presentationAreaWidth / currentPresentationPageRef.current.scaledWidth,
                  adjustedPresentationAreaHeight / currentPresentationPageRef.current.scaledHeight
                );
          } else {
            // For viewers
            const effectiveZoom = calculateEffectiveZoom(
              initialViewBoxWidthRef.current,
              currentPresentationPageRef.current.scaledViewBoxWidth
            );
            baseZoom = calculateZoomValue(
              currentPresentationPageRef.current.scaledWidth,
              currentPresentationPageRef.current.scaledHeight
            ) * effectiveZoom / HUNDRED_PERCENT;
          }

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

          const adjustedXPos = isPresenter
            ? currentPresentationPageRef.current.xOffset : currentPresentationPageRef.current.xOffset * effectiveZoom;
          const adjustedYPos = isPresenter
            ? currentPresentationPageRef.current.yOffset : currentPresentationPageRef.current.yOffset * effectiveZoom;

          setCamera(baseZoom, adjustedXPos, adjustedYPos);
        }
      }, 300);
    }
  }, [
    isMountedRef.current,
    presentationId,
    curPageIdRef.current,
    isMultiUserActive,
    isPresenter,
    animations,
  ]);

  React.useEffect(() => {
    setTldrawIsMounting(true);
    return () => {
      isMountedRef.current = false;
    };
  }, []);

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

  const customTools = [NoopTool];

  return (
    <div
      ref={whiteboardRef}
      id={"whiteboard-element"}
      key={`animations=-${animations}-${whiteboardToolbarAutoHide}-${language}`}
    >
      <Tldraw
        key={`tldrawv2-${presentationId}-${animations}`}
        forceMobile={true}
        hideUi={hasWBAccessRef.current || isPresenter ? false : true}
        onMount={handleTldrawMount}
        tools={customTools}
      />
      <Styled.TldrawV2GlobalStyle
        {...{
          hasWBAccess: hasWBAccessRef.current,
          isPresenter,
          isRTL,
          isMultiUserActive,
          isToolbarVisible,
        }}
      />
    </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,
  svgUri: PropTypes.string,
  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,
  fullscreenRef: PropTypes.instanceOf(Element),
  handleToggleFullScreen: PropTypes.func.isRequired,
  numberOfPages: PropTypes.number,
  sidebarNavigationWidth: PropTypes.number,
  presentationId: PropTypes.string,
};

Whiteboard.defaultProps = {
  fullscreenRef: undefined,
  svgUri: undefined,
  whiteboardId: undefined,
  sidebarNavigationWidth: 0,
  presentationId: undefined,
  currentUser: {
    userId: "",
  },
  isPresenter: false,
  numberOfPages: 0,
};
