import {
  addParentPointers,
  collectNodesOfType,
  findNodeOfType,
  type ParseTree,
  removeParentPointers,
  renderToText,
  traverseTree,
} from "@silverbulletmd/silverbullet/lib/tree";
import {
  encodePageRef,
  parsePageRef,
} from "@silverbulletmd/silverbullet/lib/page_ref";
import { Fragment, renderHtml, type Tag } from "./html_render.ts";
import { isLocalPath } from "@silverbulletmd/silverbullet/lib/resolve";
import type { PageMeta } from "@silverbulletmd/silverbullet/types";

export type MarkdownRenderOptions = {
  failOnUnknown?: true;
  smartHardBreak?: true;
  annotationPositions?: true;
  attachmentUrlPrefix?: string;
  preserveAttributes?: true;
  // When defined, use to inline images as data: urls
  translateUrls?: (url: string, type: "link" | "image") => string;
};

function cleanTags(values: (Tag | null)[], cleanWhitespace = false): Tag[] {
  const result: Tag[] = [];
  for (const value of values) {
    if (cleanWhitespace && typeof value === "string" && value.match(/^\s+$/)) {
      continue;
    }
    if (value) {
      result.push(value);
    }
  }
  return result;
}

function preprocess(t: ParseTree) {
  addParentPointers(t);
  traverseTree(t, (node) => {
    if (!node.type) {
      if (node.text?.startsWith("\n")) {
        const prevNodeIdx = node.parent!.children!.indexOf(node) - 1;
        const prevNodeType = node.parent!.children![prevNodeIdx]?.type;
        if (
          prevNodeType?.includes("Heading") || prevNodeType?.includes("Table")
        ) {
          node.text = node.text.slice(1);
        }
      }
    }
    return false;
  });
}

function posPreservingRender(
  t: ParseTree,
  options: MarkdownRenderOptions = {},
): Tag | null {
  const tag = render(t, options);
  if (!options.annotationPositions) {
    return tag;
  }
  if (!tag) {
    return null;
  }
  if (typeof tag === "string") {
    return tag;
  }
  if (t.from) {
    if (!tag.attrs) {
      tag.attrs = {};
    }
    tag.attrs["data-pos"] = "" + t.from;
  }
  return tag;
}

function render(
  t: ParseTree,
  options: MarkdownRenderOptions = {},
): Tag | null {
  if (t.type?.endsWith("Mark") || t.type?.endsWith("Delimiter")) {
    return null;
  }
  switch (t.type) {
    case "Document":
      return {
        name: Fragment,
        body: cleanTags(mapRender(t.children!)),
      };
    case "FrontMatter":
      return null;
    case "CommentBlock":
      // Remove, for now
      return null;
    case "ATXHeading1":
      return {
        name: "h1",
        body: cleanTags(mapRender(t.children!)),
      };
    case "ATXHeading2":
      return {
        name: "h2",
        body: cleanTags(mapRender(t.children!)),
      };
    case "ATXHeading3":
      return {
        name: "h3",
        body: cleanTags(mapRender(t.children!)),
      };
    case "ATXHeading4":
      return {
        name: "h4",
        body: cleanTags(mapRender(t.children!)),
      };
    case "ATXHeading5":
      return {
        name: "h5",
        body: cleanTags(mapRender(t.children!)),
      };
    case "ATXHeading6":
      return {
        name: "h6",
        body: cleanTags(mapRender(t.children!)),
      };
    case "Paragraph":
      return {
        name: "span",
        attrs: {
          class: "p",
        },
        body: cleanTags(mapRender(t.children!)),
      };
    // Code blocks
    case "FencedCode":
    case "CodeBlock": {
      // Clear out top-level indent blocks
      const lang = findNodeOfType(t, "CodeInfo");
      t.children = t.children!.filter((c) => c.type);
      return {
        name: "pre",
        attrs: {
          "data-lang": lang ? lang.children![0].text : undefined,
        },
        body: cleanTags(mapRender(t.children!)),
      };
    }
    case "CodeInfo":
      return null;
    case "CodeText":
      return t.children![0].text!;
    case "Blockquote":
      return {
        name: "blockquote",
        body: cleanTags(mapRender(t.children!)),
      };
    case "HardBreak":
      return {
        name: "br",
        body: "",
      };
    // Basic styling
    case "Emphasis":
      return {
        name: "em",
        body: cleanTags(mapRender(t.children!)),
      };
    case "Highlight":
      return {
        name: "span",
        attrs: {
          class: "highlight",
        },
        body: cleanTags(mapRender(t.children!)),
      };
    case "Strikethrough":
      return {
        name: "del",
        body: cleanTags(mapRender(t.children!)),
      };
    case "InlineCode":
      return {
        name: "tt",
        body: cleanTags(mapRender(t.children!)),
      };
    case "BulletList":
      return {
        name: "ul",
        body: cleanTags(mapRender(t.children!), true),
      };
    case "OrderedList":
      return {
        name: "ol",
        body: cleanTags(mapRender(t.children!), true),
      };
    case "ListItem":
      return {
        name: "li",
        body: cleanTags(mapRender(t.children!), true),
      };
    case "StrongEmphasis":
      return {
        name: "strong",
        body: cleanTags(mapRender(t.children!)),
      };
    case "HorizontalRule":
      return {
        name: "hr",
        body: "",
      };
    case "Link": {
      const linkTextChildren = t.children!.slice(1, -4);
      const urlNode = findNodeOfType(t, "URL");
      if (!urlNode) {
        return renderToText(t);
      }
      let url = urlNode.children![0].text!;
      if (isLocalPath(url)) {
        if (
          options.attachmentUrlPrefix &&
          !url.startsWith(options.attachmentUrlPrefix)
        ) {
          url = `${options.attachmentUrlPrefix}${url}`;
        }
      }
      return {
        name: "a",
        attrs: {
          href: url,
        },
        body: cleanTags(mapRender(linkTextChildren)),
      };
    }
    case "Autolink": {
      const urlNode = findNodeOfType(t, "URL");
      if (!urlNode) {
        return renderToText(t);
      }
      let url = urlNode.children![0].text!;
      if (isLocalPath(url)) {
        if (
          options.attachmentUrlPrefix &&
          !url.startsWith(options.attachmentUrlPrefix)
        ) {
          url = `${options.attachmentUrlPrefix}${url}`;
        }
      }
      return {
        name: "a",
        attrs: {
          href: url,
        },
        body: url,
      };
    }
    case "Image": {
      const altTextNode = findNodeOfType(t, "WikiLinkAlias") ||
        t.children![1];
      let altText = altTextNode && altTextNode.type !== "LinkMark"
        ? renderToText(altTextNode)
        : "";
      const dimReg = /\d*[^\|\s]*?[xX]\d*[^\|\s]*/.exec(altText);
      let style = "";
      if (dimReg) {
        const [, width, widthUnit = "px", height, heightUnit = "px"] =
          dimReg[0].match(/(\d*)(\S*?x?)??[xX](\d*)(.*)?/) ?? [];
        if (width) {
          style += `width: ${width}${widthUnit};`;
        }
        if (height) {
          style += `height: ${height}${heightUnit};`;
        }
        altText = altText.replace(dimReg[0], "").replace("|", "");
      }

      const urlNode = findNodeOfType(t, "WikiLinkPage") ||
        findNodeOfType(t, "URL");
      if (!urlNode) {
        return renderToText(t);
      }
      let url = renderToText(urlNode);
      if (urlNode.type === "WikiLinkPage") {
        url = "/" + url;
      }

      if (
        isLocalPath(url) &&
        options.attachmentUrlPrefix &&
        !url.startsWith(options.attachmentUrlPrefix)
      ) {
        url = `${options.attachmentUrlPrefix}${url}`;
      }

      return {
        name: "img",
        attrs: {
          src: url,
          alt: altText,
          style: style,
        },
        body: "",
      };
    }

    // Custom stuff
    case "WikiLink": {
      const ref = findNodeOfType(t, "WikiLinkPage")!.children![0].text!;
      let linkText = ref.split("/").pop()!;
      const aliasNode = findNodeOfType(t, "WikiLinkAlias");
      if (aliasNode) {
        linkText = aliasNode.children![0].text!;
      }
      const pageRef = parsePageRef(ref);
      return {
        name: "a",
        attrs: {
          href: `/${encodePageRef(pageRef)}`,
          class: "wiki-link",
          "data-ref": ref,
        },
        body: linkText,
      };
    }
    case "NakedURL": {
      const url = t.children![0].text!;
      return {
        name: "a",
        attrs: {
          href: url,
        },
        body: url,
      };
    }
    case "Hashtag":
      return {
        name: "span",
        attrs: {
          class: "hashtag",
        },
        body: t.children![0].text!,
      };

    case "Task": {
      let externalTaskRef = "";
      collectNodesOfType(t, "WikiLinkPage").forEach((wikilink) => {
        const pageRef = parsePageRef(wikilink.children![0].text!);
        if (!externalTaskRef && (pageRef.pos !== undefined || pageRef.anchor)) {
          externalTaskRef = wikilink.children![0].text!;
        }
      });

      return {
        name: "span",
        attrs: externalTaskRef
          ? {
            "data-external-task-ref": externalTaskRef,
          }
          : {},
        body: cleanTags(mapRender(t.children!)),
      };
    }
    case "TaskState": {
      // child[0] = marker, child[1] = state, child[2] = marker
      const stateText = t.children![1].text!;
      if ([" ", "x", "X"].includes(stateText)) {
        return {
          name: "input",
          attrs: {
            type: "checkbox",
            checked: stateText !== " " ? "checked" : undefined,
            "data-state": stateText,
          },
          body: "",
        };
      } else {
        return {
          name: "span",
          attrs: {
            class: "task-state",
          },
          body: stateText,
        };
      }
    }
    case "NamedAnchor":
      return {
        name: "a",
        attrs: {
          name: t.children![0].text?.substring(1),
        },
        body: "",
      };
    case "CommandLink": {
      // Child 0 is CommandLinkMark, child 1 is CommandLinkPage
      const command = t.children![1].children![0].text!;
      let commandText = command;
      const aliasNode = findNodeOfType(t, "CommandLinkAlias");
      const argsNode = findNodeOfType(t, "CommandLinkArgs");
      let args: any = [];

      if (argsNode) {
        args = JSON.parse(`[${argsNode.children![0].text!}]`);
      }

      if (aliasNode) {
        commandText = aliasNode.children![0].text!;
      }

      return {
        name: "button",
        attrs: {
          "data-onclick": JSON.stringify(["command", command, args]),
        },
        body: commandText,
      };
    }

    case "DeadlineDate":
      return {
        name: "span",
        attrs: {
          class: "task-deadline",
        },
        body: renderToText(t),
      };

    // Tables
    case "Table":
      return {
        name: "table",
        body: cleanTags(mapRender(t.children!)),
      };
    case "TableHeader":
      return {
        name: "thead",
        body: [
          {
            name: "tr",
            body: cleanTags(mapRender(t.children!), true),
          },
        ],
      };
    case "TableCell":
      return {
        name: "td",
        body: cleanTags(mapRender(t.children!)),
      };
    case "TableRow": {
      const children = t.children!;
      const newChildren: ParseTree[] = [];
      // Ensure there is TableCell in between every delimiter
      let lookingForCell = false;
      for (const child of children) {
        if (child.type === "TableDelimiter" && lookingForCell) {
          // We were looking for a cell, but didn't fine one: empty cell!
          // Let's inject an empty one
          newChildren.push({
            type: "TableCell",
            children: [],
          });
        }
        if (child.type === "TableDelimiter") {
          lookingForCell = true;
        }
        if (child.type === "TableCell") {
          lookingForCell = false;
        }
        newChildren.push(child);
      }
      return {
        name: "tr",
        body: cleanTags(mapRender(newChildren), true),
      };
    }
    case "Attribute":
      if (options.preserveAttributes) {
        return {
          name: "span",
          attrs: {
            class: "attribute",
          },
          body: renderToText(t),
        };
      }
      return null;
    case "Escape": {
      return {
        name: "span",
        attrs: {
          class: "escape",
        },
        body: t.children![0].text!.slice(1),
      };
    }
    case "Entity":
      return t.children![0].text!;

    case "TemplateDirective": {
      return {
        name: "span",
        attrs: {
          class: "template-directive",
        },
        body: renderToText(t),
      };
    }
    case "Superscript":
      return {
        name: "sup",
        body: cleanTags(mapRender(t.children!)),
      };
    case "Subscript":
      return {
        name: "sub",
        body: cleanTags(mapRender(t.children!)),
      };
    case "LuaDirective":
      return {
        name: "span",
        attrs: {
          class: "sb-lua-directive",
        },
        body: renderToText(t),
      };

    // Text
    case undefined:
      return t.text!;
    default:
      if (options.failOnUnknown) {
        removeParentPointers(t);
        console.error("Not handling", JSON.stringify(t, null, 2));
        throw new Error(`Unknown markdown node type ${t.type}`);
      } else {
        // Falling back to rendering verbatim
        removeParentPointers(t);
        console.warn("Not handling", JSON.stringify(t, null, 2));
        return renderToText(t);
      }
  }

  function mapRender(children: ParseTree[]) {
    return children.map((t) => posPreservingRender(t, options));
  }
}

function traverseTag(
  t: Tag,
  fn: (t: Tag) => void,
) {
  fn(t);
  if (typeof t === "string") {
    return;
  }
  if (t.body) {
    for (const child of t.body) {
      traverseTag(child, fn);
    }
  }
}

export function renderMarkdownToHtml(
  t: ParseTree,
  options: MarkdownRenderOptions = {},
  allPages: PageMeta[] = [],
) {
  preprocess(t);
  const htmlTree = posPreservingRender(t, options);
  if (htmlTree) {
    traverseTag(htmlTree, (t) => {
      if (typeof t === "string") {
        return;
      }
      if (t.name === "img" && options.translateUrls) {
        t.attrs!.src = options.translateUrls!(t.attrs!.src!, "image");
      }

      if (t.name === "a" && t.attrs!.href) {
        if (options.translateUrls) {
          t.attrs!.href = options.translateUrls!(t.attrs!.href, "link");
        }
        if (t.attrs!["data-ref"]?.length) {
          const pageRef = parsePageRef(t.attrs!["data-ref"]!);
          const pageMeta = allPages.find((p) => pageRef.page === p.name);
          if (pageMeta) {
            t.body = [(pageMeta.pageDecoration?.prefix ?? "") + t.body];
            if (pageMeta.pageDecoration?.cssClasses) {
              t.attrs!.class += " sb-decorated-object " +
                pageMeta.pageDecoration.cssClasses.join(" ").replaceAll(
                  /[^a-zA-Z0-9-_ ]/g,
                  "",
                );
            }
          }
        }
        if (t.body.length === 0) {
          t.body = [t.attrs!.href];
        }
      }
    });
  }
  return renderHtml(htmlTree);
}