allocator: std.mem.Allocator,

timer: std.time.Timer,
// fillPipeline: *sdl3.GpuGraphicsPipeline = undefined,
// linePipeline: *sdl3.GpuGraphicsPipeline = undefined,
// viewport: sdl3.GpuViewport = undefined,
scissor: gpu.Rect = .{ .x = 0, .y = 0, .w = 640, .h = 480 },

running: bool = true,
window: *sdl3.Window = undefined,
device: *gpu.GPUDevice = undefined,
shaderpath: []const u8 = undefined,
shadersuffix: []const u8 = undefined,
entrypoint: []const u8 = "main",
fragment: *gpu.GPUShader = undefined,
vertex: *gpu.GPUShader = undefined,
pipeline: *gpu.GPUGraphicsPipeline = undefined,
shaderformat: gpu.GPUShaderFormat = undefined,

colorBuffer: *gpu.GPUBuffer = undefined,
colorBufferTransfer: *gpu.GPUTransferBuffer = undefined,
totalTime: f64 = 0.0,

pub fn create(allocator: std.mem.Allocator) !*@This() {
    const self = try allocator.create(@This());
    self.* = .{
        .allocator = allocator,
        .timer = try std.time.Timer.start(),
    };

    return self;
}

pub fn frameStamp(self: *@This()) f64 {
    return @as(f64, @floatFromInt(self.timer.read())) / 1000 / 1000 / 1000;
}

pub fn startContext(self: *@This()) !void {
    try sdl3.init(.{
        .video = true,
        .gamepad = true,
    });

    self.window = sdl3.c.SDL_CreateWindow("hello-world", 640, 480, 0).?;
    self.device = gpu.createGPUDevice(.{
        .shaderformatSpirv = true,
        .shaderformatDxil = true,
        .shaderformatMsl = true,
    }, false, null);

    if (!self.device.claimWindowForGPUDevice(self.window))
        return error.UnableToClaimGpu;

    const formats = self.device.getGPUShaderFormats();

    std.debug.print("SDL Context Loaded: {}\n", .{formats});
    if (formats.shaderformatSpirv) {
        self.shaderpath = "./content/_cooked/spv"; // shaderformatSpirv
        self.shadersuffix = ".spv";
        self.shaderformat = .{ .shaderformatSpirv = true };
    } else if (formats.shaderformatMsl) {
        self.shaderpath = "./content/_cooked/msl"; // shaderformatSpirv
        self.shadersuffix = ".msl";
        self.entrypoint = "main0";
        self.shaderformat = .{ .shaderformatMsl = true };
    } else if (formats.shaderformatDxil) {
        self.shaderpath = "./content/_cooked/dxil"; // shaderformatSpirv
        self.shadersuffix = ".dxil";
        self.shaderformat = .{ .shaderformatDxil = true };
    }

    self.vertex = try self.loadShader("hello-triangle.vert", .shaderstageVertex, 0, 0, 1, 0);
    self.fragment = try self.loadShader("hello-triangle.frag", .shaderstageFragment, 0, 0, 0, 0);

    var pci = std.mem.zeroes(gpu.GPUGraphicsPipelineCreateInfo);
    pci.fragment_shader = self.fragment;
    pci.vertex_shader = self.vertex;

    pci.target_info.num_color_targets = 1;
    pci.target_info.color_target_descriptions = &[_]gpu.GPUColorTargetDescription{
        .{
            .format = self.device.getGPUSwapchainTextureFormat(self.window),
            .blend_state = std.mem.zeroes(gpu.GPUColorTargetBlendState),
        },
    };

    pci.rasterizer_state.fill_mode = .fillmodeFill;
    self.pipeline = self.device.createGPUGraphicsPipeline(&pci);

    self.colorBuffer = self.device.createGPUBuffer(&.{
        .usage = .{ .bufferusageVertex = true },
        .size = 8192 * 2,
        .props = 0,
    });

    self.colorBufferTransfer = self.device.createGPUTransferBuffer(&.{
        .usage = .transferbufferusageUpload,
        .size = 8192 * 2,
        .props = 0,
    });
}

pub fn loadFileAlloc(filename: []const u8, comptime alignment: usize, allocator: std.mem.Allocator) ![]u8 {
    var file = try std.fs.cwd().openFile(filename, .{});
    defer file.close();
    const filesize = (try file.stat()).size + 1; // add null byte
    const buffer: []align(alignment) u8 = try allocator.alignedAlloc(u8, alignment, filesize);
    errdefer allocator.free(buffer);
    try file.reader().readNoEof(buffer[0 .. buffer.len - 1]);
    buffer[buffer.len - 1] = 0;
    return buffer;
}

pub fn loadShader(
    self: *@This(),
    shaderName: []const u8,
    stage: gpu.GPUShaderStage,
    num_samplers: u32, // The number of samplers defined in the shader.
    num_storage_textures: u32, // The number of storage textures defined in the shader.
    num_storage_buffers: u32, // The number of storage buffers defined in the shader.
    num_uniform_buffers: u32, // The number of uniform buffers defined in the shader.
) !*gpu.GPUShader {
    const spath = try std.fmt.allocPrintZ(self.allocator, "{s}/{s}{s}", .{ self.shaderpath, shaderName, self.shadersuffix });
    defer self.allocator.free(spath);

    std.debug.print("loading shader {s}\n", .{spath});

    const fileContents = try loadFileAlloc(spath, 8, self.allocator);
    defer self.allocator.free(fileContents);
    const sci = gpu.GPUShaderCreateInfo{
        .code = @ptrCast(fileContents.ptr),
        .entrypoint = @ptrCast(self.entrypoint.ptr),
        .format = self.shaderformat,
        .code_size = fileContents.len - 1,
        .stage = stage,
        .num_samplers = num_samplers,
        .num_storage_textures = num_storage_textures, // The number of storage textures defined in the shader.
        .num_storage_buffers = num_storage_buffers, // The number of storage buffers defined in the shader.
        .num_uniform_buffers = num_uniform_buffers, // The number of uniform buffers defined in the shader.
        .props = 0,
    };

    return self.device.createGPUShader(&sci);
}

pub fn loop(self: *@This()) !void {
    std.debug.print("loop started\n", .{});
    try self.startContext();
    while (self.running) {
        const dt = @as(f64, @floatFromInt(self.timer.lap())) / 1000 / 1000 / 1000;
        self.update(dt);
    }
}

fn update(self: *@This(), dt: f64) void {
    while (sdl3.pollEvent()) |event| {
        switch (event.type) {
            sdl_event.quit => {
                self.running = false;
            },
            sdl_event.key_down => {
                const key = event.key.scancode;
                if (key == @intFromEnum(sdl3.Scancode.escape)) {
                    self.running = false;
                }
            },
            else => {},
        }
    }

    self.draw(dt);
    std.debug.print("frametime {d:.3}ms (fps: {d:.1})\r", .{ dt * 1000, 1 / dt });
}

pub fn draw(self: *@This(), dt: f64) void {
    self.totalTime += dt;

    const cmd = self.device.acquireGPUCommandBuffer();
    var swapchain_texture: *gpu.GPUTexture = undefined;
    {
        const buffer = self.device.mapGPUTransferBuffer(self.colorBufferTransfer, true);

        var b: [*][4]f32 = @ptrCast(@alignCast(buffer));

        b[0] = .{ @floatCast(std.math.sin(self.totalTime * 3 * 2 + 0.8) * 0.2 + 0.8), 0.4, 0.4, 0.0 };
        b[1] = .{ 0.4, @floatCast(std.math.sin(self.totalTime * 2 * 2 + 0.3) * 0.2 + 0.8), 0.4, 0.0 };
        b[2] = .{ 0.4, 0.4, @floatCast(std.math.sin(self.totalTime * 4 * 2) * 0.2 + 0.8), 0.0 };

        self.device.unmapGPUTransferBuffer(self.colorBufferTransfer);
    }

    {
        const copyPass = cmd.beginGPUCopyPass();
        defer copyPass.endGPUCopyPass();
        copyPass.uploadToGPUBuffer(&.{ .transfer_buffer = self.colorBufferTransfer, .offset = 0 }, &.{
            .buffer = self.colorBuffer,
            .offset = 0,
            .size = 4 * 12,
        }, true);
    }

    if (cmd.waitAndAcquireGPUSwapchainTexture(self.window, &swapchain_texture, null, null)) {
        var targetInfo: gpu.GPUColorTargetInfo = std.mem.zeroes(gpu.GPUColorTargetInfo);
        targetInfo.texture = swapchain_texture;
        targetInfo.clear_color = .{ .r = 0.1, .g = 0.1, .b = 0.1, .a = 1.0 };
        targetInfo.load_op = .loadopClear;
        targetInfo.store_op = .storeopStore;

        const renderpass = cmd.beginGPURenderPass(&targetInfo, 1, null);
        renderpass.bindGPUGraphicsPipeline(self.pipeline);
        renderpass.bindGPUVertexStorageBuffers(0, &self.colorBuffer, 1);
        renderpass.setGPUScissor(&self.scissor);
        renderpass.drawGPUPrimitives(3, 1, 0, 0);
        renderpass.endGPURenderPass();
    }

    std.debug.print("rendering time: {d:.3}ms ", .{self.frameStamp()});

    _ = cmd.submitGPUCommandBuffer();
}

pub fn destroy(self: *@This()) void {
    sdl3.c.SDL_DestroyWindow(self.window);
    self.device.releaseWindowFromGPUDevice(self.window);

    self.allocator.destroy(self);
}

pub fn main() !void {
    std.debug.print("hello world... n", .{});
    const app = try create(std.heap.c_allocator);
    try app.loop();
    defer app.destroy();
}

const sdl3 = @import("sdl3");
const gpu = sdl3.gpu;
const sdl_event = sdl3.events;
const std = @import("std");
