// SPDX-License-Identifier: MIT
//
// Copyright (c) 2008-2024 Antonio Niño Díaz
//
// This file is part of Nitro Engine

#include "NEMain.h"

/// @file NECamera.c

static NE_Camera **NE_UserCamera;
static int NE_MAX_CAMERAS;
static bool ne_camera_system_inited = false;

// Internal use only
ARM_CODE static void __NE_CameraUpdateMatrix(NE_Camera * cam)
{
    // From libnds, modified a bit
    int32_t side[3], forward[3], up[3];

    for (int i = 0; i < 3; i++)
        forward[i] = cam->from[i] - cam->to[i];

    normalizef32(forward);

    crossf32(cam->up, forward, side);

    normalizef32(side);

    // Recompute local up
    crossf32(forward, side, up);

    cam->matrix.m[0] = side[0];
    cam->matrix.m[1] = up[0];
    cam->matrix.m[2] = forward[0];
    cam->matrix.m[3] = 0;

    cam->matrix.m[4] = side[1];
    cam->matrix.m[5] = up[1];
    cam->matrix.m[6] = forward[1];
    cam->matrix.m[7] = 0;

    cam->matrix.m[8] = side[2];
    cam->matrix.m[9] = up[2];
    cam->matrix.m[10] = forward[2];
    cam->matrix.m[11] = 0;

    cam->matrix.m[12] = -dotf32(cam->from, side);
    cam->matrix.m[13] = -dotf32(cam->from, up);
    cam->matrix.m[14] = -dotf32(cam->from, forward);
    cam->matrix.m[15] = inttof32(1);
}

NE_Camera *NE_CameraCreate(void)
{
    if (!ne_camera_system_inited)
    {
        NE_DebugPrint("System not initialized");
        return NULL;
    }

    for (int i = 0; i < NE_MAX_CAMERAS; i++)
    {
        if (NE_UserCamera[i] != NULL)
            continue;

        NE_Camera *cam = calloc(1, sizeof(NE_Camera));
        if (cam == NULL)
        {
            NE_DebugPrint("Not enough memory");
            return NULL;
        }

        cam->to[2] = inttof32(1);
        cam->up[1] = inttof32(1);

        cam->matrix_is_updated = false;
        NE_UserCamera[i] = cam;

        return cam;
    }

    NE_DebugPrint("No free slots");

    return NULL;
}

void NE_CameraSetI(NE_Camera *cam, int xfrom, int yfrom, int zfrom,
           int xto, int yto, int zto, int xup, int yup, int zup)
{
    NE_AssertPointer(cam, "NULL pointer");

    cam->from[0] = xfrom;
    cam->from[1] = yfrom;
    cam->from[2] = zfrom;

    cam->to[0] = xto;
    cam->to[1] = yto;
    cam->to[2] = zto;

    cam->up[0] = xup;
    cam->up[1] = yup;
    cam->up[2] = zup;

    cam->matrix_is_updated = false;
}

void NE_CameraUse(NE_Camera *cam)
{
    NE_AssertPointer(cam, "NULL pointer");

    if (!cam->matrix_is_updated)
    {
        __NE_CameraUpdateMatrix(cam);
        cam->matrix_is_updated = true;
    }

    glLoadMatrix4x4(&cam->matrix);
}

ARM_CODE void NE_CameraMoveFreeI(NE_Camera *cam, int front, int right, int up)
{
    NE_AssertPointer(cam, "NULL pointer");

    cam->matrix_is_updated = false;

    int32_t vec_front[3], vec_right[3], vec_up[3];
    int32_t result[3] = { 0, 0, 0 };

    // Temporary vector used to normalize other vectors
    int temp[3];

    for (int i = 0; i < 3; i++)
        vec_front[i] = cam->to[i] - cam->from[i];

    if (front != 0)
    {
        for (int i = 0; i < 3; i++)
            temp[i] = vec_front[i];
        normalizef32((int32_t *) &temp);
        for (int i = 0; i < 3; i++)
            result[i] += mulf32(temp[i], front);
    }

    if (right != 0 || up != 0)
    {
        // The right vector is needed to get the up vector

        crossf32(vec_front, cam->up, vec_right);

        if (right != 0)
        {
            for (int i = 0; i < 3; i++)
                temp[i] = vec_right[i];
            normalizef32((int32_t *) &temp);
            for (int i = 0; i < 3; i++)
                result[i] += mulf32(temp[i], right);
        }

        if (up != 0)
        {
            crossf32(vec_right, vec_front, vec_up);
            // Last vector, not needed to use "temp"
            normalizef32((int32_t *) &vec_up);
            for (int i = 0; i < 3; i++)
                result[i] += mulf32(vec_up[i], up);
        }
    }

    for (int i = 0; i < 3; i++)
    {
        cam->from[i] += result[i];
        cam->to[i] += result[i];
    }
}

void NE_CameraMoveI(NE_Camera *cam, int x, int y, int z)
{
    NE_AssertPointer(cam, "NULL pointer");

    cam->matrix_is_updated = false;

    cam->from[0] += x;
    cam->from[1] += y;
    cam->from[2] += z;

    cam->to[0] += x;
    cam->to[1] += y;
    cam->to[2] += z;
}

ARM_CODE void NE_CameraRotate(NE_Camera *cam, int rx, int ry, int rz)
{
    NE_AssertPointer(cam, "NULL pointer");

    int cam_vector[3], result_vector[3];

    for (int i = 0; i < 3; i++)
        cam_vector[i] = cam->to[i] - cam->from[i];

    int sine, cosine;

    if (rx != 0)
    {
        cam->matrix_is_updated = false;

        sine = sinLerp(rx << 6);
        cosine = cosLerp(rx << 6);

        result_vector[1] = mulf32(cam_vector[1], cosine)
                         - mulf32(cam_vector[2], sine);
        result_vector[2] = mulf32(cam_vector[1], sine)
                         + mulf32(cam_vector[2], cosine);

        cam->to[1] = cam->from[1] + result_vector[1];
        cam->to[2] = cam->from[2] + result_vector[2];
    }

    if (ry != 0)
    {
        cam->matrix_is_updated = false;

        sine = sinLerp(ry << 6);
        cosine = cosLerp(ry << 6);

        result_vector[0] = mulf32(cam_vector[0], cosine)
                         - mulf32(cam_vector[2], sine);
        result_vector[2] = mulf32(cam_vector[0], sine)
                         + mulf32(cam_vector[2], cosine);

        cam->to[0] = cam->from[0] + result_vector[0];
        cam->to[2] = cam->from[2] + result_vector[2];
    }

    if (rz != 0)
    {
        cam->matrix_is_updated = false;

        sine = sinLerp(rz << 6);
        cosine = cosLerp(rz << 6);

        result_vector[0] = mulf32(cam_vector[0], cosine)
                         - mulf32(cam_vector[1], sine);
        result_vector[1] = mulf32(cam_vector[0], sine)
                         + mulf32(cam_vector[1], cosine);

        cam->to[0] = cam->from[0] + result_vector[0];
        cam->to[1] = cam->from[1] + result_vector[1];
    }
}

// Internal use only
ARM_CODE static void __NE_RotateVectorAxis(int32_t *vector, int angle,
                                           int x, int y, int z)
{
    // Vector must be an array of 3 ints with your vector in f32 format!
    int sin = sinLerp(angle << 6);
    int cos = cosLerp(angle << 6);

    int one_minus_cos = inttof32(1) - cos;

    int x_by_one_minus_cos = mulf32(one_minus_cos, x);
    int y_by_one_minus_cos = mulf32(one_minus_cos, y);
    int z_by_one_minus_cos = mulf32(one_minus_cos, z);

    int x_by_sin = mulf32(sin, x);
    int y_by_sin = mulf32(sin, y);
    int z_by_sin = mulf32(sin, z);

    int matrix[9];
    matrix[0] = mulf32(x, x_by_one_minus_cos) + cos;
    matrix[1] = mulf32(y, x_by_one_minus_cos) - z_by_sin;
    matrix[2] = mulf32(z, x_by_one_minus_cos) + y_by_sin;

    matrix[3] = mulf32(x, y_by_one_minus_cos) + z_by_sin;
    matrix[4] = mulf32(y, y_by_one_minus_cos) + cos;
    matrix[5] = mulf32(z, y_by_one_minus_cos) - x_by_sin;

    matrix[6] = mulf32(x, z_by_one_minus_cos) - y_by_sin;
    matrix[7] = mulf32(y, z_by_one_minus_cos) + x_by_sin;
    matrix[8] = mulf32(z, z_by_one_minus_cos) + cos;

    int result_vector[3];

    result_vector[0] = mulf32(vector[0], matrix[0])
                     + mulf32(vector[1], matrix[3])
                     + mulf32(vector[2], matrix[6]);
    result_vector[1] = mulf32(vector[0], matrix[1])
                     + mulf32(vector[1], matrix[4])
                     + mulf32(vector[2], matrix[7]);
    result_vector[2] = mulf32(vector[0], matrix[2])
                     + mulf32(vector[1], matrix[5])
                     + mulf32(vector[2], matrix[8]);

    for (int i = 0; i < 3; i++)
        vector[i] = result_vector[i];
}

void NE_CameraRotateAxisI(NE_Camera *cam, int angle, int x, int y, int z)
{
    NE_AssertPointer(cam, "NULL pointer");
    if (angle == 0)
        return;

    cam->matrix_is_updated = false;

    int32_t cam_vector[3];

    for (int i = 0; i < 3; i++)
        cam_vector[i] = cam->to[i] - cam->from[i];

    __NE_RotateVectorAxis(cam_vector, angle, x, y, z);

    for (int i = 0; i < 3; i++)
        cam->to[i] = cam->from[i] + cam_vector[i];
}

ARM_CODE void NE_CameraRotateFree(NE_Camera *cam, int rx, int ry, int rz)
{
    NE_AssertPointer(cam, "NULL pointer");

    if ((rx | ry | rz) == 0)
        return;

    cam->matrix_is_updated = false;

    int32_t vec_front[3], vec_right[3], vec_up[3];

    for (int i = 0; i < 3; i++)
        vec_front[i] = cam->to[i] - cam->from[i];

    if (ry || rx) {
        crossf32(vec_front, cam->up, vec_right);

        if (ry)
        {
            crossf32(vec_right, vec_front, vec_up);
            normalizef32(vec_up);
            NE_CameraRotateAxisI(cam, ry, vec_up[0], vec_up[1], vec_up[2]);
        }

        if (rx)
        {
            normalizef32(vec_right);
            NE_CameraRotateAxisI(cam, rx,
                                 vec_right[0], vec_right[1], vec_right[2]);
        }
    }

    if (rz)
    {
        normalizef32(vec_front);
        __NE_RotateVectorAxis(cam->up, rz,
                              vec_front[0], vec_front[1], vec_front[2]);
    }
}

void NE_CameraDelete(NE_Camera *cam)
{
    NE_AssertPointer(cam, "NULL pointer");

    for (int i = 0; i < NE_MAX_CAMERAS; i++)
    {
        if (NE_UserCamera[i] != cam)
            continue;

        NE_UserCamera[i] = NULL;
        free(cam);
        return;
    }

    NE_DebugPrint("Object not found");
}

int NE_CameraSystemReset(int max_cameras)
{
    if (ne_camera_system_inited)
        NE_CameraSystemEnd();

    if (max_cameras < 1)
        NE_MAX_CAMERAS = NE_DEFAULT_CAMERAS;
    else
        NE_MAX_CAMERAS = max_cameras;

    NE_UserCamera = calloc(NE_MAX_CAMERAS, sizeof(NE_UserCamera));
    if (NE_UserCamera == NULL)
    {
        NE_DebugPrint("Not enough memory");
        return -1;
    }

    ne_camera_system_inited = true;
    return 0;
}

void NE_CameraSystemEnd(void)
{
    if (!ne_camera_system_inited)
        return;

    for (int i = 0; i < NE_MAX_CAMERAS; i++)
    {
        if (NE_UserCamera[i])
            NE_CameraDelete(NE_UserCamera[i]);
    }

    free(NE_UserCamera);

    ne_camera_system_inited = false;
}

void NE_ViewPush(void)
{
    MATRIX_PUSH = 0;
}

void NE_ViewPop(void)
{
    MATRIX_POP = 1;
}

void NE_ViewMoveI(int x, int y, int z)
{
    MATRIX_TRANSLATE = x;
    MATRIX_TRANSLATE = y;
    MATRIX_TRANSLATE = z;
}

void NE_ViewRotate(int rx, int ry, int rz)
{
    if (rx != 0)
        glRotateXi(rx << 6);
    if (ry != 0)
        glRotateYi(ry << 6);
    if (rz != 0)
        glRotateZi(rz << 6);
}

void NE_ViewScaleI(int x, int y, int z)
{
    MATRIX_SCALE = x;
    MATRIX_SCALE = y;
    MATRIX_SCALE = z;
}
