import type { Mention } from './mentions';

export type CodeSegment =
  | { kind: 'text'; text: string }
  | { kind: 'inline'; text: string }
  | { kind: 'block'; text: string; lang: string | null };

const FENCE = /```([^\s`]*)[^\S\n]*\n?([\s\S]*?)\n?```/g;
const INLINE = /`([^`\n]+)`/g;

function splitInline(text: string, out: CodeSegment[]): void {
  let last = 0;
  for (const m of text.matchAll(INLINE)) {
    if (m.index > last) out.push({ kind: 'text', text: text.slice(last, m.index) });
    out.push({ kind: 'inline', text: m[1] });
    last = m.index + m[0].length;
  }
  if (last < text.length) out.push({ kind: 'text', text: text.slice(last) });
}

export function parseCode(text: string): CodeSegment[] {
  const out: CodeSegment[] = [];
  if (!text.includes('`')) {
    if (text) out.push({ kind: 'text', text });
    return out;
  }
  let last = 0;
  for (const m of text.matchAll(FENCE)) {
    splitInline(text.slice(last, m.index), out);
    out.push({ kind: 'block', text: m[2], lang: m[1] || null });
    last = m.index + m[0].length;
    if (text[last] === '\n') last += 1;
  }
  splitInline(text.slice(last), out);
  return out;
}

export function hasCode(segments: CodeSegment[]): boolean {
  return segments.some((s) => s.kind !== 'text');
}

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 textToHtml(text: string, pattern: RegExp | null, byText: Map<string, string>): string {
  if (!pattern) return escapeHtml(text).replace(/\n/g, '<br>');
  let out = '';
  let last = 0;
  for (const m of text.matchAll(pattern)) {
    out += escapeHtml(text.slice(last, m.index)).replace(/\n/g, '<br>');
    const userId = byText.get(m[0]) ?? m[0];
    out += `<a href="https://matrix.to/#/${escapeHtml(userId)}">${escapeHtml(m[0])}</a>`;
    last = m.index + m[0].length;
  }
  return out + escapeHtml(text.slice(last)).replace(/\n/g, '<br>');
}

/** Matrix `formatted_body` for a plain body, or null when it has no code or mentions to format. */
export function codeToHtml(body: string, mentions: Mention[] = []): string | null {
  const segments = parseCode(body);
  const byText = new Map(mentions.map((m) => [m.text, m.user_id]));
  const texts = [...byText.keys()].sort((a, b) => b.length - a.length);
  const pattern = texts.length > 0 ? new RegExp(texts.map(escapeRegExp).join('|'), 'g') : null;
  const linked = pattern !== null && segments.some((s) => s.kind === 'text' && new RegExp(pattern.source).test(s.text));
  if (!hasCode(segments) && !linked) return null;
  return segments
    .map((s) => {
      if (s.kind === 'inline') return `<code>${escapeHtml(s.text)}</code>`;
      if (s.kind === 'block') {
        const cls = s.lang ? ` class="language-${escapeHtml(s.lang)}"` : '';
        return `<pre><code${cls}>${escapeHtml(s.text)}</code></pre>`;
      }
      return textToHtml(s.text, pattern, byText);
    })
    .join('');
}
