import * as commonmark from 'commonmark';
import type { Node as MarkdownNode } from 'commonmark';

import { MarkdownNodeType } from '../common';
import { logActionRequired } from '../utilities';

const reader = new commonmark.Parser();
const writer = new commonmark.HtmlRenderer({ safe: true });

const NODE_TYPE_LIST = Object.values(MarkdownNodeType);

export type MarkdownProps = {
  as?: React.ElementType;
  config?: {
    link?: Pick<React.ComponentPropsWithoutRef<'a'>, 'target'>;
    paragraph?: Pick<React.ComponentPropsWithoutRef<'p'>, 'className'>;
  };
  className?: string;
  children?: string;
} & (
  | {
      allowList?: readonly `${MarkdownNodeType}`[];
      blockList?: never;
    }
  | {
      allowList?: never;
      blockList?: readonly `${MarkdownNodeType}`[];
    }
);

export default function Markdown({
  as: Element = 'div',
  allowList,
  blockList,
  config,
  className,
  children,
}: MarkdownProps) {
  if (!children) {
    return null;
  }

  const linkTarget = config?.link?.target ?? '_self';
  const paragraphClass = config?.paragraph?.className ?? '';

  if (allowList != null && blockList != null) {
    logActionRequired(
      'Markdown supports only one of `allowList` or `blockList` to be used at a time. `blockList` will be ignored.',
    );
  }

  const parser = (nodes: string) => {
    const parsed = reader.parse(nodes);
    const toExclude =
      allowList != null ? NODE_TYPE_LIST.filter((type) => !allowList.includes(type)) : blockList;
    return toExclude != null ? stripNodes({ parsed, blockList: toExclude }) : parsed;
  };

  const createMarkup = () => {
    const parsed = parser(children);
    return writer
      .render(parsed)
      .replace(/<a href="/g, `<a target="${linkTarget}" href="`)
      .replace(/<p>/g, `<p class="${paragraphClass}">`);
  };

  return <Element className={className} dangerouslySetInnerHTML={{ __html: createMarkup() }} />;
}

function stripNodes({ blockList, parsed }: { blockList: readonly string[]; parsed: MarkdownNode }) {
  if (!parsed) {
    return parsed;
  }

  const walker = parsed.walker();

  for (let event = walker.next(); event != null; event = walker.next()) {
    const { node } = event;
    if (blockList.includes(node.type) && !event.entering) {
      while (node.firstChild != null) {
        node.insertBefore(node.firstChild);
      }
      node.unlink();
    }
  }
  return parsed;
}
