import { Marked, type Token, type Tokens } from 'marked';
import type { Emote } from './emotes';
import type { Mention } from './mentions';

function escapeHtml(text: string): string {
  return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function escapeRegExp(text: string): string {
  return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function mentionLinker(mentions: Mention[]): (escaped: string) => string {
  const byText = new Map(mentions.filter((m) => m.text).map((m) => [escapeHtml(m.text), m.user_id]));
  if (byText.size === 0) return (escaped) => escaped;
  const names = [...byText.keys()].sort((a, b) => b.length - a.length);
  const pattern = new RegExp(names.map(escapeRegExp).join('|'), 'g');
  return (escaped) =>
    escaped.replace(pattern, (name) => `<a href="https://matrix.to/#/${escapeHtml(byText.get(name)!)}">${name}</a>`);
}

function emoteLinker(emotes: Emote[]): (escaped: string) => string {
  if (emotes.length === 0) return (escaped) => escaped;
  const byCode = new Map(emotes.map((e) => [e.shortcode, e.mxc]));
  return (escaped) =>
    escaped.replace(/:([\w+-]+):/g, (whole, code: string) => {
      const mxc = byCode.get(code);
      return mxc ? `<img data-mx-emoticon src="${escapeHtml(mxc)}" alt="${whole}" title="${whole}" height="32">` : whole;
    });
}

function textToHtml(text: string, link: (escaped: string) => string): string {
  return link(escapeHtml(text)).replace(/\n/g, '<br>');
}

const PLAIN_INLINE = new Set(['text', 'escape', 'br']);

function isPlain(tokens: Token[]): boolean {
  const blocks = tokens.filter((t) => t.type !== 'space');
  if (blocks.length !== 1 || blocks[0].type !== 'paragraph') return blocks.length === 0;
  return (blocks[0] as Tokens.Paragraph).tokens.every((t) => PLAIN_INLINE.has(t.type) && !('tokens' in t && t.tokens));
}

function markdownToHtml(body: string, link: (escaped: string) => string): string | null {
  const marked = new Marked({ gfm: true, breaks: true });
  marked.use({
    renderer: {
      text(token) {
        if ('tokens' in token && token.tokens) return false;
        return link('escaped' in token && token.escaped ? token.text : escapeHtml(token.text));
      },
      html(token) {
        return escapeHtml(token.text);
      },
      image(token) {
        return `<a href="${escapeHtml(token.href)}">${escapeHtml(token.text || token.href)}</a>`;
      },
      checkbox(token) {
        return token.checked ? '[x] ' : '[ ] ';
      }
    }
  });
  const tokens = marked.lexer(body);
  if (isPlain(tokens)) return null;
  return marked.parser(tokens).trim();
}

/** Matrix `formatted_body` for a composed message, or null when plain text says it all. */
export function formatBody(body: string, mentions: Mention[], markdown: boolean, emotes: Emote[] = []): string | null {
  const mention = mentionLinker(mentions);
  const emote = emoteLinker(emotes);
  const link = (escaped: string) => emote(mention(escaped));
  const html = markdown ? markdownToHtml(body, link) : null;
  if (html !== null) return html;
  const plain = textToHtml(body, link);
  return plain === escapeHtml(body).replace(/\n/g, '<br>') ? null : plain;
}

export function insideOpenFence(text: string, caret: number): boolean {
  return (text.slice(0, caret).match(/^\s*```/gm)?.length ?? 0) % 2 === 1;
}
