/**
 * 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 { Dispatch, useCallback, useEffect, useRef, useState } from 'react';
import * as React from 'react';
import {
  $getSelection,
  $isRangeSelection,
  COMMAND_PRIORITY_CRITICAL,
  COMMAND_PRIORITY_HIGH,
  COMMAND_PRIORITY_LOW,
  GridSelection,
  KEY_ESCAPE_COMMAND,
  LexicalEditor,
  NodeSelection,
  RangeSelection,
  SELECTION_CHANGE_COMMAND,
} from 'lexical';
import { createPortal } from 'react-dom';
import { $isAutoLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $findMatchingParent, mergeRegister } from '@lexical/utils';

import { Button } from '../../../button';
import { IconButton } from '../../../icon-button';
import { Icon } from '../../../iconography';
import { InputWithLabel } from '../../../input-with-label';
import { useTr } from '../../i18n';
import LinkPreview from '../../ui/LinkPreview';
import { getSelectedNode } from '../../utils/getSelectedNode';
import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition';
import { sanitizeUrl } from '../../utils/url';

function FloatingLinkEditor({
  editor,
  isLink,
  setIsLink,
  anchorElem,
}: {
  editor: LexicalEditor;
  isLink: boolean;
  setIsLink: Dispatch<boolean>;
  anchorElem: HTMLElement;
}): JSX.Element {
  const editorRef = useRef<HTMLDivElement | null>(null);
  const inputRef = useRef<HTMLInputElement>(null);
  const [linkUrl, setLinkUrl] = useState('');
  const [rel, setRel] = useState<string | null>(null);
  const [target, setTarget] = useState<string | null>(null);
  const [isEditMode, setEditMode] = useState(false);
  const [lastSelection, setLastSelection] = useState<RangeSelection | GridSelection | NodeSelection | null>(null);
  const tr = useTr();

  const updateLinkEditor = useCallback(() => {
    const selection = $getSelection();
    if ($isRangeSelection(selection)) {
      const node = getSelectedNode(selection);
      const parent = node.getParent();
      if ($isLinkNode(parent)) {
        setLinkUrl(parent.getURL());
        setRel(parent.getRel());
        setTarget(parent.getTarget());
      } else if ($isLinkNode(node)) {
        setLinkUrl(node.getURL());
        setRel(node.getRel());
        setRel(node.getTarget());
      } else {
        setLinkUrl('');
        setRel(null);
        setTarget(null);
      }
    }
    const editorElem = editorRef.current;
    const nativeSelection = window.getSelection();
    const activeElement = document.activeElement;

    if (editorElem === null) {
      return;
    }

    const rootElement = editor.getRootElement();

    if (
      selection !== null &&
      nativeSelection !== null &&
      rootElement !== null &&
      rootElement.contains(nativeSelection.anchorNode) &&
      editor.isEditable()
    ) {
      const domRange = nativeSelection.getRangeAt(0);
      let rect;
      if (nativeSelection.anchorNode === rootElement) {
        let inner = rootElement;
        while (inner.firstElementChild != null) {
          inner = inner.firstElementChild as HTMLElement;
        }
        rect = inner.getBoundingClientRect();
      } else {
        rect = domRange.getBoundingClientRect();
      }

      setFloatingElemPosition(rect, editorElem, anchorElem);
      setLastSelection(selection);
    } else if (!activeElement || activeElement.className !== 'link-input') {
      if (rootElement !== null) {
        setFloatingElemPosition(null, editorElem, anchorElem);
      }
      setLastSelection(null);
      setEditMode(false);
      setLinkUrl('');
    }

    return true;
  }, [anchorElem, editor]);

  useEffect(() => {
    const scrollerElem = anchorElem.parentElement;

    const update = () => {
      editor.getEditorState().read(() => {
        updateLinkEditor();
      });
    };

    window.addEventListener('resize', update);

    if (scrollerElem) {
      scrollerElem.addEventListener('scroll', update);
    }

    return () => {
      window.removeEventListener('resize', update);

      if (scrollerElem) {
        scrollerElem.removeEventListener('scroll', update);
      }
    };
  }, [anchorElem.parentElement, editor, updateLinkEditor]);

  useEffect(() => {
    return mergeRegister(
      editor.registerUpdateListener(({ editorState }) => {
        editorState.read(() => {
          updateLinkEditor();
        });
      }),

      editor.registerCommand(
        SELECTION_CHANGE_COMMAND,
        () => {
          updateLinkEditor();
          return true;
        },
        COMMAND_PRIORITY_LOW,
      ),
      editor.registerCommand(
        KEY_ESCAPE_COMMAND,
        () => {
          if (isLink) {
            setIsLink(false);
            return true;
          }
          return false;
        },
        COMMAND_PRIORITY_HIGH,
      ),
    );
  }, [editor, updateLinkEditor, setIsLink, isLink]);

  useEffect(() => {
    editor.getEditorState().read(() => {
      updateLinkEditor();
    });
  }, [editor, updateLinkEditor]);

  useEffect(() => {
    if (isEditMode && inputRef.current) {
      inputRef.current.focus();
    }
  }, [isEditMode]);

  return (
    <div ref={editorRef} className="c-rte-link-editor" data-testid="rich-text-link-editor">
      {isEditMode ? (
        <div>
          <div className="c-rte-link-editor-input-group">
            <InputWithLabel
              label={tr('linkEditorLink')}
              type="text"
              value={linkUrl}
              onChange={e => setLinkUrl(e.target.value)}
            />
          </div>

          <div className="c-rte-link-editor-input-group">
            <InputWithLabel
              label={tr('linkEditorRel')}
              type="text"
              value={rel ?? ''}
              onChange={e => setRel(e.target.value)}
            />
          </div>
          <div className="c-rte-link-editor-input-group">
            <InputWithLabel
              label={tr('linkEditorTarget')}
              type="text"
              value={target ?? ''}
              onChange={e => setTarget(e.target.value)}
            />
          </div>
          <div className="c-rte-link-editor-button-wrap">
            <Button
              data-testid="link-editor-save-button"
              onClick={() => {
                if (lastSelection !== null) {
                  if (linkUrl !== '') {
                    editor.dispatchCommand(TOGGLE_LINK_COMMAND, {
                      url: sanitizeUrl(linkUrl),
                      rel,
                      target,
                    });
                  }
                  setEditMode(false);
                }
              }}
            >
              {tr('linkEditorCommit')}
            </Button>
          </div>
        </div>
      ) : (
        <>
          <div className="c-rte-link-editor-link-input">
            <div className="c-rte-link-editor-link-preview">
              <a href={linkUrl} target="_blank" rel="noopener noreferrer">
                {linkUrl}
              </a>
              {rel || target ? (
                <div className="c-rte-link-editor-preview-attrs">
                  {rel && <div className="c-rte-link-editor-preview-attr">{rel}</div>}
                  {target && <div className="c-rte-link-editor-preview-attr">{target}</div>}
                </div>
              ) : null}
            </div>
            <div>
              <IconButton
                size="sm"
                tabIndex={0}
                onMouseDown={event => event.preventDefault()}
                onClick={() => setEditMode(true)}
                aria-label={tr('linkEditorEdit')}
                data-testid="link-editor-edit"
              >
                <Icon.Edit />
              </IconButton>
            </div>
          </div>
          <div>
            <LinkPreview url={linkUrl} />
          </div>
        </>
      )}
    </div>
  );
}

function useFloatingLinkEditorToolbar(editor: LexicalEditor, anchorElem: HTMLElement): JSX.Element | null {
  const [activeEditor, setActiveEditor] = useState(editor);
  const [isLink, setIsLink] = useState(false);

  const updateToolbar = useCallback(() => {
    const selection = $getSelection();
    if ($isRangeSelection(selection)) {
      const node = getSelectedNode(selection);
      const linkParent = $findMatchingParent(node, $isLinkNode);
      const autoLinkParent = $findMatchingParent(node, $isAutoLinkNode);

      // We don't want this menu to open for auto links.
      if (linkParent != null && autoLinkParent == null) {
        setIsLink(true);
      } else {
        setIsLink(false);
      }
    }
  }, []);

  useEffect(() => {
    return editor.registerCommand(
      SELECTION_CHANGE_COMMAND,
      (_payload, newEditor) => {
        updateToolbar();
        setActiveEditor(newEditor);
        return false;
      },
      COMMAND_PRIORITY_CRITICAL,
    );
  }, [editor, updateToolbar]);

  return isLink
    ? createPortal(
        <FloatingLinkEditor editor={activeEditor} isLink={isLink} anchorElem={anchorElem} setIsLink={setIsLink} />,
        anchorElem,
      )
    : null;
}

export default function FloatingLinkEditorPlugin({
  anchorElem = document.body,
}: {
  anchorElem?: HTMLElement;
}): JSX.Element | null {
  const [editor] = useLexicalComposerContext();
  return useFloatingLinkEditorToolbar(editor, anchorElem);
}
