/**
 * @file octorokBoss.c
 * @ingroup Enemies
 *
 * @brief Octorok boss enemy
 */
#define NENT_DEPRECATED
#include "enemy/octorokBoss.h"
#include "functions.h"
#include "game.h"
#include "object.h"
#include "projectile.h"

#define IS_FROZEN(this) ((this)->bossPhase & 1)

/*
for TAIL_END object:
this->field_0x7c.BYTES.byte1 tailRadius
*/
#define GET_TAIL_RADIUS(this) this->currentAttack // this->field_0x7c.BYTES.byte1

void OctorokBoss_Hit(OctorokBossEntity*);
void OctorokBoss_Action1(OctorokBossEntity*);
void OctorokBoss_Burning_SubAction1(OctorokBossEntity*);
void sub_080368D8(OctorokBossEntity*);
void sub_08036914(Entity*, s32, s32);
void sub_08036998(OctorokBossEntity*);
void sub_080369D0(OctorokBossEntity*, s32, s32);
void sub_08036AF0(OctorokBossEntity*, s32, s32);
void OctorokBoss_SetAttackTimer(OctorokBossEntity*);
void OctorokBoss_ResetToSubAction0(OctorokBossEntity*);
void OctorokBoss_WaitAnotherTurn(OctorokBossEntity*);
void OctorokBoss_SetWaitTurnsForNextAttack(OctorokBossEntity*);
void OctorokBoss_StartRegularAttack(OctorokBossEntity*);
void OctorokBoss_ChangePalette(OctorokBossEntity*, u32);
void sub_08036F60(OctorokBossEntity*);
void OctorokBoss_StepSound(OctorokBossEntity*, u32);
void sub_08036FE4(OctorokBossEntity*);
void OctorokBoss_OnTick(OctorokBossEntity*);
void OctorokBoss_OnDeath(OctorokBossEntity*);
void OctorokBoss_Hit_SubAction0(OctorokBossEntity*);
void OctorokBoss_Hit_SubAction1(OctorokBossEntity*);
void OctorokBoss_Hit_SubAction2(OctorokBossEntity*);
void OctorokBoss_Hit_SubAction3(OctorokBossEntity*);
void OctorokBoss_Hit_SubAction4(OctorokBossEntity*);
void OctorokBoss_Hit_SubAction5(OctorokBossEntity*);
void OctorokBoss_Hit_SubAction6(OctorokBossEntity*);
void OctorokBoss_Intro_SubAction0(OctorokBossEntity*);
void OctorokBoss_Intro_SubAction1(OctorokBossEntity*);
void OctorokBoss_Intro_SubAction2(OctorokBossEntity*);
void OctorokBoss_Intro_SubAction3(OctorokBossEntity*);
void OctorokBoss_Intro_SubAction4(OctorokBossEntity*);
void OctorokBoss_Intro_SubAction5(OctorokBossEntity*);
void OctorokBoss_Action1_AimTowardsPlayer(OctorokBossEntity*);
void OctorokBoss_Action1_WaitForTurn(OctorokBossEntity*);
void OctorokBoss_Action1_WaitForAttack(OctorokBossEntity*);
void OctorokBoss_Action1_ChargeAttack(OctorokBossEntity*);
void OctorokBoss_Action1_Attack(OctorokBossEntity*);
void OctorokBoss_Init(OctorokBossEntity*);
void OctorokBoss_Action1(OctorokBossEntity*);
void OctorokBoss_Hit(OctorokBossEntity*);
void OctorokBoss_Intro(OctorokBossEntity*);
void OctorokBoss_Burning(OctorokBossEntity*);
void OctorokBoss_Action1_Attack_Type2_0(OctorokBossEntity*);
void OctorokBoss_Action1_Attack_Type2_1(OctorokBossEntity*);
void OctorokBoss_Action1_Attack_Type2_2(OctorokBossEntity*);
void OctorokBoss_Action1_Attack_Type2_3(OctorokBossEntity*);
void OctorokBoss_ExecuteAttackSpitRock(OctorokBossEntity*);
void OctorokBoss_ExecuteAttackVacuum(OctorokBossEntity*);
void OctorokBoss_ExecuteAttackSmoke(OctorokBossEntity*);
void OctorokBoss_ExecuteAttackFreeze(OctorokBossEntity*);
void OctorokBoss_Burning_SubAction0(OctorokBossEntity*);
void OctorokBoss_Burning_SubAction1(OctorokBossEntity*);
void OctorokBoss_Burning_SubAction2(OctorokBossEntity*);

void OctorokBoss(OctorokBossEntity* this) {
    static void (*const OctorokBoss_Functions[])(OctorokBossEntity*) = {
        OctorokBoss_OnTick,
        OctorokBoss_OnTick,
        (void (*)(OctorokBossEntity*))GenericKnockback,
        OctorokBoss_OnDeath,
        (void (*)(OctorokBossEntity*))GenericConfused,
    };
    OctorokBoss_Functions[GetNextFunction(super)](this);
}

void OctorokBoss_OnDeath(OctorokBossEntity* this) {
    if (super->type == WHOLE) {
        super->action = HIT;
        super->subAction = 0;
        super->knockbackDuration = 0;
        super->health = 1;
        sub_080368D8(this);
        OctorokBoss_Hit(this);
    } else {
        DeleteThisEntity();
    }
}

/*
Hit SubActions
0: Start
1-3:
4-
*/
void OctorokBoss_Hit(OctorokBossEntity* this) {
    static void (*const OctorokBoss_Hit_SubActions[])(OctorokBossEntity*) = {
        OctorokBoss_Hit_SubAction0, OctorokBoss_Hit_SubAction1, OctorokBoss_Hit_SubAction2, OctorokBoss_Hit_SubAction3,
        OctorokBoss_Hit_SubAction4, OctorokBoss_Hit_SubAction5, OctorokBoss_Hit_SubAction6,
    };
    if (this->bossPhase == 0) {
        if (super->subAction != 3) {
            gRoomControls.camera_target = &this->heap->tailObjects[0]->base;
            this->heap->field_0x7 = 0x5a;
            sub_08078B48();
        }
    } else {
        if (this->heap->field_0x7 != 0) {
            this->heap->field_0x7--;
            sub_08078B48();
        }
    }
    OctorokBoss_Hit_SubActions[super->subAction](this);
    if (super->subAction > 3) {
        sub_08078B48();
    }
    sub_0800445C(super);
    sub_0805EC9C(super, this->unk_76, this->unk_74, this->angle.HWORD);
}

void OctorokBoss_Hit_SubAction0(OctorokBossEntity* this) {
    this->unk_76 = 0xa0;
    this->unk_74 = 0xa0;
    this->heap->fallingStonesTimer = 0;
    if (this->bossPhase == 4) {
        super->subAction = 4;
        gUnk_02034490.unk0 = 1;
        sub_08078B48();
        SoundReq(SFX_BOSS_DIE);
    } else {
        if (IS_FROZEN(this) == FALSE) {
            super->type2 = 0;
            this->timer = 0x3c;
        } else {
            if (this->heap->tailCount > 3) {
                this->heap->tailCount--;
            }
            this->heap->tailObjects[0]->currentAttack = 0;
            this->timer = 0x78;
        }
        super->subAction = 1;
    }
}

void OctorokBoss_Hit_SubAction1(OctorokBossEntity* this) {
    bool32 frozen = IS_FROZEN(this);
    u16 diffX;
    u16 diffY;

    if (frozen == 0) {
        if (this->heap->tailObjects[0]->currentAttack != 0) {
            this->heap->tailObjects[0]->currentAttack--;
        }
        // Move to the center of the screen before freezing
        diffX = 0x108 + gRoomControls.origin_x - super->x.HALF.HI + 0x4;
        diffY = gRoomControls.origin_y - super->y.HALF.HI + 0x8c;
        if (diffX > 8 || diffY > 8) {
            this->heap->field_0x2 = 1;
#if defined(JP) || defined(DEMO_JP) || defined(EU)
            super->direction = ((s32)sub_080045DA((((gRoomControls.origin_x + 0x108) << 0x10) - super->x.WORD),
                                                  (((gRoomControls.origin_y + 0x88) << 0x10) - super->y.WORD))) >>
                               3;
#else
            super->direction = ((s32)sub_080045DA(gRoomControls.origin_x + 0x108 - super->x.HALF.HI,
                                                  gRoomControls.origin_y + 0x88 - super->y.HALF.HI)) >>
                               3;
#endif
            super->speed = 0x100;
            ProcessMovement0(super);
        } else {
            // Freeze
            if (super->type2 == 0) {
                CreateObjectWithParent(super, OCTOROK_BOSS_OBJECT, 5, 0);
                CreateObjectWithParent(super, OCTOROK_BOSS_OBJECT, 5, 1);
                CreateObjectWithParent(super, OCTOROK_BOSS_OBJECT, 5, 2);
                CreateObjectWithParent(super, OCTOROK_BOSS_OBJECT, 5, 3);
                CreateObjectWithParent(super, OCTOROK_BOSS_OBJECT, 5, 4);
                super->type2 = 1;
            }
            this->heap->field_0x2 = frozen;
            this->timer--;
        }
    } else {
        u32 i;
        for (i = this->heap->tailCount - 1; i != 0; i--) {
            OctorokBossEntity* tail = this->heap->tailObjects[i - 1];
            tail->base.spriteSettings.draw |= 1;
        }
        if ((gRoomTransition.frameCount & 2) != 0) {
            CreateObjectWithParent(super, OCTOROK_BOSS_OBJECT, 6, 0);
        }
        this->timer--;
    }
    if (this->timer == 0) {
        super->subAction = 2;
        this->bossPhase++;
        UnloadGFXSlots(super);
        if (IS_FROZEN(this) == FALSE) {
            super->hitType = 0x5f;
            LoadFixedGFX(super, 0x108);
            ChangeObjPalette(super, 0xef);
            OctorokBoss_ChangePalette(this, 0xef);
            InitAnimationForceUpdate(&(this->heap->tailObjects[0]->base), 1);
        } else {
            super->hitType = 0x61;
            LoadFixedGFX(super, 0x109);
            ChangeObjPalette(super, 0xf0);
            OctorokBoss_ChangePalette(this, 0xf3);
            InitAnimationForceUpdate(&(this->heap->tailObjects[0]->base), 2);
        }
        CreateObjectWithParent(super, OCTOROK_BOSS_OBJECT, 8, 0);
    }
}

void OctorokBoss_Hit_SubAction2(OctorokBossEntity* this) {
    if (this->heap->tailObjects[0]->currentAttack != 0x80) {
        this->heap->tailObjects[0]->currentAttack++;
    } else {
        super->subAction = 3;
        this->timer = 0x96;
        gRoomControls.camera_target = &gPlayerEntity;
    }
}

void OctorokBoss_Hit_SubAction3(OctorokBossEntity* this) {
    static const u8 OctorokBoss_HealthPerPhase[] = {
        3, 1, 3, 1, 3, 1, 3, 0,
    };
    if (this->timer-- == 0) {
        super->health = OctorokBoss_HealthPerPhase[this->bossPhase];
        COLLISION_ON(super);
        super->action = ACTION1;
        super->subAction = ACTION1_AIMTOWARDSPLAYER;
        this->nextAttackIndex = 0;
        OctorokBoss_SetWaitTurnsForNextAttack(this);
    }
}

void OctorokBoss_Hit_SubAction4(OctorokBossEntity* this) {
    Entity* object;
    super->subAction = 5;
    object = CreateObjectWithParent(super, OCTOROK_BOSS_OBJECT, 9, 0);
    if (object != NULL) {
        gRoomControls.camera_target = object;
    }
}

void OctorokBoss_Hit_SubAction5(OctorokBossEntity* this) {
    super->subAction = 6;
    this->timer = 0x78;
    this->unk_80 = 0;
    this->angularSpeed.HALF.LO = 0;
}

// Wildly rotating with explosion fx
void OctorokBoss_Hit_SubAction6(OctorokBossEntity* this) {
    s16 tmp;

    this->angle.HALF.HI -= 4;
    this->phase4AttackPattern += 8;
    if ((this->unk_80 & 0x80) != 0) {
        this->unk_80 -= (Random() & 3);
        if ((this->unk_80 & 0x80) == 0) {
            this->unk_80 = ((this->unk_80 & 0x80) << 0x18) >> 0x18;
        }
    } else {
        this->unk_80 += (Random() & 3);
        if ((this->unk_80) >= 0x19) {
            this->unk_80 |= 0x80;
        }
    }
    tmp = FixedMul(gSineTable[this->phase4AttackPattern], (this->unk_80 & 0x7f) << 8);
    tmp = FixedDiv(tmp, 0x100);
    super->spriteOffsetX = tmp >> 8;
    tmp = FixedMul(gSineTable[this->phase4AttackPattern + 0x40], (this->unk_80 & 0x7f) * 0x100);
    tmp = FixedDiv(tmp, 0x100);
    super->spriteOffsetY = -((tmp << 0x10) >> 8) >> 0x10;
    if (this->timer == 0) {
        if ((gRoomTransition.frameCount & 0xfU) == 0) {
            // Explosion in the center
            CreateFx(super, FX_GIANT_EXPLOSION3, 0);
            // Explosion at the front right leg
            CreateFx(&this->heap->legObjects[0]->base, FX_GIANT_EXPLOSION3, 0);
        }
        if (++this->angularSpeed.HALF.LO == 0x79) {
            this->heap->mouthObject->base.health = 1;
            SoundReq(SFX_BOSS_DIE);
            // Kill this boss
            GenericDeath(super);
        }
    } else {
        this->timer--;
    }
}

void OctorokBoss_OnTick(OctorokBossEntity* this) {
    static void (*const OctorokBoss_Actions[])(OctorokBossEntity*) = {
        OctorokBoss_Init, OctorokBoss_Action1, OctorokBoss_Hit, OctorokBoss_Intro, OctorokBoss_Burning,
    };

    OctorokBoss_Actions[super->action](this);
    super->spriteRendering.b3 = 3;
}

const u8 gUnk_080CF08C[] = {
    0, 4, 0, 0, 1, 5, 0, 0, 1, 4, 0, 0, 1, 3, 0, 0, 1, 2, 0, 0, 2, 1, 0, 0, 1, 1, 0, 0, 3, 6, 0, 0,
};

ASM_FUNC("asm/non_matching/octorokBoss/OctorokBoss_Init.inc", void OctorokBoss_Init(OctorokBossEntity* this))

void OctorokBoss_Intro(OctorokBossEntity* this) {
    static void (*const OctorokBoss_Intro_SubActions[])(OctorokBossEntity*) = {
        OctorokBoss_Intro_SubAction0, OctorokBoss_Intro_SubAction1, OctorokBoss_Intro_SubAction2,
        OctorokBoss_Intro_SubAction3, OctorokBoss_Intro_SubAction4, OctorokBoss_Intro_SubAction5,
    };
    sub_08078B48();
    gUnk_02034490.unk0 = 1;
    sub_08036F60(this);
    OctorokBoss_Intro_SubActions[super->subAction](this);
    sub_0805EC9C(super, this->unk_76, this->unk_74, this->angle.HWORD);
}

void OctorokBoss_Intro_SubAction0(OctorokBossEntity* this) {
    // Wait until the camera is on the Octorok
    if (this->timer-- == 0) {
        super->subAction = 1;
        this->angularSpeed.HWORD = 0x100;
        this->heap->unk_0 = 2;
    }
}

void OctorokBoss_Intro_SubAction1(OctorokBossEntity* this) {
    // Rotate Octorok to player
    if (this->angle.HALF.HI == 0x80) {
        this->timer = 0x3c;
        super->subAction = 2;
        this->heap->unk_0 = 0;
        // Octorok scream
        SoundReq(SFX_159);
    } else {
        this->angle.HWORD += this->angularSpeed.HWORD;
    }
    OctorokBoss_StepSound(this, 0xf);
}

void OctorokBoss_Intro_SubAction2(OctorokBossEntity* this) {
    // Wait for scream end
    if (this->timer-- == 0) {
        super->subAction = 3;
        this->timer = 0x3c;
        gPlayerEntity.spriteSettings.draw |= 1;
        gRoomControls.camera_target = &gPlayerEntity;
        gRoomControls.unk5 = 1;
    }
}

void OctorokBoss_Intro_SubAction3(OctorokBossEntity* this) {
    // Move the camera to the player
    if (this->timer-- == 0) {
        // Move the player inside the arena
        gPlayerEntity.direction = 0x10;
        gPlayerEntity.animationState = 4;
        sub_08078AC0(0x1e, 0, 0);
        this->timer = 0x3c;
        super->subAction = 4;
    }
}

void OctorokBoss_Intro_SubAction4(OctorokBossEntity* this) {
    if (this->timer-- == 0) {
        super->subAction = 5;
        this->timer = 0x2d;
        // Make the player look towards the exit
        gPlayerEntity.animationState = 0;
    } else {
        // Spawn exclamation bubble at a certain time
        if (this->timer == 0x1e) {
            CreateSpeechBubbleExclamationMark(&gPlayerEntity, 0xc, -0x18);
        }
    }
}

void OctorokBoss_Intro_SubAction5(OctorokBossEntity* this) {
    if (gPlayerEntity.animationState == 4) {
        if (this->timer++ > 0x1e) {
            // Play boss theme, enable control and switch to main action
            super->action = ACTION1;
            super->subAction = 0;
            gRoomControls.unk5 = gPlayerEntity.animationState;
            OctorokBoss_SetAttackTimer(this);
            gUnk_02034490.unk0 = 0;
            SoundReq(BGM_BOSS_THEME);
        }
    } else {
        if (this->timer-- == 0) {
            // Player looks back towards Octorok
            gPlayerEntity.animationState = 4;
        }
    }
}

void OctorokBoss_Action1(OctorokBossEntity* this) {
    static void (*const OctorokBoss_Action1_SubActions[])(OctorokBossEntity*) = {
        OctorokBoss_Action1_AimTowardsPlayer, OctorokBoss_Action1_WaitForTurn, OctorokBoss_Action1_ChargeAttack,
        OctorokBoss_Action1_WaitForAttack,    OctorokBoss_Action1_Attack,
    };
    static const u8 OctorokBoss_LegAngleOffset[] = {
        40,
        80,
        176,
        216,
    };
    static const u8 OctorokBoss_LegAngleOffset2[] = {
        128,
        0,
        0,
        128,
    };
    Entity* object;
    u32 radius;
    u8 angle;

    if (super->type != WHOLE) {
        super->iframes = super->parent->iframes;
    }
    switch (super->type) {
        case LEG_BR:
        case LEG_FR:
        case LEG_FL:
        case LEG_BL:
            if ((((OctorokBossEntity*)super->parent)->field_0x6c.HALF.HI & 2) != 0) {
                DeleteThisEntity();
            }
            if (this->heap->mouthObject->base.health == 1) {
                if ((s16)this->unk_76 < 0) {
                    this->unk_76 -= 4;
                } else {
                    this->unk_76 += 4;
                }
                if ((s16)this->unk_74 < 0) {
                    this->unk_74 -= 4;
                } else {
                    this->unk_74 += 4;
                }
            }
            SortEntityBelow(super->parent, super);
            if (((this->heap->field_0x2 != 0) || (super->parent->action == INTRO)) ||
                (1 < (u8)(super->parent->subAction - 3))) {
                if ((s8)super->field_0xf < 0) {
                    super->field_0xf = -this->heap->unk_0;
                } else {
                    super->field_0xf = this->heap->unk_0;
                }
                sub_08036998(this);
            }
            radius = 0x10000 / ((OctorokBossEntity*)super->parent)->unk_76;
            radius = radius << 0xd >> 0x8;
            radius = radius - 0x2000;
            if (this->heap->mouthObject->base.health == 1) {
                radius = radius + 0x2200;
            } else {
                radius = (radius >> 1) + 0x2200;
            }
            angle = -(((OctorokBossEntity*)super->parent)->angle.HALF.HI + OctorokBoss_LegAngleOffset[super->type - 1]);
            sub_08036914(super, angle, radius);
            this->angle.HALF.HI = ((OctorokBossEntity*)super->parent)->angle.HALF.HI +
                                  OctorokBoss_LegAngleOffset2[super->type - 1] + this->timer;
            sub_0805EC9C(super, this->unk_76, this->unk_74, this->angle.HWORD);
            break;

        case TAIL:
            if (this->heap->mouthObject->base.health < 2) {
                DeleteThisEntity();
            }
            if ((this->heap->tailCount - 2) < super->type2) {
                DeleteThisEntity();
            }
            SortEntityAbove(super->parent, super);
            if (this->heap->tailCount - 2 == super->type2) {
                SortEntityAbove(super->parent, super);
                radius = 0x10000 / ((OctorokBossEntity*)super->parent)->unk_74;
                radius = radius << 0xd >> 0x8;
                angle = -((OctorokBossEntity*)super->parent)->angle.HALF.HI;
                sub_08036914(super, angle, radius);
                this->angle.HALF.HI = -((OctorokBossEntity*)super->parent)->angle.HALF.HI;
            }
            if (IS_FROZEN((OctorokBossEntity*)super->parent) == 0) {
                super->spriteSettings.draw |= 1;
            }
            break;

        case TAIL_END:
            if (this->heap->mouthObject->base.health < 2) {
                DeleteThisEntity();
            }
            UpdateAnimationSingleFrame(super);
            if (IS_FROZEN((OctorokBossEntity*)super->parent)) {
                sub_08036AF0(this, GET_TAIL_RADIUS(this), 0x10);
                if ((super->bitfield & 0x7f) == 7) {
                    COLLISION_OFF(super);
                    object = CreateObjectWithParent(super, OCTOROK_BOSS_OBJECT, 0, 0);
                    super->child = object;
                    if (object != NULL) {
                        object->parent = super->parent;
                        ((OctorokBossEntity*)super->child)->heap = this->heap;
                    }
                }
            } else {
                COLLISION_ON(super);
                super->spriteSettings.draw |= 1;
                sub_08036998(this);
                sub_080369D0(this, GET_TAIL_RADIUS(this), 4);
            }
            super->bitfield = 0;
            sub_0805EC9C(super, this->unk_76, this->unk_74, -this->angle.HWORD ^ 0x8000);
            break;

        case MOUTH:
            if (super->health == 1) {
                super->health = 0;
            } else {
                SortEntityBelow(super->parent, super);
                if ((super->parent->subAction != 4) && (super->health != 1)) {
                    if (this->timer > 0x1c) {
                        this->timer--;
                    }
                    if (this->unk_76 > 0x100) {
                        this->unk_76--;
                    } else {
                        this->unk_76 = 0x100;
                    }
                }
                radius = 0x10000 / ((OctorokBossEntity*)super->parent)->unk_74;
                radius = radius * (this->timer << 8) >> 8;
                angle = -(((OctorokBossEntity*)super->parent)->angle.HALF.HI + 0x80);
                sub_08036914(super, angle, radius);

                this->angle.HALF.HI = ((OctorokBossEntity*)super->parent)->angle.HALF.HI;
                sub_0805EC9C(super, this->unk_76, this->unk_74, this->angle.HWORD);
                sub_0800445C(super);
            }
            break;

        case WHOLE:
            sub_0800445C(super);
            sub_08036F60(this);
            if (this->heap->fallingStonesTimer != 0) {
                this->heap->fallingStonesTimer--;
                if ((gRoomTransition.frameCount & 3) == 0) {
                    // Falling stones
                    CreateProjectileWithParent(super, OCTOROK_BOSS_PROJECTILE, 3);
                }
            }
            OctorokBoss_Action1_SubActions[super->subAction](this);
            sub_0805EC9C(super, this->unk_76, this->unk_74, this->angle.HWORD);
            break;
    }
}

void OctorokBoss_Action1_AimTowardsPlayer(OctorokBossEntity* this) {
    s32 tmp1;
    s32 tmp2;

    tmp1 = (u8)(sub_080045DA(gPlayerEntity.x.WORD - super->x.WORD, gPlayerEntity.y.WORD - super->y.WORD) -
                (((u8)(-this->angle.HALF.HI) ^ 0x80)));
    if (IS_FROZEN(this) == FALSE) {
        tmp2 = 8;
    } else {
        tmp2 = 32;
    }
    // Probably that the boss is aiming at the player?
    if (tmp1 > -tmp2 && tmp1 < tmp2) {
        if (this->phase4AttackPattern != 0xff) {
            OctorokBoss_SetAttackTimer(this);
            return;
        }
        if (this->attackWaitTurns == 0) {
            OctorokBoss_StartRegularAttack(this);
        } else {
            OctorokBoss_WaitAnotherTurn(this); // Resets to subaction1
        }
    } else {
        // Rotate to face the player
        if ((u32)tmp1 > 0x80) {
            this->angle.HWORD += this->angularSpeed.HWORD;
            this->heap->rotation = ROTATION_CW;
        } else {
            this->angle.HWORD -= this->angularSpeed.HWORD;
            this->heap->rotation = ROTATION_CCW;
        }
    }
    super->direction = (u8)(-this->angle.HALF.HI ^ 0x80U) >> 3;
    if (IS_FROZEN(this)) {
        if (this->angularSpeed.HWORD < 0x280) {
            switch (this->bossPhase) {
                case 1:
                    this->angularSpeed.HWORD++;
                    break;
                case 3:
                    this->angularSpeed.HWORD += 2;
                    break;
            }
        }
    }
    OctorokBoss_StepSound(this, 0x1f);
}

void OctorokBoss_Action1_WaitForTurn(OctorokBossEntity* this) {
    if (((this->timer-- == 0) || (this->bossPhase == 0)) || (IS_FROZEN(this))) {
        this->attackWaitTurns--;
        OctorokBoss_ResetToSubAction0(this);
    } else {
        if (ProcessMovement0(super) == 0) {
            this->timer = 0;
        }
    }
    OctorokBoss_StepSound(this, 0x1f);
}

void OctorokBoss_Action1_WaitForAttack(OctorokBossEntity* this) {
    if (this->timer-- == 0) {
        OctorokBoss_SetWaitTurnsForNextAttack(this);
    }
}

// Charge forwards and let stones fall when a collision occurs.
void OctorokBoss_Action1_ChargeAttack(OctorokBossEntity* this) {
    bool32 knockbackCondition;

    if (this->timer == 0) {
        ProcessMovement0(super);
        knockbackCondition = 0;
        if ((super->direction != 0) && (super->direction != 0x10)) {
            knockbackCondition = ((u32)super->collisions & 0xee00) != 0;
        }
        if (((super->direction != 0x18) && (super->direction != 8)) && ((super->collisions & 0xee) != 0)) {
            knockbackCondition = 1;
        }
        if (knockbackCondition != 0) {
            super->knockbackDuration = 0x20;
            super->knockbackSpeed = 0x200;
            super->knockbackDirection = super->direction ^ 0x10;
            this->heap->fallingStonesTimer += 0x3c;
            OctorokBoss_SetAttackTimer(this);
            InitScreenShake(0x3c, 0);
            SoundReq(SFX_158);
            SoundReq(SFX_14C);
        }
    } else {
        this->timer--;
    }
    OctorokBoss_StepSound(this, 0xf);
}

void OctorokBoss_Action1_Attack(OctorokBossEntity* this) {
    static void (*const OctorokBoss_Action1_Attack_Type2s[])(OctorokBossEntity*) = {
        OctorokBoss_Action1_Attack_Type2_0,
        OctorokBoss_Action1_Attack_Type2_1,
        OctorokBoss_Action1_Attack_Type2_2,
        OctorokBoss_Action1_Attack_Type2_3,
    };
    OctorokBoss_Action1_Attack_Type2s[super->type2](this);
    sub_08036FE4(this);

    if (this->unk_80 != 0) {
        gPlayerEntity.spriteSettings.draw = 0;
        gPlayerEntity.flags &= ~ENT_COLLIDE;
        gPlayerEntity.collisionLayer = 2;
        sub_08078B48();
        sub_08077B20();
        gPlayerEntity.parent = super;
        sub_08036914(&gPlayerEntity, (u8) - (this->angle.HALF.HI + 0x80), 0x3800);
    }
}

void OctorokBoss_Action1_Attack_Type2_0(OctorokBossEntity* this) {
    if (this->currentAttack == NO_ATTACK) {
        OctorokBoss_ResetToSubAction0(this);
    } else {
        this->angularSpeed.HWORD = 0x100;
        super->type2 = 1;
        if (IS_FROZEN(this) == FALSE) {
            this->timer = 0x16;
        } else {
            this->timer = 0;
        }
        SoundReq(SFX_155);
    }
}

void OctorokBoss_Action1_Attack_Type2_1(OctorokBossEntity* this) {
    if (this->unk_74 < 0xc0) {
        this->unk_74++;
    } else {
        if (this->timer-- == 0) {
            if (this->currentAttack == ATTACK_VACUUM) {
                super->type2 = 3;
                if (IS_FROZEN(this)) {
                    this->timer = 0x3c;
                } else {
                    this->timer = 0x78;
                }
                this->heap->targetAngle = this->angle.HALF.HI;
            } else {
                super->type2 = 2;
                this->timer = 0x2d;
            }
            SoundReq(SFX_155);
        }
    }
}

void OctorokBoss_Action1_Attack_Type2_2(OctorokBossEntity* this) {
    if (this->timer == 0) {
        if (this->unk_76 < this->unk_74) {
            this->unk_74 -= 8;
            return;
        }
        super->type2 = 3;
        this->unk_74 = this->unk_76;
        if (this->currentAttack != ATTACK_SMOKE) {
            this->timer = 0x3c;
        } else {
            this->timer = 0;
            CreateObjectWithParent(super, OCTOROK_BOSS_OBJECT, 4, 0);
        }
    } else {
        this->timer--;
    }
}

void OctorokBoss_Action1_Attack_Type2_3(OctorokBossEntity* this) {
    static void (*const OctorokBoss_AttackFunctions[])(OctorokBossEntity*) = {
        OctorokBoss_ExecuteAttackSpitRock,
        OctorokBoss_ExecuteAttackVacuum,
        OctorokBoss_ExecuteAttackSmoke,
        OctorokBoss_ExecuteAttackFreeze,
    };
    if ((gRoomTransition.frameCount & 2) != 0) {
        this->heap->mouthObject->unk_76 -= 8;
    } else {
        this->heap->mouthObject->unk_76 += 8;
        if (0x180 < this->heap->mouthObject->unk_76) {
            this->heap->mouthObject->unk_76 = 0x180;
        }
    }
    if (this->currentAttack != ATTACK_VACUUM) {
        if (this->heap->mouthObject->timer < 0x20) {
            this->heap->mouthObject->timer++;
            this->heap->mouthObject->unk_76 += 8;
        }
    }
    OctorokBoss_AttackFunctions[this->currentAttack](this);
}

void OctorokBoss_ExecuteAttackSpitRock(OctorokBossEntity* this) {
    super->child = CreateProjectileWithParent(super, OCTOROK_BOSS_PROJECTILE, 0);
    if (super->child != NULL) {
        super->child->parent = super;
        super->child->direction = ((u8) - this->angle.HALF.HI ^ 0x80);
    }
    this->heap->mouthObject->timer++;

    OctorokBoss_SetAttackTimer(this);
}

void OctorokBoss_ExecuteAttackVacuum(OctorokBossEntity* this) {
    s32 tmp;

    if (this->unk_80 == 0) {
        super->direction = sub_080045DA(gPlayerEntity.x.WORD - super->x.WORD, gPlayerEntity.y.WORD - super->y.WORD);
        tmp = ((u8) - (this->angle.HALF.HI + 0x80)) - super->direction;
        if (tmp < 0) {
            tmp = -tmp;
        }
        if (tmp < 0x10) {
            if (sub_0806FC80(super, &gPlayerEntity, 0xf0) != 0) {
                if ((gPlayerState.flags & PL_FROZEN) == 0) {
                    if ((gPlayerEntity.flags & PL_MINISH) != 0) {
                        LinearMoveAngle(&gPlayerEntity, 0x280, -this->angle.HALF.HI);
                        if (sub_0806FC80(super, &gPlayerEntity, 0x48) != 0) {
                            this->unk_80 = 1;
                            this->timer = 2;
                            this->heap->targetAngle =
                                sub_080045DA((gRoomControls.origin_x + 0x108) * 0x10000 - super->x.WORD,
                                             (gRoomControls.origin_y + 0x88) * 0x10000 - super->y.WORD);
                            this->heap->targetAngle = (u8) - (this->heap->targetAngle + 0x80);
                            SoundReq(SFX_ED);
                        }
                    }
                } else {
                    gPlayerState.flags &= ~PL_FROZEN;
                }
            }
        }
        if ((gRoomTransition.frameCount & 3) == 0) {
            CreateObjectWithParent(super, OCTOROK_BOSS_OBJECT, 2, 0);
        }
    } else {
        if ((IS_FROZEN(this)) || (this->heap->targetAngle == this->angle.HALF.HI)) {
            if (this->unk_80 == 1) {
                this->unk_80 = 2;
                super->type2 = 2;
                this->timer = 0x2d;
                this->angularSpeed.HWORD = 0x100;
                this->heap->field_0x2 = 0;
                return;
            }
            this->unk_80 = 0;
            this->angularSpeed.HWORD = 0x100;
            this->heap->mouthObject->timer++;
            gPlayerEntity.spriteSettings.draw = 1;
            gPlayerEntity.flags &= ~ENT_COLLIDE;
            gPlayerEntity.collisionLayer = 1;
            sub_080792BC(0x400, (u32)(-(this->angle.HALF.HI + 0x80) * 0x1000000) >> 0x1b, 0x30);
            OctorokBoss_SetAttackTimer(this);
            SoundReq(SFX_EF);
            return;
        }
        this->timer = 2;
    }
    if (this->timer == 0) {
        this->unk_74 = this->unk_76;
        super->type2 = 0;
        OctorokBoss_SetAttackTimer(this);
    } else {
        this->timer--;
        if ((gPlayerState.flags == PL_FROZEN) && (this->timer == 0x3c)) {
            tmp = sub_080045DA(gPlayerEntity.x.WORD - super->x.WORD, gPlayerEntity.y.WORD - super->y.WORD);
            if ((u8)((tmp - ((u8) - this->angle.HALF.HI ^ 0x80))) > 0x80) {
                this->heap->targetAngle = this->angle.HALF.HI + 0x30;
            } else {
                this->heap->targetAngle = this->angle.HALF.HI - 0x30;
            }
        }
        if (IS_FROZEN(this) == FALSE) {
            if (this->heap->targetAngle != this->angle.HALF.HI) {
                this->heap->field_0x2 = 1;
                if ((u8)(this->heap->targetAngle - this->angle.HALF.HI) > 0x80) {
                    this->angle.HWORD -= this->angularSpeed.HWORD;
                } else {
                    this->angle.HWORD += this->angularSpeed.HWORD;
                }
            } else {
                this->heap->field_0x2 = IS_FROZEN(this);
            }
        }
    }
}

void OctorokBoss_ExecuteAttackSmoke(OctorokBossEntity* this) {
    if (this->timer == 0xff) {
        super->type2 = 0;
        OctorokBoss_SetAttackTimer(this);
        this->timer = 0x78;
    } else {
        this->timer++;
        ChangeLightLevel(-1);
        if ((gRoomTransition.frameCount & 3) == 0) {
            if ((gRoomTransition.frameCount & 7) == 0) {
                SoundReq(SFX_124);
            }
            CreateObjectWithParent(super, OCTOROK_BOSS_OBJECT, 3, 0);
        }
    }
}

void OctorokBoss_ExecuteAttackFreeze(OctorokBossEntity* this) {
    if (this->timer == 0) {
        this->heap->field_0x2 = 0;
        OctorokBoss_SetAttackTimer(this);
    } else {
        this->timer--;
        if ((gRoomTransition.frameCount & 3) == 0) {
            super->child = CreateProjectileWithParent(super, OCTOROK_BOSS_PROJECTILE, 2);
            if (super->child != NULL) {
                super->child->parent = super;
                super->child->direction = (u8) - this->angle.HALF.HI ^ 0x80;
            }
        }
    }
}

void OctorokBoss_Burning(OctorokBossEntity* this) {
    static void (*const OctorokBoss_Burning_SubActions[])(OctorokBossEntity*) = {
        OctorokBoss_Burning_SubAction0,
        OctorokBoss_Burning_SubAction1,
        OctorokBoss_Burning_SubAction2,
    };
    OctorokBoss_Burning_SubActions[super->subAction](this);
    if (this->heap->fallingStonesTimer != 0) {
        this->heap->fallingStonesTimer--;
        if ((gRoomTransition.frameCount & 7) == 0) {
            // Falling stones
            CreateProjectileWithParent(super, OCTOROK_BOSS_PROJECTILE, 3);
        }
    }
    sub_0805EC9C(super, this->unk_76, this->unk_74, this->angle.HWORD);
}

void OctorokBoss_Burning_SubAction0(OctorokBossEntity* this) {
    super->subAction = 1;
    super->speed = 0x200;
    super->collisions = 0;
    super->direction = (u8)(-this->angle.HALF.HI ^ 0x80U) >> 3;
    this->timer = 0x78;
    this->angularSpeed.HWORD = 0x180;
    this->heap->unk_0 = 4;
    sub_080368D8(this);
    OctorokBoss_Burning_SubAction1(this);
}

void OctorokBoss_Burning_SubAction1(OctorokBossEntity* this) {
    ProcessMovement0(super);
    if (super->collisions != 0) {
        super->subAction = 2;
        this->heap->targetAngle = this->angle.HALF.HI;
        if ((super->collisions & 0xee00) != 0) {
            this->heap->targetAngle = -this->heap->targetAngle;
        }
        if ((super->collisions & 0xee) != 0) {
            this->heap->targetAngle = -this->heap->targetAngle ^ 0x80;
        }
        super->knockbackDuration = 0x18;
        super->knockbackSpeed = 0x200;
        super->knockbackDirection = super->direction ^ 0x10;
        this->heap->fallingStonesTimer += 0x1e;
        InitScreenShake(0x1e, 0);
        SoundReq(SFX_158);
        SoundReq(SFX_14C);
    }
    if (this->timer-- == 0) {
        super->health = 0;
    }
    if ((gRoomTransition.frameCount & 0x1f) == 0) {
        SoundReq(SFX_159);
    }
}

void OctorokBoss_Burning_SubAction2(OctorokBossEntity* this) {
    if ((u32)(this->heap->targetAngle - this->angle.HALF.HI + 7) < 0xf) {
        super->subAction = 1;
        super->direction = ((u8) - this->angle.HALF.HI ^ 0x80) >> 3;
        super->collisions = 0;
        ProcessMovement0(super);
    } else {
        if ((u8)(this->heap->targetAngle - this->angle.HALF.HI) >= 0x81) {
            this->angle.HWORD -= this->angularSpeed.HWORD;
        } else {
            this->angle.HWORD += this->angularSpeed.HWORD;
        }
    }
}

void sub_080368D8(OctorokBossEntity* this) {
    if (this->unk_80 != 0) {
        gPlayerEntity.spriteSettings.draw = 1;
        gPlayerEntity.flags |= ENT_COLLIDE;
        gPlayerEntity.collisionLayer = 1;
    }
    this->unk_76 = 0xa0;
    this->unk_74 = 0xa0;
}

void sub_08036914(Entity* this, s32 angle, s32 radius) {
    s16 tmp;

    tmp = FixedMul(gSineTable[angle], radius);
    tmp = FixedDiv(tmp, 0x100);
    this->x.WORD = this->parent->x.WORD + ((tmp << 0x10) >> 8);
    tmp = FixedMul(gSineTable[angle + 0x40], radius);
    tmp = FixedDiv(tmp, 0x100);
    this->y.WORD = this->parent->y.WORD - ((tmp << 0x10) >> 8);
    this->spriteOffsetX = this->parent->spriteOffsetX;
    this->spriteOffsetY = this->parent->spriteOffsetY;
}

NONMATCH("asm/non_matching/octorokBoss/sub_08036998.inc", void sub_08036998(OctorokBossEntity* this)) {
    u32 tmp;
    s8* tmp2;
    s8 tmp3;
    s32 a, b;

    // TODO regalloc in this awful structure here
    tmp2 = &this->timer;
    tmp = super->field_0xf + (u8)*tmp2;
    *tmp2 = tmp;
    if ((s8)super->field_0xf < 0) {
        a = tmp;
        b = -super->actionDelay;
        if (a << 0x18 < b << 0x18) {
            super->field_0xf = -super->field_0xf;
        }
    } else {
        if (((s8)*tmp2) > ((s32)super->actionDelay)) {
            super->field_0xf = -super->field_0xf;
        }
    }
}
END_NONMATCH

/** Calculate tail angles regular */
ASM_FUNC("asm/non_matching/octorokBoss/sub_080369D0.inc",
         void sub_080369D0(OctorokBossEntity* this, s32 radius, s32 angleSpeed))

/** Calculate tail angles frozen sub_08036AF0 */
ASM_FUNC("asm/non_matching/octorokBoss/sub_08036AF0.inc",
         void sub_08036AF0(OctorokBossEntity* this, s32 radius, s32 angleSpeed))

void OctorokBoss_SetAttackTimer(OctorokBossEntity* this) {
    // These attack timers are only used if the boss isn't frozen and gRoomVars.field_0xc != 0x100
    static const u8 OctorokBoss_AttackTimerWeights[] = {
        48,
        96,
        80,
        32,
    };
    static const u8 OctorokBoss_AttackTimerValues[] = {
        5,
        10,
        15,
        30,
    };

    // 5: don't attack
    static const u8 OctorokBoss_Phase4AttackPattern0[] = {
        ATTACK_SPITROCK, NO_ATTACK, ATTACK_SPITROCK, ATTACK_SMOKE, END_OF_ATTACK_PATTERN,
    };
    static const u8 OctorokBoss_Phase4AttackPattern1[] = {
        ATTACK_VACUUM, NO_ATTACK, ATTACK_SPITROCK, ATTACK_SMOKE, END_OF_ATTACK_PATTERN,
    };
    static const u8 OctorokBoss_Phase4AttackPattern2[] = {
        ATTACK_SPITROCK, NO_ATTACK, ATTACK_SPITROCK, NO_ATTACK, ATTACK_SPITROCK, END_OF_ATTACK_PATTERN,
    };

    static const u8* const OctorokBoss_Phase4AttackPatterns[] = {
        OctorokBoss_Phase4AttackPattern0,
        OctorokBoss_Phase4AttackPattern1,
        OctorokBoss_Phase4AttackPattern2,
        OctorokBoss_Phase4AttackPattern1,
    };
    const u8* attackPatterns;
    if ((this->bossPhase == 4) && (this->phase4AttackPattern != 0xff)) {
        super->subAction = ACTION1_ATTACK;
        super->type2 = 0;
        this->unk_80 = 0;
        attackPatterns = OctorokBoss_Phase4AttackPatterns[this->phase4AttackPattern];
        this->currentAttack = attackPatterns[this->nextAttackIndex];
        this->nextAttackIndex += 1;
        if (this->currentAttack != END_OF_ATTACK_PATTERN) {
            return;
        }
        // End of this pattern, choose the next pattern.
        this->phase4AttackPattern = 0xff;
    }
    super->subAction = ACTION1_WAITFORATTACK;
    if (IS_FROZEN(this)) {
        switch (this->bossPhase) {
            case 1:
                this->timer = 30;
                break;
            case 3:
                this->timer = 10;
                break;
        }
    } else {
        if ((s16)gRoomVars.lightLevel != 0x100) {
            // Constantly attack when its dark.
            this->timer = 1;
        } else {
            this->timer = OctorokBoss_AttackTimerValues[GetRandomByWeight(OctorokBoss_AttackTimerWeights)];
        }
    }
}

void OctorokBoss_ResetToSubAction0(OctorokBossEntity* this) {
    this->angularSpeed.HWORD = 0x100;
    this->heap->unk_0 = 2;
    this->heap->rotation = NO_ROTATION;
    super->subAction = ACTION1_AIMTOWARDSPLAYER;
}

void OctorokBoss_WaitAnotherTurn(OctorokBossEntity* this) {
    static const u8 OctorokBoss_TurnTimeWeights[] = {
        48,
        96,
        80,
        32,
    };
    static const u8 OctorokBoss_TurnTimeValues[] = {
        70,
        80,
        90,
        100,
    };
    super->subAction = ACTION1_WAITFORTURN;
    super->speed = 0xc0;
    this->heap->unk_0 = 1;
    this->timer = OctorokBoss_TurnTimeValues[GetRandomByWeight(OctorokBoss_TurnTimeWeights)];
}

void OctorokBoss_SetWaitTurnsForNextAttack(OctorokBossEntity* this) {
    static const u8 OctorokBoss_WaitForAttackTurnsWeights[] = {
        64,
        128,
        64,
    };
    static const u8 OctorokBoss_WaitForAttackTurnsValues[] = {
        1,
        2,
        3,
    };
    this->phase4AttackPattern = 0xff;
    if (IS_FROZEN(this) == FALSE) {
        if ((s16)gRoomVars.lightLevel != 0x100) {
            // Constantly attack when its dark.
            this->attackWaitTurns = IS_FROZEN(this);
        } else {
            this->attackWaitTurns =
                OctorokBoss_WaitForAttackTurnsValues[GetRandomByWeight(OctorokBoss_WaitForAttackTurnsWeights)];
        }
    } else {
        this->attackWaitTurns = 0;
    }
    OctorokBoss_ResetToSubAction0(this);
}

void OctorokBoss_StartRegularAttack(OctorokBossEntity* this) {
    static const u8 OctorokBoss_NormalAttackPatterns[] = {
        ATTACK_VACUUM, ATTACK_SPITROCK, ATTACK_VACUUM, ATTACK_SPITROCK, ATTACK_SPITROCK,
    };
    static const u8 OctorokBoss_FrozenAttackPatterns[] = {
        ATTACK_VACUUM, ATTACK_FREEZE, ATTACK_VACUUM, ATTACK_FREEZE, ATTACK_VACUUM,
    };

    const u8* attackPattern;

    // Set us up for an attack
    super->subAction = ACTION1_ATTACK;
    super->type2 = 0;
    this->phase4AttackPattern = 0xff;
    this->unk_80 = 0;
    this->heap->field_0x2 = 0;
    if (this->bossPhase == 0) {
        // In phase 0 just spit rocks.
        this->currentAttack = ATTACK_SPITROCK;
        return;
    }
    if (this->bossPhase == 4) {
        if (((Random() & 3) == 0) || ((s16)gRoomVars.lightLevel != 0x100)) {
            super->subAction = ACTION1_SUBACTION2;
            super->speed = 0x200;
            this->timer = 0x3c;
            super->collisions = 0;
            this->heap->unk_0 = 4;
            SoundReq(SFX_159);
            return;
        }
        if (this->bossPhase == 4) {
            // Select a new attack pattern that is not the previous one.
            u32 rand;
            this->nextAttackIndex = 0;
            rand = Random() & 3;
            if (this->heap->phase4PrevAttackPattern != rand) {
                this->phase4AttackPattern = rand;
            } else {
                this->phase4AttackPattern = (this->heap->phase4PrevAttackPattern + 1) & 3;
            }
            this->heap->phase4PrevAttackPattern = this->phase4AttackPattern;
            OctorokBoss_SetAttackTimer(this);
            return;
        }
    }

    if (IS_FROZEN(this) == FALSE) {
        attackPattern = OctorokBoss_NormalAttackPatterns;
    } else {
        attackPattern = OctorokBoss_FrozenAttackPatterns;
    }
    this->currentAttack = attackPattern[this->nextAttackIndex];
    if (++this->nextAttackIndex > 4) {
        this->nextAttackIndex = 0;
    }
}

void OctorokBoss_ChangePalette(OctorokBossEntity* this, u32 paletteIndex) {
    u32 i;

    ChangeObjPalette(&this->heap->mouthObject->base, paletteIndex);

    for (i = 0; i < 4; i++) {
        ChangeObjPalette(&this->heap->legObjects[i]->base, paletteIndex);
    }

    for (i = this->heap->tailCount - 1; i != 0; i--) {
        ChangeObjPalette(&this->heap->tailObjects[i]->base, paletteIndex);
    }
}

void sub_08036F60(OctorokBossEntity* this) {
    if ((super->subAction != 4) && (IS_FROZEN(this) == FALSE)) {
        this->unk_76 += (s8)super->actionDelay;
        this->unk_74 += (s8)super->actionDelay;
        if (this->unk_76 < 0x9c) {
            super->actionDelay = 1;
        } else {
            if (this->unk_76 > 0xa4) {
                super->actionDelay = 0xff;
            }
        }
    }
}

void OctorokBoss_StepSound(OctorokBossEntity* this, u32 frameMask) {
    if ((gRoomTransition.frameCount & frameMask) == 0) {
        if (IS_FROZEN(this) == FALSE) {
            SoundReq(SFX_163);
        } else {
            SoundReq(SFX_ICE_BLOCK_SLIDE);
        }
    }
}

void sub_08036FE4(OctorokBossEntity* this) {
    if ((IS_FROZEN(this)) && (this->unk_80 == 0)) {
        if (this->angularSpeed.HWORD != 0) {
            if (this->heap->rotation != NO_ROTATION) {
                if (this->heap->rotation == ROTATION_CW) {
                    this->angle.HWORD += this->angularSpeed.HWORD;
                } else {
                    this->angle.HWORD -= this->angularSpeed.HWORD;
                }
            }
            switch (this->bossPhase) {
                case 1:
                    this->angularSpeed.HWORD--;
                    break;
                case 3:
                    this->angularSpeed.HWORD -= 2;
                    break;
            }
        }
    }
}
