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

// This example shows how to draw translucent polygons the right way. It
// calculates the distance from the camera to all of the objects in the scene,
// it sorts them, and it draws each one of them with a different polygon ID so
// that alpha blending works between them. Back faces are culled because they
// are part of the ball model and they can't be sorted with this system.
//
// You can also break the alpha blending effect.
//
// - You can revert the sorting order by pressing A to see what happens when you
//   don't do the right thing.
// - You can use one single polygon ID instead of multiple polygon IDs to see
//   how translucency breaks.
// - If you want to see translucent objects correctly you need to use back
//   culling because you can't sort the polygons of a model from back to front
//   easily (they are in a display list). Front culling also works, if you want
//   a specific effect. However, "cull none" will cause things to be displayed
//   incorrectly because polygons aren't drawn in the right order.

#include <stdlib.h>

#include <nds.h>
#include <nds/arm9/postest.h>

// Headers autogenerated from the files in BINDIRS in the Makefile
#include "sphere_bin.h"

typedef struct {
    int32_t x, y, z;
    int32_t distance;
} BallInfo;

#define NUM_BALLS 10

BallInfo Ball[NUM_BALLS];

void balls_initialize(void)
{
    for (int i = 0; i < NUM_BALLS; i++)
    {
        // Random coordinates from -4 to +4 so that they fit in a v16
        Ball[i].x = (rand() & (inttof32(8) - 1)) - inttof32(4);
        Ball[i].y = (rand() & (inttof32(8) - 1)) - inttof32(4);
        Ball[i].z = (rand() & (inttof32(8) - 1)) - inttof32(4);
    }
}

void balls_calculate_distance(void)
{
    for (int i = 0; i < NUM_BALLS; i++)
    {
        v16 x = f32tov16(Ball[i].x);
        v16 y = f32tov16(Ball[i].y);
        v16 z = f32tov16(Ball[i].z);

        PosTest(x, y, z);

        Ball[i].distance = PosTestWresult();
    }
}

static int is_ball_closer(const void *a, const void *b)
{
    const BallInfo *a_ = a;
    const BallInfo *b_ = b;

    return b_->distance - a_->distance;
}

static int is_ball_further_away(const void *a, const void *b)
{
    const BallInfo *a_ = a;
    const BallInfo *b_ = b;

    return a_->distance - b_->distance;
}

void balls_sort_by_distance(void)
{
    qsort(&Ball[0], NUM_BALLS, sizeof(BallInfo), is_ball_closer);
}

void balls_sort_by_reverse_distance(void)
{
    qsort(&Ball[0], NUM_BALLS, sizeof(BallInfo), is_ball_further_away);
}

void balls_draw(bool different_ids, int cull_mode)
{
    for (int i = 0; i < NUM_BALLS; i++)
    {
        glPushMatrix();
        glTranslatef32(Ball[i].x, Ball[i].y, Ball[i].z);

        // Each ball needs a different polygon ID
        int id;
        if (different_ids)
            id = 10 + i;
        else
            id = 10;

        glPolyFmt(POLY_ALPHA(20) | cull_mode | POLY_FORMAT_LIGHT0 | POLY_ID(id));
        glCallList(sphere_bin);

        glPopMatrix(1);
    }
}

int main(int argc, char *argv[])
{
    // Initialize GL
    glInit();

    consoleDemoInit();

    // Set mode 0, enable BG0 and set it to 3D
    videoSetMode(MODE_0_3D);

    // Set BG to black. Alpha must not be zero for fog to affect the rear plane
    glClearColor(0, 0, 0, 31);
    // The BG and polygons will have the same ID unless a polygon is highlighted
    glClearPolyID(0);
    glClearDepth(0x7FFF);

    // Set the viewport to fullscreen
    glViewport(0, 0, 255, 191);

    // Enable alpha blending
    glEnable(GL_BLEND);

    // Setup the light
    glLight(0, RGB15(31, 31, 31), 0, 0, floattov10(-1.0));

    // Setup some material properties
    glMaterialf(GL_AMBIENT, RGB15(8, 8, 8));
    glMaterialf(GL_DIFFUSE, RGB15(24, 24, 24));
    glMaterialf(GL_SPECULAR, RGB15(0, 0, 0));
    glMaterialf(GL_EMISSION, RGB15(0, 0, 0));

    // Setup the projection matrix for regular drawing
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(60, 256.0 / 192.0, 0.1, 20);

    // Use the modelview matrix while drawing
    glMatrixMode(GL_MODELVIEW);

    u32 rotateX = 0;
    u32 rotateY = 0;

    balls_initialize();

    consoleClear();

    printf("PAD:   Rotate\n");
    printf("A:     Revert sorting order\n");
    printf("B:     Use the same poly ID\n");
    printf("Y:     Set cull none\n");
    printf("X:     Set cull front\n");
    printf("(Default: cull back)\n");
    printf("\n");
    printf("START: Exit to loader");

    while (1)
    {
        // Handle key input
        scanKeys();
        u16 keys_held = keysHeld();

        // Rotate balls
        if (keys_held & KEY_UP)
            rotateX += 3 << 5;
        if (keys_held & KEY_DOWN)
            rotateX -= 3 << 5;
        if (keys_held & KEY_LEFT)
            rotateY += 3 << 5;
        if (keys_held & KEY_RIGHT)
            rotateY -= 3 << 5;

        // Save the state of the current matrix(the modelview matrix)
        glLoadIdentity();

        // Setup the camera
        gluLookAt(0.0, 0.0, 8.0, // Camera position
                  0.0, 0.0, 0.0,  // Look at
                  0.0, 1.0, 0.0); // Up

        glRotateXi(rotateX);
        glRotateYi(rotateY);

        // Draw the scene

        balls_calculate_distance();

        if (keys_held & KEY_A)
            balls_sort_by_reverse_distance();
        else
            balls_sort_by_distance();

        bool different_ids;
        if (keys_held & KEY_B)
            different_ids = false;
        else
            different_ids = true;

        int cull_mode;
        if (keys_held & KEY_Y)
            cull_mode = POLY_CULL_NONE;
        else if (keys_held & KEY_X)
            cull_mode = POLY_CULL_FRONT;
        else
            cull_mode = POLY_CULL_BACK;

        balls_draw(different_ids, cull_mode);

        // Sort transparent polygons manually, use w-buffer instead of z-buffer
        glFlush(GL_TRANS_MANUALSORT | GL_WBUFFERING);

        swiWaitForVBlank();

        if (keys_held & KEY_START)
            break;
    }

    return 0;
}
