#include "pxl8_3d_camera.h"

#include <math.h>
#include <stdlib.h>

struct pxl8_3d_camera {
    pxl8_vec3 position;
    f32 pitch;
    f32 roll;
    f32 yaw;

    pxl8_3d_camera_mode mode;

    f32 aspect;
    f32 far;
    f32 fov;
    f32 near;

    f32 ortho_bottom;
    f32 ortho_left;
    f32 ortho_right;
    f32 ortho_top;

    f32 shake_duration;
    f32 shake_intensity;
    pxl8_vec3 shake_offset;
    f32 shake_timer;
};

pxl8_3d_camera* pxl8_3d_camera_create(void) {
    pxl8_3d_camera* cam = calloc(1, sizeof(pxl8_3d_camera));
    if (!cam) return NULL;

    cam->position = (pxl8_vec3){0, 0, 0};
    cam->pitch = 0;
    cam->yaw = 0;
    cam->roll = 0;

    cam->mode = PXL8_3D_CAMERA_PERSPECTIVE;
    cam->fov = 1.0f;
    cam->aspect = 16.0f / 9.0f;
    cam->near = 1.0f;
    cam->far = 1000.0f;

    return cam;
}

void pxl8_3d_camera_destroy(pxl8_3d_camera* cam) {
    free(cam);
}

void pxl8_3d_camera_lookat(pxl8_3d_camera* cam, pxl8_vec3 eye, pxl8_vec3 target, pxl8_vec3 up) {
    if (!cam) return;

    cam->position = eye;

    pxl8_vec3 forward = pxl8_vec3_normalize(pxl8_vec3_sub(target, eye));

    cam->pitch = asinf(forward.y);
    cam->yaw = atan2f(-forward.x, -forward.z);
    cam->roll = 0;

    (void)up;
}

void pxl8_3d_camera_set_ortho(pxl8_3d_camera* cam, f32 left, f32 right, f32 bottom, f32 top, f32 near, f32 far) {
    if (!cam) return;
    cam->mode = PXL8_3D_CAMERA_ORTHO;
    cam->ortho_left = left;
    cam->ortho_right = right;
    cam->ortho_bottom = bottom;
    cam->ortho_top = top;
    cam->near = near;
    cam->far = far;
}

void pxl8_3d_camera_set_perspective(pxl8_3d_camera* cam, f32 fov, f32 aspect, f32 near, f32 far) {
    if (!cam) return;
    cam->mode = PXL8_3D_CAMERA_PERSPECTIVE;
    cam->fov = fov;
    cam->aspect = aspect;
    cam->near = near;
    cam->far = far;
}

void pxl8_3d_camera_set_position(pxl8_3d_camera* cam, pxl8_vec3 pos) {
    if (!cam) return;
    cam->position = pos;
}

void pxl8_3d_camera_set_rotation(pxl8_3d_camera* cam, f32 pitch, f32 yaw, f32 roll) {
    if (!cam) return;
    cam->pitch = pitch;
    cam->yaw = yaw;
    cam->roll = roll;
}

pxl8_vec3 pxl8_3d_camera_get_forward(const pxl8_3d_camera* cam) {
    if (!cam) return (pxl8_vec3){0, 0, -1};

    f32 cp = cosf(cam->pitch);
    f32 sp = sinf(cam->pitch);
    f32 cy = cosf(cam->yaw);
    f32 sy = sinf(cam->yaw);

    return (pxl8_vec3){
        -sy * cp,
        sp,
        -cy * cp
    };
}

pxl8_vec3 pxl8_3d_camera_get_position(const pxl8_3d_camera* cam) {
    if (!cam) return (pxl8_vec3){0, 0, 0};
    return pxl8_vec3_add(cam->position, cam->shake_offset);
}

pxl8_mat4 pxl8_3d_camera_get_projection(const pxl8_3d_camera* cam) {
    if (!cam) return pxl8_mat4_identity();

    if (cam->mode == PXL8_3D_CAMERA_PERSPECTIVE) {
        return pxl8_mat4_perspective(cam->fov, cam->aspect, cam->near, cam->far);
    } else {
        return pxl8_mat4_ortho(
            cam->ortho_left, cam->ortho_right,
            cam->ortho_bottom, cam->ortho_top,
            cam->near, cam->far
        );
    }
}

pxl8_vec3 pxl8_3d_camera_get_right(const pxl8_3d_camera* cam) {
    if (!cam) return (pxl8_vec3){1, 0, 0};

    f32 cy = cosf(cam->yaw);
    f32 sy = sinf(cam->yaw);

    return (pxl8_vec3){cy, 0, -sy};
}

pxl8_vec3 pxl8_3d_camera_get_up(const pxl8_3d_camera* cam) {
    if (!cam) return (pxl8_vec3){0, 1, 0};

    pxl8_vec3 forward = pxl8_3d_camera_get_forward(cam);
    pxl8_vec3 right = pxl8_3d_camera_get_right(cam);

    return pxl8_vec3_cross(forward, right);
}

pxl8_mat4 pxl8_3d_camera_get_view(const pxl8_3d_camera* cam) {
    if (!cam) return pxl8_mat4_identity();

    pxl8_vec3 pos = pxl8_3d_camera_get_position(cam);
    pxl8_vec3 forward = pxl8_3d_camera_get_forward(cam);
    pxl8_vec3 target = pxl8_vec3_add(pos, forward);
    pxl8_vec3 up = (pxl8_vec3){0, 1, 0};

    return pxl8_mat4_lookat(pos, target, up);
}

void pxl8_3d_camera_blend(pxl8_3d_camera* dest, const pxl8_3d_camera* a, const pxl8_3d_camera* b, f32 t) {
    if (!dest || !a || !b) return;

    dest->position = pxl8_vec3_lerp(a->position, b->position, t);
    dest->pitch = a->pitch + (b->pitch - a->pitch) * t;
    dest->yaw = a->yaw + (b->yaw - a->yaw) * t;
    dest->roll = a->roll + (b->roll - a->roll) * t;

    dest->fov = a->fov + (b->fov - a->fov) * t;
    dest->aspect = a->aspect + (b->aspect - a->aspect) * t;
    dest->near = a->near + (b->near - a->near) * t;
    dest->far = a->far + (b->far - a->far) * t;

    dest->mode = (t < 0.5f) ? a->mode : b->mode;
}

void pxl8_3d_camera_follow(pxl8_3d_camera* cam, pxl8_vec3 target, pxl8_vec3 offset, f32 smoothing, f32 dt) {
    if (!cam) return;

    pxl8_vec3 desired = pxl8_vec3_add(target, offset);

    f32 t = 1.0f - powf(1.0f - smoothing, dt * 60.0f);
    cam->position = pxl8_vec3_lerp(cam->position, desired, t);

    pxl8_vec3 forward = pxl8_vec3_normalize(pxl8_vec3_sub(target, cam->position));
    cam->pitch = asinf(forward.y);
    cam->yaw = atan2f(-forward.x, -forward.z);
}

void pxl8_3d_camera_shake(pxl8_3d_camera* cam, f32 intensity, f32 duration) {
    if (!cam) return;
    cam->shake_intensity = intensity;
    cam->shake_duration = duration;
    cam->shake_timer = duration;
}

void pxl8_3d_camera_update(pxl8_3d_camera* cam, f32 dt) {
    if (!cam) return;

    if (cam->shake_timer > 0) {
        cam->shake_timer -= dt;

        f32 decay = cam->shake_timer / cam->shake_duration;
        f32 intensity = cam->shake_intensity * decay;

        cam->shake_offset.x = ((f32)rand() / (f32)RAND_MAX * 2.0f - 1.0f) * intensity;
        cam->shake_offset.y = ((f32)rand() / (f32)RAND_MAX * 2.0f - 1.0f) * intensity;
        cam->shake_offset.z = ((f32)rand() / (f32)RAND_MAX * 2.0f - 1.0f) * intensity;

        if (cam->shake_timer <= 0) {
            cam->shake_offset = (pxl8_vec3){0, 0, 0};
        }
    }
}

pxl8_projected_point pxl8_3d_camera_world_to_screen(const pxl8_3d_camera* cam, pxl8_vec3 world_pos, u32 screen_width, u32 screen_height) {
    pxl8_projected_point result = {0, 0, 0.0f, false};
    if (!cam) return result;

    pxl8_mat4 view = pxl8_3d_camera_get_view(cam);
    pxl8_mat4 proj = pxl8_3d_camera_get_projection(cam);
    pxl8_mat4 vp = pxl8_mat4_mul(proj, view);

    pxl8_vec4 clip = pxl8_mat4_mul_vec4(vp, (pxl8_vec4){world_pos.x, world_pos.y, world_pos.z, 1.0f});

    if (clip.w <= 0.0f) return result;

    f32 inv_w = 1.0f / clip.w;
    f32 ndc_x = clip.x * inv_w;
    f32 ndc_y = clip.y * inv_w;
    f32 ndc_z = clip.z * inv_w;

    if (ndc_x < -1.0f || ndc_x > 1.0f || ndc_y < -1.0f || ndc_y > 1.0f) return result;

    result.x = (i32)((ndc_x + 1.0f) * 0.5f * (f32)screen_width);
    result.y = (i32)((1.0f - ndc_y) * 0.5f * (f32)screen_height);
    result.depth = ndc_z;
    result.visible = true;

    return result;
}
