const std = @import("std");
const approxEqAbs = std.math.approxEqAbs;
const floatEps = std.math.floatEps;
const degToRad = std.math.degreesToRadians;

const glfw = @import("zglfw");
const zm = @import("zmath");

const Shader = @import("shader.zig");

const Camera = @This();

width: f32,
height: f32,
position: zm.Vec,
orientation: zm.Vec = zm.f32x4(0, 0, -1, 1),
up: zm.Vec = zm.f32x4(0, 1, 0, 1),
view: zm.Mat,
projection: zm.Mat,

initial_speed: f32,
sensitivity: f32 = 100.0,

controller: struct {
    forward: bool = false,
    backward: bool = false,
    left: bool = false,
    right: bool = false,
    up: bool = false,
    down: bool = false,
    accelerate: bool = false,
},

pub fn init(position: zm.Vec, width: f32, height: f32, initial_speed: f32) Camera {
    return .{
        .width = width,
        .height = height,
        .position = position,
        .initial_speed = initial_speed,
        .controller = .{},
        .view = zm.identity(),
        .projection = zm.identity(),
    };
}

pub fn updateProjection(self: *Camera, near: f32, far: f32) void {
    self.projection = zm.perspectiveFovRhGl(std.math.degreesToRadians(60), self.width / self.height, near, far);
}

pub fn updateView(self: *Camera) void {
    self.view = zm.lookAtRh(self.position, self.position + self.orientation, self.up);
}

pub fn updateControls(self: *Camera, key: glfw.Key, action: glfw.Action) void {
    const bool_action = @intFromEnum(action) != 0;
    switch (key) {
        .a => self.controller.left = bool_action,
        .w => self.controller.forward = bool_action,
        .s => self.controller.backward = bool_action,
        .d => self.controller.right = bool_action,
        .space => self.controller.up = bool_action,
        .left_control => self.controller.down = bool_action,
        .left_shift => self.controller.accelerate = bool_action,
        else => {},
    }
}

pub fn move(self: *Camera, delta_time: f32) void {
    var current_speed = zm.f32x4s(self.initial_speed * delta_time);
    if (self.controller.accelerate) {
        current_speed = zm.f32x4s(self.initial_speed * delta_time * 2);
    }

    if (self.controller.forward) {
        self.position += self.orientation * current_speed;
    }
    if (self.controller.backward) {
        self.position -= self.orientation * current_speed;
    }
    if (self.controller.left) {
        self.position -= zm.normalize3(zm.cross3(self.orientation, self.up)) * current_speed;
    }
    if (self.controller.right) {
        self.position += zm.normalize3(zm.cross3(self.orientation, zm.normalize3(self.up))) * current_speed;
    }
    if (self.controller.up) {
        self.position += zm.normalize3(zm.cross3(zm.cross3(self.orientation, self.up), self.orientation)) * current_speed;
    }
    if (self.controller.down) {
        self.position -= zm.normalize3(zm.cross3(zm.cross3(self.orientation, self.up), self.orientation)) * current_speed;
    }
}

pub fn updateAngle(self: *Camera, xpos: f32, ypos: f32) void {
    // Normalizes and shifts the coordinates of the cursor such that they begin in the middle of the screen
    // and then "transforms" them into degrees
    const rot_x: f32 = self.sensitivity * (ypos - (self.height / 2)) / self.height;
    const rot_y: f32 = self.sensitivity * (xpos - (self.width / 2)) / self.width;

    // Calculates upcoming vertical change in the Orientation
    // Rotate the view vector by the horizontal angle around the vertical axis
    const horizontal_axis_rotation = zm.quatFromNormAxisAngle(self.up, degToRad(-rot_y));
    self.orientation = zm.normalize3(zm.rotate(horizontal_axis_rotation, self.orientation));

    // Rotate the view vector by the vertical angle around the horizontal axis
    const u = zm.cross3(self.orientation, self.up);
    const vertical_axis_rotation = zm.quatFromNormAxisAngle(u, degToRad(-rot_x));

    const new_orientation = zm.normalize3(zm.rotate(vertical_axis_rotation, self.orientation));
    if (!approxEqAbs(f32, new_orientation[1], -1, 0.001)) {
        self.orientation = new_orientation;
    }
}
