#include "ClockShuffle.h"
#include "Rando/Rando.h"
#include "Rando/Logic/Logic.h"
#include "2s2h/GameInteractor/GameInteractor.h"
#include "2s2h/CustomMessage/CustomMessage.h"
#include "2s2h/ShipUtils.h"

extern "C" {
#include "overlays/actors/ovl_En_Test4/z_en_test4.h"
#include "overlays/actors/ovl_Obj_Tokei_Step/z_obj_tokei_step.h"
void EnTest4_GetBellTimeOnDay3(EnTest4* thisx);
void EnTest4_GetBellTimeAndShrinkScreenBeforeDay3(EnTest4* thisx, PlayState* play);
void ObjTokeiStep_SetupOpen(ObjTokeiStep* thisx);
void ObjTokeiStep_DrawOpen(Actor* thisx, PlayState* play);
void ObjTokeiStep_DoNothing(ObjTokeiStep* thisx, PlayState* play);
}

static constexpr u16 DAWN_TIME = CLOCK_TIME(6, 0);
static constexpr u16 DUSK_TIME = CLOCK_TIME(18, 0);

int Rando::ClockItems::GetHalfDayIndexFromClockItem(RandoItemId clockItemId) {
    switch (clockItemId) {
        case RI_TIME_DAY_1:
            return HALF_DAY1_DAY;
        case RI_TIME_NIGHT_1:
            return HALF_DAY1_NIGHT;
        case RI_TIME_DAY_2:
            return HALF_DAY2_DAY;
        case RI_TIME_NIGHT_2:
            return HALF_DAY2_NIGHT;
        case RI_TIME_DAY_3:
            return HALF_DAY3_DAY;
        case RI_TIME_NIGHT_3:
            return HALF_DAY3_NIGHT;
        default:
            return INVALID;
    }
}

RandoItemId Rando::ClockItems::GetClockItemFromHalfDayIndex(int halfDayIndex) {
    if (halfDayIndex < 0 || halfDayIndex >= HALF_COUNT) {
        return RI_UNKNOWN;
    }

    static const RandoItemId clockItemMap[] = {
        RI_TIME_DAY_1,   // HALF_DAY1_DAY   (index 0)
        RI_TIME_NIGHT_1, // HALF_DAY1_NIGHT (index 1)
        RI_TIME_DAY_2,   // HALF_DAY2_DAY   (index 2)
        RI_TIME_NIGHT_2, // HALF_DAY2_NIGHT (index 3)
        RI_TIME_DAY_3,   // HALF_DAY3_DAY   (index 4)
        RI_TIME_NIGHT_3, // HALF_DAY3_NIGHT (index 5)
    };

    return clockItemMap[halfDayIndex];
}

u8 Rando::ClockItems::GetAllOwnedHalfDaysMask() {
    u8 ownedMask = 0;
    for (int i = 0; i < HALF_COUNT; ++i) {
        if (Rando::Logic::OwnsClockHalfDay(i)) {
            ownedMask |= (1 << i);
        }
    }
    return ownedMask;
}

int Rando::ClockItems::FindOwnedHalfDay(bool fromEnd) {
    if (fromEnd) {
        for (int i = HALF_COUNT - 1; i >= 0; --i) {
            if (Rando::Logic::OwnsClockHalfDay(i)) {
                return i;
            }
        }
    } else {
        for (int i = 0; i < HALF_COUNT; ++i) {
            if (Rando::Logic::OwnsClockHalfDay(i)) {
                return i;
            }
        }
    }
    return Rando::ClockItems::INVALID;
}

static int FindNextOwnedHalfDayAfter(int startHalfDay, u8 ownedMask) {
    if (startHalfDay < 0 || startHalfDay >= Rando::ClockItems::HALF_COUNT) {
        return Rando::ClockItems::TERMINAL_STATE;
    }

    for (int halfDayIndex = startHalfDay + 1; halfDayIndex < Rando::ClockItems::HALF_COUNT; ++halfDayIndex) {
        if (ownedMask & (1 << halfDayIndex)) {
            return halfDayIndex;
        }
    }

    return Rando::ClockItems::TERMINAL_STATE;
}

bool Rando::ClockItems::IsClockItem(RandoItemId itemId) {
    return (itemId >= RI_TIME_DAY_1 && itemId <= RI_TIME_NIGHT_3) || itemId == RI_TIME_PROGRESSIVE;
}

bool Rando::ClockItems::IsDayClock(RandoItemId itemId) {
    return itemId == RI_TIME_DAY_1 || itemId == RI_TIME_DAY_2 || itemId == RI_TIME_DAY_3;
}

static bool IsNight(u16 time) {
    return (time < DAWN_TIME) || (time >= DUSK_TIME);
}

static u16 GetConfiguredTerminalTime() {
    int terminalMinutes = RANDO_SAVE_OPTIONS[RO_CLOCK_TERMINAL_TIME];
    return CLOCK_TIME(0, terminalMinutes);
}

static bool IsInTerminalRange(s32 day, u16 time) {
    if (day != 3) {
        return false;
    }

    u16 terminalTime = GetConfiguredTerminalTime();

    if (terminalTime >= DUSK_TIME) {
        return (time >= terminalTime) || (time < DAWN_TIME);
    } else {
        return (time >= terminalTime && time < DAWN_TIME);
    }
}

static int GetHalfDayIndexFromTime(s32 day, u16 time) {
    if (day < 1) {
        return 0;
    }
    int halfDay = (day - 1) * 2;
    if (IsNight(time)) {
        halfDay++;
    }
    return halfDay;
}

static int GetCurrentHalfDayIndex() {
    u16 currentTime = gSaveContext.save.time;
    s32 currentDay = gSaveContext.save.day;

    if (currentDay >= 4 || currentDay == 0) {
        return Rando::ClockItems::TERMINAL_STATE;
    }

    if (IsInTerminalRange(currentDay, currentTime)) {
        return Rando::ClockItems::TERMINAL_STATE;
    }

    bool isNight = IsNight(currentTime);
    return (currentDay - 1) * 2 + (isNight ? 1 : 0);
}

void Rando::ClockShuffle::SetTimeToHalfDayStart(int halfDayIndex) {
    if (halfDayIndex < 0 || halfDayIndex >= Rando::ClockItems::HALF_COUNT) {
        return;
    }

    // Calculate day number and time from half-day index
    u8 dayNumber = (halfDayIndex / 2) + 1;
    u16 startTime = (halfDayIndex & 1) ? DUSK_TIME : DAWN_TIME;

    gSaveContext.save.day = dayNumber;
    gSaveContext.save.time = startTime;
}

static bool SceneNeedsReloadForTimeSkip(s16 sceneId) {
    return sceneId == SCENE_BOWLING;
}

static void ForceSceneReload() {
    Player* player = GET_PLAYER(gPlayState);

    gPlayState->nextEntrance = gSaveContext.save.entrance;
    gPlayState->transitionTrigger = TRANS_TRIGGER_START;
    gPlayState->transitionType = TRANS_TYPE_FADE_BLACK_FAST;

    Play_SetRespawnData(gPlayState, RESPAWN_MODE_RETURN, gSaveContext.save.entrance, gPlayState->roomCtx.curRoom.num,
                        PLAYER_PARAMS(0xFF, PLAYER_START_MODE_B), &player->actor.world.pos, player->actor.world.rot.y);

    gSaveContext.nextTransitionType = TRANS_TYPE_FADE_BLACK;
    gSaveContext.respawnFlag = 2;
}

static bool CheckSkippedTime(u8* day, u16* time) {
    if (gPlayState->envCtx.sceneTimeSpeed == 0 || Player_InCsMode(gPlayState) || *day >= 4) {
        return false;
    }

    // Terminal state is always accessible regardless of Night 3 ownership
    if (IsInTerminalRange(*day, *time)) {
        return false;
    }

    int currentHalfDay = (*day < 1) ? 0 : ((*day - 1) * 2 + (IsNight(*time) ? 1 : 0));

    if (Rando::Logic::OwnsClockHalfDay(currentHalfDay)) {
        return false;
    }

    int nextHalfDay = Rando::ClockItems::TERMINAL_STATE;
    for (int i = currentHalfDay + 1; i < Rando::ClockItems::HALF_COUNT; ++i) {
        if (Rando::Logic::OwnsClockHalfDay(i)) {
            nextHalfDay = i;
            break;
        }
    }

    if (nextHalfDay == Rando::ClockItems::TERMINAL_STATE) {
        *day = 3;
        *time = GetConfiguredTerminalTime();
    } else {
        *day = (nextHalfDay / 2) + 1;
        *time = (nextHalfDay & 1) ? DUSK_TIME : DAWN_TIME;
    }

    return true;
}

// Validates current time during EnTest4 update. If player is in an unowned half-day,
// skips ahead to the next owned half-day (or terminal state). Handles all necessary
// game state updates (weather, actors, music, etc.).
// Returns true if time was skipped.
static bool CheckAndSkipUnownedTime(Actor* timeActor) {
    EnTest4* enTest4 = (EnTest4*)timeActor;
    u8 day = gSaveContext.save.day;
    u16 time = gSaveContext.save.time + CLOCK_TIME(0, 1);

    if (IsNight(enTest4->prevTime) && !IsNight(time)) {
        day++;
    }

    if (day < 4 && CheckSkippedTime(&day, &time)) {
        gSaveContext.save.day = day;
        gSaveContext.save.time = time;

        if (SceneNeedsReloadForTimeSkip(gPlayState->sceneId)) {
            ForceSceneReload();
            return true;
        }

        gWeatherMode = WEATHER_MODE_CLEAR;
        gPlayState->envCtx.lightningState = LIGHTNING_OFF;

        if (time == DAWN_TIME) {
            // Set to NIGHT and decrement day so vanilla's dawn transition runs on the next frame.
            // EnTest4 will detect the night-to-day crossing and trigger the proper dawn sequence.
            enTest4->daytimeIndex = 0;
            gSaveContext.save.day--;
        } else {
            enTest4->daytimeIndex = IsNight(time) ? 0 : 1;
            Interface_NewDay(gPlayState, gSaveContext.save.day);
            Environment_NewDay(&gPlayState->envCtx);

            // Flip numSetupActors to trigger actor kill/respawn for the new half-day.
            gPlayState->numSetupActors = -gPlayState->numSetupActors;

            enTest4->prevBellTime = time;
            if (gSaveContext.save.day == 3) {
                EnTest4_GetBellTimeOnDay3(enTest4);
            } else {
                EnTest4_GetBellTimeAndShrinkScreenBeforeDay3(enTest4, gPlayState);
            }
        }

        if (gPlayState->sceneSequences.ambienceId != AMBIENCE_ID_13) {
            SEQCMD_STOP_SEQUENCE(SEQ_PLAYER_AMBIENCE, 0);
            SEQCMD_STOP_SEQUENCE(SEQ_PLAYER_BGM_MAIN, 240);
            gSaveContext.seqId = NA_BGM_DISABLED;
            gSaveContext.ambienceId = AMBIENCE_ID_DISABLED;
            Environment_PlaySceneSequence(gPlayState);
        }

        // Reset screen scale to prevent lingering shrink effect from bell tolling
        gSaveContext.screenScaleFlag = false;
        gSaveContext.screenScale = 1000.0f;

        if (gSaveContext.save.day == 3 && IsNight(time)) {
            ObjTokeiStep* objTokeiStep = (ObjTokeiStep*)Actor_FindNearby(gPlayState, &GET_PLAYER(gPlayState)->actor,
                                                                         ACTOR_OBJ_TOKEI_STEP, ACTORCAT_BG, 99999.9f);
            if (objTokeiStep != NULL && objTokeiStep->actionFunc == ObjTokeiStep_DoNothing) {
                objTokeiStep->dyna.actor.draw = ObjTokeiStep_DrawOpen;
                ObjTokeiStep_SetupOpen(objTokeiStep);
            }
        }

        enTest4->prevTime = time - CLOCK_TIME(0, 1);
        return true;
    }
    return false;
}

bool Rando::ClockShuffle::IsTimeOwnedForClockShuffle(s32 day, u16 time) {
    if (!RANDO_SAVE_OPTIONS[RO_CLOCK_SHUFFLE]) {
        return true;
    }
    if (day < 1 || day > 3) {
        return true;
    }
    if (IsInTerminalRange(day, time)) {
        return true;
    }

    int halfDayIndex = GetHalfDayIndexFromTime(day, time);
    return Rando::Logic::OwnsClockHalfDay(halfDayIndex);
}

std::string Rando::ClockShuffle::GetTimeDescriptionForMessage(s32 day, u16 time) {
    if (IsInTerminalRange(day, time)) {
        return "%rFinal Hours%w";
    }

    bool isNight = IsNight(time);
    std::string description = isNight ? "%rNight of the " : "%rDawn of the ";

    if (day == 1) {
        description += "First";
    } else if (day == 2) {
        description += "Second";
    } else if (day == 3) {
        description += "Third";
    } else {
        description += "Unknown";
    }

    description += " Day%w";
    return description;
}

static void ProcessClockShuffleMessage(u16* textId, bool* loadFromMessageTable, bool isSongOfTime) {
    auto entry = CustomMessage::LoadVanillaMessageTableEntry(*textId);
    int targetHalfDay;

    if (isSongOfTime) {
        targetHalfDay = Rando::ClockItems::FindOwnedHalfDay(false);
    } else {
        int currentHalfDay = GetCurrentHalfDayIndex();
        u8 ownedHalfDaysMask = Rando::ClockItems::GetAllOwnedHalfDaysMask();
        targetHalfDay = FindNextOwnedHalfDayAfter(currentHalfDay, ownedHalfDaysMask);
    }

    std::string destinationText;
    if (targetHalfDay == Rando::ClockItems::TERMINAL_STATE) {
        destinationText = "%rFinal Hours%w";
    } else {
        int targetDay = (targetHalfDay / 2) + 1;
        bool isNight = (targetHalfDay & 1);
        destinationText = isNight ? "%rNight of the " : "%rDawn of the ";
        if (targetDay == 1) {
            destinationText += "First";
        } else if (targetDay == 2) {
            destinationText += "Second";
        } else if (targetDay == 3) {
            destinationText += "Third";
        }
        destinationText += " Day%w";
    }

    if (isSongOfTime) {
        entry.msg = "Save and return to " + destinationText + "?\n%gYes\nNo\xC2";
    } else { // Song of Double Time
        entry.msg = "Time moves strangely...\nProceed to " + destinationText + "?\n%gYes\nNo\xC2";
    }

    CustomMessage::LoadCustomMessageIntoFont(entry);
    *loadFromMessageTable = false;
}

// Called at OnFileLoad and OnPlayDestroy. Validates time at cycle start or when a day telop
// transition is pending (respawnFlag -4). Jumps to first owned half-day if needed.
static void EnforceOwnedTime() {
    bool isCycleStart =
        (gSaveContext.save.day == 0 || (gSaveContext.save.day == 1 && gSaveContext.save.time == CLOCK_TIME(6, 0)));
    bool isPendingDayTelop = (CHECK_EVENTINF(EVENTINF_TRIGGER_DAYTELOP) && gSaveContext.respawnFlag == -4);

    if (!isCycleStart && !isPendingDayTelop) {
        return;
    }

    // Determine target half-day index
    int targetHalfDay = 0;
    if (isPendingDayTelop) {
        u8 nextDay = gSaveContext.save.day + 1;
        targetHalfDay = (nextDay < 1) ? 0 : ((nextDay - 1) * 2);
    } else {
        targetHalfDay = 0;
    }

    // Find next owned half-day
    int validHalfDay = Rando::ClockItems::TERMINAL_STATE;

    if (Rando::Logic::OwnsClockHalfDay(targetHalfDay)) {
        validHalfDay = targetHalfDay;
    } else {
        for (int i = targetHalfDay + 1; i < Rando::ClockItems::HALF_COUNT; ++i) {
            if (Rando::Logic::OwnsClockHalfDay(i)) {
                validHalfDay = i;
                break;
            }
        }
    }

    if (validHalfDay != Rando::ClockItems::TERMINAL_STATE) {
        if (validHalfDay == targetHalfDay) {
            return;
        }

        if (validHalfDay & 1) { // Night
            gSaveContext.save.day = (validHalfDay / 2) + 1;
            gSaveContext.save.time = DUSK_TIME;
        } else { // Day
            gSaveContext.save.day = (validHalfDay / 2);
            gSaveContext.save.time = CLOCK_TIME(6, 0) - 1;
        }
    } else {
        // Terminal State
        gSaveContext.save.day = 3;
        gSaveContext.save.time = GetConfiguredTerminalTime();
    }

    if (isPendingDayTelop) {
        CLEAR_EVENTINF(EVENTINF_TRIGGER_DAYTELOP);
        gSaveContext.respawnFlag = -8;
    }
}

void Rando::ClockShuffle::OnFileLoad() {
    bool shouldRegister = IS_RANDO && RANDO_SAVE_OPTIONS[RO_CLOCK_SHUFFLE];

    if (shouldRegister) {
        EnforceOwnedTime();
    }

    COND_HOOK(OnPlayDestroy, shouldRegister, []() { EnforceOwnedTime(); });

    COND_ID_HOOK(ShouldActorUpdate, ACTOR_EN_TEST4, shouldRegister, [](Actor* actor, bool* should) {
        // Skip time checks if a transition cutscene is in progress
        if (!CHECK_EVENTINF(EVENTINF_17)) {
            if (CheckAndSkipUnownedTime(actor)) {
                *should = false;
                return;
            }
        }
        *should = true;
    });

    COND_ID_HOOK(OnOpenText, 0x1B8A, shouldRegister, [](u16* textId, bool* loadFromMessageTable) {
        ProcessClockShuffleMessage(textId, loadFromMessageTable, true);
    });

    auto onDoubleTime = [](u16* textId, bool* loadFromMessageTable) {
        ProcessClockShuffleMessage(textId, loadFromMessageTable, false);
    };

    COND_ID_HOOK(OnOpenText, 0x1B91, shouldRegister, onDoubleTime);
    COND_ID_HOOK(OnOpenText, 0x1B90, shouldRegister, onDoubleTime);
    COND_ID_HOOK(OnOpenText, 0x1B8F, shouldRegister, onDoubleTime);
    COND_ID_HOOK(OnOpenText, 0x1B92, shouldRegister, onDoubleTime);
    COND_ID_HOOK(OnOpenText, 0x1B8E, shouldRegister, onDoubleTime);

    COND_VB_SHOULD(VB_TIME_UNTIL_MOON_CRASH_CALCULATION, shouldRegister, {
        *should = false;
        u32* timeVar = va_arg(args, u32*);
        u8 ownedHalfDaysMask = Rando::ClockItems::GetAllOwnedHalfDaysMask();
        u32 totalHours = 0;

        for (int halfDayIndex = 0; halfDayIndex < Rando::ClockItems::HALF_COUNT; ++halfDayIndex) {
            if (ownedHalfDaysMask & (1 << halfDayIndex)) {
                totalHours += 12;
            }
        }

        bool shouldIncludeTerminalHours = (GetCurrentHalfDayIndex() == Rando::ClockItems::TERMINAL_STATE) ||
                                          !Rando::Logic::OwnsClockHalfDay(Rando::ClockItems::HALF_DAY3_NIGHT);

        if (shouldIncludeTerminalHours) {
            u16 terminalTime = GetConfiguredTerminalTime();
            u32 terminalZeroed = ZERO_DAY_START(terminalTime);
            u32 timeDiff = (DAY_LENGTH - terminalZeroed) % DAY_LENGTH;
            u32 terminalHours = (timeDiff + CLOCK_TIME_HOUR - 1) / CLOCK_TIME_HOUR;
            totalHours += terminalHours;
        }

        u32 ownedTime = totalHours * CLOCK_TIME_HOUR;
        *timeVar = ownedTime;
    });
}
