/* eslint-disable no-param-reassign */
import React, {
  ClipboardEventHandler,
  KeyboardEventHandler,
  ReactNode,
  forwardRef,
  useCallback,
  useState,
} from 'react';
import { Box, Scroll, Text } from 'folds';
import { Descendant, Editor, createEditor } from 'slate';
import {
  Slate,
  Editable,
  withReact,
  RenderLeafProps,
  RenderElementProps,
  RenderPlaceholderProps,
} from 'slate-react';
import { withHistory } from 'slate-history';
import { BlockType } from './types';
import { RenderElement, RenderLeaf } from './Elements';
import { CustomElement } from './slate';
import * as css from './Editor.css';
import { toggleKeyboardShortcut } from './keyboard';

// Factory — NOT a module-level constant. Slate's slate-react keeps its
// internal NODE_TO_INDEX / NODE_TO_PARENT WeakMaps keyed by object
// identity (slate#6016, slate#4850). When two `<Slate initialValue=…>`
// instances share the same array+leaf reference (e.g. channel composer
// and thread drawer composer mounted at the same time), the second
// mount overwrites the first's WeakMap entry → `findPath` returns the
// wrong index → "Unable to find the path for Slate node: {text:''}"
// crash on cold-load. Fresh array per call sidesteps the collision.
const makeInitialValue = (): CustomElement[] => [
  {
    type: BlockType.Paragraph,
    children: [{ text: '' }],
  },
];

const withInline = (editor: Editor): Editor => {
  const { isInline } = editor;

  editor.isInline = (element) =>
    [BlockType.Mention, BlockType.Emoticon, BlockType.Link, BlockType.Command].includes(
      element.type
    ) || isInline(element);

  return editor;
};

const withVoid = (editor: Editor): Editor => {
  const { isVoid } = editor;

  editor.isVoid = (element) =>
    [BlockType.Mention, BlockType.Emoticon, BlockType.Command].includes(element.type) ||
    isVoid(element);

  return editor;
};

export const useEditor = (): Editor => {
  const [editor] = useState(() => withInline(withVoid(withReact(withHistory(createEditor())))));
  return editor;
};

export type EditorChangeHandler = (value: Descendant[]) => void;
type CustomEditorProps = {
  editableName?: string;
  top?: ReactNode;
  bottom?: ReactNode;
  before?: ReactNode;
  after?: ReactNode;
  // When set, renders in place of the text-input row (the Editable) while
  // keeping the composer card, the Slate context and the top/bottom slots
  // mounted. Used by the voice recorder so the input morphs inline instead of
  // the whole composer being swapped out. See docs/plans/voice_messages.md.
  replaceEditable?: ReactNode;
  maxHeight?: string;
  editor: Editor;
  placeholder?: string;
  onKeyDown?: KeyboardEventHandler;
  onKeyUp?: KeyboardEventHandler;
  onChange?: EditorChangeHandler;
  onPaste?: ClipboardEventHandler;
};
export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
  (
    {
      editableName,
      top,
      bottom,
      before,
      after,
      replaceEditable,
      maxHeight = '50vh',
      editor,
      placeholder,
      onKeyDown,
      onKeyUp,
      onChange,
      onPaste,
    },
    ref
  ) => {
    // Fresh value per CustomEditor instance — see comment on
    // `makeInitialValue` for why we cannot share a module-level const.
    // useState ensures we don't recreate the value across renders of
    // the same editor (Slate would not accept a new array reference
    // anyway after the editor has mounted).
    const [initialValue] = useState(makeInitialValue);
    const renderElement = useCallback(
      (props: RenderElementProps) => <RenderElement {...props} />,
      []
    );

    const renderLeaf = useCallback((props: RenderLeafProps) => <RenderLeaf {...props} />, []);

    const handleKeydown: KeyboardEventHandler = useCallback(
      (evt) => {
        onKeyDown?.(evt);
        const shortcutToggled = toggleKeyboardShortcut(editor, evt);
        if (shortcutToggled) evt.preventDefault();
      },
      [editor, onKeyDown]
    );

    const renderPlaceholder = useCallback(
      ({ attributes, children }: RenderPlaceholderProps) => (
        <span {...attributes} className={css.EditorPlaceholderContainer}>
          {/* Inner component to style the actual text position and appearance */}
          <Text as="span" className={css.EditorPlaceholderTextVisual} truncate>
            {children}
          </Text>
        </span>
      ),
      []
    );

    return (
      <div className={css.Editor} ref={ref}>
        <Slate editor={editor} initialValue={initialValue} onChange={onChange}>
          {top}
          {replaceEditable ?? (
            <Box alignItems="Start">
              {before && (
                <Box className={css.EditorOptions} alignItems="Center" gap="100" shrink="No">
                  {before}
                </Box>
              )}
              <Scroll
                className={css.EditorTextareaScroll}
                variant="SurfaceVariant"
                style={{ maxHeight }}
                size="300"
                visibility="Hover"
                hideTrack
              >
                <Editable
                  data-editable-name={editableName}
                  className={css.EditorTextarea}
                  placeholder={placeholder}
                  renderPlaceholder={renderPlaceholder}
                  renderElement={renderElement}
                  renderLeaf={renderLeaf}
                  onKeyDown={handleKeydown}
                  onKeyUp={onKeyUp}
                  onPaste={onPaste}
                />
              </Scroll>
              {after && (
                <Box className={css.EditorOptions} alignItems="Center" gap="100" shrink="No">
                  {after}
                </Box>
              )}
            </Box>
          )}
          {bottom}
        </Slate>
      </div>
    );
  }
);
