import {
  $getRoot,
  $isElementNode,
  $isLineBreakNode,
  $isParagraphNode,
  $isTextNode,
  TextNode,
  type ElementNode,
  type EditorState,
  LexicalEditor,
  TextFormatType,
} from 'lexical';
import { $isCodeNode } from '@lexical/code';
import { $isLinkNode } from '@lexical/link';
import { $isListItemNode, $isListNode } from '@lexical/list';
import { $isHorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
import { $isHeadingNode, $isQuoteNode, HeadingTagType } from '@lexical/rich-text';
import { $isTableCellNode, $isTableNode, $isTableRowNode } from '@lexical/table';

import type {
  CrystallizeRichTextNode,
  CrystallizeRichText,
  CrystallizeRichTextInlineFormattedNodes,
} from '../types/crystallize-rich-text-types';
import type { CrystallizeRichTextHeadingTypes } from '../types/crystallize-rich-text-types/headings';

const headingMapper: Record<HeadingTagType, CrystallizeRichTextHeadingTypes> = {
  h1: 'heading1',
  h2: 'heading2',
  h3: 'heading3',
  h4: 'heading4',
  h5: 'heading5',
  h6: 'heading6',
};

export function lexicalToCrystallizeRichText({
  editorState,
}: {
  editor?: LexicalEditor;
  editorState: EditorState;
}): CrystallizeRichText {
  const crystallizeRichText: CrystallizeRichTextNode['children'] = [];

  editorState.read(() => {
    handleNode($getRoot(), crystallizeRichText);

    function handleNode(lexicalNode: ElementNode, parentChildren: CrystallizeRichTextNode[]) {
      let crystallizeNode: CrystallizeRichTextNode | null = null;

      let innerContentsIsTakenCareOf = false;

      // Paragraph
      if ($isParagraphNode(lexicalNode)) {
        crystallizeNode = {
          kind: 'block',
          type: 'paragraph',
        };
      }
      // Headings
      else if ($isHeadingNode(lexicalNode)) {
        const tag = lexicalNode.getTag();

        crystallizeNode = {
          kind: 'block',
          type: headingMapper[tag],
        };
      }
      // Link
      else if ($isLinkNode(lexicalNode)) {
        const href = lexicalNode.getURL();
        const target = lexicalNode.getTarget();
        const rel = lexicalNode.getRel();

        crystallizeNode = {
          kind: 'inline',
          type: 'link',
          metadata: {
            href,
            ...(target && { target }),
            ...(rel && { rel }),
          },
        };
      }
      // List
      else if ($isListNode(lexicalNode)) {
        const listType = lexicalNode.getListType();
        crystallizeNode = {
          kind: 'block',
          type: listType === 'number' ? 'ordered-list' : 'unordered-list',
        };
      }
      // List item
      else if ($isListItemNode(lexicalNode)) {
        crystallizeNode = {
          kind: 'block',
          type: 'list-item',
        };
      }
      // Quote
      else if ($isQuoteNode(lexicalNode)) {
        crystallizeNode = {
          kind: 'block',
          type: 'quote',
        };
      }
      // Line break
      else if ($isLineBreakNode(lexicalNode)) {
        crystallizeNode = {
          kind: 'inline',
          type: 'line-break',
        };
      }
      // Code
      else if ($isCodeNode(lexicalNode)) {
        crystallizeNode = {
          kind: 'block',
          type: 'preformatted',
          children: [
            {
              type: 'code',
              kind: 'block',
              textContent: lexicalNode.getTextContent(),
              metadata: {
                language: lexicalNode.getLanguage(),
              },
            },
          ],
        } as CrystallizeRichTextNode;

        innerContentsIsTakenCareOf = true;
      } else if ($isTableNode(lexicalNode)) {
        crystallizeNode = {
          kind: 'block',
          type: 'table',
        };
      } else if ($isTableRowNode(lexicalNode)) {
        crystallizeNode = {
          kind: 'block',
          type: 'table-row',
        };
      } else if ($isTableCellNode(lexicalNode)) {
        crystallizeNode = {
          kind: 'inline',
          type: lexicalNode.hasHeader() ? 'table-head-cell' : 'table-cell',
        };
      }

      if (crystallizeNode) {
        parentChildren.push(crystallizeNode);
      }

      if (!innerContentsIsTakenCareOf) {
        /**
         * If there is only a single child, and that
         * child does not have a format set, then we can
         * set the textContent directly on the parent
         */
        let onlyChildIsPureTextContent = false;

        const children = lexicalNode.getChildren();
        if (children?.length > 0) {
          let parentChildrenToUse = parentChildren;
          if (crystallizeNode) {
            if (children.length === 1 && parent) {
              const [first] = children;
              if ($isTextNode(first)) {
                const childTextNode = getTextChild(first);
                if (!childTextNode.type) {
                  crystallizeNode.textContent = childTextNode.textContent;
                  onlyChildIsPureTextContent = true;
                }
              }
            }

            if (!onlyChildIsPureTextContent) {
              crystallizeNode.children = [];
              parentChildrenToUse = crystallizeNode.children;
            }
          }

          if (!onlyChildIsPureTextContent) {
            children.forEach(childNode => {
              if ($isElementNode(childNode)) {
                handleNode(childNode, parentChildrenToUse);
              } else if ($isHorizontalRuleNode(childNode)) {
                parentChildrenToUse.push({ kind: 'block', type: 'horizontal-line' });
              } else if ($isTextNode(childNode)) {
                parentChildrenToUse.push(getTextChild(childNode));
              } else if ($isLineBreakNode(childNode)) {
                parentChildrenToUse.push({ kind: 'inline', type: 'line-break' });
              }
            });
          }
        }
      }
    }
  });

  return crystallizeRichText;
}

const lexicalFormatToCrystallizeType: Record<TextFormatType, CrystallizeRichTextInlineFormattedNodes['type']> = {
  bold: 'strong',
  italic: 'emphasized',
  underline: 'underlined',
  strikethrough: 'deleted',
  subscript: 'subscripted',
  superscript: 'superscripted',
  highlight: 'emphasized',
  code: 'code',
};

/**
 * Creates a rich text node with the ability to have
 * nested inline text nodes.
 */
function getTextChild(n: TextNode): CrystallizeRichTextNode {
  let currentChild: CrystallizeRichTextNode = {
    kind: 'inline',
  };
  let textChild: CrystallizeRichTextNode | null = null;

  function checkFormat(format: TextFormatType) {
    const type = lexicalFormatToCrystallizeType[format];
    if (n.hasFormat(format) && type) {
      if (!textChild) {
        currentChild = textChild = {
          kind: 'inline',
          type,
        } as CrystallizeRichTextNode;
      } else {
        currentChild.children = [
          {
            kind: 'inline',
            type,
          },
        ];

        currentChild = currentChild.children[0];
      }
    }
  }

  (Object.keys(lexicalFormatToCrystallizeType) as TextFormatType[]).forEach(checkFormat);

  // Safe guard for unknown lexical types
  if (!textChild) {
    currentChild = textChild = {
      kind: 'inline',
    };
  }

  currentChild.textContent = n.getTextContent();

  return textChild;
}
