import {
  type JSX,
  memo,
  type ReactNode,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { GROWI_IS_CONTENT_RENDERING_ATTR } from '@growi/core/dist/consts';
import { debounce } from 'throttle-debounce';

import type { IGraphViewerGlobal } from '..';
import { generateMxgraphData } from '../utils/embed';
import { isGraphViewerGlobal } from '../utils/global';

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

declare global {
  var GraphViewer: IGraphViewerGlobal;
}

export type DrawioViewerProps = {
  isDarkMode: 'true' | 'false';
  diagramIndex: number;
  bol: number;
  eol: number;
  children?: ReactNode;
  onRenderingStart?: () => void;
  onRenderingUpdated?: (mxfile: string | null) => void;
};

export type DrawioEditByViewerProps = {
  bol: number;
  eol: number;
  drawioMxFile: string;
};

export const DrawioViewer = memo((props: DrawioViewerProps): JSX.Element => {
  const {
    isDarkMode,
    diagramIndex,
    bol,
    eol,
    children,
    onRenderingStart,
    onRenderingUpdated,
  } = props;

  const drawioContainerRef = useRef<HTMLDivElement>(null);

  const [error, setError] = useState<Error>();

  const renderDrawio = useCallback(() => {
    if (drawioContainerRef.current == null) {
      return;
    }

    if (!('GraphViewer' in window && isGraphViewerGlobal(GraphViewer))) {
      // Do nothing if loading has not been terminated.
      // Alternatively, GraphViewer.processElements() will be called in Script.onLoad.
      // see DrawioViewerScript.tsx
      return;
    }

    const mxgraphs = drawioContainerRef.current.getElementsByClassName(
      'mxgraph',
    ) as HTMLCollectionOf<HTMLElement>;
    if (mxgraphs.length > 0) {
      // This component should have only one '.mxgraph' element
      const div = mxgraphs[0];

      if (div != null) {
        div.innerHTML = '';
        div.style.width = '';
        div.style.height = '';

        // render diagram with createViewerForElement
        try {
          GraphViewer.useResizeSensor = false;
          GraphViewer.prototype.checkVisibleState = false;
          GraphViewer.prototype.lightboxZIndex = 1055; // set $zindex-modal
          GraphViewer.prototype.toolbarZIndex = 1055; // set $zindex-modal
          GraphViewer.createViewerForElement(div);
        } catch (err) {
          setError(err);
        }
      }
    }
  }, []);

  const renderDrawioWithDebounce = useMemo(
    () => debounce(200, renderDrawio),
    [renderDrawio],
  );

  const mxgraphHtml = useMemo(() => {
    setError(undefined);

    if (children == null) {
      return '';
    }

    const code = Array.isArray(children)
      ? children
          .filter((elem) => typeof elem === 'string') // omit non-string elements (e.g. br element generated by line-breaks option)
          .join('')
      : children.toString();

    let mxgraphData: string | undefined;
    try {
      mxgraphData = generateMxgraphData(code, isDarkMode === 'true');
    } catch (err) {
      setError(err);
    }

    return `<div class="mxgraph" data-mxgraph="${mxgraphData}"></div>`;
  }, [children, isDarkMode]);

  useEffect(() => {
    if (mxgraphHtml.length > 0) {
      onRenderingStart?.();
      renderDrawioWithDebounce();
    }
  }, [mxgraphHtml, onRenderingStart, renderDrawioWithDebounce]);

  useEffect(() => {
    if (error != null) {
      onRenderingUpdated?.(null);
      // finish rendering to allow auto-scroll system to detect the upcoming layout shift
      drawioContainerRef.current?.setAttribute(
        GROWI_IS_CONTENT_RENDERING_ATTR,
        'false',
      );
    }
  }, [error, onRenderingUpdated]);

  // ****************  detect data-mxgraph has rendered ****************
  useEffect(() => {
    const container = drawioContainerRef.current;
    if (container == null) return;

    const observerCallback = (mutationRecords: MutationRecord[]) => {
      for (const record of mutationRecords) {
        const target = record.target as HTMLElement;

        const mxgraphData = target.dataset.mxgraph;
        if (mxgraphData != null) {
          onRenderingUpdated?.(JSON.parse(mxgraphData).xml);
          drawioContainerRef.current?.setAttribute(
            GROWI_IS_CONTENT_RENDERING_ATTR,
            'false',
          );
        }
      }
    };

    const observer = new MutationObserver(observerCallback);
    observer.observe(container, { childList: true, subtree: true });
    return () => {
      observer.disconnect();
    };
  }, [onRenderingUpdated]);
  // *******************************  end  *******************************

  // *******************  detect container is resized  *******************
  useEffect(() => {
    if (drawioContainerRef.current == null) {
      return;
    }

    const observer = new ResizeObserver((entries) => {
      for (const _entry of entries) {
        onRenderingStart?.();
        // Signal re-rendering in progress so the auto-scroll system can detect the upcoming layout shift
        drawioContainerRef.current?.setAttribute(
          GROWI_IS_CONTENT_RENDERING_ATTR,
          'true',
        );
        renderDrawioWithDebounce();
      }
    });
    observer.observe(drawioContainerRef.current);
    return () => {
      observer.disconnect();
    };
  }, [onRenderingStart, renderDrawioWithDebounce]);
  // *******************************  end  *******************************

  return (
    <div
      key={`drawio-viewer-${diagramIndex}`}
      ref={drawioContainerRef}
      className={`drawio-viewer ${styles['drawio-viewer']} p-2`}
      data-begin-line-number-of-markdown={bol}
      data-end-line-number-of-markdown={eol}
      {...{ [GROWI_IS_CONTENT_RENDERING_ATTR]: 'true' }}
    >
      {/* show error */}
      {error != null && (
        <span className="text-muted">
          <span className="material-symbols-outlined me-1">error</span>
          {error.name && <strong>{error.name}: </strong>}
          {error.message}
        </span>
      )}

      {error == null && (
        // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
        <div dangerouslySetInnerHTML={{ __html: mxgraphHtml }} />
      )}
    </div>
  );
});
DrawioViewer.displayName = 'DrawioViewer';
