// retoor <retoor@molodetz.nl>

import Highlighter from "./highlighter.js";

const SAFE_HREF = /^(https?:\/\/|\/|#|mailto:)/i;

export default class Markdown {
  constructor() {
    this.highlighter = new Highlighter();
    this.baseUrl = "";
  }

  escape(text) {
    return String(text)
      .split(String.fromCharCode(0))
      .join("")
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .split(String.fromCharCode(34))
      .join("&quot;");
  }

  renderInline(text) {
    const tick = String.fromCharCode(96);
    const codeRe = new RegExp(tick + "([^" + tick + "]+)" + tick, "g");
    let out = this.escape(text);
    const tokens = [];
    const stash = (html) => {
      const key = String.fromCharCode(0) + tokens.length + String.fromCharCode(0);
      tokens.push(html);
      return key;
    };
    out = out.replace(codeRe, (_m, code) => stash(`<code class="md-code">${code}</code>`));
    const imgRe = new RegExp("!\\[([^\\]]*)\\]\\(([^)\\s]+)\\)", "g");
    out = out.replace(imgRe, (_m, alt, src) => stash(this._image(alt, src)));
    const linkRe = new RegExp("\\[([^\\]]+)\\]\\(([^)\\s]+)\\)", "g");
    out = out.replace(linkRe, (_m, label, href) => stash(this._link(label, href)));
    out = this._autolink(out, stash);
    out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
    out = out.replace(/__([^_]+)__/g, "<strong>$1</strong>");
    out = out.replace(/(^|[^*])\*([^*\s][^*]*?)\*/g, "$1<em>$2</em>");
    out = out.replace(/~~([^~]+)~~/g, "<del>$1</del>");
    out = out.replace(new RegExp(String.fromCharCode(0) + "(\\d+)" + String.fromCharCode(0), "g"), (_m, i) => tokens[Number(i)]);
    return out;
  }

  _resolveHref(href) {
    if (/^(https?:\/\/|mailto:|#)/i.test(href)) {
      return href;
    }
    if (href.startsWith("/") && !href.startsWith("//")) {
      const base = (this.baseUrl || (typeof globalThis !== "undefined" && globalThis.location ? globalThis.location.origin : "")).replace(/\/+$/, "");
      return base + href;
    }
    return null;
  }

  _autolink(text, stash) {
    const NUL = String.fromCharCode(0);
    const absRe = new RegExp("(^|[\\s(])(https?:\\/\\/[^\\s<)" + NUL + "]+)", "g");
    text = text.replace(absRe, (_m, pre, url) => pre + this._autoAnchor(url, stash));
    const relRe = new RegExp("(^|[\\s(])(\\/[A-Za-z0-9._~\\-][A-Za-z0-9._~\\-\\/%?=&;#:+@]*)", "g");
    text = text.replace(relRe, (_m, pre, url) => pre + this._autoAnchor(url, stash));
    return text;
  }

  _autoAnchor(rawUrl, stash) {
    let url = rawUrl;
    let trail = "";
    const match = url.match(/[.,;:!?)\]}'"]+$/);
    if (match) {
      trail = match[0];
      url = url.slice(0, url.length - trail.length);
    }
    const href = url ? this._resolveHref(url) : null;
    if (!href) {
      return rawUrl;
    }
    return stash(`<a href="${href}" target="_blank" rel="noopener noreferrer">${url}</a>`) + trail;
  }

  _link(label, href) {
    const resolved = this._resolveHref(href);
    if (!resolved || !SAFE_HREF.test(resolved)) {
      return this.escape(label);
    }
    return `<a href="${resolved}" target="_blank" rel="noopener noreferrer">${label}</a>`;
  }

  _image(alt, src) {
    if (!SAFE_HREF.test(src)) {
      return this.escape(alt);
    }
    return `<img class="md-img" src="${src}" alt="${this.escape(alt)}" loading="lazy">`;
  }

  _isTableSeparator(line) {
    if (!line || line.indexOf("-") === -1 || line.indexOf("|") === -1) {
      return false;
    }
    const cells = this._tableCells(line);
    return cells.length > 0 && cells.every((cell) => /^:?-+:?$/.test(cell));
  }

  _tableCells(line) {
    let body = line.trim();
    if (body.startsWith("|")) body = body.slice(1);
    if (body.endsWith("|")) body = body.slice(0, -1);
    return body.split("|").map((cell) => cell.trim());
  }

  _tableAligns(line) {
    return this._tableCells(line).map((spec) => {
      const left = spec.startsWith(":");
      const right = spec.endsWith(":");
      if (left && right) return "center";
      if (right) return "right";
      if (left) return "left";
      return "";
    });
  }

  _table(header, aligns, rows) {
    const style = (index) => (aligns[index] ? ` style="text-align:${aligns[index]}"` : "");
    const head = header.map((cell, i) => `<th${style(i)}>${this.renderInline(cell)}</th>`).join("");
    const body = rows
      .map((row) => "<tr>" + header.map((_c, i) => `<td${style(i)}>${this.renderInline(row[i] || "")}</td>`).join("") + "</tr>")
      .join("");
    return `<table class="md-table"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`;
  }

  _codeBlock(lines, lang) {
    const body = this.highlighter.highlight(lines.join("\n"));
    const label = lang ? ` data-lang="${this.escape(lang)}"` : "";
    return (
      '<div class="md-codewrap">' +
      '<button class="md-copy" type="button" title="Copy" aria-label="Copy code">Copy</button>' +
      `<pre class="md-pre"${label}><code>${body}</code></pre>` +
      "</div>"
    );
  }

  render(text) {
    const FENCE = String.fromCharCode(96).repeat(3);
    const lines = String(text || "").split("\n");
    const blocks = [];
    let paragraph = [];
    let list = null;

    const flushParagraph = () => {
      if (paragraph.length) {
        blocks.push(`<p>${paragraph.map((l) => this.renderInline(l)).join("<br>")}</p>`);
        paragraph = [];
      }
    };
    const flushList = () => {
      if (list) {
        blocks.push(`<${list.tag}>${list.items.join("")}</${list.tag}>`);
        list = null;
      }
    };

    for (let i = 0; i < lines.length; i += 1) {
      const line = lines[i];

      if (line.startsWith(FENCE)) {
        flushParagraph();
        flushList();
        const lang = line.slice(FENCE.length).trim();
        const buffer = [];
        i += 1;
        while (i < lines.length && !lines[i].startsWith(FENCE)) {
          buffer.push(lines[i]);
          i += 1;
        }
        blocks.push(this._codeBlock(buffer, lang));
        continue;
      }

      if (!line.trim()) {
        flushParagraph();
        flushList();
        continue;
      }

      if (line.indexOf("|") !== -1 && i + 1 < lines.length && this._isTableSeparator(lines[i + 1])) {
        flushParagraph();
        flushList();
        const header = this._tableCells(line);
        const aligns = this._tableAligns(lines[i + 1]);
        i += 2;
        const rows = [];
        while (i < lines.length && lines[i].trim() && lines[i].indexOf("|") !== -1) {
          rows.push(this._tableCells(lines[i]));
          i += 1;
        }
        i -= 1;
        blocks.push(this._table(header, aligns, rows));
        continue;
      }

      const heading = line.match(/^(#{1,6})\s+(.*)$/);
      if (heading) {
        flushParagraph();
        flushList();
        const level = heading[1].length;
        blocks.push(`<h${level}>${this.renderInline(heading[2])}</h${level}>`);
        continue;
      }

      if (/^([-*_])\1{2,}$/.test(line.trim())) {
        flushParagraph();
        flushList();
        blocks.push("<hr>");
        continue;
      }

      if (/^>\s?/.test(line)) {
        flushParagraph();
        flushList();
        blocks.push(`<blockquote>${this.renderInline(line.replace(/^>\s?/, ""))}</blockquote>`);
        continue;
      }

      const ordered = line.match(/^\s*(\d+)\.\s+(.*)$/);
      const unordered = line.match(/^\s*[-*+]\s+(.*)$/);
      if (ordered || unordered) {
        flushParagraph();
        const tag = ordered ? "ol" : "ul";
        const content = ordered ? ordered[2] : unordered[1];
        if (!list || list.tag !== tag) {
          flushList();
          list = { tag, items: [] };
        }
        list.items.push(`<li>${this.renderInline(content)}</li>`);
        continue;
      }

      flushList();
      paragraph.push(line);
    }

    flushParagraph();
    flushList();
    return blocks.join("\n");
  }
}
