const std = @import("std");

const glfw = @import("zglfw");
const zgl = @import("zgl");
const zgui = @import("zgui");
const zm = @import("zmath");
const Mat = zm.Mat;
const zmesh = @import("zmesh");
const Shape = zmesh.Shape;
const zstbi = @import("zstbi");

const gl_helper = @import("gl_helper/root.zig");
const Shader = gl_helper.Shader;
const Texture = gl_helper.Texture;
const Mesh = gl_helper.Mesh;
const Model = gl_helper.Model;
const ByteColor = gl_helper.Color.Byte;
const FloatColor = gl_helper.Color.Float;
const robot_texture_data = ByteColor.green;
const glfw_helper = @import("glfw_helper/root.zig");
const GlfwContect = glfw_helper.GlfwContext;
const AppState = glfw_helper.AppState;
const Tree = @import("tree.zig").Tree;

const floor_texture_data =
    ByteColor.blue ++ ByteColor.white ++
    ByteColor.white ++ ByteColor.blue;

var app_state = AppState.init(
    600,
    600,
    .robot,
    zm.f32x4(0, 1, 3, 1),
    zm.f32x4(0, 1, 0.5, 1),
    1,
);

const window_flags = zgui.WindowFlags{
    .no_move = true,
    .no_resize = true,
    .no_title_bar = true,
    .no_nav_inputs = true,
};

pub fn main() !void {
    var debug_allocator = std.heap.DebugAllocator(.{}).init;
    defer _ = debug_allocator.deinit();
    const allocator = debug_allocator.allocator();

    const context = try GlfwContect.init(&app_state, "Lab 6");

    zmesh.init(allocator);
    defer zmesh.deinit();

    zstbi.init(allocator);
    defer zstbi.deinit();

    zgui.init(allocator);
    defer zgui.deinit();

    zgui.io.setIniFilename(null);

    zgui.backend.init(context.window);
    defer zgui.backend.deinit();

    const style = zgui.getStyle();
    style.setColorsBuiltin(.dark);
    style.window_border_size = 0;

    // shaders
    const model_shader = try Shader.initFile("shaders/default.vert", "shaders/default.frag");
    defer model_shader.delete();

    const light_shader = try Shader.initFile("shaders/light.vert", "shaders/light.frag");
    defer light_shader.delete();

    const skybox_shader = try Shader.initFile("shaders/skybox.vert", "shaders/skybox.frag");
    defer skybox_shader.delete();

    // textures
    const floor_texture: []const Texture = &.{
        Texture.init(0, .@"2d"),
    };
    floor_texture[0].id.parameter(.mag_filter, .nearest);
    floor_texture[0].id.parameter(.min_filter, .nearest);
    floor_texture[0].load(&floor_texture_data, 2, 2, .unsigned_byte, 4);

    const robot_texture: []const Texture = &.{
        Texture.init(0, .@"2d"),
    };
    robot_texture[0].id.parameter(.mag_filter, .nearest);
    robot_texture[0].id.parameter(.min_filter, .nearest);
    robot_texture[0].load(&robot_texture_data, 1, 1, .unsigned_byte, 4);

    // meshes
    const floor = initFloor(floor_texture);
    defer floor.delete();

    var robot = try initRobot(allocator, robot_texture);
    defer robot.deinit(allocator);

    // uniform data
    var light_color = zm.f32x4(1, 1, 1, 1);

    light_shader.updateUniformVec4("u_position", &.{app_state.light_pos});
    model_shader.updateUniformMatrix("u_model_matrix", &.{zm.identity()});
    model_shader.updateUniformMatrix("u_normal_matrix", &.{zm.identity()});

    // uniform buffer objects
    const matrix_ubo = gl_helper.initUbo(
        zm.Mat,
        &.{ model_shader, light_shader, skybox_shader },
        "SharedMatrices",
        0,
        2,
    );
    defer matrix_ubo.delete();
    app_state.camera.updateProjection(0.1, 200);
    matrix_ubo.subData(0, zm.Mat, &.{app_state.camera.projection});

    const light_ubo = gl_helper.initUbo(
        zm.Vec,
        &.{ model_shader, light_shader },
        "Light",
        1,
        3,
    );
    defer light_ubo.delete();
    light_ubo.subData(0, zm.Vec, &.{light_color});
    light_ubo.subData(@sizeOf(zm.Vec), zm.Vec, &.{app_state.light_pos});

    // time handling
    var previous_time = glfw_helper.getTime();
    var current_time: f64 = 0;
    var delta_time: f64 = undefined;
    // var frame_counter: u16 = 0;

    // OpenGL setup
    const clear_color = FloatColor.sky_blue;
    zgl.clearColor(clear_color[0], clear_color[1], clear_color[2], clear_color[3]);
    zgl.enable(.program_point_size);
    zgl.enable(.depth_test);
    zgl.enable(.cull_face);

    while (!context.window.shouldClose()) : (context.postRender()) {
        // input
        app_state.camera.move(@floatCast(delta_time));

        // ubo updates
        matrix_ubo.subData(0, Mat, &.{app_state.camera.projection});
        app_state.camera.updateView();
        matrix_ubo.subData(@sizeOf(Mat), Mat, &.{app_state.camera.view});

        light_ubo.subData(@sizeOf(zm.Vec) * 2, zm.Vec, &.{app_state.camera.position});

        // render
        zgl.clear(.{ .color = true, .depth = true });

        switch (app_state.scene) {
            .robot => {
                model_shader.updateUniformMatrix("u_model_matrix", &.{zm.identity()});
                model_shader.updateUniformMatrix("u_normal_matrix", &.{zm.identity()});
                floor.draw(model_shader);

                // animateRobot(&frame_counter, robot);
                robot.draw(model_shader);
            },
            .fish => {},
        }

        light_shader.use();
        zgl.drawArrays(.points, 0, 1);

        // GUI
        drawGui(light_ubo, light_shader, &light_color);

        // time
        current_time = glfw_helper.getTime();
        delta_time = current_time - previous_time;
        previous_time = current_time;
    }
}

fn initFloor(textures: ?[]const Texture) Mesh {
    var shape = Shape.initPlane(1, 1);
    var inverted_shape = Shape.initPlane(1, 1);
    defer shape.deinit();
    defer inverted_shape.deinit();

    inverted_shape.invert(0, 0);
    shape.merge(inverted_shape);

    shape.translate(-0.5, -0.5, 0);
    shape.rotate(std.math.degreesToRadians(-90), 1, 0, 0);
    shape.scale(2, 1, 2);
    if (shape.texcoords) |texcoords| {
        for (texcoords) |*coord| {
            coord[0] *= 6;
            coord[1] *= 6;
        }
    }

    return Mesh.initShape(shape, textures, .triangles);
}

fn initRobot(allocator: std.mem.Allocator, textures: ?[]const Texture) !Model {
    // var meshes = try std.ArrayList(Mesh).initCapacity(allocator, 14);
    // var translations = try std.ArrayList(Mat).initCapacity(allocator, 14);

    var shape = Shape.initCube();
    defer shape.deinit();

    shape.unweld();
    shape.computeNormals();
    shape.translate(-0.5, -1, -0.5);
    // shape.rotate(std.math.degreesToRadians(180), 1, 0, 0);

    // var foot = shape.clone();
    // defer foot.deinit();
    // foot.scale(0.1, 0.025, 0.15);
    // meshes.appendNTimesAssumeCapacity(Mesh.initShape(foot, textures, .triangles), 2);
    // translations.appendAssumeCapacity(zm.translation(-0.07, 0.025, 0));
    // translations.appendAssumeCapacity(zm.translation(0.07, 0.025, 0));

    // var leg = shape.clone();
    // defer leg.deinit();
    // leg.scale(0.08, 0.2, 0.08);
    // meshes.appendNTimesAssumeCapacity(Mesh.initShape(leg, textures, .triangles), 4);
    // translations.appendAssumeCapacity(zm.translation(-0.07, 0.225, 0));
    // translations.appendAssumeCapacity(zm.translation(0.07, 0.225, 0));
    // translations.appendAssumeCapacity(zm.translation(-0.07, 0.425, 0));
    // translations.appendAssumeCapacity(zm.translation(0.07, 0.425, 0));

    // var arm = shape.clone();
    // defer arm.deinit();
    // arm.scale(0.08, 0.2, 0.08);
    // meshes.appendNTimesAssumeCapacity(Mesh.initShape(arm, textures, .triangles), 4);
    // translations.appendAssumeCapacity(zm.translation(-0.3 / 2.0 - 0.04, 0.225 + 0.4, 0));
    // translations.appendAssumeCapacity(zm.translation(0.3 / 2.0 + 0.04, 0.225 + 0.4, 0));
    // translations.appendAssumeCapacity(zm.translation(-0.3 / 2.0 - 0.04, 0.425 + 0.4, 0));
    // translations.appendAssumeCapacity(zm.translation(0.3 / 2.0 + 0.04, 0.425 + 0.4, 0));

    // var hand = shape.clone();
    // defer hand.deinit();
    // hand.scale(0.025, 0.08, 0.07);
    // meshes.appendNTimesAssumeCapacity(Mesh.initShape(hand, textures, .triangles), 2);
    // translations.appendAssumeCapacity(zm.translation(-0.3 / 2.0 - 0.04, 0.425, 0));
    // translations.appendAssumeCapacity(zm.translation(0.3 / 2.0 + 0.04, 0.425, 0));

    var chest = shape.clone();
    defer chest.deinit();
    chest.scale(0.3, 0.4, 0.15);
    // meshes.appendAssumeCapacity(Mesh.initShape(chest, textures, .triangles));
    // translations.appendAssumeCapacity(zm.translation(0, 0.425 + 0.4, 0));

    var model_data = Tree(Model.JointData).init(
        .{
            .mesh = Mesh.initShape(chest, textures, .triangles),
            .translation = zm.translation(0, 0.425 + 0.4, 0),
            .rotation = zm.qidentity(),
        },
    );

    var head = shape.clone();
    defer head.deinit();
    head.scale(0.1, 0.1, 0.1);

    try model_data.root.append(allocator, .{
        .mesh = Mesh.initShape(head, textures, .triangles),
        .translation = zm.translation(0, 0.425 + 0.4 + 0.1, 0),
        .rotation = zm.qidentity(),
    });
    // meshes.appendAssumeCapacity(Mesh.initShape(head, textures, .triangles));
    // translations.appendAssumeCapacity(zm.translation(0, 0.425 + 0.4 + 0.1, 0));

    return Model.init(model_data);
}

// fn initRobot(allocator: std.mem.Allocator, textures: ?[]const Texture) Model {
//     var shape = Shape.initCube();
//     defer shape.deinit();

//     shape.unweld();
//     shape.computeNormals();
//     shape.translate(-0.5, -1, -0.5);

//     var chest = shape.clone();
//     defer chest.deinit();
//     chest.scale(0.3, 0.4, 0.15);

//     var model_data = Tree(Model.JointData).init(.{
//         .mesh = Mesh.initShape(chest, textures, .triangles),
//         .translation = zm.translation(0, 0.425 + 0.4, 0),
//         .rotation = zm.qidentity(),
//     });

//     var head = shape.clone();
//     defer head.deinit();
//     head.scale(0.1, 0.1, 0.1);

//     try model_data.root.append(allocator, .{
//         .mesh = Mesh.initShape(head, textures, .triangles),
//         .translation = zm.translation(0, 0.425 + 0.4 + 0.1, 0),
//         .rotation = zm.qidentity(),
//     });

//     return Model.init(model_data);
// }

// fn animateRobot(frame_counter: *u16, robot: Model) void {
//     if (frame_counter.* > robot.animation_frames) {
//         robot.translations.items[2] = zm.mul(zm.rotationX(-std.math.degreesToRadians(1)), robot.translations.items[2]);
//         robot.translations.items[3] = zm.mul(zm.rotationX(-std.math.degreesToRadians(1)), robot.translations.items[3]);
//     } else {
//         robot.translations.items[2] = zm.mul(zm.rotationX(std.math.degreesToRadians(1)), robot.translations.items[2]);
//         robot.translations.items[3] = zm.mul(zm.rotationX(std.math.degreesToRadians(1)), robot.translations.items[3]);
//     }

//     frame_counter.* += 1;

//     if (frame_counter.* > robot.animation_frames * 2) {
//         frame_counter.* = 0;
//     }
// }

fn drawGui(light_ubo: zgl.Buffer, light_shader: gl_helper.Shader, light_color: *zm.Vec) void {
    if (app_state.render_gui) {
        zgui.backend.newFrame(@intCast(app_state.width), @intCast(app_state.height));
        zgui.setNextWindowPos(.{ .x = 0, .y = 0, .cond = .appearing });
        zgui.setNextWindowSize(.{ .w = @floatFromInt(app_state.width), .h = 0, .cond = .appearing });
        if (zgui.begin(
            "##Window",
            .{ .flags = window_flags },
        )) {
            _ = zgui.radioButtonStatePtr("Robot", .{ .v_button = 0, .v = @ptrCast(&app_state.scene) });
            zgui.sameLine(.{});
            _ = zgui.radioButtonStatePtr("Fish", .{ .v_button = 1, .v = @ptrCast(&app_state.scene) });

            if (zgui.sliderFloat3(
                "Light position",
                .{
                    .v = @ptrCast(&app_state.light_pos),
                    .min = -2,
                    .max = 2,
                    .cfmt = "%.2f",
                    .flags = .{ .always_clamp = true },
                },
            )) {
                light_ubo.subData(@sizeOf(zm.Vec), zm.Vec, &.{app_state.light_pos});
                light_shader.updateUniformVec4("u_position", &.{app_state.light_pos});
            }

            if (zgui.colorEdit3(
                "Light Color",
                .{
                    .col = @ptrCast(light_color),
                    .flags = .{},
                },
            )) {
                light_ubo.subData(0, zm.Vec, &.{light_color.*});
            }
        }
        zgui.end();
        zgui.backend.draw();
    }
}

comptime {
    std.testing.refAllDeclsRecursive(@This());
}
