import cloneDeep from 'lodash/cloneDeep';
import Token from 'markdown-it/lib/token';

const TOC = '[toc]';
const TOC_RE = /^\[toc\]/im;
const repeatAsterisk = (string: string, num: number) => new Array(num + 1).join(string) + '*';
const safeString = (str: string) => {
  return str
    .toLowerCase()
    .trim()
    .replace(/<[!\/a-z].*?>/gi, '')
    .replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, '')
    .replace(/\s/g, '-');
};

let mdInstance: any = null;
export default function (md: any, options: Record<string, any>) {
  const opts: Record<string, any> = {
    maxLevel: 6,
    ...options,
  };
  let tocHtml = '';
  mdInstance = cloneDeep(md);

  const generateTree = (headings: Array<any>) => {
    const tree: Record<'nodes', Array<any>> = { nodes: [] };
    headings.forEach((heading) => {
      if (heading.level > opts.maxLevel) {
        return;
      }

      let lastItem = tree;
      for (let i = 1; i < heading.level; i++) {
        if (lastItem.nodes.length === 0) {
          lastItem.nodes.push({
            heading: {},
            nodes: [],
          });
        }
        lastItem = lastItem.nodes[lastItem.nodes.length - 1];
      }
      lastItem.nodes.push({
        heading: heading,
        nodes: [],
      });
    });
    return tree;
  };

  md.core.ruler.push('init_toc', (state: any) => {
    const tokens = state.tokens;
    const headings = [];
    let tocTokens = [];
    for (let i = 0; i < tokens.length; i++) {
      if (tokens[i].type === 'heading_close') {
        const heading = tokens[i - 1];
        const heading_close = tokens[i];
        if (heading.type === 'inline') {
          let content;
          if (heading.children && heading.children.length > 0 && heading.children[0].type === 'link_open') {
            content = heading.children[1].content;
            heading._tocAnchor = safeString(content);
          } else {
            content = heading.content;
            heading._tocAnchor = safeString(heading.children.reduce((s, t) => s + t.content, ''));
          }
          headings.push({
            content,
            anchor: safeString(content),
            level: Number(heading_close.tag.substr(1, 1)),
          });
        }
      }
    }
    const tocTree = generateTree(headings);
    const headerRecord = new Map<string, number>();
    const tocTree2Html = (nodes: any[], indent = 0) => {
      return nodes
        .map((item: any) => {
          let node = `${item.heading.content ? repeatAsterisk('  ', indent) : ''}`;
          if (item.heading.content) {
            if (headerRecord.has(item.heading.anchor)) {
              headerRecord.set(item.heading.anchor, headerRecord.get(item.heading.anchor)! + 1);
              node += ` [${item.heading.content}](#${item.heading.anchor}-${headerRecord.get(item.heading.anchor)})\n`;
            } else {
              headerRecord.set(item.heading.anchor, 0);
              node += ` [${item.heading.content}](#${item.heading.anchor})\n`;
            }
          } else {
            node += '\n';
          }
          if (item.nodes.length) {
            node += item.heading.content ? tocTree2Html(item.nodes, indent + 1) : tocTree2Html(item.nodes, indent);
          }
          return node;
        })
        .join('');
    };
    const toc = tocTree2Html(tocTree.nodes);
    tocTokens = mdInstance.parse(toc, {});
    tocHtml = mdInstance.renderer.render(tocTokens, mdInstance.options);
  });

  md.inline.ruler.after('emphasis', 'toc', (state: any, silent: any) => {
    let token;
    let match;

    if (silent) {
      return false;
    }
    match = TOC_RE.exec(state.src);
    match = !match ? [] : match.filter((m) => m);
    if (match.length < 1) {
      return false;
    }
    token = state.push('toc_open', 'toc', 1);
    token.markup = TOC;
    token = state.push('toc_body', '', 0);
    token = state.push('toc_close', 'toc', -1);

    state.pos = state.pos + TOC.length;
    return true;
  });

  const space = () => {
    return { ...new Token('text', '', 0), content: ' ' };
  };

  const renderAnchorLink = (anchor: any, linkOptions: Record<string, any>, tokens: any, idx: number) => {
    const attrs = [];
    if (linkOptions.anchorClassName !== null && linkOptions.anchorClassName !== undefined) {
      attrs.push(['class', linkOptions.anchorClassName]);
    }

    attrs.push(['href', `#${anchor}`]);

    const openLinkToken = {
      ...new Token('link_open', 'a', 1),
      attrs,
    };
    const closeLinkToken = new Token('link_close', 'a', -1);

    const renderAnchorLinkSymbol = () => {
      if (linkOptions.anchorLinkSymbolClassName) {
        return [
          {
            ...new Token('span_open', 'span', 1),
            attrs: [['class', linkOptions.anchorLinkSymbolClassName]],
          },
          {
            ...new Token('text', '', 0),
            content: linkOptions.anchorLinkSymbol,
          },
          new Token('span_close', 'span', -1),
        ];
      } else {
        return [
          {
            ...new Token('text', '', 0),
            content: linkOptions.anchorLinkSymbol,
          },
        ];
      }
    };

    if (linkOptions.wrapHeadingTextInAnchor) {
      tokens[idx + 1].children.unshift(openLinkToken);
      tokens[idx + 1].children.push(closeLinkToken);
    } else {
      const linkTokens = [openLinkToken, ...renderAnchorLinkSymbol(), closeLinkToken];

      const actionOnArray = {
        false: 'push',
        true: 'unshift',
      };

      if (linkOptions.anchorLinkSpace) {
        linkTokens[actionOnArray[linkOptions?.anchorLinkBefore]](space());
      }
      tokens[idx + 1].children[actionOnArray[linkOptions.anchorLinkBefore]](...linkTokens);
    }
  };

  const originalHeadingOpen =
    md.renderer.rules.heading_open ||
    function (...args: any[]) {
      const [tokens, idx, , , self] = args;
      return self.renderToken(tokens, idx, options);
    };

  md.renderer.rules.heading_open = function (...args: any[]) {
    const [tokens, idx, , ,] = args;

    const attrs = (tokens[idx].attrs = tokens[idx].attrs || []);
    const anchor = tokens[idx + 1]._tocAnchor;
    attrs.push(['id', anchor]);

    if (opts.anchorLink) {
      renderAnchorLink(anchor, opts, tokens, idx);
    }

    return originalHeadingOpen.apply(this, args);
  };

  md.renderer.rules.toc_open = () => '';
  md.renderer.rules.toc_close = () => '';
  md.renderer.rules.toc_body = () => tocHtml;
}