/* eslint-disable no-param-reassign */ import { Descendant, Text } from 'slate'; import parse from 'html-dom-parser'; import { ChildNode, Element, isText, isTag } from 'domhandler'; import { sanitizeCustomHtml } from '../../utils/sanitize'; import { BlockType, MarkType } from './types'; import { BlockQuoteElement, CodeBlockElement, CodeLineElement, EmoticonElement, HeadingElement, HeadingLevel, InlineElement, MentionElement, OrderedListElement, ParagraphElement, UnorderedListElement, } from './slate'; import { createEmoticonElement, createMentionElement } from './utils'; import { parseMatrixToRoom, parseMatrixToRoomEvent, parseMatrixToUser, testMatrixTo, } from '../../plugins/matrix-to'; import { tryDecodeURIComponent } from '../../utils/dom'; import { escapeMarkdownInlineSequences, escapeMarkdownBlockSequences, } from '../../plugins/markdown'; type ProcessTextCallback = (text: string) => string; const getText = (node: ChildNode): string => { if (isText(node)) { return node.data; } if (isTag(node)) { return node.children.map((child) => getText(child)).join(''); } return ''; }; const getInlineNodeMarkType = (node: Element): MarkType | undefined => { if (node.name === 'b' || node.name === 'strong') { return MarkType.Bold; } if (node.name === 'i' || node.name === 'em') { return MarkType.Italic; } if (node.name === 'u') { return MarkType.Underline; } if (node.name === 's' || node.name === 'del') { return MarkType.StrikeThrough; } if (node.name === 'code') { if (node.parent && 'name' in node.parent && node.parent.name === 'pre') { return undefined; // Don't apply `Code` mark inside a
 tag
    }
    return MarkType.Code;
  }

  if (node.name === 'span' && node.attribs['data-mx-spoiler'] !== undefined) {
    return MarkType.Spoiler;
  }

  return undefined;
};

const getInlineMarkElement = (
  markType: MarkType,
  node: Element,
  getChild: (child: ChildNode) => InlineElement[]
): InlineElement[] => {
  const children = node.children.flatMap(getChild);
  const mdSequence = node.attribs['data-md'];
  if (mdSequence !== undefined) {
    children.unshift({ text: mdSequence });
    children.push({ text: mdSequence });
    return children;
  }
  children.forEach((child) => {
    if (Text.isText(child)) {
      child[markType] = true;
    }
  });
  return children;
};

const getInlineNonMarkElement = (node: Element): MentionElement | EmoticonElement | undefined => {
  if (node.name === 'img' && node.attribs['data-mx-emoticon'] !== undefined) {
    const { src, alt } = node.attribs;
    if (!src) return undefined;
    return createEmoticonElement(src, alt || 'Unknown Emoji');
  }
  if (node.name === 'a') {
    const href = tryDecodeURIComponent(node.attribs.href);
    if (typeof href !== 'string') return undefined;
    if (testMatrixTo(href)) {
      const userMention = parseMatrixToUser(href);
      if (userMention) {
        return createMentionElement(userMention, getText(node) || userMention, false);
      }
      const roomMention = parseMatrixToRoom(href);
      if (roomMention) {
        return createMentionElement(
          roomMention.roomIdOrAlias,
          getText(node) || roomMention.roomIdOrAlias,
          false,
          undefined,
          roomMention.viaServers
        );
      }
      const eventMention = parseMatrixToRoomEvent(href);
      if (eventMention) {
        return createMentionElement(
          eventMention.roomIdOrAlias,
          getText(node) || eventMention.roomIdOrAlias,
          false,
          eventMention.eventId,
          eventMention.viaServers
        );
      }
    }
  }
  return undefined;
};

const getInlineElement = (node: ChildNode, processText: ProcessTextCallback): InlineElement[] => {
  if (isText(node)) {
    return [{ text: processText(node.data) }];
  }

  if (isTag(node)) {
    const markType = getInlineNodeMarkType(node);
    if (markType) {
      return getInlineMarkElement(markType, node, (child) => {
        if (markType === MarkType.Code) return [{ text: getText(child) }];
        return getInlineElement(child, processText);
      });
    }

    const inlineNode = getInlineNonMarkElement(node);
    if (inlineNode) return [inlineNode];

    if (node.name === 'a') {
      const children = node.childNodes.flatMap((child) => getInlineElement(child, processText));
      children.unshift({ text: '[' });
      children.push({ text: `](${node.attribs.href})` });
      return children;
    }

    return node.childNodes.flatMap((child) => getInlineElement(child, processText));
  }

  return [];
};

const parseBlockquoteNode = (
  node: Element,
  processText: ProcessTextCallback
): BlockQuoteElement[] | ParagraphElement[] => {
  const quoteLines: Array = [];
  let lineHolder: InlineElement[] = [];

  const appendLine = () => {
    if (lineHolder.length === 0) return;

    quoteLines.push(lineHolder);
    lineHolder = [];
  };

  node.children.forEach((child) => {
    if (isText(child)) {
      lineHolder.push({ text: processText(child.data) });
      return;
    }
    if (isTag(child)) {
      if (child.name === 'br') {
        lineHolder.push({ text: '' });
        appendLine();
        return;
      }

      if (child.name === 'p') {
        appendLine();
        quoteLines.push(child.children.flatMap((c) => getInlineElement(c, processText)));
        return;
      }

      lineHolder.push(...getInlineElement(child, processText));
    }
  });
  appendLine();

  const mdSequence = node.attribs['data-md'];
  if (mdSequence !== undefined) {
    return quoteLines.map((lineChildren) => ({
      type: BlockType.Paragraph,
      children: [{ text: `${mdSequence} ` }, ...lineChildren],
    }));
  }

  return [
    {
      type: BlockType.BlockQuote,
      children: quoteLines.map((lineChildren) => ({
        type: BlockType.QuoteLine,
        children: lineChildren,
      })),
    },
  ];
};
const parseCodeBlockNode = (node: Element): CodeBlockElement[] | ParagraphElement[] => {
  const codeLines = getText(node).trim().split('\n');

  const mdSequence = node.attribs['data-md'];
  if (mdSequence !== undefined) {
    const pLines = codeLines.map((text) => ({
      type: BlockType.Paragraph,
      children: [{ text }],
    }));
    const childCode = node.children[0];
    const className =
      isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs.class ?? '' : '';
    const prefix = { text: `${mdSequence}${className.replace('language-', '')}` };
    const suffix = { text: mdSequence };
    return [
      { type: BlockType.Paragraph, children: [prefix] },
      ...pLines,
      { type: BlockType.Paragraph, children: [suffix] },
    ];
  }

  return [
    {
      type: BlockType.CodeBlock,
      children: codeLines.map((text) => ({
        type: BlockType.CodeLine,
        children: [{ text }],
      })),
    },
  ];
};
const parseListNode = (
  node: Element,
  processText: ProcessTextCallback
): OrderedListElement[] | UnorderedListElement[] | ParagraphElement[] => {
  const listLines: Array = [];
  let lineHolder: InlineElement[] = [];

  const appendLine = () => {
    if (lineHolder.length === 0) return;

    listLines.push(lineHolder);
    lineHolder = [];
  };

  node.children.forEach((child) => {
    if (isText(child)) {
      lineHolder.push({ text: processText(child.data) });
      return;
    }
    if (isTag(child)) {
      if (child.name === 'br') {
        lineHolder.push({ text: '' });
        appendLine();
        return;
      }

      if (child.name === 'li') {
        appendLine();
        listLines.push(child.children.flatMap((c) => getInlineElement(c, processText)));
        return;
      }

      lineHolder.push(...getInlineElement(child, processText));
    }
  });
  appendLine();

  const mdSequence = node.attribs['data-md'];
  if (mdSequence !== undefined) {
    const prefix = mdSequence || '-';
    const [starOrHyphen] = prefix.match(/^\*|-$/) ?? [];
    return listLines.map((lineChildren) => ({
      type: BlockType.Paragraph,
      children: [
        { text: `${starOrHyphen ? `${starOrHyphen} ` : `${prefix}. `} ` },
        ...lineChildren,
      ],
    }));
  }

  if (node.name === 'ol') {
    return [
      {
        type: BlockType.OrderedList,
        children: listLines.map((lineChildren) => ({
          type: BlockType.ListItem,
          children: lineChildren,
        })),
      },
    ];
  }

  return [
    {
      type: BlockType.UnorderedList,
      children: listLines.map((lineChildren) => ({
        type: BlockType.ListItem,
        children: lineChildren,
      })),
    },
  ];
};
const parseHeadingNode = (
  node: Element,
  processText: ProcessTextCallback
): HeadingElement | ParagraphElement => {
  const children = node.children.flatMap((child) => getInlineElement(child, processText));

  const headingMatch = node.name.match(/^h([123456])$/);
  const [, g1AsLevel] = headingMatch ?? ['h3', '3'];
  const level = parseInt(g1AsLevel, 10);

  const mdSequence = node.attribs['data-md'];
  if (mdSequence !== undefined) {
    return {
      type: BlockType.Paragraph,
      children: [{ text: `${mdSequence} ` }, ...children],
    };
  }

  return {
    type: BlockType.Heading,
    level: (level <= 3 ? level : 3) as HeadingLevel,
    children,
  };
};

export const domToEditorInput = (
  domNodes: ChildNode[],
  processText: ProcessTextCallback,
  processLineStartText: ProcessTextCallback
): Descendant[] => {
  const children: Descendant[] = [];

  let lineHolder: InlineElement[] = [];

  const appendLine = () => {
    if (lineHolder.length === 0) return;

    children.push({
      type: BlockType.Paragraph,
      children: lineHolder,
    });
    lineHolder = [];
  };

  domNodes.forEach((node) => {
    if (isText(node)) {
      if (lineHolder.length === 0) {
        // we are inserting first part of line
        // it may contain block markdown starting data
        // that we may need to escape.
        lineHolder.push({ text: processLineStartText(node.data) });
        return;
      }
      lineHolder.push({ text: processText(node.data) });
      return;
    }
    if (isTag(node)) {
      if (node.name === 'br') {
        lineHolder.push({ text: '' });
        appendLine();
        return;
      }

      if (node.name === 'p') {
        appendLine();
        children.push({
          type: BlockType.Paragraph,
          children: node.children.flatMap((child) => getInlineElement(child, processText)),
        });
        return;
      }

      if (node.name === 'blockquote') {
        appendLine();
        children.push(...parseBlockquoteNode(node, processText));
        return;
      }
      if (node.name === 'pre') {
        appendLine();
        children.push(...parseCodeBlockNode(node));
        return;
      }
      if (node.name === 'ol' || node.name === 'ul') {
        appendLine();
        children.push(...parseListNode(node, processText));
        return;
      }

      if (node.name.match(/^h[123456]$/)) {
        appendLine();
        children.push(parseHeadingNode(node, processText));
        return;
      }

      lineHolder.push(...getInlineElement(node, processText));
    }
  });
  appendLine();

  return children;
};

export const htmlToEditorInput = (unsafeHtml: string, markdown?: boolean): Descendant[] => {
  const sanitizedHtml = sanitizeCustomHtml(unsafeHtml);

  const processText = (partText: string) => {
    if (!markdown) return partText;
    return escapeMarkdownInlineSequences(partText);
  };

  const domNodes = parse(sanitizedHtml);
  const editorNodes = domToEditorInput(domNodes, processText, (lineStartText: string) => {
    if (!markdown) return lineStartText;
    return escapeMarkdownBlockSequences(lineStartText, processText);
  });
  return editorNodes;
};

export const plainToEditorInput = (text: string, markdown?: boolean): Descendant[] => {
  const editorNodes: Descendant[] = text.split('\n').map((lineText) => {
    const paragraphNode: ParagraphElement = {
      type: BlockType.Paragraph,
      children: [
        {
          text: markdown
            ? escapeMarkdownBlockSequences(lineText, escapeMarkdownInlineSequences)
            : lineText,
        },
      ],
    };
    return paragraphNode;
  });
  return editorNodes;
};