//TODO: consider eliminating the TextCard struct
//TODO: add some kind of "update" command to support pulling new oracle data
//TODO: implement oracleFileName as a cli arg
//TODO: add more robust tests
//TODO: support other list formats
//TODO: support multiple copies of cards
const std = @import("std");

// https://github.com/Hejsil/zig-clap
const clap = @import("clap");

// https://codeberg.org/dude_the_builder/zg
const DisplayWidth = @import("DisplayWidth");

// https://github.com/AndreRenaud/PDFGen
// including the header file only works for a full build, "zig run" can't find it
const c = @cImport({
    @cInclude("pdfgen.h");
});
const print = std.debug.print;
const io = std.io;
const fs = std.fs;
const json = std.json;
const cwd = fs.cwd();
const indexOf = std.mem.indexOf;
const expect = std.testing.expect;
const assert = std.debug.assert;

const Card = struct {
    name: []const u8 = "",          //string
    mana_cost: []const u8 = "",     //string
    cmc: f32 = 0,                   //float
    type_line: []const u8 = "",     //string
    oracle_text: []const u8 = "",   //string
    power: ?[]const u8 = null,      //coerced to string
    toughness: ?[]const u8 = null,  //coerced to string
    card_faces: ?[]Card = null,     //array of cards
    loyalty: ?[]const u8 = null,    //coerced to string
    isFace: bool = undefined,        //cheeky little property that I added
};

const TextCard = struct {
    lines: [][]const u8 = undefined,
};

const CardSortContext = struct {
    const Self = @This();
    list: []TextCard,
    pub fn lessThan(ctx: Self, a: usize, b: usize) bool {
        return ctx.list[a].lines.len < ctx.list[b].lines.len;
    }
};

//dimension constants and defaults
const cardWidth = 30;
const cardHeight = 32;
const minCardHeight = 5;
const pageHeight = 66;      
const pdfHeight = c.PDF_LETTER_HEIGHT;
const pdfWidth = c.PDF_LETTER_WIDTH;

const formatString = "{s: <" ++ std.fmt.digits2(cardWidth) ++ "}";
const lineFormatter = "|" ++ formatString;
const spacer: []const u8 = "|" ++ (" " ** cardWidth);
const fullWidthSpacer = (spacer ** 3) ++ "\n";
const pageWidth = fullWidthSpacer.len;

//values that may vary depending on user-supplied arguments
var oracleFileName = "oracle-data/oracle-cards-20240701090158.json";
var listFileName: []const u8 = undefined;   //the name of the file with the list of cards to print
var constantHeight = true;                   //whether or not cards must have a constant height

test "Check constants" {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const paddedLine = try std.fmt.allocPrint(gpa.allocator(), lineFormatter, .{"hello> woooooooooooooooooooorl"});
    try expect(paddedLine.len == spacer.len);
}

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    const params = comptime clap.parseParamsComptime(
        \\-h, --help            Display this help and exit.
        \\-l, --listFile <str>  The list of cards to print       
        \\-c, --constant <str>  "true" or "false" (case-sensitive, whether or not the height of cards may vary)
    );

    const res = clap.parse(clap.Help, &params, clap.parsers.default, .{
        .allocator = allocator,
    }) catch |err| {
        return err;
    };
    if(res.args.listFile) |name| {
        listFileName = name;
    } else {
      return error.ExpectedArgument;
    }
    if(res.args.constant) |choice| {
        constantHeight = std.mem.eql(u8, choice, "true");
    }

    const oracleFile = try cwd.openFile(oracleFileName, .{});
    var jsonReader = json.reader(allocator, oracleFile.reader());
    const parsedJson = try json.parseFromTokenSource([]Card, allocator, &jsonReader, .{ .ignore_unknown_fields = true });

    var cardNames = std.BufSet.init(allocator);
    const listText = try cwd.readFileAlloc(allocator, listFileName, 1024 * 100);
    var listLines = std.mem.splitAny(u8, listText, "\n");
    while (listLines.next()) |line| {
        if (line.len < 5) break;
        const cardName = line[indexOf(u8, line, " ").? + 1 .. indexOf(u8, line, "(").? - 1];
        try cardNames.insert(cardName);
    }

    var cards = std.StringArrayHashMap(TextCard).init(allocator);
    for (parsedJson.value) |*cardObj| {
            if (cardNames.contains(cardObj.name)) {
                cardObj.isFace = false;
                const printableCard = try card(cardObj.*, allocator);
                try cards.put(cardObj.name, printableCard);
            }
    }

    cards.sort(CardSortContext{ .list = cards.values() });
    var allPrinted = std.ArrayList([]const u8).init(allocator);
    const pdf_doc: *c.pdf_doc = @ptrCast(c.pdf_create(pdfWidth, pdfHeight, &c.pdf_info{ .creator = ("My Software" ++ " " ** 53).* }).?);

    var rowToPrint = std.ArrayList(TextCard).init(allocator);
    for (cards.values()) |cardText| {
        try rowToPrint.append(cardText);
        if (rowToPrint.items.len >= 3) {
            try cardRow.print(allocator, rowToPrint.items, &allPrinted);
            rowToPrint.clearAndFree();
        }
    } else {
        try cardRow.print(allocator, rowToPrint.items, &allPrinted);
        for (allPrinted.items) |printLine| {
            print("{s}", .{printLine});
        }
        rowToPrint.clearAndFree();
    }
    var page = c.pdf_append_page(pdf_doc);
    _ = c.pdf_set_font(pdf_doc, "Courier");
    for (allPrinted.items, 1..) |text, rowNum| {
        const pageRelative = rowNum % pageHeight;
        const pageOffset: f32 = 12 * @as(f32, @floatFromInt(pageRelative));
        if (pageRelative == 0) {
            page = c.pdf_append_page(pdf_doc);
        }
        _ = c.pdf_add_text(pdf_doc, page, try std.mem.Allocator.dupeZ(allocator, u8, text), 8, 10, pdfHeight - pageOffset, c.PDF_BLACK);
    }

    _ = c.pdf_save(pdf_doc, "output.pdf");
    _ = c.pdf_destroy(pdf_doc);
}

fn card(
    cardObj: Card,
    allocator: std.mem.Allocator,
) !TextCard {
    var cardText = std.ArrayList([]const u8).init(allocator);

    var fullUnformattedText = std.ArrayList(u8).init(allocator);

    if (cardObj.card_faces == null or cardObj.isFace) {
        try fullUnformattedText.appendSlice(try std.mem.concat(allocator, u8, &[_][]const u8{
            cardObj.name,
            if (cardObj.mana_cost.len > 0) " " else "",
            cardObj.mana_cost,
            " (",
            cardObj.type_line,
            ") >> ",
            cardObj.oracle_text,
            if (cardObj.power) |_| " (" else "",
            cardObj.power orelse "",
            if (cardObj.power) |_| "/" else "",
            cardObj.toughness orelse "",
            if (cardObj.power) |_| ") " else "",
            if(cardObj.loyalty) |_| "[" else "",
            cardObj.loyalty orelse "",
            if(cardObj.loyalty) |_| "]" else "",
        }));

    }

    if (cardObj.card_faces) |faces| {
        for (faces, 0..) |*face, idx| {
            face.isFace = true;
            const faceText = (try card(face.*, allocator)).lines;
            try fullUnformattedText.appendSlice(std.mem.trim(u8, try std.mem.join(allocator, " ", faceText), "\n"));
            if (idx == 0) try fullUnformattedText.appendSlice("// ");
        }
    }

    var line = std.ArrayList(u8).init(allocator);
    var wordIterator = std.mem.splitAny(u8, fullUnformattedText.items, "\n ");
    while (wordIterator.next()) |word| {
        if (line.items.len + word.len + 1 >= cardWidth) {
            try cardText.append(try line.toOwnedSlice());
            line.clearAndFree();
        }
        try line.appendSlice(word);
        try line.append(' ');
        assert(line.items.len < 30);
    } else {
        try cardText.append(try line.toOwnedSlice());
    }
    if(!cardObj.isFace and constantHeight) {
        assert(cardText.items.len <= cardHeight);
        try cardText.appendNTimes(" " ** (cardWidth - 2), cardHeight - cardText.items.len);
    } else if(!cardObj.isFace) {
        try cardText.append(" " ** (cardWidth - 2));
    }
    assert(cardText.items.len <= cardHeight);
    return TextCard{ .lines = try cardText.toOwnedSlice() };
}

const linesList = std.MultiArrayList(cardRow);
const cardRow = struct {
    first: []const u8 = spacer,
    second: []const u8 = spacer,
    third: []const u8 = spacer,
    last: []const u8 = "\n",
    fn print(allocator: std.mem.Allocator, cards: []TextCard, allPrinted: *std.ArrayList([]const u8)) !void {
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};

        var lines = linesList{};
        defer lines.deinit(gpa.allocator());
        const dwd = try DisplayWidth.DisplayWidthData.init(allocator);
        const dw = DisplayWidth{ .data = &dwd };

        for (cards, 0..) |cardObj, cardNo| {
            const cardText = cardObj.lines;
            for (cardText, 0..) |line, idx| {
                const strippedLine = std.mem.trimRight(u8, line, " ");
                const paddedLine = try std.fmt.allocPrint(gpa.allocator(), lineFormatter, .{strippedLine});
                assert(dw.strWidth(paddedLine) == dw.strWidth(spacer));
                const placeholder = if (idx < lines.items(.first).len) lines.get(idx) else cardRow{};
                const new: cardRow = switch (cardNo) {
                    0 => .{ .first = paddedLine },
                    1 => .{ .first = placeholder.first, .second = paddedLine },
                    2 => .{ .first = placeholder.first, .second = placeholder.second, .third = paddedLine },
                    else => unreachable,
                };
                if (idx < lines.items(.first).len) {
                    lines.set(idx, new);
                } else {
                    try lines.append(gpa.allocator(), new);
                }
            }
        }
        const rowHeight = lines.items(.first).len;
        while (pageHeight - (allPrinted.items.len % pageHeight) <= rowHeight) {
            assert(rowHeight <= pageHeight);
            try allPrinted.append(fullWidthSpacer);
        }
        for (lines.items(.first), 0..) |_, idx| {
            const line = lines.get(idx);
            const fullLine = try std.mem.concat(gpa.allocator(), u8, &[_][]const u8{ line.first, line.second, line.third, line.last });
            assert(dw.strWidth(fullLine) == dw.strWidth(fullWidthSpacer));
            try allPrinted.append(fullLine);
        }
    }
};

