//! Tiny markdown -> HTML converter, used at build time to render docs into
//! the project's static site (web served by tangled). Deliberately minimal: it
//! supports exactly the constructs the README uses — headings, fenced code,
//! unordered lists, paragraphs, inline code, and links. Not a general engine.
//!
//! usage: md2html <input.md> <output.html>

const std = @import("std");
const Io = std.Io;
const Writer = Io.Writer;

const head =
    \\<!doctype html>
    \\<html lang="en">
    \\<head>
    \\<meta charset="utf-8">
    \\<meta name="viewport" content="width=device-width, initial-scale=1">
    \\<title>zigman guide</title>
    \\<meta name="description" content="Practical guide for using zigman in shell workflows and agent loops.">
    \\<link rel="stylesheet" href="site.css">
    \\<script defer src="nav.js"></script>
    \\</head>
    \\<body>
    \\<main>
    \\  <section class="window" aria-labelledby="title">
    \\    <nav class="bar" aria-label="Primary">
    \\      <a data-nav-item data-hotkey="h" data-nav-target="home" href="index.html">home</a>
    \\      <a data-nav-item data-hotkey="p" href="prompt.md">setup</a>
    \\      <a data-nav-item data-hotkey="l" data-nav-target="llms" href="llms.txt">llms</a>
    \\      <a data-nav-item data-hotkey="s" href="https://tangled.org/zzstoatzz.io/zigman">source</a>
    \\      <a data-nav-item data-hotkey="r" href="https://ziglang.org/documentation/master/">langref</a>
    \\    </nav>
    \\    <article class="doc">
    \\
;

const foot =
    \\    </article>
    \\  </section>
    \\</main>
    \\</body>
    \\</html>
    \\
;

pub fn main(init: std.process.Init) !void {
    const a = init.gpa;
    const io = init.io;

    var it = init.minimal.args.iterate();
    _ = it.skip();
    const in_path = it.next() orelse return error.MissingInput;
    const out_path = it.next() orelse return error.MissingOutput;

    const cwd = Io.Dir.cwd();
    const md = try cwd.readFileAlloc(io, in_path, a, .unlimited);
    defer a.free(md);

    var out: Writer.Allocating = .init(a);
    defer out.deinit();
    const w = &out.writer;

    try w.writeAll(head);
    try convert(a, w, md);
    try w.writeAll(foot);

    try cwd.writeFile(io, .{ .sub_path = out_path, .data = out.written() });
}

fn convert(a: std.mem.Allocator, w: *Writer, md: []const u8) !void {
    var lines = std.mem.splitScalar(u8, md, '\n');
    var in_list = false;
    var para: std.ArrayList(u8) = .empty;
    defer para.deinit(a);

    // close any open block (paragraph or list) before starting a new one
    const flush = struct {
        fn call(ww: *Writer, p: *std.ArrayList(u8), list: *bool) !void {
            if (p.items.len > 0) {
                try ww.writeAll("<p>");
                try inlineMd(ww, p.items);
                try ww.writeAll("</p>\n");
                p.clearRetainingCapacity();
            }
            if (list.*) {
                try ww.writeAll("</ul>\n");
                list.* = false;
            }
        }
    }.call;

    while (lines.next()) |line| {
        // pass raw <details>/<summary> block tags through untouched
        if (rawBlock(std.mem.trim(u8, line, " \t"))) {
            try flush(w, &para, &in_list);
            try w.writeAll(line);
            try w.writeByte('\n');
            continue;
        }

        if (std.mem.startsWith(u8, line, "```")) {
            try flush(w, &para, &in_list);
            try w.writeAll("<pre><code>");
            while (lines.next()) |code| {
                if (std.mem.startsWith(u8, code, "```")) break;
                try escape(w, code);
                try w.writeByte('\n');
            }
            try w.writeAll("</code></pre>\n");
            continue;
        }

        const heading = headingLevel(line);
        if (heading > 0) {
            try flush(w, &para, &in_list);
            try w.print("<h{d}>", .{heading});
            try inlineMd(w, std.mem.trimStart(u8, line[heading..], " "));
            try w.print("</h{d}>\n", .{heading});
            continue;
        }

        if (std.mem.startsWith(u8, line, "- ")) {
            if (para.items.len > 0) try flush(w, &para, &in_list);
            if (!in_list) {
                try w.writeAll("<ul>\n");
                in_list = true;
            }
            try w.writeAll("<li>");
            try inlineMd(w, line[2..]);
            try w.writeAll("</li>\n");
            continue;
        }

        if (std.mem.trim(u8, line, " \t").len == 0) {
            try flush(w, &para, &in_list);
            continue;
        }

        // plain prose line: accumulate into the current paragraph, joining
        // wrapped lines with a space
        if (in_list) try flush(w, &para, &in_list);
        if (para.items.len > 0) try para.append(a, ' ');
        try para.appendSlice(a, line);
    }
    try flush(w, &para, &in_list);
}

/// inline markdown: `code` spans and [text](url) links; everything else escaped.
fn inlineMd(w: *Writer, text: []const u8) !void {
    var i: usize = 0;
    while (i < text.len) {
        const c = text[i];
        if (c == '`') {
            if (std.mem.indexOfScalarPos(u8, text, i + 1, '`')) |end| {
                try w.writeAll("<code>");
                try escape(w, text[i + 1 .. end]);
                try w.writeAll("</code>");
                i = end + 1;
                continue;
            }
        } else if (c == '[') {
            if (parseLink(text[i..])) |lnk| {
                try w.writeAll("<a href=\"");
                try escape(w, lnk.url);
                try w.writeAll("\">");
                try escape(w, lnk.text);
                try w.writeAll("</a>");
                i += lnk.len;
                continue;
            }
        }
        try escapeByte(w, c);
        i += 1;
    }
}

const Link = struct { text: []const u8, url: []const u8, len: usize };

fn parseLink(s: []const u8) ?Link {
    const close = std.mem.indexOfScalar(u8, s, ']') orelse return null;
    if (close + 1 >= s.len or s[close + 1] != '(') return null;
    const paren = std.mem.indexOfScalarPos(u8, s, close + 2, ')') orelse return null;
    return .{ .text = s[1..close], .url = s[close + 2 .. paren], .len = paren + 1 };
}

/// lines that are a raw <details>/<summary> tag are emitted verbatim so the
/// README's collapsible sections render in both the repo view and the page.
fn rawBlock(line: []const u8) bool {
    for ([_][]const u8{ "<details", "</details", "<summary", "</summary" }) |tag| {
        if (std.mem.startsWith(u8, line, tag)) return true;
    }
    return false;
}

fn headingLevel(line: []const u8) u8 {
    var n: u8 = 0;
    while (n < line.len and line[n] == '#') n += 1;
    if (n > 0 and n < line.len and line[n] == ' ') return n;
    return 0;
}

fn escape(w: *Writer, s: []const u8) !void {
    for (s) |c| try escapeByte(w, c);
}

fn escapeByte(w: *Writer, c: u8) !void {
    switch (c) {
        '&' => try w.writeAll("&amp;"),
        '<' => try w.writeAll("&lt;"),
        '>' => try w.writeAll("&gt;"),
        '"' => try w.writeAll("&quot;"),
        else => try w.writeByte(c),
    }
}
