//TODO: re-implement sorting
//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");
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, //technically a float? but I think we can always cast safely cast. EDIT: NOPE
    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,
};


//dimension constants and defaults
const cardWidth = 30;
const cardHeight = 32;
const minCardHeight = 5;
const pageHeight = 66;
var heightMayVary = false;

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

    //TODO: properly handle program arguments
    _ = args.next(); //handle program name (args[0])
    const listFileName: []const u8 = args.next() orelse {
        return error.ExpectedArgument;
    };
    heightMayVary = stringToBool(args.next());

    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();
    const pdf_doc: *c.pdf_doc = @ptrCast(c.pdf_create(c.PDF_A4_WIDTH, c.PDF_A4_HEIGHT, &c.pdf_info{.creator = ("My Software" ++ " " ** 53).* }).?);
    _ = c.pdf_set_font(pdf_doc, "Times-Roman");
    const page = c.pdf_append_page(pdf_doc);
    _ = c.pdf_add_text(pdf_doc, page, "This is text", 12, 10, 100, c.PDF_BLACK);
    // _ = c.pdf_add_line(pdf_doc, null, 50, 24, 150, 24, 3, c.PDF_BLACK);
    _ = c.pdf_save(pdf_doc, "output.pdf");
    _ = c.pdf_destroy(pdf_doc);

    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 listReader = (try cwd.openFile(listFileName, .{})).reader();
    var line = std.ArrayList(u8).init(allocator);
    while (listReader.streamUntilDelimiter(line.writer(), '\n', null)) {
        defer line.clearRetainingCapacity();
        const cardName = line.items[indexOf(u8, line.items, " ").? + 1 .. indexOf(u8, line.items, "(").? - 1];
        try cardNames.append(try allocator.dupe(u8, cardName));
    } else |err| switch (err) {
        error.EndOfStream => {},
        else => return err,
    }

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

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

    var rowToPrint = std.ArrayList(TextCard).init(allocator);
    // var cardIterator = cards.valueIterator();
    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);
        std.debug.print("{s}", .{allPrinted.items});
        rowToPrint.clearAndFree();
    }
}

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

    for (fullUnformattedText.items) |char| {
        try switch (char) {
            '\n', ' ' => addWord(&word, &line, &cardText),
            else => if (std.ascii.isASCII(char)) {
                try word.append(char);
            },
        };
    } else {
        try addWord(&word, &line, &cardText);
        const lineToBeAdded = std.mem.trimRight(u8, try line.toOwnedSlice(), " ");
        // const lineToBeAdded = try line.toOwnedSlice();
        try cardText.append(lineToBeAdded);
        line.clearAndFree();
    }
    while (wrongCardHeight(cardText.items.len)) {
        try cardText.append(" " ** (cardWidth - 2));
    }
    try cardText.append(" " ** (cardWidth - 2));
    // return cardText.items;
    return TextCard{ .lines = try cardText.toOwnedSlice() };
}

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

fn addWord(word: *std.ArrayList(u8), line: *std.ArrayList(u8), cardText: *std.ArrayList([]const u8)) !void {
    if (line.items.len + word.items.len >= (cardWidth)) {
        const lineToBeAdded = std.mem.trimRight(u8, try line.toOwnedSlice(), " ");
        assert(lineToBeAdded.len < 30);
        try cardText.append(lineToBeAdded);
        line.clearAndFree();
    }
    try line.appendSlice(word.items);
    try line.append(' ');
    word.clearAndFree();
}
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(u8)) !void {
        _ = allocator;
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};

        var lines = linesList{};
        defer lines.deinit(gpa.allocator());

        for (cards, 0..) |cardObj, cardNo| {
            //const cardText = try card(cardObj);
            const cardText = cardObj.lines;
            for (cardText, 0..) |line, idx| {
                //this step is probably unnecessary
                const strippedLine = std.mem.trimRight(u8, line, " ");
                const paddedLine = try std.fmt.allocPrint(gpa.allocator(), lineFormatter, .{strippedLine});
                const theoreticalLength = (paddedLine.len * 3) + 1;
                assert(paddedLine.len == spacer.len);
                assert(theoreticalLength == pageWidth);
                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 (remainingPage(allPrinted.items.len) <= rowHeight) {
            try allPrinted.appendSlice(fullWidthSpacer);
        }
        for (lines.items(.first), 0..) |_, idx| {
            const line = lines.get(idx);
            const printedWidth = line.first.len + line.second.len + line.third.len + line.last.len;
            assert(printedWidth == pageWidth);
            const fullLine = try std.mem.concat(gpa.allocator(), u8, &[_][]const u8{ line.first, line.second, line.third, line.last });
            try allPrinted.appendSlice(fullLine);
        }
    }
};

test "Remaining page length" {
    try expect(remainingPage(67) == 0);
    try expect(remainingPage(1) == 0);
}
fn remainingPage(length: usize) usize {
    assert(length % pageWidth == 0);
    return pageHeight - ((length / pageWidth) % pageHeight);
}

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