#pragma once
#include <cuda_helper.hpp>

struct quat {
    float x;
    float y;
    float z;
    float w;
};

inline quat make_quat(float w, float3 axis) {
    return {axis.x, axis.y, axis.z, w};
}

inline float3 operator*(quat q, float3 v) {
    const float3 QuatVector = make_float3(q.x, q.y, q.z);
    const float3 uv = cross(QuatVector, v);
    const float3 uuv = cross(QuatVector, uv);

    return v + ((uv * q.w) + uuv) * 2;
}

inline float to_radians(float deg) {
    return deg * Pi / 180.f;
}

enum Camera_Movement { FORWARD, BACKWARD, LEFT, RIGHT, UP, DOWN };

class Camera {
public:
    // Arcball Camera
    Camera(float3 pos = make_float3(5, 0, 0)) {
        this->fps = false;
        position = normalize(pos);
        radius = length(pos);

        float3 tempRight = cross(position, {0, 1, 0});
        if (length(tempRight) == 0) {
            tempRight = cross(position, {0, 0, 1});
        }
        up = normalize(cross(-position, normalize(tempRight)));
        front = normalize(center - position);
    }

    //FPS Camera
    Camera(float3 pos, float3 up, float3 front) {
        this->fps = true;
        position = pos;
        radius = 1;
        this->up = up;
        this->front = front;
        if (cross(up, front) == make_float3(0)) {
            // assume up vector false
            float3 tempRight = cross(position, {0, 1, 0});
            if (length(tempRight) == 0) {
                tempRight = cross(position, {0, 0, 1});
            }
            this->up = normalize(cross(front, normalize(tempRight)));
        }
    }

    void UVWFrame(float3& U, float3& V, float3& W, float aspect) const {
        float3 lookat = center;
        if (fps) {
            lookat = getEye() + front;
        }
        W = lookat - getEye(); // Do not normalize W -- it implies focal length
        float wlen = length(W);
        U = normalize(cross(W, up));
        V = normalize(cross(U, W));

        float vlen = wlen * tanf(0.5f * fov * Pi / 180.0f);
        V *= vlen;
        float ulen = vlen * aspect;
        U *= ulen;
    }

    bool processKeyboard(Camera_Movement direction, float deltaTime) {
        if (fps) {
            const float velocity = 2.5f * deltaTime;
            if (direction == FORWARD) {
                position += front * velocity;
            }
            if (direction == BACKWARD) {
                position -= front * velocity;
            }
            if (direction == LEFT) {
                position -= cross(front, up) * velocity;
            }
            if (direction == RIGHT) {
                position += cross(front, up) * velocity;
            }
            if (direction == UP) {
                position += up * velocity;
            }
            if (direction == DOWN) {
                position -= up * velocity;
            }

            return true;
        }
        return false;
    }

    void processMouseMovement(float dx, float dy) {
        if (fps) {
            Yaw(to_radians(dx * -speed));
            Pitch(to_radians(dy * -speed));
        } else {
            Azimut(to_radians(dx * -speed));
            Elevation(to_radians(dy * -speed));
        }
    }

    void processMouseScroll(float scroll) {
        if (!fps) {
            radius -= scroll * 0.5f;
            if (radius < 0.01f) {
                radius = 0.01;
            }
        }
    }

    [[nodiscard]] float3 getEye() const {
        if (fps) {
            return position;
        }
        return position * radius;
    }

    void print() {
        auto pos = position * radius;
        auto fr = front;
        printf("\"Pos\" %f %f %f \"Up\" %f %f %f \"Front\" %f %f %f; Radius: %f \n", pos.x, pos.y, pos.z, up.x, up.y, up.z,
               fr.x, fr.y, fr.z, radius);
    }

    void switchCam() {
        if (fps) {
            radius = length(position);
            position = normalize(position);
            front = normalize(center - position);
            float3 tempRight = cross(position, {0, 1, 0});
            if (length(tempRight) == 0) {
                tempRight = cross(position, {0, 0, 1});
            }
            up = normalize(cross(-position, normalize(tempRight)));
        } else {
            position = position * radius;
            radius = 1.f;
        }
        fps = !fps;
    }

    void Azimut(float angle) {
        float3 axis = up;
        float w = cosf(0.5f * angle);
        float f = sinf(0.5f * angle) / sqrtf(axis.x * axis.x + axis.y * axis.y + axis.z * axis.z);
        axis *= f;

        quat Q = make_quat(w, axis);
        position = Q * position;
        front = Q * front;
    }

    void getVecs(float3& pos, float3& up, float3& front) {
        pos = getEye();
        up = this->up;
        front = this->front;
    }

private:
    void Elevation(float angle) {
        float3 axis = -normalize(cross(up, front));
        float w = cosf(0.5f * angle);
        float f = sinf(0.5f * angle) / sqrtf(axis.x * axis.x + axis.y * axis.y + axis.z * axis.z);
        axis *= f;

        quat Q = make_quat(w, axis);
        position = Q * position;
        up = Q * up;
        front = Q * front;
    }

    void Yaw(float angle) {
        float3 axis = up;
        float w = cosf(0.5f * angle);
        float f = sinf(0.5f * angle) / sqrtf(axis.x * axis.x + axis.y * axis.y + axis.z * axis.z);
        axis *= f;


        quat Q = make_quat(w, axis);
        front = Q * front;
    }

    void Pitch(float angle) {
        float3 axis = -normalize(cross(up, front));
        float w = cosf(0.5f * angle);
        float f = sinf(0.5f * angle) / sqrtf(axis.x * axis.x + axis.y * axis.y + axis.z * axis.z);
        axis *= f;

        quat Q = make_quat(w, axis);
        up = Q * up;
        front = Q * front;
    }

    float3 position;
    float radius;
    float3 up;
    float3 front;
    float3 center = {0, 0, 0};
    float fov = 45.f;
    float speed = 0.4;
    bool fps = false;
};
