const std = @import("std");

const GitObject = struct {
    type: u3,
    data: []const u8,
};

fn getSize(buffer: []const u8, ignoreTypeBits: bool) struct { size: u64, bytelen: u64 } {
    var size: u64 = 0;
    var counter: u6 = 0;
    while (true) {
        if (counter == 0) {
            if (ignoreTypeBits) {
                const bits: u4 = @truncate(buffer[counter]);
                size = bits;
            } else {
                const bits: u7 = @truncate(buffer[counter]);
                size = bits;
            }
        } else {
            if (ignoreTypeBits) {
                const bits: u7 = @truncate(buffer[counter]);
                size += @as(u64, bits) << (7 * (counter - 1) + 4);
            } else {
                const bits: u7 = @truncate(buffer[counter]);
                size += @as(u64, bits) << (7 * (counter));
            }
        }

        if (buffer[counter] & 0b10000000 == 0) {
            break;
        }

        counter += 1;
    }

    const nBytes = counter + 1;

    return .{
        .size = size,
        .bytelen = nBytes,
    };
}

fn getOffset(buffer: []const u8) struct { offset: u64, bytelen: u64 } {
    var offset: u64 = 0;
    var counter: u4 = 0;
    while (true) {
        const bits: u7 = @truncate(buffer[counter]);
        offset <<= 7;
        offset += @as(u64, bits);

        if (buffer[counter] & 0b10000000 == 0) {
            break;
        }

        counter += 1;
    }

    const nBytes = counter + 1;

    if (nBytes >= 2) {
        for (1..nBytes) |i| {
            offset += std.math.pow(u64, 2, 7 * i);
        }
    }
    return .{
        .offset = offset,
        .bytelen = nBytes,
    };
}

fn decompress(alloc: std.mem.Allocator, inBuffer: []const u8, size: usize) ![]u8 {
    const outBuffer = try alloc.alloc(u8, size);
    errdefer alloc.free(outBuffer);

    var inFbs = std.io.fixedBufferStream(inBuffer);
    const reader = inFbs.reader();

    var outFbs = std.io.fixedBufferStream(outBuffer);
    const writer = outFbs.writer();

    try std.compress.zlib.decompress(reader, writer);

    return outBuffer;
}

fn applyDelta(alloc: std.mem.Allocator, baseData: []const u8, deltaData: []const u8) ![]const u8 {
    const baseObjectSize = getSize(deltaData, false);
    const resultObjectSize = getSize(deltaData[baseObjectSize.bytelen..deltaData.len], false);
    const deltaDataOffset = baseObjectSize.bytelen + resultObjectSize.bytelen;

    // std.debug.print("base: {}, result: {}\n", .{ baseObjectSize.size, resultObjectSize.size });

    const result = try alloc.alloc(u8, resultObjectSize.size);
    var resultCounter: u64 = 0;

    var counter: u64 = 0;
    while (true) {
        const b = deltaData[deltaDataOffset + counter];

        if (b & 0b10000000 != 0) {
            // if (b == 0b10010000) {
            //     counter += 1;
            // } else {
            var dataOffset: u64 = 0;
            var dataSize: u64 = 0;
            var bitsSet: u8 = 0;
            for (0..4) |i| { // offset bits
                if (b & (@as(u64, 1) << @min(3, i)) != 0) {
                    dataOffset += @as(u64, deltaData[deltaDataOffset + counter + 1 + bitsSet]) << @min(3 * 8, i * 8);
                    bitsSet += 1;
                }
            }
            for (4..7) |i| { // size bits
                if (b & (@as(u64, 1) << @min(6, i)) != 0) {
                    dataSize += @as(u64, deltaData[deltaDataOffset + counter + 1 + bitsSet]) << @min(6 * 8, (i - 4) * 8);
                    bitsSet += 1;
                }
            }
            counter += bitsSet;

            // std.debug.print("copying {} bytes of from {} data[{b:0>8}]: {s}\n", .{ dataSize, dataOffset, b, baseData[dataOffset .. dataOffset + dataSize] });

            std.mem.copyForwards(
                u8,
                result[resultCounter..result.len],
                baseData[dataOffset .. dataOffset + dataSize],
            );

            resultCounter += dataSize;
            // }
        } else {
            const dataSize: u7 = @truncate(b);
            // std.debug.print("pasting {} bytes: {s}\n", .{ dataSize, deltaData[deltaDataOffset + counter + 1 .. deltaDataOffset + counter + 1 + dataSize] });
            std.mem.copyForwards(
                u8,
                result[resultCounter..result.len],
                deltaData[deltaDataOffset + counter + 1 .. deltaDataOffset + counter + 1 + dataSize],
            );
            resultCounter += dataSize;
            counter += dataSize;
        }

        counter += 1;
        if (deltaDataOffset + counter >= deltaData.len)
            break;
    }

    alloc.free(deltaData);
    return result;
}

fn parseObject(alloc: std.mem.Allocator, objectType: u3, objectData: []const u8) !git.Object {
    if (objectType == 3) {
        return .{
            .data = objectData,
            .blob = .{
                .data = objectData[0..@min(objectData.len, 1000)],
            },
        };
    } else if (objectType == 2) {
        var treeEntries = git.Tree.init(alloc);

        var counter: u64 = 0;
        while (counter < objectData.len) {
            const modeLen = std.mem.indexOfScalar(u8, objectData[counter..objectData.len], ' ') orelse break;
            const mode = objectData[counter .. counter + modeLen];
            counter += modeLen + 1;

            var nameLen: u64 = 0;
            while (counter + nameLen < objectData.len and objectData[counter + nameLen] != 0) {
                nameLen += 1;
            }
            const name = objectData[counter .. counter + nameLen];
            counter += nameLen + 1;

            if (counter + 20 <= objectData.len) {
                const hash: u160 = std.mem.readVarInt(u160, objectData[counter .. counter + 20], .big);
                counter += 20;

                treeEntries.append(.{
                    .mode = alloc.dupe(u8, mode),
                    .name = alloc.dupe(u8, name),
                    .hash = hash,
                });
            }
        }
        return .{
            .tree = treeEntries,
        };
    } else {
        std.debug.print("type: {}\n{s}\n", .{ objectType, objectData });
    }
}

const PackFile = struct {
    alloc: std.mem.Allocator,
    version: i32,
    objectNames: std.ArrayList(u160),
    objectOffsets: std.ArrayList(u32),
    packBuffer: []const u8,
    idxBuffer: []const u8,
    // TODO:
    // - decompress
    // - applyDelta

    pub fn init(alloc: std.mem.Allocator, packBuffer: []const u8, idxBuffer: []const u8) !PackFile {
        var result: PackFile = undefined;
        result.alloc = alloc;
        result.packBuffer = packBuffer;
        result.idxBuffer = idxBuffer;

        result.version = std.mem.readInt(i32, idxBuffer[4..8], .big);

        // N-th entry of this table records the number of objects in the corresponding pack,
        // the first byte of whose object name is less than or equal to N.
        var fanoutTable: [256]u32 = undefined;
        result.objectNames = std.ArrayList(u160).init(alloc);
        // result.crc32s = std.ArrayList(u32).init(alloc);
        result.objectOffsets = std.ArrayList(u32).init(alloc);

        for (0..256) |i| {
            fanoutTable[i] = std.mem.readVarInt(u32, idxBuffer[8 + i * 4 .. 8 + i * 4 + 4], .big);
            const numObjects = if (i > 0) fanoutTable[i] - fanoutTable[i - 1] else fanoutTable[i];
            // print("{} objects starting with {x:02}\n", .{ numObjects, i });

            for (0..numObjects) |j| {
                const nameOffset = 4 + 4 + 4 * 256 + (j + if (i > 0) fanoutTable[i - 1] else 0) * 20;
                const objectName = idxBuffer[nameOffset .. nameOffset + 20];
                const objectNameInt = std.mem.readVarInt(u160, objectName, .big);
                // print("object name: {x}\n", .{objectNameInt});
                try result.objectNames.append(objectNameInt);
            }
        }

        for (0..result.objectNames.items.len) |i| {
            const offsetOffset = 4 + 4 + 4 * 256 + result.objectNames.items.len * (20 + 4) + i * 4;
            const offsetInt = std.mem.readVarInt(u32, idxBuffer[offsetOffset .. offsetOffset + 4], .big);

            try result.objectOffsets.append(offsetInt);
        }

        return result;
    }

    pub fn deinit(self: @This()) void {
        self.objectNames.deinit();
        self.objectOffsets.deinit();
    }

    pub fn findObjectOffset(self: @This(), name: u160) ?usize {
        for (0..self.objectNames.items.len) |i| {
            if (self.objectNames.items[i] == name)
                return self.objectOffsets.items[i];
        }
        return null;
    }

    pub fn getObject(self: @This(), alloc: std.mem.Allocator, index: u64) !git.Object {
        var objectType: u3 = @truncate(self.packBuffer[index] >> 4);
        const objectSize = getSize(self.packBuffer[index..self.packBuffer.len], true);

        // std.debug.print("getting object at index {}. type: {}, size: {}.\n", .{
        //     index,
        //     objectType,
        //     objectSize.size,
        // });

        var objectData: []const u8 = undefined;

        if (objectType == 6) {
            const offsetSize = getOffset(
                self.packBuffer[index + objectSize.bytelen .. self.packBuffer.len],
            );

            const baseIndex = index - offsetSize.offset;

            const baseObject = try self.getObject(alloc, baseIndex);
            defer self.alloc.free(baseObject.data);

            const deltaData = try decompress(
                self.alloc,
                self.packBuffer[index + objectSize.bytelen + offsetSize.bytelen .. self.packBuffer.len],
                objectSize.size,
            );
            objectData = try applyDelta(self.alloc, baseObject.data, deltaData);
            objectType = baseObject.type;
        } else if (objectType == 7) {
            const baseObjectName = self.packBuffer[index + objectSize.bytelen .. index + objectSize.bytelen + 20];
            const baseObjectNameInt = std.mem.readVarInt(u160, baseObjectName, .big);
            var baseObjectOffset: ?u64 = null;
            for (0..self.objectNames.items.len) |i| {
                if (baseObjectNameInt == self.objectNames.items[i]) {
                    baseObjectOffset = i;
                    break;
                }
            }
            if (baseObjectOffset) |offset| {
                const baseObject = try self.getObject(offset);
                defer self.alloc.free(baseObject.data);
                const deltaData = try decompress(
                    self.alloc,
                    self.packBuffer[index + objectSize.bytelen + 20 .. self.packBuffer.len],
                    objectSize.size,
                );
                objectData = try applyDelta(self.alloc, baseObject.data, deltaData);
                objectType = baseObject.type;
            } else {
                std.debug.print("object {x} not found\n", .{baseObjectNameInt});
            }
        } else {
            objectData = try decompress(
                self.alloc,
                self.packBuffer[index + objectSize.bytelen .. self.packBuffer.len],
                objectSize.size,
            );
        }

        return try parseObject(alloc, objectType, objectData);
    }
};

const git = struct {
    const Blob = struct {
        data: []u8,
    };
    const TreeEntry = struct {
        mode: []u8,
        name: []u8,
        hash: u160,
    };
    const Tree = std.ArrayList(TreeEntry);
    const Commit = struct {
        author: []u8,
        message: []u8,
        parent: u160,
        Tree: u160,
    };
    const Object = struct {
        usingnamespace union(enum) {
            blob: Blob,
            tree: Tree,
            commit: Commit,
        };
        data: []const u8,
    };
    const Repo = struct {
        pub fn init(alloc: std.mem.Allocator, path: []const u8) !@This() {
            const gitDir = try std.fs.cwd().openDir(path, .{});

            var objects = std.AutoHashMap(u160, Object).init(alloc);

            var iter = gitDir.iterate();
            while (try iter.next()) |dirEntry| {
                if (std.mem.endsWith(u8, dirEntry.name, ".idx")) {
                    var packPathBuffer: [128]u8 = undefined;
                    const idxPath = try gitDir.realpathAlloc(alloc, dirEntry.name);
                    const packPath = try std.fmt.bufPrint(
                        &packPathBuffer,
                        "{s}.pack",
                        .{idxPath[0 .. idxPath.len - 4]},
                    );

                    const packBuffer = try std.fs.cwd().readFileAlloc(alloc, packPath, 1024 * 1024 * 1024 * 1024);
                    const idxBuffer = try std.fs.cwd().readFileAlloc(alloc, idxPath, 1024 * 1024 * 1024 * 1024);
                    defer alloc.free(packBuffer);
                    defer alloc.free(idxBuffer);

                    const pf = try PackFile.init(alloc, packBuffer, idxBuffer);
                    defer pf.deinit();

                    for (0..pf.objectNames.items.len) |i| {
                        try objects.put(
                            pf.objectNames.items[i],
                            try pf.getObject(alloc, pf.objectOffsets.items[i]),
                        );
                    }
                }
            }
            return .{
                .alloc = alloc,
                .objects = objects,
            };
        }

        pub fn deinit(self: *@This()) void {
            self.objects.deinit();
        }

        alloc: std.mem.Allocator,
        objects: std.AutoHashMap(u160, Object),
    };
};

pub fn main() !void {
    // allocator
    var allocator = std.heap.GeneralPurposeAllocator(.{}){};
    const alloc = allocator.allocator();
    defer {
        const res = allocator.deinit();
        std.debug.print("{}\n", .{res});
    }

    // open repo
    var repo = try git.Repo.init(alloc, "../microwindows/.git/");

    // input
    const r = std.io.getStdIn().reader();
    var inputBuffer = std.mem.zeroes([1024]u8);

    // print HEAD
    // const head = repo.getHead();
    // const headObject = head.getObject();
    // std.debug.print("HEAD: {x:0>40}\n", .{headObject.id});

    // main loop
    while (true) {
        // read line
        const input = r.readUntilDelimiter(&inputBuffer, '\n') catch continue;

        // parse object id
        const id = std.fmt.parseInt(u160, input, 16) catch continue;

        // print object
        if (repo.objects.get(id)) |o| {
            switch (o) {
                .Blob => |blob| {
                    std.debug.print("blob: {s}\n\n", .{blob.data});
                },
                .Tree => |tree| {
                    for (tree.entries) |entry| {
                        std.debug.print("{s} {s} {x:0>40}\n", .{ entry.mode, entry.name, entry.hash });
                    }
                    std.debug.print("\n", .{});
                },
                .Commit => |commit| {
                    std.debug.print("commit: {}", .{commit});
                },
            }
        }
    }
}
