
/**
 * Behavior for bhvMontyMole, bhvMontyMoleHole, and bhvMontyMoleRock.
 * The monty mole holes are placed statically. The monty moles repeatedly
 * search for an available hole to pop out of.
 */

/**
 * The particles that pop out of the ground when a monty mole rises from a hole.
 */
static struct SpawnParticlesInfo sMontyMoleRiseFromGroundParticles = {
    /* behParam:        */ 0,
    /* count:           */ 3,
    /* model:           */ MODEL_SAND_DUST,
    /* offsetY:         */ 0,
    /* forwardVelBase:  */ 4,
    /* forwardVelRange: */ 4,
    /* velYBase:        */ 10,
    /* velYRange:       */ 15,
    /* gravity:         */ -4,
    /* dragStrength:    */ 0,
    /* sizeBase:        */ 10.0f,
    /* sizeRange:       */ 7.0f,
};

/**
 * Hitbox for monty mole.
 */
static struct ObjectHitbox sMontyMoleHitbox = {
    /* interactType:      */ INTERACT_BOUNCE_TOP,
    /* downOffset:        */ 0,
    /* damageOrCoinValue: */ 2,
    /* health:            */ -1,
    /* numLootCoins:      */ 0,
    /* radius:            */ 70,
    /* height:            */ 50,
    /* hurtboxRadius:     */ 30,
    /* hurtboxHeight:     */ 40,
};

/**
 * Hitbox for monty mole rock.
 */
static struct ObjectHitbox sMontyMoleRockHitbox = {
    /* interactType:      */ INTERACT_MR_BLIZZARD,
    /* downOffset:        */ 15,
    /* damageOrCoinValue: */ 1,
    /* health:            */ 99,
    /* numLootCoins:      */ 0,
    /* radius:            */ 30,
    /* height:            */ 15,
    /* hurtboxRadius:     */ 30,
    /* hurtboxHeight:     */ 15,
};

/**
 * The particles that spawn when a monty mole rock breaks.
 */
static struct SpawnParticlesInfo sMontyMoleRockBreakParticles = {
    /* behParam:        */ 0,
    /* count:           */ 2,
    /* model:           */ MODEL_PEBBLE,
    /* offsetY:         */ 10,
    /* forwardVelBase:  */ 4,
    /* forwardVelRange: */ 4,
    /* velYBase:        */ 10,
    /* velYRange:       */ 15,
    /* gravity:         */ -4,
    /* dragStrength:    */ 0,
    /* sizeBase:        */ 8.0f,
    /* sizeRange:       */ 4.0f,
};

// TODO: bss

/**
 * The first hole in the list of monty mole holes. The list is a singly linked
 * list using the parentObj field.
 */
struct Object *sMontyMoleHoleList;

/**
 * The number of nearby monty moles that have been killed in a row.
 */
s32 sMontyMoleKillStreak;

/**
 * The position of the last killed monty mole, used for determining whether
 * the next killed monty mole is nearby.
 */
f32 sMontyMoleLastKilledPosX;
f32 sMontyMoleLastKilledPosY;
f32 sMontyMoleLastKilledPosZ;

/**
 * Link all objects with the given behavior using parentObj.
 * The result is a singly linked list in reverse processing order. Return the
 * start of this list.
 */
static struct Object *link_objects_with_behavior(const BehaviorScript *behavior) {
    const BehaviorScript *behaviorAddr;
    struct Object *obj;
    struct Object *lastObject;
    struct ObjectNode *listHead;

    behaviorAddr = segmented_to_virtual(behavior);
    lastObject = NULL;

    listHead = &gObjectLists[get_object_list_from_behavior(behaviorAddr)];

    obj = (struct Object *) listHead->next;
    while (obj != (struct Object *) listHead) {
        if (obj->behavior == behaviorAddr && obj->activeFlags != ACTIVE_FLAGS_DEACTIVATED) {
            obj->parentObj = lastObject;
            lastObject = obj;
        }

        obj = (struct Object *) obj->header.next;
    }

    return lastObject;
}

/**
 * Select a random hole that is within minDistToMario and 1500 of mario, and
 * whose cooldown is zero. Return NULL if no hole is available.
 */
static struct Object *monty_mole_select_available_hole(f32 minDistToMario) {
    struct Object *hole = sMontyMoleHoleList;
    s32 numAvailableHoles = 0;

    while (hole != NULL) {
        if (hole->oMontyMoleHoleCooldown == 0) {
            if (hole->oDistanceToMario < 1500.0f && hole->oDistanceToMario > minDistToMario) {
                numAvailableHoles++;
            }
        }

        hole = hole->parentObj;
    }

    if (numAvailableHoles != 0) {
        s32 selectedHole = (s32)(RandomFloat() * numAvailableHoles);

        hole = sMontyMoleHoleList;
        numAvailableHoles = 0;

        while (hole != NULL) {
            if (hole->oMontyMoleHoleCooldown == 0) {
                if (hole->oDistanceToMario < 1500.0f && hole->oDistanceToMario > minDistToMario) {
                    if (numAvailableHoles == selectedHole) {
                        return hole;
                    }

                    numAvailableHoles++;
                }
            }

            hole = hole->parentObj;
        }
    }

    return NULL;
}

/**
 * Update function for bhvMontyMoleHole.
 */
void bhv_monty_mole_hole_update(void) {
    // If hole list hasn't been constructed yet, construct it
    if (o->parentObj == o) {
        sMontyMoleHoleList = link_objects_with_behavior(bhvMontyMoleHole);
        sMontyMoleKillStreak = 0;
    } else if (o->oMontyMoleHoleCooldown > 0) {
        o->oMontyMoleHoleCooldown -= 1;
    }
}

/**
 * Spawn dirt particles when rising out of the ground.
 *
 * TODO: (Scrub C) monty_mole_spawn_dirt_particles, water_bomb_spawn_explode_particles, and
 * func_80306ED4 all have similar issues with their functions, none of which match legitimately.
 */
void monty_mole_spawn_dirt_particles(s8 offsetY, s8 velYBase) {
#if defined(VERSION_JP) || defined(VERSION_US)
    sMontyMoleRiseFromGroundParticles.offsetY = offsetY;
    sMontyMoleRiseFromGroundParticles.velYBase = velYBase;
    cur_obj_spawn_particles(&sMontyMoleRiseFromGroundParticles);
#else
    s8 tempVelYBase = velYBase;
    s8 tempOffsetY = offsetY;

    do {
        sMontyMoleRiseFromGroundParticles.offsetY = tempOffsetY;
        sMontyMoleRiseFromGroundParticles.velYBase = tempVelYBase;
        cur_obj_spawn_particles(&sMontyMoleRiseFromGroundParticles);
    } while (0);
#endif
}

/**
 * Init function for bhvMontyMole.
 */
void bhv_monty_mole_init(void) {
    o->oMontyMoleCurrentHole = NULL;
}

/**
 * Search for an available hole. Once one is available, claim it and enter
 * either the rise from hole or jump out of hole action.
 */
static void monty_mole_act_select_hole(void) {
    f32 minDistToMario;

    if (o->oBehParams2ndByte != MONTY_MOLE_BP_NO_ROCK) {
        minDistToMario = 200.0f;
    } else if (gMarioStates[0].forwardVel < 8.0f) {
        minDistToMario = 100.0f;
    } else {
        minDistToMario = 500.0f;
    }

    // Select a hole to pop out of
    if ((o->oMontyMoleCurrentHole = monty_mole_select_available_hole(minDistToMario)) != NULL) {
        cur_obj_play_sound_2(SOUND_OBJ2_MONTY_MOLE_APPEAR);

        // Mark hole as unavailable
        o->oMontyMoleCurrentHole->oMontyMoleHoleCooldown = -1;

        // Position at hole
        o->oPosX = o->oMontyMoleCurrentHole->oPosX;
        o->oPosY = o->oFloorHeight = o->oMontyMoleCurrentHole->oPosY;
        o->oPosZ = o->oMontyMoleCurrentHole->oPosZ;

        o->oFaceAnglePitch = 0;
        o->oMoveAngleYaw = o->oMontyMoleCurrentHole->oAngleToMario;

        if (o->oDistanceToMario > 500.0f || minDistToMario > 100.0f || RandomSign() < 0) {
            o->oAction = MONTY_MOLE_ACT_RISE_FROM_HOLE;
            o->oVelY = 3.0f;
            o->oGravity = 0.0f;
            monty_mole_spawn_dirt_particles(0, 10);
        } else {
            o->oAction = MONTY_MOLE_ACT_JUMP_OUT_OF_HOLE;
            o->oVelY = 50.0f;
            o->oGravity = -4.0f;
            monty_mole_spawn_dirt_particles(0, 20);
        }

        cur_obj_unhide();
        cur_obj_become_tangible();
    }
}

/**
 * Move upward until high enough, then enter the spawn rock action.
 */
static void monty_mole_act_rise_from_hole(void) {
    cur_obj_init_animation_with_sound(1);

    if (o->oMontyMoleHeightRelativeToFloor >= 49.0f) {
        o->oPosY = o->oFloorHeight + 50.0f;
        o->oVelY = 0.0f;

        if (cur_obj_check_if_near_animation_end()) {
            o->oAction = MONTY_MOLE_ACT_SPAWN_ROCK;
        }
    }
}

/**
 * If mario is close enough, then spawn a rock and enter the throw rock action.
 * Otherwise, enter the begin jump into hole action.
 */
static void monty_mole_act_spawn_rock(void) {
    struct Object *rock;

    if (func_802F92B0(2)) {
        if (o->oBehParams2ndByte != MONTY_MOLE_BP_NO_ROCK
            && abs_angle_diff(o->oAngleToMario, o->oMoveAngleYaw) < 0x4000
            && (rock = spawn_object(o, MODEL_PEBBLE, bhvMontyMoleRock)) != NULL) {
            o->prevObj = rock;
            o->oAction = MONTY_MOLE_ACT_THROW_ROCK;
        } else {
            o->oAction = MONTY_MOLE_ACT_BEGIN_JUMP_INTO_HOLE;
        }
    }
}

/**
 * Wait until mario is relatively close, then set vely to 40 and enter the jump
 * into hole action.
 */
static void monty_mole_act_begin_jump_into_hole(void) {
    if (func_802F92B0(3) || obj_is_near_to_and_facing_mario(1000.0f, 0x4000)) {
        o->oAction = MONTY_MOLE_ACT_JUMP_INTO_HOLE;
        o->oVelY = 40.0f;
        o->oGravity = -6.0f;
    }
}

/**
 * Throw the held rock, then enter the begin jump into hole action.
 */
static void monty_mole_act_throw_rock(void) {
    if (func_802F92EC(8, 10)) {
        cur_obj_play_sound_2(SOUND_OBJ_MONTY_MOLE_ATTACK);
        o->prevObj = NULL;
    }

    if (cur_obj_check_if_near_animation_end()) {
        o->oAction = MONTY_MOLE_ACT_BEGIN_JUMP_INTO_HOLE;
    }
}

/**
 * Tilt downward and wait until close to landing, then enter the hide action.
 */
static void monty_mole_act_jump_into_hole(void) {
    func_802F927C(0);

    o->oFaceAnglePitch = -atan2s(o->oVelY, -4.0f);

    if (o->oVelY < 0.0f && o->oMontyMoleHeightRelativeToFloor < 120.0f) {
        o->oAction = MONTY_MOLE_ACT_HIDE;
        o->oGravity = 0.0f;
        monty_mole_spawn_dirt_particles(-80, 15);
    }
}

/**
 * Become intangible and enter the select hole action.
 */
static void monty_mole_hide_in_hole(void) {
    o->oMontyMoleCurrentHole->oMontyMoleHoleCooldown = 30;
    o->oAction = MONTY_MOLE_ACT_SELECT_HOLE;
    o->oVelY = 0.0f;

    //! Even though the object becomes intangible here, it is still possible
    //  for a bob-omb to interact with it later in the frame (since bob-ombs
    //  come after monty moles in processing order).
    //  This means that the monty mole can be attacked while in the select hole
    //  action. If no hole is available (e.g. because mario is too far away),
    //  the game will crash because of the line above that accesses
    //  oMontyMoleCurrentHole.
    cur_obj_become_intangible();
}

/**
 * Wait to land on the floor, then hide.
 */
static void monty_mole_act_hide(void) {
    cur_obj_init_animation_with_sound(1);

    if (o->oMoveFlags & OBJ_MOVE_MASK_ON_GROUND) {
        cur_obj_hide();
        monty_mole_hide_in_hole();
    } else {
        approach_f32_ptr(&o->oVelY, -4.0f, 0.5f);
    }
}

/**
 * Jump upward and then fall back down. Then enter the begin jump into hole
 * action.
 */
static void monty_mole_act_jump_out_of_hole(void) {
    if (o->oVelY > 0.0f) {
        cur_obj_init_animation_with_sound(9);
    } else {
        func_802F927C(4);

        if (o->oMontyMoleHeightRelativeToFloor < 50.0f) {
            o->oPosY = o->oFloorHeight + 50.0f;
            o->oAction = MONTY_MOLE_ACT_BEGIN_JUMP_INTO_HOLE;
            o->oVelY = o->oGravity = 0.0f;
        }
    }
}

/**
 * Update function for bhvMontyMole.
 */
void bhv_monty_mole_update(void) {
    // PARTIAL_UPDATE

    o->oDeathSound = SOUND_OBJ_DYING_ENEMY1;
    cur_obj_update_floor_and_walls();

    o->oMontyMoleHeightRelativeToFloor = o->oPosY - o->oFloorHeight;

    switch (o->oAction) {
        case MONTY_MOLE_ACT_SELECT_HOLE:
            monty_mole_act_select_hole();
            break;
        case MONTY_MOLE_ACT_RISE_FROM_HOLE:
            monty_mole_act_rise_from_hole();
            break;
        case MONTY_MOLE_ACT_SPAWN_ROCK:
            monty_mole_act_spawn_rock();
            break;
        case MONTY_MOLE_ACT_BEGIN_JUMP_INTO_HOLE:
            monty_mole_act_begin_jump_into_hole();
            break;
        case MONTY_MOLE_ACT_THROW_ROCK:
            monty_mole_act_throw_rock();
            break;
        case MONTY_MOLE_ACT_JUMP_INTO_HOLE:
            monty_mole_act_jump_into_hole();
            break;
        case MONTY_MOLE_ACT_HIDE:
            monty_mole_act_hide();
            break;
        case MONTY_MOLE_ACT_JUMP_OUT_OF_HOLE:
            monty_mole_act_jump_out_of_hole();
            break;
    }

    // Spawn a 1-up if you kill 8 monty moles
    if (obj_check_attacks(&sMontyMoleHitbox, o->oAction)) {
        if (sMontyMoleKillStreak != 0) {
            f32 dx = o->oPosX - sMontyMoleLastKilledPosX;
            f32 dy = o->oPosY - sMontyMoleLastKilledPosY;
            f32 dz = o->oPosZ - sMontyMoleLastKilledPosZ;

            f32 distToLastKill = sqrtf(dx * dx + dy * dy + dz * dz);

            //! The two farthest holes on the bottom level of TTM are more than
            //  1500 units away from each other, so the counter resets if you
            //  attack moles in these holes consecutively.
            if (distToLastKill < 1500.0f) {
                if (sMontyMoleKillStreak == 7) {
                    play_puzzle_jingle();
                    spawn_object(o, MODEL_1UP, bhv1upWalking);
                }
            } else {
                sMontyMoleKillStreak = 0;
            }
        }

        //! No overflow check
        sMontyMoleKillStreak += 1;

        sMontyMoleLastKilledPosX = o->oPosX;
        sMontyMoleLastKilledPosY = o->oPosY;
        sMontyMoleLastKilledPosZ = o->oPosZ;

        monty_mole_hide_in_hole();

        // Throw rock if holding one
        o->prevObj = NULL;
    }

    cur_obj_move_standard(78);
}

/**
 * Wait for monty mole to throw the rock, then enter the move action.
 */
static void monty_mole_rock_act_held(void) {
    // The position is offset since the monty mole is throwing it with its hand
    o->oParentRelativePosX = 80.0f;
    o->oParentRelativePosY = -50.0f;
    o->oParentRelativePosZ = 0.0f;

    if (o->parentObj->prevObj == NULL) {
        f32 distToMario = o->oDistanceToMario;
        if (distToMario > 600.0f) {
            distToMario = 600.0f;
        }

        o->oAction = MONTY_MOLE_ROCK_ACT_MOVE;

        // The angle is adjusted to compensate for the start position offset
        o->oMoveAngleYaw = (s32)(o->parentObj->oMoveAngleYaw + 0x1F4 - distToMario * 0.1f);

        o->oForwardVel = 40.0f;
        o->oVelY = distToMario * 0.08f + 8.0f;

        o->oMoveFlags = 0;
    }
}

/**
 * Move, then despawn after hitting the ground or water.
 */
static void monty_mole_rock_act_move(void) {
    cur_obj_update_floor_and_walls();

    if (o->oMoveFlags & (OBJ_MOVE_MASK_ON_GROUND | OBJ_MOVE_ENTERED_WATER)) {
        cur_obj_spawn_particles(&sMontyMoleRockBreakParticles);
        obj_mark_for_deletion(o);
    }

    cur_obj_move_standard(78);
}

/**
 * Update function for bhvMontyMoleRock.
 */
void bhv_monty_mole_rock_update(void) {
    // PARTIAL_UPDATE
    //! Since we can prevent them from despawning using partial updates, we
    //  can fill up object slots to crash the game.

    obj_check_attacks(&sMontyMoleRockHitbox, o->oAction);

    switch (o->oAction) {
        case MONTY_MOLE_ROCK_ACT_HELD:
            monty_mole_rock_act_held();
            break;
        case MONTY_MOLE_ROCK_ACT_MOVE:
            monty_mole_rock_act_move();
            break;
    }
}
