// SPDX-License-Identifier: MIT
//
// Copyright (c) 2009-2014 Cesar Rincon "NightFox"
// Copyright (c) 2023 Antonio Niño Díaz "AntonioND"
//
// NightFox LIB - 256 color sprites functions
// http://www.nightfoxandco.com/

#include <stdarg.h>
#include <stdio.h>
#include <string.h>

#include <nds.h>

#include "nf_2d.h"
#include "nf_basic.h"
#include "nf_sprite256.h"

// Buffers to store the sprite graphics and palettes
char *NF_BUFFER_SPR256GFX[NF_SLOTS_SPR256GFX];
char *NF_BUFFER_SPR256PAL[NF_SLOTS_SPR256PAL];

// Information structures for sprite graphics and palettes
NF_TYPE_SPR256GFX_INFO NF_SPR256GFX[NF_SLOTS_SPR256GFX];
NF_TYPE_SPR256PAL_INFO NF_SPR256PAL[NF_SLOTS_SPR256PAL];

// Structures for sprite graphics and palettes stored in VRAM
NF_TYPE_SPR256VRAM_INFO NF_SPR256VRAM[2][128];
NF_TYPE_SPRPALSLOT_INFO NF_SPRPALSLOT[2][16];

// Structure with OAM information of sprites
NF_TYPE_SPRITEOAM_INFO NF_SPRITEOAM[2][128];

// Main VRAM control structure for sprites
NF_TYPE_SPRVRAM_INFO NF_SPRVRAM[2];

void NF_InitSpriteBuffers(void)
{
    // Initialize graphics buffers
    for (int n = 0; n < NF_SLOTS_SPR256GFX; n++)
    {
        NF_BUFFER_SPR256GFX[n] = NULL;
        NF_SPR256GFX[n].size = 0;
        NF_SPR256GFX[n].width = 0;
        NF_SPR256GFX[n].height = 0;
        NF_SPR256GFX[n].available = true; // Mark as available
    }

    // Initialize palette buffers
    for (int n = 0; n < NF_SLOTS_SPR256PAL; n++)
    {
        NF_BUFFER_SPR256PAL[n] = NULL;
        NF_SPR256PAL[n].size = 0;
        NF_SPR256PAL[n].available = true; // Mark as available
    }
}

void NF_ResetSpriteBuffers(void)
{
    // Free graphics and palette buffers
    for (int n = 0; n < NF_SLOTS_SPR256GFX; n++)
        free(NF_BUFFER_SPR256GFX[n]);

    for (int n = 0; n < NF_SLOTS_SPR256PAL; n++)
        free(NF_BUFFER_SPR256PAL[n]);

    // Reset sprite system
    NF_InitSpriteBuffers();
}

void NF_InitSpriteSys(int screen, ...)
{
    va_list options;
    va_start(options, screen);
    u8 mode = va_arg(options, int);
    va_end(options);

    // Initialize sprite allocator and OAM information
    for (int n = 0; n < 128; n++)
    {
        // Graphics in VRAM
        NF_SPR256VRAM[screen][n].size = 0;
        NF_SPR256VRAM[screen][n].width = 0;
        NF_SPR256VRAM[screen][n].height = 0;
        NF_SPR256VRAM[screen][n].address = 0;
        NF_SPR256VRAM[screen][n].ramid = 0;
        NF_SPR256VRAM[screen][n].framesize = 0;
        NF_SPR256VRAM[screen][n].lastframe = 0;
        NF_SPR256VRAM[screen][n].keepframes = false;
        NF_SPR256VRAM[screen][n].inuse = false; // Mark as not in use

        // OAM
        NF_SPRITEOAM[screen][n].index = n;
        NF_SPRITEOAM[screen][n].x = 0;
        NF_SPRITEOAM[screen][n].y = 0;
        NF_SPRITEOAM[screen][n].layer = 0;
        NF_SPRITEOAM[screen][n].pal = 0;
        NF_SPRITEOAM[screen][n].size = SpriteSize_8x8;
        NF_SPRITEOAM[screen][n].color = SpriteColorFormat_256Color;
        NF_SPRITEOAM[screen][n].gfx = NULL;
        NF_SPRITEOAM[screen][n].rot = -1;
        NF_SPRITEOAM[screen][n].doublesize = false;
        NF_SPRITEOAM[screen][n].hide = true;
        NF_SPRITEOAM[screen][n].hflip = false;
        NF_SPRITEOAM[screen][n].vflip = false;
        NF_SPRITEOAM[screen][n].mosaic = false;
        NF_SPRITEOAM[screen][n].gfxid = 0;
        NF_SPRITEOAM[screen][n].frame = 0;
        NF_SPRITEOAM[screen][n].framesize = 0;
        NF_SPRITEOAM[screen][n].lastframe = 0;
        NF_SPRITEOAM[screen][n].created = false; // Mark sprite as not created
    }

    // Initialize VRAM allocator
    if (mode == 128)
        NF_SPRVRAM[screen].max = 131072;
    else
        NF_SPRVRAM[screen].max = 65536;

    NF_SPRVRAM[screen].free = NF_SPRVRAM[screen].max; // All memory is available
    NF_SPRVRAM[screen].last = 0;
    NF_SPRVRAM[screen].deleted = 0;
    NF_SPRVRAM[screen].fragmented = 0;
    NF_SPRVRAM[screen].inarow = NF_SPRVRAM[screen].max; // All memory is contiguous
    for (int n = 0; n < 128; n++)
    {
        NF_SPRVRAM[screen].pos[n] = 0;
        NF_SPRVRAM[screen].size[n] = 0;
    }

    // Initialize palette information
    for (int n = 0; n < 16; n++)
    {
        NF_SPRPALSLOT[screen][n].inuse = false;
        NF_SPRPALSLOT[screen][n].ramslot = 0;
    }

    // Setup 2D engine and VRAM banks of the specified screen
    if (screen == 0)
    {
        REG_DISPCNT |= DISPLAY_SPR_ACTIVE; // Enable sprites in the top screen
        vramSetBankB(VRAM_B_LCD); // Enable CPU writes to VRAM_B
        memset((void *)0x06400000, 0, 131072); // Clear VRAM_B
        vramSetBankB(VRAM_B_MAIN_SPRITE_0x06400000); // VRAM_B is used for sprites (128 KB)
        NF_SPRVRAM[screen].next = 0x06400000;
        vramSetBankF(VRAM_F_LCD); // VRAM_F used for sprite ext. palettes (8 / 16 KB)
        memset((void *)0x06890000, 0, 8192); // Clear VRAM_F

        // Enable OAM with 128 or 64 mapping and extended palettes enabled
        if (mode == 128)
            oamInit(&oamMain, SpriteMapping_1D_128, true);
        else
            oamInit(&oamMain, SpriteMapping_1D_64, true);
    }
    else
    {
        REG_DISPCNT_SUB |= DISPLAY_SPR_ACTIVE; // Enable sprites in the bottom screen
        vramSetBankD(VRAM_D_LCD); // Enable CPU writes to VRAM_D
        memset((void *)0x06600000, 0, 131072); // Clear VRAM_D
        vramSetBankD(VRAM_D_SUB_SPRITE);
        NF_SPRVRAM[screen].next = 0x06600000;
        vramSetBankI(VRAM_I_LCD); // VRAM_I used for sprite ext. palettes (8 / 16 KB)
        memset((void *)0x068A0000, 0, 8192); // Clear VRAM_I

        // Enable OAM with 128 or 64 mapping and extended palettes enabled
        if (mode == 128)
            oamInit(&oamSub, SpriteMapping_1D_128, true);
        else
            oamInit(&oamSub, SpriteMapping_1D_64, true);
    }
}

void NF_LoadSpriteGfx(const char *file, u32 id, u32 width, u32 height)
{
    if (id >= NF_SLOTS_SPR256GFX)
        NF_Error(106, "Sprite graphics", NF_SLOTS_SPR256GFX);

    if (!NF_SPR256GFX[id].available)
        NF_Error(109, "Sprite graphics", id);

    // Free the buffer if it was in use
    free(NF_BUFFER_SPR256GFX[id]);
    NF_BUFFER_SPR256GFX[id] = NULL;

    // File path
    char filename[256];

    // Load .IMG file
    snprintf(filename, sizeof(filename), "%s/%s.img", NF_ROOTFOLDER, file);
    NF_FileLoad(filename, &NF_BUFFER_SPR256GFX[id], &NF_SPR256GFX[id].size, 0);

    // Save information about the graphics
    NF_SPR256GFX[id].width = width;
    NF_SPR256GFX[id].height = height;

    NF_SPR256GFX[id].available = false; // Mark ID as being in use
}

void NF_UnloadSpriteGfx(u32 id)
{
    if (id >= NF_SLOTS_SPR256GFX)
        NF_Error(106, "Sprite graphics", NF_SLOTS_SPR256GFX);

    if (NF_SPR256GFX[id].available)
        NF_Error(110, "Sprite graphics", id);

    // Free buffer
    free(NF_BUFFER_SPR256GFX[id]);

    // Reset variables
    NF_BUFFER_SPR256GFX[id] = NULL;
    NF_SPR256GFX[id].size = 0;
    NF_SPR256GFX[id].width = 0;
    NF_SPR256GFX[id].height = 0;
    NF_SPR256GFX[id].available = true; // Mark as available
}

void NF_LoadSpritePal(const char *file, u32 id)
{
    if (id >= NF_SLOTS_SPR256PAL)
        NF_Error(106, "Sprite palette", NF_SLOTS_SPR256PAL);

    if (!NF_SPR256PAL[id].available)
        NF_Error(109, "Sprite palette", id);

    // Free buffer if it was used already
    free(NF_BUFFER_SPR256PAL[id]);
    NF_BUFFER_SPR256PAL[id] = NULL;

    // FIle path
    char filename[256];

    // Load .PAL file, and allocate space for at least 256 colors
    snprintf(filename, sizeof(filename), "%s/%s.pal", NF_ROOTFOLDER, file);
    NF_FileLoad(filename, &NF_BUFFER_SPR256PAL[id], &NF_SPR256PAL[id].size, 256 * 2);

    // Mark it as being in use
    NF_SPR256PAL[id].available = false;
}

void NF_UnloadSpritePal(u32 id)
{
    if (id >= NF_SLOTS_SPR256PAL)
        NF_Error(106, "Sprite palette", NF_SLOTS_SPR256PAL);

    if (NF_SPR256PAL[id].available)
        NF_Error(110, "Sprite palette", id);

    // Free buffer
    free(NF_BUFFER_SPR256PAL[id]);

    // Reset variables
    NF_BUFFER_SPR256PAL[id] = NULL;
    NF_SPR256PAL[id].size = 0;
    NF_SPR256PAL[id].available = true; // Mark slot as free
}

void NF_VramSpriteGfx(int screen, u32 ram, u32 vram, bool keepframes)
{
    if (ram >= NF_SLOTS_SPR256GFX)
        NF_Error(106, "Sprite graphics", (NF_SLOTS_SPR256GFX - 1));

    if (NF_SPR256GFX[ram].available)
        NF_Error(110, "Sprite graphics", ram);

    if (vram > 127)
        NF_Error(106, "VRAM graphics", 127);

    if (NF_SPR256VRAM[screen][vram].inuse)
        NF_Error(109, "VRAM", vram);

    s16 id = 255; // This will hold the ID of a free slot

    // Calculate the size of one frame. One time is 64 bytes (8 * 8)
    u32 width = NF_SPR256GFX[ram].width / 8;
    u32 height = NF_SPR256GFX[ram].height / 8;
    NF_SPR256VRAM[screen][vram].framesize = (width * height) * 64;

    // Calculate the last frame of the animation
    NF_SPR256VRAM[screen][vram].lastframe =
            (NF_SPR256GFX[ram].size / NF_SPR256VRAM[screen][vram].framesize) - 1;
    NF_SPR256VRAM[screen][vram].inuse = true; // Mark this slot as being in use

    // Calculate the size of the graphics to copy depending on whether all
    // frames need to be copied or not.
    u32 gfxsize;
    if (keepframes) // Copy the first frame only
        gfxsize = NF_SPR256VRAM[screen][vram].framesize;
    else // Copy all frames
        gfxsize = NF_SPR256GFX[ram].size;

    // Update the available VRAM
    NF_SPRVRAM[screen].free -= gfxsize;
    if (NF_SPRVRAM[screen].free < 0) // If there isn't enough VRAM, fail
        NF_Error(113, "Sprites", gfxsize);

    // Try to reuse any deleted block
    if (NF_SPRVRAM[screen].deleted > 0)
    {
        // First, try to find any block with the same size
        for (int n = 0; n < NF_SPRVRAM[screen].deleted; n++)
        {
            if (NF_SPRVRAM[screen].size[n] == gfxsize)
            {
                id = n;
                break;
            }
        }

        // If not, try to find any block that is big enough, fragmenting it
        if (id == 255)
        {
            for (int n = 0; n < NF_SPRVRAM[screen].deleted; n++)
            {
                if (NF_SPRVRAM[screen].size[n] > gfxsize)
                {
                    id = n;
                    break;
                }
            }
        }
    }

    if (id != 255)
    {
        // We have managed to reuse a deleted block, determine if we need to
        // reorganize the VRAM allocation structures.

        bool organize = true; // Do we have to reorganize the free blocks array?

        // Copy graphics to VRAM
        NF_DmaMemCopy((void *)NF_SPRVRAM[screen].pos[id], NF_BUFFER_SPR256GFX[ram], gfxsize);
        // Save the destination address
        NF_SPR256VRAM[screen][vram].address = NF_SPRVRAM[screen].pos[id];

        // If not all the size has been used, split this block into used and free
        if (gfxsize < NF_SPRVRAM[screen].size[id])
        {
            // Calculate the new free space
            u32 size = NF_SPRVRAM[screen].size[id] - gfxsize;

            // Update data
            NF_SPRVRAM[screen].pos[id] += gfxsize; // New free space address
            NF_SPRVRAM[screen].size[id] = size;    // New free space size
            NF_SPRVRAM[screen].fragmented -= gfxsize; // Reduce the number or fragmented bytes
            organize = false; // We don't need to reorganize anything after this
        }
        else
        {
            // If all the size has been used, simply reduce the counter of
            // fragmented bytes.
            NF_SPRVRAM[screen].fragmented -= NF_SPRVRAM[screen].size[id];
        }

        // Do we have to reorganize the array of free blocks?
        if (organize)
        {
            int last_reuse = NF_SPRVRAM[screen].deleted - 1;

            // If there are more than one deleted blocks and this isn't the last
            // position.
            if ((last_reuse > 0) && (id != last_reuse))
            {
                // Copy the values of the last position in this one
                NF_SPRVRAM[screen].pos[id] = NF_SPRVRAM[screen].pos[last_reuse];
                NF_SPRVRAM[screen].size[id] = NF_SPRVRAM[screen].size[last_reuse];
            }
            NF_SPRVRAM[screen].deleted--; // Update counter of free blocks
        }

    }
    else
    {
        // We couldn't reuse any deleted block, add the new graphics to the end
        // of the used VRAM.

        // Update the contiguous available VRAM (the last block at the end)
        NF_SPRVRAM[screen].inarow -= gfxsize;

        // If there isn't enough VRAM, fail
        if (NF_SPRVRAM[screen].inarow < 0)
            NF_Error(113, "Sprites", gfxsize);

        // Copy graphics to VRAM
        NF_DmaMemCopy((void *)NF_SPRVRAM[screen].next, NF_BUFFER_SPR256GFX[ram], gfxsize);

        // Copy location and size in VRAM
        NF_SPR256VRAM[screen][vram].address = NF_SPRVRAM[screen].next;
        NF_SPRVRAM[screen].last = NF_SPRVRAM[screen].next;

        // Calculate the next available address
        NF_SPRVRAM[screen].next += gfxsize;
    }

    // Save graphics information
    NF_SPR256VRAM[screen][vram].size = gfxsize;
    NF_SPR256VRAM[screen][vram].width = NF_SPR256GFX[ram].width;
    NF_SPR256VRAM[screen][vram].height = NF_SPR256GFX[ram].height;
    NF_SPR256VRAM[screen][vram].ramid = ram; // Source slot in RAM
    NF_SPR256VRAM[screen][vram].keepframes = keepframes;
}

void NF_FreeSpriteGfx(int screen, u32 id)
{
    if (!NF_SPR256VRAM[screen][id].inuse)
        NF_Error(110, "Sprite graphics", id);

    // Clear graphics from VRAM
    memset((void *)NF_SPR256VRAM[screen][id].address, 0, NF_SPR256VRAM[screen][id].size);

    // Update free VRAM
    NF_SPRVRAM[screen].free += NF_SPR256VRAM[screen][id].size;

    // Save address and size of the deleted block to be reused
    NF_SPRVRAM[screen].pos[NF_SPRVRAM[screen].deleted] = NF_SPR256VRAM[screen][id].address;
    NF_SPRVRAM[screen].size[NF_SPRVRAM[screen].deleted] = NF_SPR256VRAM[screen][id].size;

    // Increment counter of deleted blocks
    NF_SPRVRAM[screen].deleted++;

    // Increment counter of fragmented memory
    NF_SPRVRAM[screen].fragmented += NF_SPR256VRAM[screen][id].size;

    // Reset graphics state
    NF_SPR256VRAM[screen][id].size = 0;
    NF_SPR256VRAM[screen][id].width = 0;
    NF_SPR256VRAM[screen][id].height = 0;
    NF_SPR256VRAM[screen][id].address = 0;
    NF_SPR256VRAM[screen][id].framesize = 0;
    NF_SPR256VRAM[screen][id].lastframe = 0;
    NF_SPR256VRAM[screen][id].inuse = false;

    // If the memory is too fragmented, defragment it
    if (NF_SPRVRAM[screen].fragmented >= (NF_SPRVRAM[screen].inarow / 2))
        NF_VramSpriteGfxDefrag(screen);
}

void NF_VramSpriteGfxDefrag(int screen)
{
    // Allocate temporary buffer with all the currently used VRAM
    u32 used_vram = (NF_SPRVRAM[screen].max - NF_SPRVRAM[screen].free) + 1;
    char *buffer = malloc(used_vram);
    if (buffer == NULL) // Not enough RAM
        NF_Error(102, NULL, used_vram);

    char *address[128];     // RAM address of each sprite
    u32 size[128];          // Size of buffer
    u32 ram = 0;            // Pointer to the current RAM address
    u32 frame_address = 0;  // Address of the current frame

    // Copy data from VRAM to the temporary buffer in RAM
    for (int n = 0; n < 128; n++)
    {
        // Copy all used graphics to the temporary buffer
        if (NF_SPR256VRAM[screen][n].inuse)
        {
            address[n] = buffer + ram;
            size[n] = NF_SPR256VRAM[screen][n].size;
            NF_DmaMemCopy(address[n], (void *)NF_SPR256VRAM[screen][n].address, size[n]);
            ram += size[n];
        }
    }

    // Reset VRAM allocation information
    NF_SPRVRAM[screen].free = NF_SPRVRAM[screen].max;
    NF_SPRVRAM[screen].last = 0;
    NF_SPRVRAM[screen].deleted = 0;
    NF_SPRVRAM[screen].fragmented = 0;
    NF_SPRVRAM[screen].inarow = NF_SPRVRAM[screen].max;

    for (int n = 0; n < 128; n++)
    {
        NF_SPRVRAM[screen].pos[n] = 0;
        NF_SPRVRAM[screen].size[n] = 0;
    }

    if (screen == 0)
        NF_SPRVRAM[screen].next = 0x06400000;
    else
        NF_SPRVRAM[screen].next = 0x06600000;

    // Now copy all data from the temporary buffer to VRAM
    for (int n = 0; n < 128; n++)
    {
        if (NF_SPR256VRAM[screen][n].inuse)
        {
            NF_DmaMemCopy((void*)NF_SPRVRAM[screen].next, address[n], size[n]);
            NF_SPR256VRAM[screen][n].address = NF_SPRVRAM[screen].next;

            NF_SPRVRAM[screen].free -= size[n];
            NF_SPRVRAM[screen].inarow -= size[n];
            NF_SPRVRAM[screen].last = NF_SPRVRAM[screen].next;
            NF_SPRVRAM[screen].next += size[n];
        }
    }

    // Link the new graphics addresses to the sprites
    for (int n = 0; n < 128; n++)
    {
        if (NF_SPRITEOAM[screen][n].created)
        {
            if (NF_SPR256VRAM[screen][NF_SPRITEOAM[screen][n].gfxid].keepframes)
            {
                // If keepframes is enabled, only assign the base address of the
                // graphics
                NF_SPRITEOAM[screen][n].gfx =
                    (u32 *)NF_SPR256VRAM[screen][NF_SPRITEOAM[screen][n].gfxid].address;
            }
            else
            {
                // If keepframes is disabled calculate the offset of the
                // graphics
                frame_address = NF_SPR256VRAM[screen][NF_SPRITEOAM[screen][n].gfxid].address
                    + (NF_SPRITEOAM[screen][n].framesize * NF_SPRITEOAM[screen][n].frame);
                NF_SPRITEOAM[screen][n].gfx = (u32 *)frame_address;
            }
        }
    }

    // Free temporary buffer
    free(buffer);
}

void NF_VramSpritePal(int screen, u32 id, u32 slot)
{
    if (id >= NF_SLOTS_SPR256PAL)
        NF_Error(106, "Sprite palette", NF_SLOTS_SPR256PAL);

    if (NF_SPR256PAL[id].available)
        NF_Error(110, "Sprite palette", id);

    if (slot > 15)
        NF_Error(106, "Sprite palette slot", 15);

    // Copy palette to VRAM
    if (screen == 0)
    {
        u32 address = 0x06890000 + (slot * 256 * 2);
        vramSetBankF(VRAM_F_LCD); // Enable CPU writes to VRAM_F
        NF_DmaMemCopy((void *)address, NF_BUFFER_SPR256PAL[id], NF_SPR256PAL[id].size);
        vramSetBankF(VRAM_F_SPRITE_EXT_PALETTE);
    }
    else
    {
        u32 address = 0x068A0000 + (slot * 256 * 2);
        vramSetBankI(VRAM_I_LCD); // Enable CPU writes to VRAM_I
        NF_DmaMemCopy((void *)address, NF_BUFFER_SPR256PAL[id], NF_SPR256PAL[id].size);
        vramSetBankI(VRAM_I_SUB_SPRITE_EXT_PALETTE);
    }

    NF_SPRPALSLOT[screen][slot].inuse = true; // Mark slot as in use
    NF_SPRPALSLOT[screen][slot].ramslot = id; // Save slot where the palette is stored
}

void NF_CreateSprite(int screen, u32 id, u32 gfx, u32 pal, s32 x, s32 y)
{
    if (id > 127)
        NF_Error(106, "Sprite ID", 127);

    if (gfx > 127)
        NF_Error(106, "Sprite graphics", 127);

    if (!NF_SPR256VRAM[screen][gfx].inuse)
        NF_Error(111, "Sprite graphics", gfx);

    if (pal > 15)
        NF_Error(106, "Sprite palette slot", 15);

    if (!NF_SPRPALSLOT[screen][pal].inuse)
        NF_Error(111, "Sprite palette", pal);

    NF_SPRITEOAM[screen][id].index = id;
    NF_SPRITEOAM[screen][id].gfx = (u32 *)NF_SPR256VRAM[screen][gfx].address; // VRAM address
    NF_SPRITEOAM[screen][id].pal = pal;
    NF_SPRITEOAM[screen][id].x = x;
    NF_SPRITEOAM[screen][id].y = y;
    NF_SPRITEOAM[screen][id].color = SpriteColorFormat_256Color;
    NF_SPRITEOAM[screen][id].hide = false; // Show sprite
    NF_SPRITEOAM[screen][id].gfxid = gfx; // Graphics index
    NF_SPRITEOAM[screen][id].created = true; // Mark sprite as created

    if ((NF_SPR256VRAM[screen][gfx].width == 8) && (NF_SPR256VRAM[screen][gfx].height == 8))
    {
        if (NF_SPRVRAM[screen].max != 131072) // In mode 1D_128 this size is invalid
            NF_SPRITEOAM[screen][id].size = SpriteSize_8x8;
        else
            NF_Error(120, NULL, id);
    }
    else
    {
        struct
        {
            int x, y;
            SpriteSize size;
        }
        size_array[] =
        {
            { 16, 16, SpriteSize_16x16 },
            { 32, 32, SpriteSize_32x32 },
            { 64, 64, SpriteSize_64x64 },
            { 16, 8, SpriteSize_16x8 },
            { 32, 8, SpriteSize_32x8 },
            { 32, 16, SpriteSize_32x16 },
            { 64, 32, SpriteSize_64x32 },
            { 8, 16, SpriteSize_8x16 },
            { 8, 32, SpriteSize_8x32 },
            { 16, 32, SpriteSize_16x32 },
            { 32, 64, SpriteSize_32x64 },
            { 0, 0, 0 },
        };

        int i = 0;
        while (1)
        {
            if (size_array[i].x == 0)
                NF_Error(106, "Sprite size", 0);

            if ((NF_SPR256VRAM[screen][gfx].width == size_array[i].x) &&
                (NF_SPR256VRAM[screen][gfx].height == size_array[i].y))
            {
                NF_SPRITEOAM[screen][id].size = size_array[i].size;
                break;
            }

            i++;
        }
    }

    NF_SPRITEOAM[screen][id].lastframe = NF_SPR256VRAM[screen][gfx].lastframe;
    NF_SPRITEOAM[screen][id].framesize = NF_SPR256VRAM[screen][gfx].framesize;

    NF_SPRITEOAM[screen][id].frame = 0; // Start at frame 0
}

void NF_DeleteSprite(int screen, u32 id)
{
    if (id > 127)
        NF_Error(106, "Sprite ID", 127);

    // Verify that the sprite is created
    if (!NF_SPRITEOAM[screen][id].created)
    {
        char text[4];
        snprintf(text, sizeof(text), "%d", screen);
        NF_Error(112, text, id);
    }

    // Reset sprite state
    NF_SPRITEOAM[screen][id].index = id;
    NF_SPRITEOAM[screen][id].x = 0;
    NF_SPRITEOAM[screen][id].y = 0;
    NF_SPRITEOAM[screen][id].layer = 0;
    NF_SPRITEOAM[screen][id].pal = 0;
    NF_SPRITEOAM[screen][id].size = SpriteSize_8x8;
    NF_SPRITEOAM[screen][id].color = SpriteColorFormat_256Color;
    NF_SPRITEOAM[screen][id].gfx = NULL;
    NF_SPRITEOAM[screen][id].rot = -1;
    NF_SPRITEOAM[screen][id].doublesize = false;
    NF_SPRITEOAM[screen][id].hide = true;
    NF_SPRITEOAM[screen][id].hflip = false;
    NF_SPRITEOAM[screen][id].vflip = false;
    NF_SPRITEOAM[screen][id].mosaic = false;
    NF_SPRITEOAM[screen][id].gfxid = 0;
    NF_SPRITEOAM[screen][id].frame = 0;
    NF_SPRITEOAM[screen][id].framesize = 0;
    NF_SPRITEOAM[screen][id].lastframe = 0;
    NF_SPRITEOAM[screen][id].created = false; // Mark sprite as not created
}

void NF_SpriteOamSet(int screen)
{
    if (screen == 0)
    {
        for (int n = 0; n < 128; n++)
        {
            oamSet(&oamMain,
                NF_SPRITEOAM[screen][n].index,
                NF_SPRITEOAM[screen][n].x,
                NF_SPRITEOAM[screen][n].y,
                NF_SPRITEOAM[screen][n].layer,
                NF_SPRITEOAM[screen][n].pal,
                NF_SPRITEOAM[screen][n].size,
                NF_SPRITEOAM[screen][n].color,
                NF_SPRITEOAM[screen][n].gfx,
                NF_SPRITEOAM[screen][n].rot,
                NF_SPRITEOAM[screen][n].doublesize,
                NF_SPRITEOAM[screen][n].hide,
                NF_SPRITEOAM[screen][n].hflip,
                NF_SPRITEOAM[screen][n].vflip,
                NF_SPRITEOAM[screen][n].mosaic);
        }
    }
    else
    {
        for (int n = 0; n < 128; n++)
        {
            oamSet(&oamSub,
                NF_SPRITEOAM[screen][n].index,
                NF_SPRITEOAM[screen][n].x,
                NF_SPRITEOAM[screen][n].y,
                NF_SPRITEOAM[screen][n].layer,
                NF_SPRITEOAM[screen][n].pal,
                NF_SPRITEOAM[screen][n].size,
                NF_SPRITEOAM[screen][n].color,
                NF_SPRITEOAM[screen][n].gfx,
                NF_SPRITEOAM[screen][n].rot,
                NF_SPRITEOAM[screen][n].doublesize,
                NF_SPRITEOAM[screen][n].hide,
                NF_SPRITEOAM[screen][n].hflip,
                NF_SPRITEOAM[screen][n].vflip,
                NF_SPRITEOAM[screen][n].mosaic);
        }
    }
}

void NF_SpriteSetPalColor(int screen, u32 pal, u32 number, u32 r, u32 g, u32 b)
{
    if (!NF_SPRPALSLOT[screen][pal].inuse)
        NF_Error(111, "Sprite palette", pal);

    // Pack RGB value
    u32 rgb = r | (g << 5) | (b << 10);

    // Modify palette
    if (screen == 0)
    {
        u32 address = 0x06890000 + (pal * 256 * 2) + (number * 2);
        vramSetBankF(VRAM_F_LCD); // Enable CPU writes to VRAM_F
        *((u16 *)address) = rgb;
        vramSetBankF(VRAM_F_SPRITE_EXT_PALETTE);
    }
    else
    {
        u32 address = 0x068A0000 + (pal * 256 * 2) + (number * 2);
        vramSetBankI(VRAM_I_LCD); // Enable CPU writes to VRAM_I
        *((u16 *)address) = rgb;
        vramSetBankI(VRAM_I_SUB_SPRITE_EXT_PALETTE);
    }
}

void NF_SpriteEditPalColor(int screen, u32 pal, u32 number, u32 r, u32 g, u32 b)
{
    if (!NF_SPRPALSLOT[screen][pal].inuse)
        NF_Error(111, "Sprite palette", pal);

    // Pack RGB value
    u32 rgb = r | (g << 5) | (b << 10);

    u32 hibyte = (rgb >> 8) & 0xff;
    u32 lobyte = rgb & 0xff;

    *(NF_BUFFER_SPR256PAL[NF_SPRPALSLOT[screen][pal].ramslot] + (number * 2)) = lobyte;
    *(NF_BUFFER_SPR256PAL[NF_SPRPALSLOT[screen][pal].ramslot] + ((number * 2) + 1)) = hibyte;
}

void NF_SpriteUpdatePalette(int screen, u32 pal)
{
    if (!NF_SPRPALSLOT[screen][pal].inuse)
        NF_Error(111, "Sprite palette", pal);

    // Get slot where the palette is stored in RAM
    u32 slot = NF_SPRPALSLOT[screen][pal].ramslot;

    // Update palette in VRAM
    if (screen == 0)
    {
        u32 address = 0x06890000 + (pal * 256 * 2);
        vramSetBankF(VRAM_F_LCD); // Enable CPU writes to VRAM_F
        NF_DmaMemCopy((void *)address, NF_BUFFER_SPR256PAL[slot], NF_SPR256PAL[slot].size);
        vramSetBankF(VRAM_F_SPRITE_EXT_PALETTE);
    }
    else
    {
        u32 address = 0x068A0000 + (pal * 256 * 2);
        vramSetBankI(VRAM_I_LCD); // Enable CPU writes to VRAM_I
        NF_DmaMemCopy((void *)address, NF_BUFFER_SPR256PAL[slot], NF_SPR256PAL[slot].size);
        vramSetBankI(VRAM_I_SUB_SPRITE_EXT_PALETTE);
    }
}

void NF_SpriteGetPalColor(int screen, u32 pal, u32 number, u8 *r, u8 *g, u8 *b)
{
    if (!NF_SPRPALSLOT[screen][pal].inuse)
        NF_Error(111, "Sprite palette", pal);

    u32 ramslot = NF_SPRPALSLOT[screen][pal].ramslot;

    u32 lobyte = *(NF_BUFFER_SPR256PAL[ramslot] + (number * 2));
    u32 hibyte = *(NF_BUFFER_SPR256PAL[ramslot] + ((number * 2) + 1));

    // Get RGB value
    u32 rgb = (hibyte << 8) | lobyte;

    // Split components
    *r = rgb & 0x1F;
    *g = (rgb >> 5) & 0x1F;
    *b = (rgb >> 10) & 0x1F;
}
