import { sanitize } from "isomorphic-dompurify";
import Link from "next/link";
import React, { Fragment } from "react";
import { Poppable } from "../poppables/components/poppable";
import { Accordion, AccordionContent } from "../accordion";

export const TokenRenderers = new Map<string, TokenRenderer<any>>();

export function buildIdentifierMap(): [
  TokenIdentifierMap,
  IdentifierRegistration
] {
  const TokenIdentifiers = new Map<string, TokenIdentifier<any>>();

  function registerIdentifier<M>(
    type: string,
    match: RegExp,
    parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
    renderFunction: TokenRenderer<M>
  ): void;
  function registerIdentifier<M>(
    type: string,
    match: RegExp,
    parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
    renderFunction: TokenRenderer<M>,
    openTagRx: RegExp,
    closeTagRx: RegExp
  ): void;
  function registerIdentifier<M = Record<string, string>>(
    type: string,
    match: RegExp,
    parseFunction: (s: string, rx: RegExp) => IdentifiedToken<M>,
    renderFunction: TokenRenderer<M>,
    openTagRx?: RegExp,
    closeTagRx?: RegExp
  ) {
    TokenIdentifiers.set(type, {
      rx: match,
      parse(s) {
        const identifiedToken = parseFunction(s, this.rx);
        const token: TokenAttributes = {
          render: renderFunction,
          type,
        };

        return { ...token, ...identifiedToken } as Token<M>;
      },
      search:
        openTagRx && closeTagRx
          ? (s, start, end) => {
              return search(
                s,
                start,
                end,
                new RegExp(openTagRx, "g"),
                new RegExp(closeTagRx, "g")
              );
            }
          : undefined,
    });
    TokenRenderers.set(type, renderFunction);
  }

  return [TokenIdentifiers, registerIdentifier];
}

export const buildOnlyDefaultElements = () => {
  const [TokenIdentifiers, registerIdentifier] = buildIdentifierMap();

  TokenRenderers.set("text", (t: Token<any>) => {
    return (
      <span className="whitespace-pre-wrap">
        {t.content.replaceAll(/\\n ?/g, "\n")}
      </span>
    );
  });

  const usedIds: string[] = [];

  const rendersContentOnly = true;
  const rendersChildrenOnly = true;

  // grid
  registerIdentifier<{ columns: string }>(
    "grid",
    /(?<!\/)(?:\[\])+\n+((?:.|\n)*?)\n+\/\[\]/g,
    (s) => {
      const rx = /((?:\[\])+)\n+([\s\S]*)\n+\/\[\]/;
      const [_, columns, content] = s.match(rx) || [
        "",
        "..",
        "Unable to parse grid",
      ];
      return {
        content,
        raw: s,
        metadata: {
          columns: (columns.length / 2).toString(),
        },
        uuid: crypto.randomUUID(),
        rendersChildrenOnly,
      };
    },
    (token) => {
      const { children, metadata } = token;
      return (
        <div
          style={
            {
              "--grid-cols": metadata.columns,
            } as React.CSSProperties
          }
          className="grid grid-cols-dynamic gap-x-8 mb-6"
        >
          {children?.map((c) => {
            const Comp = c.metadata.span ? Fragment : "div";
            return (
              <Comp className="p" key={c.uuid}>
                {c.render(c)}
              </Comp>
            );
          })}
        </div>
      );
    },
    /(?<!\/)(?:\[\])+/g,
    /\/\[\]/g
  );

  // card
  registerIdentifier(
    "card",
    /\[{2}[\s\S]*?\n+\]{2}/g,
    (s) => {
      const rx = /\[{2}((?:!?)(?:[0-9]?))\s*?\n+([\s\S]*)\n+\]{2}/;
      const match = s.match(rx);
      const [_, isBlockOrSpan, content] = match || ["", "", s];

      const isBlock = isBlockOrSpan.includes("!");
      const span = Number(isBlockOrSpan.replace("!", ""));

      return {
        content: content.trim(),
        raw: s,
        metadata: {
          isBlock,
          span,
        },
        uuid: crypto.randomUUID(),
        rendersChildrenOnly,
      };
    },
    (token) => {
      const { children, metadata } = token;
      return (
        <div
          data-block={!!metadata.isBlock}
          style={
            {
              "--v-span": metadata.span || 1,
            } as React.CSSProperties
          }
          className="data-[block=false]:card data-[block=false]:mb-6 col-span-2"
        >
          {children?.map((e) => (
            <Fragment key={e.uuid}>{e.render(e)}</Fragment>
          ))}
        </div>
      );
    },
    /\[\[/g,
    /\]\]/g
  );

  // fenced code block
  registerIdentifier(
    "code",
    /`{3}\n+((?:.|\n)*?)\n+`{3}/g,
    (s, rx) => {
      return {
        content: s.match(new RegExp(rx, ""))?.at(1) || "Unable to parse code",
        raw: s,
        metadata: {},
        uuid: crypto.randomUUID(),
        rendersContentOnly,
      };
    },
    (token) => {
      return (
        <pre className="whitespace-pre-wrap bg-black/20 p-2 rounded-md">
          {token.content}
        </pre>
      );
    }
  );

  // list
  registerIdentifier(
    "list",
    /(?<=\n\n?|^) *-\s([\s\S]*?)(?=\n\n|$)/g,
    (s, rx) => {
      return {
        content: s.match(new RegExp(rx, ""))?.at(0) || "Unable to parse list",
        raw: s,
        metadata: {
          initialDepth:
            s.replace("\n", "").split("-").at(0)?.length.toString() || "1",
        },
        uuid: crypto.randomUUID(),
        rendersChildrenOnly,
      };
    },
    (token) => {
      const { children, metadata } = token;
      return (
        <>
          <ul
            data-depth={(Number(metadata.initialDepth) / 2) % 3}
            className="data-[depth='2']:list-[circle] data-[depth='1']:list-[square] list-disc ml-6"
          >
            {children?.map((c) => {
              return (
                <li key={c.uuid} data-depth={metadata.initialDepth}>
                  {c.children?.map((c: Token<any>) => (
                    <Fragment key={c.uuid}>{c.render(c)}</Fragment>
                  ))}
                </li>
              );
            })}
          </ul>
        </>
      );
    }
  );

  // ordered-list
  registerIdentifier(
    "ordered-list",
    /(?<=\n\n|^)\s*\d+\.\s([\s\S]*?)(?=\n\n|$)/g,
    (s, rx) => {
      // debugger;
      return {
        content:
          s.match(new RegExp(rx, ""))?.at(0) || "Unable to parse ordered list",
        raw: s,
        metadata: {
          // initialDepth:
          //   s.replace("\n", "").split(/\d+\./).at(0)?.length.toString() || "1",
        },
        uuid: crypto.randomUUID(),
        rendersChildrenOnly,
      };
    },
    (token) => {
      const { children } = token;
      debugger;
      return (
        <>
          <ol
            // data-depth={(Number(metadata.initialDepth) / 2) % 3}
            className="ml-6 list-decimal"
          >
            {children?.map((c) => {
              return (
                <li key={c.uuid}>
                  {c.children?.map((c: Token<any>) => (
                    <Fragment key={c.uuid}>{c.render(c)}</Fragment>
                  ))}
                </li>
              );
            })}
          </ol>
        </>
      );
    }
  );

  // ordered list-item
  // list-item
  registerIdentifier(
    "list-item",
    /(?<=^|\n) *(?:-|\d+\.)\s(.*?)(?=\n|$)/g,
    (s, rx) => {
      return {
        content:
          s.match(new RegExp(rx, ""))?.at(1) || "Unable to parse list-item",
        raw: s,
        metadata: {
          initialDepth:
            s.replace("\n", "").split("-").at(0)?.length.toString() || "1",
        },
        uuid: crypto.randomUUID(),
      };
    },
    (token) => {
      const { children, metadata } = token;
      return (
        <li data-depth={metadata.initialDepth} className="ml-2">
          {children?.map((c) => (
            <Fragment key={c.uuid}>{c.render(c)}</Fragment>
          ))}
        </li>
      );
    }
  );

  // heading
  registerIdentifier(
    "heading",
    /^#+\s(.*?)$/gm,
    (s, rx) => {
      const content =
        s.match(new RegExp(rx, ""))?.at(1) || "Unable to parse heading";
      return {
        content: content,
        raw: s,
        metadata: {
          strength: s.match(/#/g)?.length.toString() || "1",
          id: generateId(content, usedIds),
        },
        uuid: crypto.randomUUID(),
        rendersContentOnly,
      };
    },
    (token) => {
      return (
        <div
          id={token.metadata.id}
          data-strength={token.metadata.strength}
          className={`
        font-bold
        data-[strength="1"]:text-4xl
        data-[strength="2"]:text-3xl
        data-[strength="3"]:text-2xl
      `}
        >
          {token.content}
        </div>
      );
    }
  );

  // image
  registerIdentifier(
    "image",
    /\!\[(.*?)\]\((.*?)\)/g,
    (s, rx) => {
      const [_, title, src] = s.match(new RegExp(rx, ""))!;

      return {
        // content: inline,
        content: title.trim(),
        raw: s,
        metadata: {
          src,
        },
        uuid: crypto.randomUUID(),
        rendersContentOnly,
      };
    },
    (token) => {
      const { metadata } = token;
      metadata.src = metadata.src as string;
      if (metadata.src.startsWith("<svg")) {
        return (
          <div
            dangerouslySetInnerHTML={{
              __html: sanitize(metadata.src, {
                USE_PROFILES: { svg: true },
              }),
            }}
          ></div>
        );
      }
      // eslint-disable-next-line @next/next/no-img-element
      return <img src={metadata.src} alt={token.content} />;
    }
  );

  // anchor
  registerIdentifier(
    "anchor",
    /(?<![\!^])\[(.*?)\]\((.*?)\)/g,
    (s, rx) => {
      let preset,
        [_, title, href] = s.match(new RegExp(rx, ""))!;
      const match = title.match(/~{2}(cta|button)?(.*)/);

      if (match) {
        [_, preset, title] = match;
      }

      const classes = {
        button: "btn-primary inline-block",
        cta: "btn-secondary inline-block uppercase",
      };
      return {
        content: title.trim(),
        raw: s,
        metadata: {
          href,
          classes: classes[preset as keyof typeof classes],
        },
        uuid: crypto.randomUUID(),
        rendersContentOnly,
      };
    },
    (token) => {
      const { metadata } = token;
      return (
        <Link
          className={
            metadata.classes ||
            "dark:text-primary-600 underline dark:no-underline"
          }
          href={metadata.href}
        >
          {token.content}
        </Link>
      );
    }
  );

  // inline-code
  registerIdentifier(
    "inline-code",
    /(?<=\s|^)`(.*?)`(?=[\s,.!?)]|$)/gi,
    (s, rx) => {
      return {
        content:
          s.match(new RegExp(rx, "i"))?.at(1) || "Unable to parse inline-code",
        raw: s,
        metadata: {},
        uuid: crypto.randomUUID(),
        rendersContentOnly,
      };
    },
    (token) => {
      return (
        <span className="p-1 rounded-md font-mono bg-black/20 border border-mixed-100/20 mx-1">
          {token.content}
        </span>
      );
    }
  );

  // bold
  registerIdentifier(
    "bold",
    /\*{2}(.*?)\*{2}/g,
    (s, rx) => {
      return {
        content: s.match(new RegExp(rx, "i"))?.at(1) || "Unable to parse bold",
        raw: s,
        metadata: {},
        uuid: crypto.randomUUID(),
        rendersContentOnly,
      };
    },
    (token) => {
      return <span className="font-bold">{token.content}</span>;
    }
  );

  // italic
  registerIdentifier(
    "italic",
    /(?<!\*)\*([^\*]+?)\*(?!\*)/g,
    (s, rx) => {
      return {
        content:
          s.match(new RegExp(rx, "i"))?.at(1) || "Unable to parse italic",
        raw: s,
        metadata: {},
        uuid: crypto.randomUUID(),
        rendersContentOnly,
      };
    },
    (token) => {
      return <span className="italic">{token.content}</span>;
    }
  );

  // popover
  registerIdentifier(
    "popover",
    /\^\[(.*?)\]\<<(.*?)\>>/g,
    (s, rx) => {
      const [_, title, content] = s.match(new RegExp(rx, ""))!;

      return {
        content,
        raw: s,
        metadata: { title },
        uuid: crypto.randomUUID(),
        rendersContentOnly,
      };
    },
    (token) => {
      const { children, metadata, uuid } = token;
      return (
        <Poppable
          content={
            children?.map((c) => (
              <Fragment key={uuid}>{c.render(c)}</Fragment>
            )) || token.content
          }
          preferredAlign="centered"
          preferredEdge="bottom"
          className="cursor-pointer mx-2"
        >
          <span className="border-b-2 border-dotted border-white">
            {metadata.title}
          </span>
        </Poppable>
      );
    }
  );

  registerIdentifier(
    "accordion",
    /\[accordion(\s.*?)?]\n+((?:.|\n)*?)\n+\[\/accordion\]/g,
    (s, rx) => {
      const [_, title, content] = s.match(new RegExp(rx, ""))!;

      return {
        content,
        raw: s,
        metadata: { title },
        uuid: crypto.randomUUID(),
      };
    },
    (token) => {
      const { children, metadata } = token;
      return (
        <div className="bg-black/20 p-1 accordion">
          <Accordion title={metadata.title || "Expand"}>
            <AccordionContent>
              {children?.map((e) => (
                <Fragment key={e.uuid}>{e.render(e)}</Fragment>
              ))}
            </AccordionContent>
          </Accordion>
        </div>
      );
    }
  );

  registerIdentifier(
    "p",
    /(?<=\n\n)([\s\S]*?)(?=\n\n)/g,
    (s) => {
      return {
        content: s.replace("\n", " "),
        raw: s,
        metadata: {},
        uuid: crypto.randomUUID(),
      };
    },
    (token) => {
      const { children } = token;

      return (
        <div className="p">
          {children?.map((e) => {
            return <Fragment key={e.uuid}>{e.render(e)}</Fragment>;
          })}
        </div>
      );
    }
  );

  registerIdentifier(
    "hr",
    /^-{3,}$/gm,
    (s) => {
      return {
        content: s,
        raw: s,
        metadata: {},
        uuid: crypto.randomUUID(),
        rendersContentOnly,
      };
    },
    () => {
      return <div className="w-full border-b border-mixed-500 my-3"></div>;
    }
  );

  registerIdentifier(
    "comment",
    /<!--[\s\S]+?-->/g,
    (s) => {
      return {
        content: "",
        metadata: { comment: s },
        raw: "",
        uuid: crypto.randomUUID(),
        rendersContentOnly,
      };
    },
    () => {
      return <></>;
    }
  );

  registerIdentifier(
    "frontmatter",
    /^---([\s\S]*?)---/g,
    (s, rx) => {
      return {
        content: "",
        metadata: {
          frontmatterString: s.match(rx)?.at(0) || "",
        },
        raw: "",
        uuid: "frontmatter",
      };
    },
    (token) => {
      return <>{token.raw}</>;
    }
  );

  registerIdentifier(
    "table",
    /(?<=\n|^)\| [\s\S]*? \|(?=(\n|$)(?!\|))/g,
    (s) => {
      const splitMarker = "{{^^}}";
      const original = s;

      let columnPattern: string[] = [];
      let fullWidth = false;
      s = s.replace(/^\| (<-)?[-|\s^]+(->)? \|$/gm, (e) => {
        if (!columnPattern.length) {
          columnPattern = e.split("|").filter((e) => e);
        }
        if (e.match(/^\| <-.*?-> \|/gm)) {
          fullWidth = true;
        }
        return splitMarker;
      });

      const rowSections = s.split(splitMarker).map((s) =>
        s
          .split("\n")
          .filter((r) => !!r)
          .map((r) =>
            r
              .split("|")
              .map((c) => c.trim())
              .filter((c) => !!c)
          )
      );

      let headerRows: string[][] = [],
        bodyRows: string[][] = [],
        footerRows: string[][] = [];

      switch (rowSections.length) {
        case 1:
          bodyRows = rowSections[0];
          break;
        case 2:
          headerRows = rowSections[0];
          bodyRows = rowSections[1];
          break;
        case 3:
          headerRows = rowSections[0];
          bodyRows = rowSections[1];
          footerRows = rowSections[3];
          break;
      }

      const maxColumns = Math.max(
        ...[...headerRows, ...bodyRows, ...footerRows].map((r) => r.length)
      );

      return {
        content: original,
        raw: original,
        metadata: {
          headerRows: headerRows,
          bodyRows: bodyRows,
          footerRows: footerRows,
          columns: maxColumns,
          columnPattern,
          fullWidth,
        },
        uuid: crypto.randomUUID(),
      };
    },
    (t) => {
      const {
        headerRows,
        bodyRows,
        footerRows,
        columns,
        columnPattern,
        fullWidth,
      } = t.metadata;

      return (
        <table
          data-full-width={fullWidth}
          className="md-table data-[full-width=true]:w-full"
        >
          {!!headerRows && (
            <thead>
              {headerRows.map((r, i) => (
                <tr key={r.join() + i}>
                  {r.concat(Array(columns - r.length).fill("")).map((c) => {
                    const child = t.children?.find((child) => child.raw === c);
                    return (
                      <th key={r.join() + i + c}>
                        {child?.render(child) || c}
                      </th>
                    );
                  })}
                </tr>
              ))}
            </thead>
          )}
          {!!bodyRows && (
            <tbody>
              {bodyRows.map((r, i) => (
                <tr key={r.join() + i}>
                  {r.concat(Array(columns - r.length).fill("")).map((c, i) => {
                    const child = t.children?.find((child) => child.raw === c);
                    return (
                      <td
                        key={r.join() + i + c}
                        className="data-[center=true]:text-center"
                        data-center={
                          !!(
                            columnPattern?.at(i) &&
                            columnPattern.at(i)?.includes("^")
                          )
                        }
                      >
                        {child?.render(child) || c}
                      </td>
                    );
                  })}
                </tr>
              ))}
            </tbody>
          )}
          {!!footerRows && (
            <tfoot>
              {footerRows.map((r, i) => (
                <tr key={r.join() + i}>
                  {r.concat(Array(columns - r.length).fill("")).map((c) => {
                    const child = t.children?.find((child) => child.raw === c);
                    return (
                      <td key={r.join() + i + c}>
                        {child?.render(child) || c}
                      </td>
                    );
                  })}
                </tr>
              ))}
            </tfoot>
          )}
        </table>
      );
    }
  );

  return TokenIdentifiers;
};

function findMatchingClosedParenthesis(
  str: string,
  openRegex: RegExp,
  closedRegex: RegExp
): number | null {
  let openings = 0;
  let closings = 0;

  openRegex = new RegExp(openRegex, "g");
  closedRegex = new RegExp(closedRegex, "g");

  let lastOpeningSuccessIndex = 0;
  let lastClosingSuccessIndex = 0;

  do {
    const openingMatch = openRegex.exec(str);
    const closingMatch = closedRegex.exec(str);

    if (openingMatch && !closingMatch) {
      throw Error("Things have gone horribly wrong");
    }

    // if ((!openingMatch && closingMatch) || (!openingMatch && !closingMatch)) break;

    if (
      openingMatch &&
      closingMatch &&
      openingMatch.index < closingMatch.index
    ) {
      openings++;
      lastOpeningSuccessIndex = openingMatch.index + openingMatch[0].length;
      closedRegex.lastIndex = lastClosingSuccessIndex;
    } else if (
      (!openingMatch && closingMatch) ||
      (openingMatch && closingMatch && openingMatch.index > closingMatch.index)
    ) {
      closings++;
      lastClosingSuccessIndex = closingMatch.index + closingMatch[0].length;
      openRegex.lastIndex = lastOpeningSuccessIndex;
    } else {
      return closingMatch?.index ?? null;
    }
  } while (openings > closings);

  return closedRegex.lastIndex;
}

interface SearchResult {
  start: number;
  end: number;
  text: string;
  lastIndex: number;
}

function search(
  s: string,
  start: number,
  end: number,
  openRx: RegExp,
  closeRx: RegExp
): SearchResult {
  const oldEnd = end;

  const newEnd = findMatchingClosedParenthesis(
    s,
    // s.substring(0, end - start),
    openRx,
    closeRx
  );

  if (newEnd === null) throw Error("There was an issue finding a closing tag");

  end = newEnd + start;

  return {
    start,
    end,
    text: s.substring(0, newEnd),
    lastIndex: oldEnd === end ? end : start + s.match(openRx)![0].length,
  };
}

// Finds a unique id for things like headings
function generateId(t: string, usedIds: string[]) {
  let id = t
    .toLowerCase()
    .replace(/[^a-z\s]/gi, "")
    .trim()
    .replaceAll(" ", "-");
  let idNum = 1;
  while (usedIds.includes(id)) {
    id = id.replace(/-[0-9]+$/g, "");
    id += "-" + idNum;
    idNum++;
  }
  return id;
}
