// memory tracker
//
// todo.. rename this to memory.zig

const std = @import("std");
const core = @import("core.zig");
const builtin = @import("builtin");

lock: std.Thread.Mutex = .{},
backingAllocator: std.mem.Allocator,

allocationsCount: u32 = 0,
totalAllocSize: usize = 0,
eventsCount: usize = 0,

peakAllocations: u32 = 0,
peakAllocSize: usize = 0,

stackCompactor: ?*core.StackCompactor = undefined,

timeline: ?EventTimeline,

//const EventTimeline = core.algorithm.PagedVector(AllocEvent);
const EventTimeline = struct {
    timelineAllocator: std.mem.Allocator,
    events: core.algorithm.PagedVector(AllocEvent),
    timestamps: core.algorithm.PagedVector(f64), // timestamp in milliseconds

    pub fn init(timelineAllocator: std.mem.Allocator) !@This() {
        return .{
            .timelineAllocator = timelineAllocator,
            .events = try core.algorithm.PagedVector(AllocEvent).init(timelineAllocator),
            .timestamps = try core.algorithm.PagedVector(f64).init(timelineAllocator),
        };
    }

    pub inline fn pushAlloc(self: *@This(), compactor: *core.StackCompactor, ptr: usize, size: usize) void {
        const callstackId = compactor.getCallStack();
        self.timestamps.append(core.getEngineTime()) catch unreachable;
        self.events.append(.{
            .callstackId = callstackId,
            .address = ptr,
            .event = .{ .alloc = .{ .size = size } },
        }) catch unreachable;
    }

    pub inline fn pushResize(self: *@This(), compactor: *core.StackCompactor, ptr: usize, old_len: usize, new_len: usize) void {
        const callstackId = compactor.getCallStack();
        self.timestamps.append(core.getEngineTime()) catch unreachable;
        self.events.append(.{
            .callstackId = callstackId,
            .address = ptr,
            .event = .{ .resize = .{ .oldSize = old_len, .newSize = new_len } },
        }) catch unreachable;
    }

    pub inline fn pushFree(self: *@This(), compactor: *core.StackCompactor, ptr: usize, size: usize) void {
        const callstackId = compactor.getCallStack();
        self.timestamps.append(core.getEngineTime()) catch unreachable;
        self.events.append(.{
            .callstackId = callstackId,
            .address = ptr,
            .event = .{ .free = .{ .size = size } },
        }) catch unreachable;
    }
};

// warning, due to laziness this uses timelineAllocator, which will leak
pub fn dumpTimeline(filename: []const u8) !void {
    if (MTGet()) |tracker| {
        tracker.lock.lock();
        defer tracker.lock.unlock();
        if (tracker.stackCompactor) |compactor| {
            if (tracker.timeline) |timeline| {
                const cwd = std.fs.cwd();
                const ofile = try std.fmt.allocPrint(timeline.timelineAllocator, core.DefaultSavePath ++ "/{s}", .{filename});
                defer timeline.timelineAllocator.free(ofile);

                var obuf = std.ArrayList(u8).init(timeline.timelineAllocator);
                defer obuf.deinit();
                var writer = obuf.writer();

                var i: usize = 0;

                while (i < timeline.events.len()) : (i += 1) {
                    const event = timeline.events.get(i);
                    const timestamp = timeline.timestamps.get(i);

                    try writer.print("{d}: callstack: {x} @0x{x} ", .{
                        timestamp.*,
                        event.callstackId,
                        event.address,
                    });
                    switch (event.event) {
                        .alloc => |x| {
                            try writer.print("alloc {d} bytes\n", .{x.size});
                        },
                        .free => |x| {
                            try writer.print("free {d} bytes\n", .{x.size});
                        },
                        .resize => |x| {
                            try writer.print("resize {d} -> {d} bytes\n", .{ x.oldSize, x.newSize });
                        },
                    }
                }
                var iter = compactor.stackMap.iterator();
                while (iter.next()) |x| {
                    const callstackId = x.key_ptr.*;
                    const stack = x.value_ptr.*;
                    try writer.print("{x}:\n", .{callstackId});
                    for (stack.debugStr) |frame| {
                        if (frame) |f| {
                            try writer.print("{s}\n", .{f});
                        } else {
                            try writer.print("no file\n", .{});
                        }
                    }
                }

                try cwd.makePath(core.DefaultSavePath);
                try cwd.writeFile(.{
                    .sub_path = ofile,
                    .data = obuf.items,
                });
            }
        } else {
            core.engine_logs("skipping writing memory timeline as timeline is not enabled");
        }
    }
}

const AllocEventType = enum { alloc, free, resize };

const AllocEvent = struct {
    callstackId: u32,
    address: usize,
    event: union(AllocEventType) {
        alloc: struct { size: usize },
        free: struct { size: usize },
        resize: struct { oldSize: usize, newSize: usize },
    },
};

pub var vtable: std.mem.Allocator.VTable = .{
    .alloc = alloc,
    .free = free,
    .resize = resize,
    .remap = std.mem.Allocator.noRemap,
};

pub fn init(backingAllocator: std.mem.Allocator, settings: SetupSettings) @This() {
    // use the ansi allocator

    const EnableMemoryTimeline = settings.timeline and builtin.os.tag == .windows;

    if (EnableMemoryTimeline) {
        core.engine_logs("[dmt] Enabling Detailed Memory Tracking");
    }

    const stackCompactor = if (EnableMemoryTimeline) core.StackCompactor.create(std.heap.c_allocator) catch null else null;

    return .{
        .backingAllocator = backingAllocator,
        .stackCompactor = stackCompactor,
        .timeline = if (EnableMemoryTimeline) EventTimeline.init(std.heap.c_allocator) catch null else null,
    };
}

pub fn allocator(self: *@This()) std.mem.Allocator {
    return .{
        .ptr = self,
        .vtable = &vtable,
    };
}

pub fn alloc(ctx: *anyopaque, len: usize, ptr_align: std.mem.Alignment, ret_addr: usize) ?[*]u8 {
    var self: *@This() = @alignCast(@ptrCast(ctx));
    const rv = self.backingAllocator.vtable.alloc(self.backingAllocator.ptr, len, ptr_align, ret_addr);

    {
        self.lock.lock();
        defer self.lock.unlock();
        self.allocationsCount += 1;
        self.totalAllocSize += len;
        self.eventsCount += 1;

        if (self.totalAllocSize > self.peakAllocSize) {
            self.peakAllocSize = self.totalAllocSize;
        }

        if (self.allocationsCount > self.peakAllocations) {
            self.peakAllocations = self.allocationsCount;
        }

        if (self.timeline) |*timeline| {
            timeline.pushAlloc(self.stackCompactor.?, @intFromPtr(rv), len);
        }
    }
    return rv;
}

pub fn resize(ctx: *anyopaque, buf: []u8, buf_align: std.mem.Alignment, new_len: usize, ret_addr: usize) bool {
    var self: *@This() = @alignCast(@ptrCast(ctx));
    {
        self.lock.lock();
        defer self.lock.unlock();
        self.totalAllocSize = self.totalAllocSize - buf.len + new_len;
        self.eventsCount += 1;

        if (self.timeline) |*timeline| {
            timeline.pushResize(self.stackCompactor.?, @intFromPtr(buf.ptr), buf.len, new_len);
        }
    }

    return self.backingAllocator.vtable.resize(
        self.backingAllocator.ptr,
        buf,
        buf_align,
        new_len,
        ret_addr,
    );
}

pub fn free(ctx: *anyopaque, buf: []u8, buf_align: std.mem.Alignment, ret_addr: usize) void {
    var self: *@This() = @alignCast(@ptrCast(ctx));
    {
        self.lock.lock();
        defer self.lock.unlock();

        self.allocationsCount -= 1;
        self.totalAllocSize -= buf.len;
        self.eventsCount += 1;

        if (self.timeline) |*timeline| {
            timeline.pushFree(self.stackCompactor.?, @intFromPtr(buf.ptr), buf.len);
        }
    }
    self.backingAllocator.vtable.free(self.backingAllocator.ptr, buf, buf_align, ret_addr);
}

pub fn printStats(self: @This()) void {
    std.debug.print("allocations: {d}\n", .{self.allocationsCount});
    std.debug.print("memory committed: {d}\n", .{self.totalAllocSize});
}

pub fn deinit(self: *@This()) void {
    _ = self;
}

pub fn addUntrackedAllocation(self: *@This(), allocatedSize: usize) void {
    self.totalAllocSize += allocatedSize;
}

pub fn removeUntrackedAllocation(self: *@This(), allocatedSize: usize) void {
    self.totalAllocSize -= allocatedSize;
}

var gMemTracker: ?*@This() = null;

pub const SetupSettings = struct {
    timeline: bool = false,
};

pub fn MTSetup(backingAllocator: std.mem.Allocator, settings: SetupSettings) void {
    gMemTracker = backingAllocator.create(@This()) catch unreachable;
    gMemTracker.?.* = @This().init(backingAllocator, settings);
}

pub fn MTShutdown() void {
    var backingAllocator = gMemTracker.?.backingAllocator;
    gMemTracker.?.deinit();
    backingAllocator.destroy(gMemTracker.?);
    gMemTracker = null;
}

pub fn MTGet() ?*@This() {
    return gMemTracker;
}

// todo replace all these functions with a virtual table
pub fn MTAddUntrackedAllocation(allocatedSize: usize) void {
    if (gMemTracker) |mt|
        mt.addUntrackedAllocation(allocatedSize);
}

pub fn MTRemoveAllocation(allocatedSize: usize) void {
    if (gMemTracker) |mt|
        mt.removeUntrackedAllocation(allocatedSize);
}

pub fn MTPrintStatsDelta() void {
    if (gMemTracker) |mt| {
        core.engine_log("allocated size: {d} ({d:.3} MiB)", .{
            mt.totalAllocSize,
            @as(f64, @floatFromInt(mt.totalAllocSize)) / 1024 / 1024,
        });
        core.engine_log("peak allocated size size: {d} ({d:.3} MiB) ({d} peak allocations)", .{
            mt.peakAllocSize,
            @as(f64, @floatFromInt(mt.peakAllocSize)) / 1024 / 1024,
            mt.peakAllocations,
        });
    }
}

pub fn PrintStatsWithTag(comptime tag: []const u8) void {
    core.engine_logs(tag);
    MTPrintStatsDelta();
}
