const std = @import("std");
const p2 = @import("p2");

const PackedFileEntry = @import("PackedFileEntry.zig");

const packerfs = @import("packerfs.zig");
pub const PackerFS = packerfs.PackerFS;

pub const PackerBytesMapping = packerfs.PackerBytesMapping;

const constants = @import("constants.zig");
pub const PackerMagic = constants.PackerMagic;
pub const littleEndian = constants.littleEndian;

// this is invalidated if a PackedArhive is modified
pub const PackerBytesRef = struct {
    typeTag: []const u8,
    raw_bytes: []const u8,
};

const PackedArchiveContents = std.ArrayListAlignedUnmanaged(u8, 8);

// only responsible for reading and writing archives from disk
// not intended to be the end API
pub const PackedArchive = struct {
    allocator: std.mem.Allocator,
    contents: *PackedArchiveContents,
    // file by convention is little-endian for u32s and u64s
    headerEntries: std.ArrayListUnmanaged(PackedFileEntry),
    entriesByString: std.StringHashMapUnmanaged(usize), // does not get serialized.
    contentBuffer: []const u8 = undefined,
    finished: bool = false,

    pub fn initEmpty(allocator: std.mem.Allocator) !@This() {
        const self: @This() = .{
            .contents = try allocator.create(PackedArchiveContents),
            .allocator = allocator,
            .headerEntries = .{},
            .entriesByString = .{},
        };
        self.contents.* = .{};

        return self;
    }

    pub fn appendFileByBytes(self: *@This(), fileType: []const u8, fileName: []const u8, bytes: []const u8) !void {
        try p2.assert(self.finished != true);
        const writer = self.contents.writer(self.allocator);
        const fileOffset = self.contents.items.len;

        try writer.writeAll(bytes);
        const bytesWritten = bytes.len;

        // if bytes written + fileOffset results in a non-64-bit aligned offset, then we shall pad with zeroes until we hit a 32 bit aligned offset
        const padding = 8 - ((bytesWritten + fileOffset) % 8);
        try writer.writeByteNTimes(0x0, padding);

        const newHeaderEntry = try PackedFileEntry.init(fileType, fileName, fileOffset, @intCast(bytes.len));
        try self.addHeader(newHeaderEntry);
    }

    // finish building, set externalContentData if this contents does NOT contain
    // the actual content data, such as in the case where the content is
    // sent over a network or if the content is embedded with @embedFile
    pub fn finishBuilding(self: *@This(), externalContentData: bool) !void {
        if (!externalContentData) {
            self.contentBuffer = self.contents.items;
        }

        for (self.headerEntries.items, 0..) |*entry, i| {
            try self.entriesByString.put(self.allocator, entry.getFileName(), i);
        }
        self.finished = true;
    }

    fn loadHeadersFromReader(self: *@This(), reader: anytype) !usize {
        var iterator = try PackedFileEntry.ReaderIterator.init(reader);
        while (try iterator.next(reader)) |entry| {
            try self.headerEntries.append(self.allocator, entry);
        }
        return iterator.bytesRead;
    }

    fn loadContentFromReader(self: *@This(), reader: anytype) !usize {
        const writer = self.contents.writer(self.allocator);
        var totalRead: usize = 0;

        var readBuffer: [1024]u8 = undefined;
        var bytesRead = try reader.read(&readBuffer);
        totalRead += bytesRead;
        _ = try writer.write((&readBuffer)[0..bytesRead]);

        while (bytesRead > 0) {
            bytesRead = try reader.read(&readBuffer);
            _ = try writer.write((&readBuffer)[0..bytesRead]);
            totalRead += bytesRead;
        }

        return totalRead;
    }

    fn setContentRefToOffset(self: *@This(), bytes: []const u8, offset: usize) void {
        self.contentBuffer = bytes[offset..];
    }

    pub fn loadFromBytes(self: *@This(), bytes: []const u8) !void {
        try p2.assert(self.finished != true);
        var reader = std.io.fixedBufferStream(bytes);
        const bytesRead = try self.loadHeadersFromReader(&reader);
        self.setContentRefToOffset(bytes, bytesRead);
        try self.finishBuilding(true);
    }

    pub fn loadFromFile(self: *@This(), filePath: []const u8) !void {
        try p2.assert(self.finished != true);
        // open file for reading
        const file = try std.fs.cwd().openFile(filePath, .{});
        defer file.close();

        const reader = file.reader();
        _ = try self.loadHeadersFromReader(reader);
        const contentBytes = try self.loadContentFromReader(reader);

        try self.finishBuilding(false);

        std.debug.print("total content bytes {d}\n", .{contentBytes});
    }

    pub fn writeToFile(self: @This(), filePath: []const u8) !void {

        // create file
        const file = try p2.createFileWithPath(filePath);
        defer file.close();

        const writer = file.writer();
        // write out pack magic and number of header elements to indicate start of file.
        try writer.writeAll(&@as([4]u8, @bitCast(PackerMagic)));
        try writer.writeAll(&@as([4]u8, @bitCast(
            @as(
                u32,
                @intCast(self.headerEntries.items.len),
            ),
        )));

        var headerSize: usize = 0;

        for (self.headerEntries.items) |entry| {
            headerSize += try entry.writeHeader(writer, @intCast(self.headerEntries.items.len));
        }

        std.debug.print("total header size = {d}\n", .{headerSize});

        // write out contents buffer,
        try writer.writeAll(self.contents.items);
    }

    pub fn addHeader(self: *@This(), fileEntry: PackedFileEntry) !void {
        try self.headerEntries.append(self.allocator, fileEntry);
    }

    pub fn updateHeaderOffset(self: @This(), offset: u64) void {
        const p = @as(*u64, @ptrCast(@alignCast(self.contents.items.ptr)));
        p.* = offset;
    }

    pub fn debugPrintAllFiles(self: @This()) void {
        for (self.headerEntries.items) |entry| {
            std.debug.print("> {s}({s}, size = {d} bytes)\n", .{
                entry.getFileName(),
                entry.getTypeName(),
                entry.fileLen,
            });
        }

        std.debug.print("totalContentSize = {d}\n", .{
            self.contents.items.len - 8,
        });
    }

    // only used for debugging or while generating a PackerFileSystem.
    //
    // using a PackerFileSystem is the better method of getting file contents
    pub fn getFileByName(self: *@This(), fileName: []const u8) ?PackerBytesRef {
        if (!self.finished) {
            return null;
        }

        if (self.entriesByString.get(fileName)) |entryOffset| {
            const header = &self.headerEntries.items[entryOffset];
            var bytes: []const u8 = undefined;

            bytes.ptr = @ptrCast(&self.contentBuffer[header.fileOffset]);
            bytes.len = header.fileLen;

            return .{
                .typeTag = header.getTypeName(),
                .raw_bytes = bytes,
            };
        }

        return null;
    }

    pub fn deinit(self: *@This()) void {
        self.contents.deinit(self.allocator);
        self.headerEntries.deinit(self.allocator);
        if (self.finished) {
            self.entriesByString.deinit(self.allocator);
        }
        self.allocator.destroy(self.contents);
    }
};

fn loadHeadersFromReader(self: *@This(), reader: anytype) !usize {
    var bytesRead: usize = 0;
    // read and verify magic
    var magic: [4]u8 align(4) = undefined;
    try p2.assert(try reader.read(&magic) == 4);
    try p2.assert(p2.arrayTo_u32(magic) == PackerMagic);
    std.debug.print("magic = {s}\n", .{magic});
    bytesRead += 4;

    // read header entries count
    var entriesCount_read: [4]u8 align(4) = undefined;
    try p2.assert(try reader.read(&entriesCount_read) == 4);
    const entriesCount = p2.arrayTo_u32(entriesCount_read);
    std.debug.print("entriesCount = {d}\n", .{entriesCount});
    bytesRead += 4;

    for (0..entriesCount) |_| {
        const newEntry = try self.headerEntries.addOne(self.allocator);
        try newEntry.loadFromReader(reader);
        bytesRead += PackedFileEntry.calculateHeaderLen();
    }

    return bytesRead;
}
