#include "patches.h"
#include "bk_api.h"
#include "misc_funcs.h"
#include "save_extension.h"
#include "note_saving.h"

#include "functions.h"
#include "prop.h"
#include "core2/timedfunc.h"

// Vanilla declarations.
typedef struct map_info{
    s16 map_id;
    s16 level_id;
    char* name;
}MapInfo;

extern struct {
    s32 unk0;
    s32 unk4;
    u8 unk8[0x25];
} gFileProgressFlags;

MapInfo * func_8030AD00(enum map_e map_id);
enum map_e map_get(void);
enum level_e level_get(void);
void progressDialog_showDialogMaskZero(s32);
void fxSparkle_musicNote(s16 position[3]);
void item_inc(enum item_e item);
void item_adjustByDiffWithoutHud(enum item_e item, s32 diff);
bool func_802FADD4(enum item_e item_id);
s32 item_getCount(enum item_e item);
s32 jiggyscore_leveltotal(s32 lvl);
void itemPrint_reset(void);
s32 bitfield_get_bit(u8 *array, s32 index);

extern s32 D_80385F30[0x2C];
extern s32 D_80385FE8;

extern struct {
    Cube *cubes;
    f32 margin;
    s32 min[3];
    s32 max[3];
    s32 stride[2];
    s32 cubeCnt; 
    s32 unk2C;
    s32 width[3];
    Cube *unk3C; // fallback cube?
    Cube *unk40; // some other fallback cube?
    s32 unk44; // index of some sort
} sCubeList;

// New declarations.
typedef struct {
    u16 static_note_count;
    u16 dynamic_note_count;
    u16 start_note_index;
    u16 level_id;
} MapNoteData;

MapNoteData map_note_data[512]; // One entry per map, with room for extras in case any mods add additional maps.
u16 level_note_counts[256]; // One entry per level, with room for extras in case any mods add additional levels.

typedef struct {
    u32 note_index;
} NoteSavingExtensionData;

PropExtensionId note_saving_prop_extension_id;

// Note saving can only savely be changed while in the lair, so this value is only updated when in the lair.
bool note_saving_enabled_cached = FALSE;
// Override for allowing mods to disable note saving.
bool note_saving_override_disabled = FALSE;

u32 spawned_static_note_count = 0;

// Per-map values containing the number of dynamic notes to despawn for the current session.
// This is calculated when a new level is entered for every map in the level.
u8 map_dynamic_note_despawn_counts[512];

bool recomp_in_demo_playback_game_mode();

void init_note_saving() {
    note_saving_prop_extension_id = bkrecomp_extend_prop_all(sizeof(NoteSavingExtensionData));

    // Collected from map data.
    map_note_data[MAP_2_MM_MUMBOS_MOUNTAIN].static_note_count = 85;
    map_note_data[MAP_2_MM_MUMBOS_MOUNTAIN].dynamic_note_count = 5;
    map_note_data[MAP_5_TTC_BLUBBERS_SHIP].static_note_count = 8;
    map_note_data[MAP_6_TTC_NIPPERS_SHELL].static_note_count = 6;
    map_note_data[MAP_7_TTC_TREASURE_TROVE_COVE].static_note_count = 82;
    map_note_data[MAP_A_TTC_SANDCASTLE].static_note_count = 4;
    map_note_data[MAP_B_CC_CLANKERS_CAVERN].static_note_count = 72;
    map_note_data[MAP_C_MM_TICKERS_TOWER].static_note_count = 6;
    map_note_data[MAP_D_BGS_BUBBLEGLOOP_SWAMP].static_note_count = 83;
    map_note_data[MAP_D_BGS_BUBBLEGLOOP_SWAMP].dynamic_note_count = 5;
    map_note_data[MAP_E_MM_MUMBOS_SKULL].static_note_count = 4;
    map_note_data[MAP_10_BGS_MR_VILE].static_note_count = 6;
    map_note_data[MAP_11_BGS_TIPTUP].static_note_count = 6;
    map_note_data[MAP_12_GV_GOBIS_VALLEY].static_note_count = 70;
    map_note_data[MAP_13_GV_MEMORY_GAME].static_note_count = 4;
    map_note_data[MAP_14_GV_SANDYBUTTS_MAZE].static_note_count = 7;
    map_note_data[MAP_15_GV_WATER_PYRAMID].static_note_count = 4;
    map_note_data[MAP_16_GV_RUBEES_CHAMBER].static_note_count = 8;
    map_note_data[MAP_1A_GV_INSIDE_JINXY].static_note_count = 7;
    map_note_data[MAP_1B_MMM_MAD_MONSTER_MANSION].static_note_count = 47;
    map_note_data[MAP_1C_MMM_CHURCH].static_note_count = 10;
    map_note_data[MAP_1D_MMM_CELLAR].static_note_count = 4;
    map_note_data[MAP_21_CC_WITCH_SWITCH_ROOM].static_note_count = 6;
    map_note_data[MAP_22_CC_INSIDE_CLANKER].static_note_count = 16;
    map_note_data[MAP_23_CC_GOLDFEATHER_ROOM].static_note_count = 6;
    map_note_data[MAP_24_MMM_TUMBLARS_SHED].static_note_count = 4;
    map_note_data[MAP_25_MMM_WELL].static_note_count = 7;
    map_note_data[MAP_26_MMM_NAPPERS_ROOM].static_note_count = 8;
    map_note_data[MAP_27_FP_FREEZEEZY_PEAK].static_note_count = 82;
    map_note_data[MAP_29_MMM_NOTE_ROOM].static_note_count = 9;
    map_note_data[MAP_2D_MMM_BEDROOM].static_note_count = 4;
    map_note_data[MAP_2F_MMM_WATERDRAIN_BARREL].static_note_count = 5;
    map_note_data[MAP_30_MMM_MUMBOS_SKULL].static_note_count = 2;
    map_note_data[MAP_31_RBB_RUSTY_BUCKET_BAY].static_note_count = 43;
    map_note_data[MAP_34_RBB_ENGINE_ROOM].static_note_count = 16;
    map_note_data[MAP_35_RBB_WAREHOUSE].static_note_count = 4;
    map_note_data[MAP_37_RBB_CONTAINER_1].static_note_count = 8;
    map_note_data[MAP_38_RBB_CONTAINER_3].static_note_count = 4;
    map_note_data[MAP_39_RBB_CREW_CABIN].static_note_count = 4;
    map_note_data[MAP_3B_RBB_STORAGE_ROOM].static_note_count = 5;
    map_note_data[MAP_3C_RBB_KITCHEN].static_note_count = 5;
    map_note_data[MAP_3D_RBB_NAVIGATION_ROOM].static_note_count = 4;
    map_note_data[MAP_3F_RBB_CAPTAINS_CABIN].static_note_count = 3;
    map_note_data[MAP_40_CCW_HUB].static_note_count = 4;
    map_note_data[MAP_43_CCW_SPRING].static_note_count = 16;
    map_note_data[MAP_44_CCW_SUMMER].static_note_count = 16;
    map_note_data[MAP_45_CCW_AUTUMN].static_note_count = 37;
    map_note_data[MAP_46_CCW_WINTER].static_note_count = 16;
    map_note_data[MAP_48_FP_MUMBOS_SKULL].static_note_count = 6;
    map_note_data[MAP_4C_CCW_AUTUMN_MUMBOS_SKULL].static_note_count = 4;
    map_note_data[MAP_53_FP_CHRISTMAS_TREE].static_note_count = 12;
    map_note_data[MAP_5C_CCW_AUTUMN_ZUBBA_HIVE].static_note_count = 4;
    map_note_data[MAP_60_CCW_AUTUMN_NABNUTS_HOUSE].static_note_count = 3;
    map_note_data[MAP_8B_RBB_ANCHOR_ROOM].static_note_count = 4;
}

RECOMP_EXPORT PropExtensionId bkrecomp_notesaving_get_note_saving_prop_extension_id()
{
    return note_saving_prop_extension_id;
}

RECOMP_EXPORT void bkrecomp_notesaving_clear_all_map_note_counts() {
    for (u32 i = 0; i < ARRLEN(map_note_data); i++) {
        map_note_data[i].static_note_count = 0;
        map_note_data[i].dynamic_note_count = 0;
    }
}

RECOMP_EXPORT void bkrecomp_notesaving_set_map_static_note_count(u32 map_id, u16 static_note_count) {
    if (map_id >= ARRLEN(map_note_data)) {
        recomp_error("Mod error: Attempted to set static note count of an invalid map ID\n");
    }
    map_note_data[map_id].static_note_count = static_note_count;
}

RECOMP_EXPORT void bkrecomp_notesaving_set_map_dynamic_note_count(u32 map_id, u16 dynamic_note_count) {
    if (map_id >= ARRLEN(map_note_data)) {
        recomp_error("Mod error: Attempted to set dynamic note count of an invalid map ID\n");
    }
    map_note_data[map_id].dynamic_note_count = dynamic_note_count;
}

RECOMP_EXPORT void bkrecomp_notesaving_force_disabled(bool disabled) {
    note_saving_override_disabled = disabled;
}

// Notes are always saved, but this function controls whether to use the saved data to prevent notes from spawning and adjust the note score. 
RECOMP_EXPORT s32 bkrecomp_note_saving_enabled() {
    return recomp_get_note_saving_enabled();
}

RECOMP_EXPORT s32 bkrecomp_note_saving_active() {
    return note_saving_enabled_cached;
}

void calculate_map_start_note_indices() {
    for (u32 map_id = 0; map_id < ARRLEN(map_note_data); map_id++) {
        MapNoteData* note_data = &map_note_data[map_id];
        MapInfo* info = func_8030AD00(map_id);
        if (info != NULL) {
            note_data->level_id = info->level_id;
            note_data->start_note_index = level_note_counts[info->level_id];
            level_note_counts[info->level_id] += note_data->static_note_count + note_data->dynamic_note_count;
        }
        else {
            note_data->level_id = 0;
        }
    }
}

s32 level_id_to_level_array_index(enum level_e level_id) {
    switch (level_id) {
        case LEVEL_1_MUMBOS_MOUNTAIN:
            return 0;
        case LEVEL_2_TREASURE_TROVE_COVE:
            return 1;
        case LEVEL_3_CLANKERS_CAVERN:
            return 2;
        case LEVEL_4_BUBBLEGLOOP_SWAMP:
            return 3;
        case LEVEL_5_FREEZEEZY_PEAK:
            return 4;
        case LEVEL_7_GOBIS_VALLEY:
            return 5;
        case LEVEL_A_MAD_MONSTER_MANSION:
            return 6;
        case LEVEL_9_RUSTY_BUCKET_BAY:
            return 7;
        case LEVEL_8_CLICK_CLOCK_WOOD:
            return 8;
        default:
            return -1;
    }
}

bool is_note_collected(enum map_e map_id, enum level_e level_id, u8 note_index) {
    if (map_id >= ARRLEN(map_note_data)) {
        return FALSE;
    }

    s32 level_array_index = level_id_to_level_array_index(level_id);
    if (level_array_index == -1 || level_array_index >= ARRLEN(loaded_file_extension_data.level_notes)) {
        return FALSE;
    }

    MapNoteData *note_data = &map_note_data[map_id];
    note_index += note_data->start_note_index;

    u32 byte_index = note_index / 8;
    u32 bit_index = note_index % 8;

    return (loaded_file_extension_data.level_notes[level_array_index].bytes[byte_index] & (1 << bit_index)) != 0;
}

RECOMP_EXPORT bool bkrecomp_is_note_collected(enum map_e map_id, enum level_e level_id, u8 note_index)
{
    return is_note_collected(map_id, level_id, note_index);
}

void set_note_collected(enum map_e map_id, enum level_e level_id, u8 note_index) {
    if (map_id >= ARRLEN(map_note_data)) {
        return;
    }

    s32 level_array_index = level_id_to_level_array_index(level_id);
    if (level_array_index == -1 || level_array_index >= ARRLEN(loaded_file_extension_data.level_notes)) {
        return;
    }

    MapNoteData *note_data = &map_note_data[map_id];
    note_index += note_data->start_note_index;

    u32 byte_index = note_index / 8;
    u32 bit_index = note_index % 8;

    loaded_file_extension_data.level_notes[level_array_index].bytes[byte_index] |= (1 << bit_index);
}
RECOMP_EXPORT void bkrecomp_set_note_collected(enum map_e map_id, enum level_e level_id, u8 note_index)
{
	set_note_collected(map_id, level_id, note_index);
}


void collect_dynamic_note(enum map_e map_id, enum level_e level_id) {
    if (map_id < ARRLEN(map_note_data)) {
        MapNoteData *map_data = &map_note_data[map_id];
        s32 start_note_index = map_data->static_note_count + map_data->start_note_index;
        s32 map_dynamic_note_count = map_data->dynamic_note_count;

        // Set the first unset dynamic note bit for this map.
        for (s32 i = 0; i < map_dynamic_note_count; i++) {
            s32 note_index = start_note_index + i;
            if (!is_note_collected(map_id, level_id, note_index)) {
                set_note_collected(map_id, level_id, note_index);
                break;
            }
        }
    }
}
RECOMP_EXPORT void bkrecomp_collect_dynamic_note(enum map_e map_id, enum level_e level_id)
{
    collect_dynamic_note(map_id, level_id);
}

s32 dynamic_note_collected_count(enum map_e map_id) {
    s32 ret = 0;
    if (map_id < ARRLEN(map_note_data)) {
        MapNoteData *map_data = &map_note_data[map_id];
        s32 start_note_index = map_data->static_note_count + map_data->start_note_index;
        s32 map_dynamic_note_count = map_data->dynamic_note_count;

        // Check the dynamic note bits for this map.
        for (s32 i = 0; i < map_dynamic_note_count; i++) {
            s32 note_index = start_note_index + i;
            if (is_note_collected(map_id, map_data->level_id, note_index)) {
                ret++;
            }
        }
    }
    return ret;
}
RECOMP_EXPORT s32 bkrecomp_dynamic_note_collected_count(enum map_e map_id)
{
    return dynamic_note_collected_count(map_id);
}

void note_saving_on_map_load() {
    spawned_static_note_count = 0;

    // Prevent the note score passed dialog from running if note saving is enabled.
    if (note_saving_enabled_cached) {
        // This flag controls whether Bottles will tell you when you pass your note score.
        levelSpecificFlags_set(LEVEL_FLAG_34_UNKNOWN, TRUE);
    }
}

void note_saving_update() {
    // When in the lair or file select, update the cached note saving enabled state.
    if (level_get() == LEVEL_6_LAIR || map_get() == MAP_91_FILE_SELECT) {
        if (note_saving_override_disabled) {
            note_saving_enabled_cached = FALSE;
        }
        else {
            note_saving_enabled_cached = bkrecomp_note_saving_enabled();
        }
    }
}

void note_saving_handle_static_note(Cube *c, Prop *p) {

    // If note saving is enabled, check if this note has been collected and remove it if so.
    if (note_saving_enabled_cached) {
        if (is_note_collected(map_get(), level_get(), spawned_static_note_count)) {
            // Clear the note's alive bit.
            p->spriteProp.unk8_4 = FALSE;
        }
    }

    NoteSavingExtensionData* note_data = (NoteSavingExtensionData*)bkrecomp_get_extended_prop_data(c, p, note_saving_prop_extension_id);
    note_data->note_index = spawned_static_note_count;

    spawned_static_note_count++;
}

void note_saving_handle_dynamic_note(Actor *actor, ActorMarker *marker) {
    if (note_saving_enabled_cached) {
        s32 map_id = map_get();
        if (map_id < ARRLEN(map_dynamic_note_despawn_counts)) {
            if (map_dynamic_note_despawn_counts[map_id] > 0) {
                map_dynamic_note_despawn_counts[map_id]--;
                // Clear the note's alive bit so it doesn't draw for good measure.
                marker->propPtr->unk8_4 = FALSE;
                // Set the actor as despawned.
                actor->despawn_flag = TRUE;
            }
        }
    }
}

bool prop_in_cube(Cube *c, Prop *p) {
    s32 prop_index = p - c->prop2Ptr;
    if (prop_index >= 0 && prop_index < c->prop2Cnt) {
        return TRUE;
    }
    return FALSE;
}

Cube *find_cube_for_prop(Prop *p) {
    for (s32 i = 0; i < sCubeList.cubeCnt; i++) {
        Cube *cur_cube = &sCubeList.cubes[i];
        if (prop_in_cube(cur_cube, p)) {
            return cur_cube;
        }
    }
    
    if (prop_in_cube(sCubeList.unk3C, p)) {
        return sCubeList.unk3C;
    }

    if (prop_in_cube(sCubeList.unk40, p)) {
        return sCubeList.unk40;
    }

    return NULL;
}

RECOMP_DECLARE_EVENT(bkrecomp_note_collected_event(enum map_e map_id, enum level_e level_id, u8 note_index));
RECOMP_DECLARE_EVENT(bkrecomp_dynamic_note_collected_event(enum map_e map_id, enum level_e level_id));

// @recomp Patched to track collected notes.
RECOMP_PATCH void __baMarker_resolveMusicNoteCollision(Prop *arg0) {
    // @recomp Set that the note was collected if this isn't demo playback.
    if (!recomp_in_demo_playback_game_mode()) {
        // Check if this is an actor prop and collect a dynamic note if so.
        if (arg0->is_actor) {
            collect_dynamic_note(map_get(), level_get());
            bkrecomp_dynamic_note_collected_event(map_get(), level_get());
        }
        // Otherwise, make sure this is a sprite prop and use the prop data.
        else if (!arg0->is_3d) {
            Cube *prop_cube = find_cube_for_prop(arg0);
            if (prop_cube != NULL) {
                NoteSavingExtensionData* note_data = (NoteSavingExtensionData*)bkrecomp_get_extended_prop_data(prop_cube, arg0, note_saving_prop_extension_id);
                set_note_collected(map_get(), level_get(), note_data->note_index);
                bkrecomp_note_collected_event(map_get(), level_get(), note_data->note_index);
            }
        }
    }

    if (!func_802FADD4(ITEM_1B_VILE_VILE_SCORE)) {
        item_inc(ITEM_C_NOTE);
    } else {
        item_adjustByDiffWithoutHud(ITEM_C_NOTE, 1);
    }
    if (item_getCount(ITEM_C_NOTE) < 100) {
        coMusicPlayer_playMusic(COMUSIC_9_NOTE_COLLECTED, 16000);
        timedFunc_set_1(0.75f, progressDialog_showDialogMaskZero, FILEPROG_3_MUSIC_NOTE_TEXT);
    }
    fxSparkle_musicNote(arg0->unk4);
}

s32 get_collected_note_count(enum level_e level) {
    s32 level_array_index = level_id_to_level_array_index(level);
    if (level_array_index == -1) {
        return 0;
    }

    s32 count = 0;
    for (int i = 0; i < ARRLEN(loaded_file_extension_data.level_notes[0].bytes); i++) {
        u8 cur_byte = loaded_file_extension_data.level_notes[level_array_index].bytes[i];
        count += __builtin_popcount(cur_byte);
    }

    return count;
}

// @recomp Patched to restore the saved note count when entering a level and reset the per-map collected dynamic note counts.
RECOMP_PATCH void itemscore_levelReset(enum level_e level){
    int i;
    
    for(i = 0; i < 6; i++){
        D_80385F30[ITEM_0_HOURGLASS_TIMER + i] = 0;
        D_80385F30[ITEM_6_HOURGLASS + i] = 0;
    }
    
    D_80385F30[ITEM_C_NOTE] = 0;
    D_80385F30[ITEM_E_JIGGY] = jiggyscore_leveltotal(level);
    D_80385F30[ITEM_12_JINJOS] = 0;
    D_80385F30[ITEM_17_AIR] = 3600;
    D_80385F30[ITEM_18_GOLD_BULLIONS] = 0;
    D_80385F30[ITEM_19_ORANGE] = 0;
    D_80385F30[ITEM_23_ACORNS] = 0;
    D_80385F30[ITEM_1A_PLAYER_VILE_SCORE] = 0;
    D_80385F30[ITEM_1B_VILE_VILE_SCORE] = 0;
    D_80385F30[ITEM_1F_GREEN_PRESENT] = 0;
    D_80385F30[ITEM_20_BLUE_PRESENT] = 0;
    D_80385F30[ITEM_21_RED_PRESENT] = 0;
    D_80385F30[ITEM_22_CATERPILLAR] = 0;
    itemPrint_reset();
    D_80385FE8 = 1;

    // @recomp If note saving is currently enabled, set load the note count for the current level.
    if (note_saving_enabled_cached) {
        D_80385F30[ITEM_C_NOTE] = get_collected_note_count(level);
    }

    // @recomp Set the number of dynamic notes to respawn for each map in the level.
    for (s32 map_id = 0; map_id < ARRLEN(map_note_data); map_id++) {
        MapNoteData *map_data = &map_note_data[map_id];
        if (map_data->level_id == level) {
            map_dynamic_note_despawn_counts[map_id] = dynamic_note_collected_count(map_id);
        }
        else {
            map_dynamic_note_despawn_counts[map_id] = 0;
        }
    }
}

// @recomp Patched to return true for FILEPROG_99_PAST_50_NOTE_DOOR_TEXT if note saving is enabled.
// That flag controls whether to show the "Grunty's magic stops you from taking the notes..." dialog.
RECOMP_PATCH bool fileProgressFlag_get(enum file_progress_e index) {
    if (note_saving_enabled_cached && index == FILEPROG_99_PAST_50_NOTE_DOOR_TEXT) {
        return TRUE;
    }
    
    return bitfield_get_bit(gFileProgressFlags.unk8, index);
}
