// SPDX-License-Identifier: CC0-1.0
//
// SPDX-FileContributor: Antonio Niño Díaz, 2024

// Billboards are 2D objects drawn in a 3D world that always face the camera.
// For example, if you're drawing many spheres in a 3D environment, using 2D
// circles as billboards to represent the spheres can save you a lot of polygons
// and, depending on your situation, it won't look worse than drawing sphere
// models all over the place.

#include <stdio.h>
#include <math.h>

#include <nds.h>

// Header autogenerated for each PNG file inside GFXDIRS in the Makefile
#include "ball.h"
#include "neon.h"

__attribute__((noreturn)) void wait_forever(void)
{
    printf("Press START to exit.");

    while (1)
    {
        swiWaitForVBlank();

        scanKeys();
        if (keysHeld() & KEY_START)
            exit(1);
    }
}

int main(int argc, char **argv)
{
    int textureID[2];

    // Setup sub screen for the text console
    consoleDemoInit();

    videoSetMode(MODE_0_3D);

    glInit();

    glEnable(GL_TEXTURE_2D);
    glEnable(GL_ANTIALIAS);

    // The background must be fully opaque and have a unique polygon ID
    // (different from the polygons that are going to be drawn) so that
    // alpha blending works.
    glClearColor(10, 10, 10, 31);
    glClearPolyID(63);

    glClearDepth(0x7FFF);

    glViewport(0, 0, 255, 191);

    // Setup some VRAM as memory for textures and texture palettes
    vramSetBankA(VRAM_A_TEXTURE);
    vramSetBankF(VRAM_F_TEX_PALETTE);

    // Generate IDs for two textures
    glGenTextures(2, &textureID[0]);

    // Load ball texture
    glBindTexture(0, textureID[0]);
    if (glTexImageNtr2D(GL_RGB256, 32, 32,
                        TEXGEN_TEXCOORD | GL_TEXTURE_COLOR0_TRANSPARENT,
                        ballBitmap, NULL) == 0)
    {
        printf("Failed to load texture 1\n");
        wait_forever();
    }
    if (glColorTableNtr(ballPalLen / 2, ballPal) == 0)
    {
        printf("Failed to load palette 1\n");
        wait_forever();
    }

    // Load ground texture
    glBindTexture(0, textureID[1]);
    if (glTexImageNtr2D(GL_RGBA, 128, 128, TEXGEN_TEXCOORD, neonBitmap, NULL) == 0)
    {
        printf("Failed to load texture 2\n");
        wait_forever();
    }

    // Setup matrices
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(70, 256.0 / 192.0, 0.1, 40);

    int32_t camera_pos[3] = { floattof32(0), floattof32(2), floattof32(3) };
    int32_t ball_pos[3] = { floattof32(0), floattof32(0), floattof32(0) };

    int32_t ball_size = 1;

    // Clear console
    consoleClear();

    // Print some controls
    printf("L/R/X/B: Move camera\n");
    printf("PAD:     Move ball\n");
    printf("SELECT:  Change ball size\n");
    printf("START:   Exit to loader\n");
    printf("\n");

    while (1)
    {
        // Synchronize game loop to the screen refresh
        swiWaitForVBlank();

        // Setup camera and get its matrix
        // -------------------------------

        glMatrixMode(GL_MODELVIEW);

        glLoadIdentity();

        gluLookAtf32(camera_pos[0], camera_pos[1], camera_pos[2], // Position
                     0, 0, 0,                                     // Look at
                     0, floattof32(1.0), 0);                      // Up

        // Get 3x3 modelview matrix. We will extract its vectors to use them for
        // our calculations later. Note that the vectors are normalized, so we
        // can invert the matrix by transposing it.
        int matrix[9];
        glGetFixed(GL_GET_MATRIX_VECTOR, matrix);

        int32_t right[3] = { matrix[0], matrix[3], matrix[6] };
        int32_t up[3] =    { matrix[1], matrix[4], matrix[7] };
        int32_t front[3] = { matrix[2], matrix[5], matrix[8] };

        // Create the inverse matrix of the vector matrix
        const m3x3 to_billboard_space = {{
             right[0],  right[1],  right[2],
                up[0],     up[1],     up[2],
            -front[0], -front[1], -front[2],
        }};

        // Handle user input
        // -----------------

        scanKeys();

        uint16_t keys = keysHeld();
        if (keys & KEY_START)
            break;

        uint16_t keys_down = keysDown();
        if (keys_down & KEY_SELECT)
        {
            if (ball_size == 2)
                ball_size = 1;
            else
                ball_size = 2;
        }

        // Move camera left/right and front/back from the camera's perspective

        int32_t front_speed = 0;
        int32_t side_speed = 0;

        if (keys & KEY_X)
            front_speed = floattof32(-0.05);
        if (keys & KEY_B)
            front_speed = floattof32(0.05);

        if (keys & KEY_L)
            side_speed = floattof32(-0.05);
        if (keys & KEY_R)
            side_speed = floattof32(0.05);

        camera_pos[0] += mulf32(front_speed, front[0]) + mulf32(side_speed, right[0]);
        camera_pos[1] += mulf32(front_speed, front[1]) + mulf32(side_speed, right[1]);
        camera_pos[2] += mulf32(front_speed, front[2]) + mulf32(side_speed, right[2]);

        // Move the ball in regular world's coordinates

        if (keys & KEY_LEFT)
            ball_pos[0] += floattov16(-0.05);
        if (keys & KEY_RIGHT)
            ball_pos[0] += floattov16(0.05);

        if (keys & KEY_UP)
            ball_pos[2] += floattov16(-0.05);
        if (keys & KEY_DOWN)
            ball_pos[2] += floattov16(0.05);

        // Draw 3D scene
        // -------------

        // Draw billboarded ball

        glPolyFmt(POLY_ALPHA(31) | POLY_CULL_NONE);

        glBindTexture(0, textureID[0]);

        glColor3f(1, 1, 1);

        glPushMatrix();

            // Move to the position of the ball first
            glTranslate3f32(ball_pos[0], ball_pos[1], ball_pos[2]);

            // Rotate the world so that the axes are in billboard space
            glMultMatrix3x3(&to_billboard_space);

            // Now we can scale the billboard if we want
            glScalef32(inttof32(ball_size), inttof32(ball_size), inttof32(1));

            glBegin(GL_QUADS);

                glTexCoord2t16(inttot16(32), 0);
                glVertex3v16(floattov16(-0.5), floattov16(1), 0);

                glTexCoord2t16(0, 0);
                glVertex3v16(floattov16(0.5), floattov16(1), 0);

                glTexCoord2t16(0, inttot16(32));
                glVertex3v16(floattov16(0.5), 0, 0);

                glTexCoord2t16(inttot16(32), inttot16(32));
                glVertex3v16(floattov16(-0.5), 0, 0);

            glEnd();

        glPopMatrix(1);

        // Draw ground

        glBindTexture(0, textureID[1]);

        glColor3f(1, 1, 1);

        glBegin(GL_QUADS);

            glTexCoord2t16(0, 0);
            glVertex3f(-2.0, 0.0, 2.0);

            glTexCoord2t16(0, inttot16(128));
            glVertex3f(2.0, 0.0, 2.0);

            glTexCoord2t16(inttot16(128), inttot16(128));
            glVertex3f(2.0, 0.0, -2.0);

            glTexCoord2t16(inttot16(128), 0);
            glVertex3f(-2.0, 0.0, -2.0);

        glEnd();

        // Draw X, Y and Z axis

        glBindTexture(0, 0); // Don't use a texture

        glBegin(GL_TRIANGLES);

            glColor3f(1, 0, 0);

            glVertex3f(0, 0, 0);
            glVertex3f(5, 0, 0);
            glVertex3f(5, 0, 0);

            glColor3f(0, 1, 0);

            glVertex3f(0, 0, 0);
            glVertex3f(0, 5, 0);
            glVertex3f(0, 5, 0);

            glColor3f(0, 0, 1);

            glVertex3f(0, 0, 0);
            glVertex3f(0, 0, 5);
            glVertex3f(0, 0, 5);

        glEnd();

        glFlush(0);
    }

    glDeleteTextures(2, &textureID[0]);

    return 0;
}
