'use client';

import { useEffect, useMemo, useRef } from 'react';
import makeCancellable from 'make-cancellable-promise';
import makeEventProps from 'make-event-props';
import clsx from 'clsx';
import mergeRefs from 'merge-refs';
import invariant from 'tiny-invariant';
import warning from 'warning';

import PageContext from './PageContext.js';

import Message from './Message.js';
import Canvas from './Page/Canvas.js';
import TextLayer from './Page/TextLayer.js';
import AnnotationLayer from './Page/AnnotationLayer.js';

import { cancelRunningTask, isProvided, makePageCallback } from './shared/utils.js';

import useDocumentContext from './shared/hooks/useDocumentContext.js';
import useResolver from './shared/hooks/useResolver.js';

import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
import type { EventProps } from 'make-event-props';
import type {
  ClassName,
  CustomRenderer,
  CustomTextRenderer,
  NodeOrRenderer,
  OnGetAnnotationsError,
  OnGetAnnotationsSuccess,
  OnGetStructTreeError,
  OnGetStructTreeSuccess,
  OnGetTextError,
  OnGetTextSuccess,
  OnPageLoadError,
  OnPageLoadSuccess,
  OnRenderAnnotationLayerError,
  OnRenderAnnotationLayerSuccess,
  OnRenderError,
  OnRenderSuccess,
  OnRenderTextLayerError,
  OnRenderTextLayerSuccess,
  PageCallback,
  RenderMode,
} from './shared/types.js';

const defaultScale = 1;

export type PageProps = {
  _className?: string;
  _enableRegisterUnregisterPage?: boolean;
  /**
   * Canvas background color. Any valid `canvas.fillStyle` can be used.
   *
   * @example 'transparent'
   */
  canvasBackground?: string;
  /**
   * A prop that behaves like [ref](https://reactjs.org/docs/refs-and-the-dom.html), but it's passed to `<canvas>` rendered by `<PageCanvas>` component.
   *
   * @example (ref) => { this.myCanvas = ref; }
   * @example this.ref
   * @example ref
   */
  canvasRef?: React.Ref<HTMLCanvasElement>;
  children?: React.ReactNode;
  /**
   * Class name(s) that will be added to rendered element along with the default `react-pdf__Page`.
   *
   * @example 'custom-class-name-1 custom-class-name-2'
   * @example ['custom-class-name-1', 'custom-class-name-2']
   */
  className?: ClassName;
  /**
   * Function that customizes how a page is rendered. You must set `renderMode` to `"custom"` to use this prop.
   *
   * @example MyCustomRenderer
   */
  customRenderer?: CustomRenderer;
  /**
   * Function that customizes how a text layer is rendered.
   *
   * @example ({ str, itemIndex }) => str.replace(/ipsum/g, value => `<mark>${value}</mark>`)
   */
  customTextRenderer?: CustomTextRenderer;
  /**
   * The ratio between physical pixels and device-independent pixels (DIPs) on the current device.
   *
   * @default window.devicePixelRatio
   * @example 1
   */
  devicePixelRatio?: number;
  /**
   * What the component should display in case of an error.
   *
   * @default 'Failed to load the page.'
   * @example 'An error occurred!'
   * @example <p>An error occurred!</p>
   * @example this.renderError
   */
  error?: NodeOrRenderer;
  /**
   * Page height. If neither `height` nor `width` are defined, page will be rendered at the size defined in PDF. If you define `width` and `height` at the same time, `height` will be ignored. If you define `height` and `scale` at the same time, the height will be multiplied by a given factor.
   *
   * @example 300
   */
  height?: number;
  /**
   * The path used to prefix the src attributes of annotation SVGs.
   *
   * @default ''
   * @example '/public/images/'
   */
  imageResourcesPath?: string;
  /**
   * A prop that behaves like [ref](https://reactjs.org/docs/refs-and-the-dom.html), but it's passed to main `<div>` rendered by `<Page>` component.
   *
   * @example (ref) => { this.myPage = ref; }
   * @example this.ref
   * @example ref
   */
  inputRef?: React.Ref<HTMLDivElement | null>;
  /**
   * What the component should display while loading.
   *
   * @default 'Loading page…'
   * @example 'Please wait!'
   * @example <p>Please wait!</p>
   * @example this.renderLoader
   */
  loading?: NodeOrRenderer;
  /**
   *  What the component should display in case of no data.
   *
   * @default 'No page specified.'
   * @example 'Please select a page.'
   * @example <p>Please select a page.</p>
   * @example this.renderNoData
   */
  noData?: NodeOrRenderer;
  /**
   * Function called in case of an error while loading annotations.
   *
   * @example (error) => alert('Error while loading annotations! ' + error.message)
   */
  onGetAnnotationsError?: OnGetAnnotationsError;
  /**
   * Function called when annotations are successfully loaded.
   *
   * @example (annotations) => alert('Now displaying ' + annotations.length + ' annotations!')
   */
  onGetAnnotationsSuccess?: OnGetAnnotationsSuccess;
  /**
   * Function called in case of an error while loading structure tree.
   *
   * @example (error) => alert('Error while loading structure tree! ' + error.message)
   */
  onGetStructTreeError?: OnGetStructTreeError;
  /**
   * Function called when structure tree is successfully loaded.
   *
   * @example (structTree) => alert(JSON.stringify(structTree))
   */
  onGetStructTreeSuccess?: OnGetStructTreeSuccess;
  /**
   * Function called in case of an error while loading text layer items.
   *
   * @example (error) => alert('Error while loading text layer items! ' + error.message)
   */
  onGetTextError?: OnGetTextError;
  /**
   * Function called when text layer items are successfully loaded.
   *
   * @example ({ items, styles }) => alert('Now displaying ' + items.length + ' text layer items!')
   */
  onGetTextSuccess?: OnGetTextSuccess;
  /**
   * Function called in case of an error while loading the page.
   *
   * @example (error) => alert('Error while loading page! ' + error.message)
   */
  onLoadError?: OnPageLoadError;
  /**
   * Function called when the page is successfully loaded.
   *
   * @example (page) => alert('Now displaying a page number ' + page.pageNumber + '!')
   */
  onLoadSuccess?: OnPageLoadSuccess;
  /**
   * Function called in case of an error while rendering the annotation layer.
   *
   * @example (error) => alert('Error while rendering annotation layer! ' + error.message)
   */
  onRenderAnnotationLayerError?: OnRenderAnnotationLayerError;
  /**
   * Function called when annotations are successfully rendered on the screen.
   *
   * @example () => alert('Rendered the annotation layer!')
   */
  onRenderAnnotationLayerSuccess?: OnRenderAnnotationLayerSuccess;
  /**
   * Function called in case of an error while rendering the page.
   *
   * @example (error) => alert('Error while loading page! ' + error.message)
   */
  onRenderError?: OnRenderError;
  /**
   * Function called when the page is successfully rendered on the screen.
   *
   * @example () => alert('Rendered the page!')
   */
  onRenderSuccess?: OnRenderSuccess;
  /**
   * Function called in case of an error while rendering the text layer.
   *
   * @example (error) => alert('Error while rendering text layer! ' + error.message)
   */
  onRenderTextLayerError?: OnRenderTextLayerError;
  /**
   * Function called when the text layer is successfully rendered on the screen.
   *
   * @example () => alert('Rendered the text layer!')
   */
  onRenderTextLayerSuccess?: OnRenderTextLayerSuccess;
  /**
   * Which page from PDF file should be displayed, by page index. Ignored if `pageNumber` prop is provided.
   *
   * @default 0
   * @example 1
   */
  pageIndex?: number;
  /**
   * Which page from PDF file should be displayed, by page number. If provided, `pageIndex` prop will be ignored.
   *
   * @default 1
   * @example 2
   */
  pageNumber?: number;
  /**
   * pdf object obtained from `<Document />`'s `onLoadSuccess` callback function.
   *
   * @example pdf
   */
  pdf?: PDFDocumentProxy | false;
  registerPage?: undefined;
  /**
   * Whether annotations (e.g. links) should be rendered.
   *
   * @default true
   * @example false
   */
  renderAnnotationLayer?: boolean;
  /**
   * Whether forms should be rendered. `renderAnnotationLayer` prop must be set to `true`.
   *
   * @default false
   * @example true
   */
  renderForms?: boolean;
  /**
   * Rendering mode of the document. Can be `"canvas"`, `"custom"` or `"none"`. If set to `"custom"`, `customRenderer` must also be provided.
   *
   * @default 'canvas'
   * @example 'custom'
   */
  renderMode?: RenderMode;
  /**
   * Whether a text layer should be rendered.
   *
   * @default true
   * @example false
   */
  renderTextLayer?: boolean;
  /**
   * Rotation of the page in degrees. `90` = rotated to the right, `180` = upside down, `270` = rotated to the left.
   *
   * @default 0
   * @example 90
   */
  rotate?: number | null;
  /**
   * Page scale.
   *
   * @default 1
   * @example 0.5
   */
  scale?: number;
  unregisterPage?: undefined;
  /**
   * Page width. If neither `height` nor `width` are defined, page will be rendered at the size defined in PDF. If you define `width` and `height` at the same time, `height` will be ignored. If you define `width` and `scale` at the same time, the width will be multiplied by a given factor.
   *
   * @example 300
   */
  width?: number;
} & EventProps<PageCallback | false | undefined>;

/**
 * Displays a page.
 *
 * Should be placed inside `<Document />`. Alternatively, it can have `pdf` prop passed, which can be obtained from `<Document />`'s `onLoadSuccess` callback function, however some advanced functions like linking between pages inside a document may not be working correctly.
 */
export default function Page(props: PageProps): React.ReactElement {
  const documentContext = useDocumentContext();

  const mergedProps = { ...documentContext, ...props };
  const {
    _className = 'react-pdf__Page',
    _enableRegisterUnregisterPage = true,
    canvasBackground,
    canvasRef,
    children,
    className,
    customRenderer: CustomRenderer,
    customTextRenderer,
    devicePixelRatio,
    error = 'Failed to load the page.',
    height,
    inputRef,
    loading = 'Loading page…',
    noData = 'No page specified.',
    onGetAnnotationsError: onGetAnnotationsErrorProps,
    onGetAnnotationsSuccess: onGetAnnotationsSuccessProps,
    onGetStructTreeError: onGetStructTreeErrorProps,
    onGetStructTreeSuccess: onGetStructTreeSuccessProps,
    onGetTextError: onGetTextErrorProps,
    onGetTextSuccess: onGetTextSuccessProps,
    onLoadError: onLoadErrorProps,
    onLoadSuccess: onLoadSuccessProps,
    onRenderAnnotationLayerError: onRenderAnnotationLayerErrorProps,
    onRenderAnnotationLayerSuccess: onRenderAnnotationLayerSuccessProps,
    onRenderError: onRenderErrorProps,
    onRenderSuccess: onRenderSuccessProps,
    onRenderTextLayerError: onRenderTextLayerErrorProps,
    onRenderTextLayerSuccess: onRenderTextLayerSuccessProps,
    pageIndex: pageIndexProps,
    pageNumber: pageNumberProps,
    pdf,
    registerPage,
    renderAnnotationLayer: renderAnnotationLayerProps = true,
    renderForms = false,
    renderMode = 'canvas',
    renderTextLayer: renderTextLayerProps = true,
    rotate: rotateProps,
    scale: scaleProps = defaultScale,
    unregisterPage,
    width,
    ...otherProps
  } = mergedProps;

  const [pageState, pageDispatch] = useResolver<PDFPageProxy>();
  const { value: page, error: pageError } = pageState;
  const pageElement = useRef<HTMLDivElement>(null);

  invariant(
    pdf,
    'Attempted to load a page, but no document was specified. Wrap <Page /> in a <Document /> or pass explicit `pdf` prop.',
  );

  const pageIndex = isProvided(pageNumberProps) ? pageNumberProps - 1 : (pageIndexProps ?? null);

  const pageNumber = pageNumberProps ?? (isProvided(pageIndexProps) ? pageIndexProps + 1 : null);

  const rotate = rotateProps ?? (page ? page.rotate : null);

  const scale = useMemo(() => {
    if (!page) {
      return null;
    }

    // Be default, we'll render page at 100% * scale width.
    let pageScale = 1;

    // Passing scale explicitly null would cause the page not to render
    const scaleWithDefault = scaleProps ?? defaultScale;

    // If width/height is defined, calculate the scale of the page so it could be of desired width.
    if (width || height) {
      const viewport = page.getViewport({ scale: 1, rotation: rotate as number });
      if (width) {
        pageScale = width / viewport.width;
      } else if (height) {
        pageScale = height / viewport.height;
      }
    }

    return scaleWithDefault * pageScale;
  }, [height, page, rotate, scaleProps, width]);

  // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on pdf change
  useEffect(
    function hook() {
      return () => {
        if (!isProvided(pageIndex)) {
          // Impossible, but TypeScript doesn't know that
          return;
        }

        if (_enableRegisterUnregisterPage && unregisterPage) {
          unregisterPage(pageIndex);
        }
      };
    },
    [_enableRegisterUnregisterPage, pdf, pageIndex, unregisterPage],
  );

  /**
   * Called when a page is loaded successfully
   */
  function onLoadSuccess() {
    if (onLoadSuccessProps) {
      if (!page || !scale) {
        // Impossible, but TypeScript doesn't know that
        return;
      }

      onLoadSuccessProps(makePageCallback(page, scale));
    }

    if (_enableRegisterUnregisterPage && registerPage) {
      if (!isProvided(pageIndex) || !pageElement.current) {
        // Impossible, but TypeScript doesn't know that
        return;
      }

      registerPage(pageIndex, pageElement.current);
    }
  }

  /**
   * Called when a page failed to load
   */
  function onLoadError() {
    if (!pageError) {
      // Impossible, but TypeScript doesn't know that
      return;
    }

    warning(false, pageError.toString());

    if (onLoadErrorProps) {
      onLoadErrorProps(pageError);
    }
  }

  // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on pdf and pageIndex change
  useEffect(
    function resetPage() {
      pageDispatch({ type: 'RESET' });
    },
    [pageDispatch, pdf, pageIndex],
  );

  useEffect(
    function loadPage() {
      if (!pdf || !pageNumber) {
        return;
      }

      const cancellable = makeCancellable(pdf.getPage(pageNumber));
      const runningTask = cancellable;

      cancellable.promise
        .then((nextPage) => {
          pageDispatch({ type: 'RESOLVE', value: nextPage });
        })
        .catch((error) => {
          pageDispatch({ type: 'REJECT', error });
        });

      return () => cancelRunningTask(runningTask);
    },
    [pageDispatch, pdf, pageNumber],
  );

  // biome-ignore lint/correctness/useExhaustiveDependencies: Ommitted callbacks so they are not called every time they change
  useEffect(() => {
    if (page === undefined) {
      return;
    }

    if (page === false) {
      onLoadError();
      return;
    }

    onLoadSuccess();
  }, [page, scale]);

  const childContext = useMemo(
    () =>
      // Technically there cannot be page without pageIndex, pageNumber, rotate and scale, but TypeScript doesn't know that
      page && isProvided(pageIndex) && pageNumber && isProvided(rotate) && isProvided(scale)
        ? {
            _className,
            canvasBackground,
            customTextRenderer,
            devicePixelRatio,
            onGetAnnotationsError: onGetAnnotationsErrorProps,
            onGetAnnotationsSuccess: onGetAnnotationsSuccessProps,
            onGetStructTreeError: onGetStructTreeErrorProps,
            onGetStructTreeSuccess: onGetStructTreeSuccessProps,
            onGetTextError: onGetTextErrorProps,
            onGetTextSuccess: onGetTextSuccessProps,
            onRenderAnnotationLayerError: onRenderAnnotationLayerErrorProps,
            onRenderAnnotationLayerSuccess: onRenderAnnotationLayerSuccessProps,
            onRenderError: onRenderErrorProps,
            onRenderSuccess: onRenderSuccessProps,
            onRenderTextLayerError: onRenderTextLayerErrorProps,
            onRenderTextLayerSuccess: onRenderTextLayerSuccessProps,
            page,
            pageIndex,
            pageNumber,
            renderForms,
            renderTextLayer: renderTextLayerProps,
            rotate,
            scale,
          }
        : null,
    [
      _className,
      canvasBackground,
      customTextRenderer,
      devicePixelRatio,
      onGetAnnotationsErrorProps,
      onGetAnnotationsSuccessProps,
      onGetStructTreeErrorProps,
      onGetStructTreeSuccessProps,
      onGetTextErrorProps,
      onGetTextSuccessProps,
      onRenderAnnotationLayerErrorProps,
      onRenderAnnotationLayerSuccessProps,
      onRenderErrorProps,
      onRenderSuccessProps,
      onRenderTextLayerErrorProps,
      onRenderTextLayerSuccessProps,
      page,
      pageIndex,
      pageNumber,
      renderForms,
      renderTextLayerProps,
      rotate,
      scale,
    ],
  );

  const eventProps = useMemo(
    () =>
      makeEventProps(otherProps, () =>
        page ? (scale ? makePageCallback(page, scale) : undefined) : page,
      ),
    // biome-ignore lint/correctness/useExhaustiveDependencies: FIXME
    [otherProps, page, scale],
  );

  const pageKey = `${pageIndex}@${scale}/${rotate}`;

  function renderMainLayer() {
    switch (renderMode) {
      case 'custom': {
        invariant(
          CustomRenderer,
          `renderMode was set to "custom", but no customRenderer was passed.`,
        );

        return <CustomRenderer key={`${pageKey}_custom`} />;
      }
      case 'none':
        return null;
      case 'canvas':
      default:
        return <Canvas key={`${pageKey}_canvas`} canvasRef={canvasRef} />;
    }
  }

  function renderTextLayer() {
    if (!renderTextLayerProps) {
      return null;
    }

    return <TextLayer key={`${pageKey}_text`} />;
  }

  function renderAnnotationLayer() {
    if (!renderAnnotationLayerProps) {
      return null;
    }

    return <AnnotationLayer key={`${pageKey}_annotations`} />;
  }

  function renderChildren() {
    return (
      <PageContext.Provider value={childContext}>
        {renderMainLayer()}
        {renderTextLayer()}
        {renderAnnotationLayer()}
        {children}
      </PageContext.Provider>
    );
  }

  function renderContent() {
    if (!pageNumber) {
      return <Message type="no-data">{typeof noData === 'function' ? noData() : noData}</Message>;
    }

    if (pdf === null || page === undefined || page === null) {
      return (
        <Message type="loading">{typeof loading === 'function' ? loading() : loading}</Message>
      );
    }

    if (pdf === false || page === false) {
      return <Message type="error">{typeof error === 'function' ? error() : error}</Message>;
    }

    return renderChildren();
  }

  return (
    <div
      className={clsx(_className, className)}
      data-page-number={pageNumber}
      // Assertion is needed for React 18 compatibility
      ref={mergeRefs(inputRef as React.Ref<HTMLDivElement>, pageElement)}
      style={{
        ['--scale-factor' as string]: `${scale}`,
        backgroundColor: canvasBackground || 'white',
        position: 'relative',
        minWidth: 'min-content',
        minHeight: 'min-content',
      }}
      {...eventProps}
    >
      {renderContent()}
    </div>
  );
}
