import React from "react";
import {
  renderNodeRule,
  StructuredText,
  type StructuredTextDocument,
} from "react-datocms";
import {
  type Heading,
  type InlineNode,
  isCode,
  isHeading,
  isParagraph,
  type Mark,
  type Paragraph,
} from "datocms-structured-text-utils";

import CallToAction from "@/components/call-to-action";
import CodeNode from "@/components/code";
import {
  LAST_SPACE_REGEX,
  UNICODE_NO_BREAK_SPACE,
  widont,
} from "@/helpers/string";
import type { TextBlock } from "@/types";

import styles from "./styles.module.scss";

interface renderChildProps {
  child: InlineNode;
  index: number;
}

const renderChild = ({ child, index }: renderChildProps) => {
  const markToTagName: { [key: Mark]: keyof JSX.IntrinsicElements } = {
    emphasis: "em",
    underline: "u",
    strikethrough: "del",
    highlight: "mark",
    strong: "strong",
    code: "code",
  };

  const injectMarks = (
    textEl: React.ReactNode,
    marks: Mark[] = [],
  ): React.ReactNode => {
    return marks.reduce((acc, mark) => {
      const Tag = markToTagName[mark];
      /* eslint-disable */
      // @ts-ignore
      return <Tag>{acc}</Tag>;
    }, textEl);
  };

  switch (child.type) {
    case "span":
      const segments = child.value.split("\n");
      return segments.map((segment: string, segmentIndex: number) => {
        const balancedSegment = widont(segment, {
          preserveTrailingWhitespace: true,
        });

        return (
          <React.Fragment key={`segment-${segmentIndex}`}>
            {injectMarks(balancedSegment, child.marks)}
            {segmentIndex < segments.length - 1 ? <br /> : null}
          </React.Fragment>
        );
      });
    case "link":
      const content = child.children.map((c) => widont(c.value)).join();
      const href = child.url.toLowerCase();
      return (
        <CallToAction key={index} href={href}>
          {content}
        </CallToAction>
      );
    default:
      return null;
  }
};

interface renderChildrenProps {
  children: InlineNode[];
}

const renderChildren = ({ children }: renderChildrenProps) => {
  const renderedChildren: Array<JSX.Element | JSX.Element[] | null> = [];

  for (let index = 0; index < children.length; index++) {
    const child = children[index];
    const nextChild = children[index + 1];
    const prevChild = children[index - 1];
    const nextIsLast = index + 1 === children.length - 1;

    // Prevent widowing for edge case where the last semantic word in a nested
    // text node is a link surrounded by spans.
    if (
      child.type === "link" &&
      prevChild &&
      prevChild.type === "span" &&
      nextChild &&
      nextChild.type === "span" &&
      nextIsLast &&
      /^[^a-zA-Z0-9]+$/.test(nextChild.value)
    ) {
      const matchPrevChild = prevChild.value.match(LAST_SPACE_REGEX);
      const [_match, _precedingChar, lastWord, trailingWhitespace] =
        matchPrevChild ? matchPrevChild : [];
      const restWords = prevChild.value.replace(LAST_SPACE_REGEX, "$1");

      if (restWords.trim()) {
        // TODO: Check that the last item in renderedChildren matches prevChild
        renderedChildren.pop();
        renderedChildren.push(
          renderChild({
            child: {
              type: "span",
              value: restWords + trailingWhitespace,
            },
            index: index - 1,
          }),
        );
      }

      renderedChildren.push(
        <span key={`${child.type}-${index}`} className={styles.nowrap}>
          {renderChild({
            child: { type: "span", value: lastWord },
            index,
          })}
          {UNICODE_NO_BREAK_SPACE}
          {renderChild({
            child,
            index,
          })}
        </span>,
      );
    } else {
      // Render child as normal
      renderedChildren.push(renderChild({ child, index }));
    }
  }

  return renderedChildren;
};

interface HeadingNodeProps {
  node: Heading;
}

const HeadingNode = ({ node }: HeadingNodeProps) => {
  const HeadingTag = `h${node.level}` as
    | "h1"
    | "h2"
    | "h3"
    | "h4"
    | "h5"
    | "h6";
  const combinedChildren = renderChildren({ children: node.children });
  return <HeadingTag>{combinedChildren}</HeadingTag>;
};

interface ParagraphNodeProps {
  node: Paragraph;
}

const ParagraphNode = ({ node }: ParagraphNodeProps) => (
  <p>
    {node.children.map((child, index) => (
      <React.Fragment key={index}>
        {renderChild({ child, index })}
      </React.Fragment>
    ))}
  </p>
);

type TextProps = TextBlock;

export default function Text({ content }: TextProps) {
  return (
    <div className={styles.text}>
      <StructuredText
        data={content.value as StructuredTextDocument}
        customNodeRules={[
          renderNodeRule(isHeading, ({ node, key }) => (
            <HeadingNode key={key} node={node} />
          )),
          renderNodeRule(isParagraph, ({ node, key }) => (
            <ParagraphNode key={key} node={node} />
          )),
          renderNodeRule(isCode, ({ node, key }) => (
            <CodeNode
              key={key}
              code={node.code}
              language={node.language}
              linesToBeHighlighted={node.highlight}
            />
          )),
        ]}
      />
    </div>
  );
}
