/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { ReactProviderExtension } from '@lexical/react/ReactProviderExtension';
import { defineExtension, declarePeerDependency, shallowMergeConfig } from 'lexical';
import { LexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useMemo, useSyncExternalStore, Suspense } from 'react';
import { createPortal } from 'react-dom';
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

// Do not require this module directly! Use normal `invariant` calls.

function formatDevErrorMessage(message) {
  throw new Error(message);
}

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

/** @internal */
function useReactDecorators(editor, ErrorBoundary) {
  const [subscribe, getSnapshot] = useMemo(() => [cb => editor.registerDecoratorListener(cb), () => editor.getDecorators()], [editor]);
  const decorators = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);

  // Return decorators defined as React Portals
  return useMemo(() => {
    const decoratedPortals = [];
    for (const nodeKey in decorators) {
      const element = editor.getElementByKey(nodeKey);
      if (element !== null) {
        const reactDecorator = /*#__PURE__*/jsx(ErrorBoundary, {
          onError: e => {
            editor._onError(e);
          },
          children: /*#__PURE__*/jsx(Suspense, {
            fallback: null,
            children: decorators[nodeKey]
          })
        });
        decoratedPortals.push(/*#__PURE__*/createPortal(reactDecorator, element, nodeKey));
      }
    }
    return decoratedPortals;
  }, [ErrorBoundary, decorators, editor]);
}

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */
function buildEditorComponent(config, context) {
  const [editor] = context;
  const rawConfigDecorators = config.decorators.map(El =>
  // eslint-disable-next-line react/jsx-key -- wrapped later
  typeof El === 'function' ? /*#__PURE__*/jsx(El, {
    context: context
  }) : El);
  return function EditorComponent(props) {
    const {
      EditorChildrenComponent = config.EditorChildrenComponent,
      ErrorBoundary = config.ErrorBoundary,
      contentEditable = config.contentEditable,
      children
    } = props;
    const decorators = useReactDecorators(editor, ErrorBoundary);
    const configDecorators = useMemo(() => rawConfigDecorators.map((decorator, i) => /*#__PURE__*/jsx(ErrorBoundary, {
      onError: e => {
        editor._onError(e);
      },
      children: /*#__PURE__*/jsx(Suspense, {
        fallback: null,
        children: decorator
      })
    }, i)), [ErrorBoundary]);
    return /*#__PURE__*/jsx(LexicalComposerContext.Provider, {
      value: context,
      children: /*#__PURE__*/jsxs(EditorChildrenComponent, {
        context: context,
        contentEditable: contentEditable,
        children: [children, configDecorators, decorators]
      })
    });
  };
}

/**
 * @example
 * The default EditorChildrenComponent implementation
 * ```jsx
 * return (
 *   <>
 *     {contentEditable}
 *     {children}
 *   </>
 * );
 * ```
 */
function DefaultEditorChildrenComponent({
  contentEditable,
  children
}) {
  return /*#__PURE__*/jsxs(Fragment, {
    children: [contentEditable, children]
  });
}
const initialConfig = {
  EditorChildrenComponent: DefaultEditorChildrenComponent,
  ErrorBoundary: LexicalErrorBoundary,
  contentEditable: /*#__PURE__*/jsx(ContentEditable, {}),
  decorators: []
};

/**
 * An extension to use or configure React for use with Lexical. In an editor, you
 * would typically use {@link LexicalExtensionComposer} (for React projects) or
 * {@link ReactPluginHostExtension} (to use React Extensions and plug-ins in a non-React
 * project).
 *
 * See {@link ReactConfig} for more detailed exextensionations of how to use
 * the config for this Extension.
 *
 * For an Extension developer, you can defineConfig() override the extension with
 * decorators to add JSX inside the editor context that is not
 * location-dependent (e.g. floating UI that does not need to be mounted in
 * some specific location, or effects that return null).
 */
const ReactExtension = defineExtension({
  build(editor, config, state) {
    const providerPeer = state.getPeer(ReactProviderExtension.name);
    if (!providerPeer) {
      {
        formatDevErrorMessage(`No ReactProviderExtension detected. You must use ReactPluginHostExtension or LexicalExtensionComposer to host React extensions. The following extensions depend on ReactExtension: ${[...state.getDirectDependentNames()].join(' ')}`);
      }
    }
    const context = [editor, {
      getTheme: () => editor._config.theme
    }];
    const Component = buildEditorComponent(config, context);
    return {
      Component,
      context
    };
  },
  config: initialConfig,
  mergeConfig(a, b) {
    const config = shallowMergeConfig(a, b);
    if (b.decorators) {
      config.decorators = b.decorators.length > 0 ? [...a.decorators, ...b.decorators] : a.decorators;
    }
    return config;
  },
  name: '@lexical/react/React',
  peerDependencies: [
  // We are not trying to avoid the import, just the direct dependency,
  // so using the extension directly is fine.
  declarePeerDependency(ReactProviderExtension.name)]
});

export { DefaultEditorChildrenComponent, ReactExtension };
