const std = @import("std");

const raylib_helper = @import("lib_helpers/raylib_helper.zig");
const raylib = raylib_helper.raylib;
const v3 = raylib_helper.v3;
const znoise = @import("znoise");

const chunks = @import("world/chunk.zig");

const TILE_TEXTURE_RESOLUTION = 16;

const benchmark_chunk_meshing = false;
const debug = true;

pub fn drawCameraPosition(camera: raylib.Camera3D, x: i32, y: i32) !void {
    var buf: [256:0]u8 = undefined;

    const slice = try std.fmt.bufPrintZ(
        &buf,
        "position: {d:.2} {d:.2} {d:.2}",
        .{ camera.position.x, camera.position.y, camera.position.z },
    );

    raylib.DrawText(slice, x, y, 20, raylib.YELLOW);
}

pub fn moveCamera(camera: *raylib.Camera3D, vec: raylib.Vector3) void {
    camera.position = v3.add(camera.position, vec);
    camera.target = v3.add(camera.target, vec);
}

pub fn main() !void {
    if (!debug) raylib.SetTraceLogLevel(raylib.LOG_ERROR);

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const a7r = gpa.allocator();
    defer {
        const status = gpa.deinit();
        if (status == .leak) std.debug.print("MEMORY LEAK DETECTED!!!!!!!!!!!!!!!!!!!!!!\n", .{}) else std.debug.print("no leaks detected.\n", .{});
    }

    raylib.SetConfigFlags(raylib.FLAG_WINDOW_RESIZABLE | raylib.FLAG_FULLSCREEN_MODE);

    const display = raylib.GetCurrentMonitor();
    raylib.InitWindow(raylib.GetMonitorWidth(display), raylib.GetMonitorHeight(display), "voxel test");
    defer raylib.CloseWindow();

    raylib.DisableCursor();
    raylib.SetWindowState(raylib.FLAG_VSYNC_HINT);
    raylib.SetWindowState(raylib.FLAG_FULLSCREEN_MODE);

    var camera = raylib.Camera3D{
        .position = raylib.Vector3{ .x = 0, .y = 0, .z = 0 },
        .up = raylib.Vector3{ .x = 0, .y = 1, .z = 0 },
        .target = raylib.Vector3{ .x = 0, .y = 0, .z = -1 },
        .fovy = 45,
        .projection = raylib.CAMERA_PERSPECTIVE,
    };

    const tiles = raylib.LoadImage("resources/images/tiles.png");
    const texture = raylib.LoadTextureFromImage(tiles);
    const tile_rows = @divExact(@as(u32, @intCast(tiles.height)), TILE_TEXTURE_RESOLUTION);
    const tile_columns = @divExact(@as(u32, @intCast(tiles.width)), TILE_TEXTURE_RESOLUTION);
    defer raylib.UnloadTexture(texture);
    raylib.UnloadImage(tiles);

    const shader = raylib.LoadShader("resources/shaders/tiling.vs", "resources/shaders/tiling.fs");
    defer raylib.UnloadShader(shader);
    raylib.SetShaderValue(shader, raylib.GetShaderLocation(shader, "textureTiling"), &.{ @as(f32, @floatFromInt(tile_columns)), @as(f32, @floatFromInt(tile_rows)) }, raylib.SHADER_UNIFORM_VEC2);

    var chunk = try chunks.Chunk.init(a7r);
    defer chunk.deinit();

    const height_generator = znoise.FnlGenerator{ .seed = 413445 };
    const tile_type_generator = znoise.FnlGenerator{ .seed = 4435, .frequency = 0.1 };
    for (0..32) |raw_x| for (0..32) |raw_y| for (0..32) |raw_z| {
        const x: u5 = @intCast(raw_x);
        const y: u5 = @intCast(raw_y);
        const z: u5 = @intCast(raw_z);
        const xf: f32 = @floatFromInt(raw_x);
        const yf: f32 = @floatFromInt(raw_y);
        const zf: f32 = @floatFromInt(raw_z);
        const height: f32 = (height_generator.noise2(xf, zf) + 1) * 16;
        const tile_type: u32 = if (tile_type_generator.noise3(xf, yf, zf) > 0) 1 else 2;
        if (height >= yf) chunk.setTile(x, y, z, tile_type);
    };

    if (benchmark_chunk_meshing) {
        var tmp: u64 = 0;
        for (0..500) |_| {
            const start = try std.time.Instant.now();
            const model = raylib.LoadModelFromMesh(try chunk.createMesh(tile_rows, tile_columns));
            defer raylib.UnloadModel(model);
            const end = try std.time.Instant.now();
            tmp += end.since(start);
        }
        std.debug.print("\nchunk meshing time: {d:.3}ms\n\n", .{
            @as(f64, @floatFromInt(tmp)) / std.time.ns_per_ms / 500,
        });
    }

    const model = raylib.LoadModelFromMesh(try chunk.createMesh(tile_rows, tile_columns));
    defer raylib.UnloadModel(model);
    model.materials[0].maps[raylib.MATERIAL_MAP_DIFFUSE].texture = texture;
    model.materials[0].shader = shader;

    while (!raylib.WindowShouldClose()) {
        raylib.ClearBackground(raylib.BLACK);

        const right = v3.neg(v3.nor(v3.cross(camera.up, v3.sub(camera.target, camera.position))));
        const forward = v3.cross(right, v3.new(0, -1, 0));

        const speed = @as(f32, if (raylib.IsKeyDown(raylib.KEY_LEFT_CONTROL)) 25 else 5) * raylib.GetFrameTime();
        var movement = v3.new(0, 0, 0);

        if (raylib.IsKeyDown(raylib.KEY_SPACE)) movement.y += 1;
        if (raylib.IsKeyDown(raylib.KEY_LEFT_SHIFT)) movement.y -= 1;

        if (raylib.IsKeyDown(raylib.KEY_W)) movement = v3.add(movement, forward);
        if (raylib.IsKeyDown(raylib.KEY_S)) movement = v3.sub(movement, forward);
        if (raylib.IsKeyDown(raylib.KEY_D)) movement = v3.add(movement, right);
        if (raylib.IsKeyDown(raylib.KEY_A)) movement = v3.sub(movement, right);

        moveCamera(&camera, v3.scl(v3.nor(movement), speed));

        const delta = raylib.GetMouseDelta();
        camera.target = v3.add(camera.position, v3.rotate(v3.sub(camera.target, camera.position), v3.new(0, 1, 0), -0.005 * delta.x));
        camera.target = v3.add(camera.position, v3.rotate(v3.sub(camera.target, camera.position), right, -0.005 * delta.y));

        raylib.BeginDrawing();
        defer raylib.EndDrawing();

        const model_position = v3.new(0, 0, 0);

        {
            raylib.BeginMode3D(camera);
            defer raylib.EndMode3D();
            raylib.BeginShaderMode(shader);
            defer raylib.EndShaderMode();

            raylib.DrawModel(model, model_position, 0.5, raylib.WHITE);
        }

        raylib.DrawFPS(10, 10);
        try drawCameraPosition(camera, 10, 30);
    }
}

test "empty test xd" {}
