//c library usage based on https://medium.com/@eddo2626/lets-learn-zig-4-using-c-libraries-in-zig-5fcc3206f0dc
const std = @import("std");
const clap = @import("clap");

//using the zg lib: https://codeberg.org/dude_the_builder/zg
const DisplayWidth = @import("DisplayWidth");
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 = "",         //coerced to string
    toughness: []const u8 = "",     //coerced to string
    card_faces: ?[]Card = null,     //array of cards
};

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;      
var heightMayVary = true;   //whether or not cards must have a constant height

const formatString = "{s: <" ++ std.fmt.digits2(cardWidth) ++ "}";
const lineFormatter = "|" ++ formatString;
const spacer: []const u8 = "|" ++ (" " ** cardWidth);
const oracleFileName = "/home/lumenk/Documents/code/zig/proxy-print/src/oracle-cards-20240701090158.json"; //"oracle-cards-20240205220208.js";
const fullWidthSpacer = (spacer ** 3) ++ "\n";
const pageWidth = fullWidthSpacer.len;

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 args = try std.process.argsWithAllocator(std.heap.page_allocator);

    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       
        \\
    );

    const res = clap.parse(clap.Help, &params, clap.parsers.default, .{
        .allocator = allocator,
    }) catch |err| {
        return err;
    };
    if(res.args.listFile == null) return error.ExpectedArgument;
    const listFileName = res.args.listFile.?;

    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.ArrayList([]const u8).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.append(try allocator.dupe(u8, cardName));
    }

    var cards = std.StringArrayHashMap(TextCard).init(allocator);
    for (parsedJson.value) |cardObj| {
        for (cardNames.items, 0..) |cardName, i| {
            if (std.mem.eql(u8, cardName, cardObj.name)) {
                const printableCard = try card(cardObj, allocator, false);
                try cards.put(cardObj.name, printableCard);
                _ = cardNames.orderedRemove(i);
            }
        }
    }

    cards.sort(CardSortContext{ .list = cards.values() });
    var allPrinted = std.ArrayList([]const u8).init(allocator);
    const pdfHeight = c.PDF_LETTER_HEIGHT;
    const pdfWidth = c.PDF_LETTER_WIDTH;
    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});
        }
        // std.debug.print("{s}", .{allPrinted.items});
        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) {
            _ = c.pdf_add_text(pdf_doc, page, try std.mem.Allocator.dupeZ(allocator, u8, text), 8, 10, pdfHeight - pageOffset, c.PDF_BLACK);
        } else {
            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,
    isFace: bool,
) !TextCard {
    var cardText = std.ArrayList([]const u8).init(allocator);

    var fullUnformattedText = std.ArrayList(u8).init(allocator);
    if (!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,
            ") >> ",
        }));
    }
    try fullUnformattedText.appendSlice(try std.mem.concat(allocator, u8, &[_][]const u8{
        cardObj.oracle_text,
        if (cardObj.power.len > 0) " (" else "",
        cardObj.power,
        if (cardObj.power.len > 0) "/" else "",
        cardObj.toughness,
        if (cardObj.power.len > 0) ") " else "",
    }));
    if (cardObj.card_faces) |faces| {
        for (faces, 0..) |face, idx| {
            const faceText = (try card(face, allocator, true)).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 word = 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 line.appendSlice(word);
            try line.append(' ');
            assert(line.items.len < 30);
        } else {
            try cardText.append(try line.toOwnedSlice());
            line.clearAndFree();
            try line.appendSlice(word);
            try line.append(' ');
        }
    } else {
        try cardText.append(try line.toOwnedSlice());
    }

    while (wrongCardHeight(cardText.items.len)) {
        try cardText.append(" " ** (cardWidth - 2));
    }
    try cardText.append(" " ** (cardWidth - 2));
    return TextCard{ .lines = try cardText.toOwnedSlice() };
}

fn wrongCardHeight(length: usize) bool {
    return (!heightMayVary and length < cardHeight) or length < minCardHeight;
}

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) {
            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);
        }
    }
};

fn stringToBool(str: ?[]const u8) bool {
    return std.mem.eql(u8, (str orelse "false"), "true");
}
