/*****************************************************************************
 * Copyright (c) 2014-2026 OpenRCT2 developers
 *
 * For a complete list of all authors, please refer to contributors.md
 * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
 *
 * OpenRCT2 is licensed under the GNU General Public License version 3.
 *****************************************************************************/

#include "Vehicle.h"

#include "../GameState.h"
#include "../audio/Audio.h"
#include "../entity/Particle.h"
#include "../entity/Yaw.hpp"
#include "../math/Trigonometry.hpp"
#include "../world/Map.h"
#include "../world/tile_element/TileElement.h"
#include "../world/tile_element/TrackElement.h"
#include "Ride.h"
#include "RideData.h"
#include "RideEntry.h"
#include "TrackData.h"
#include "VehicleData.h"
#include "ted/TrackElementDescriptor.h"

#include <cassert>

namespace OpenRCT2
{
    using namespace OpenRCT2::Audio;
    using namespace OpenRCT2::TrackMetadata;

    constexpr int16_t kVehicleMaxSpinSpeed = 1536;
    constexpr int16_t kVehicleMinSpinSpeed = -kVehicleMaxSpinSpeed;

    /**
     *
     *  rct2: 0x006D9249
     */
    void Vehicle::UpdateSwinging()
    {
        auto curRide = GetRide();
        if (curRide == nullptr)
            return;

        auto rideEntry = GetRideEntry();
        if (rideEntry == nullptr)
            return;

        // SubState for this ride means swinging state
        // 0 == first swing
        // 3 == full swing
        uint8_t swingState = sub_state;
        if (rideEntry->flags.has(RideEntryFlag::inverterShipSwingMode))
        {
            swingState += 4;
            if (rideEntry->flags.has(RideEntryFlag::magicCarpetSwingMode))
                swingState += 4;
        }

        const auto spriteMap = kSwingingTimeToSpriteMaps[swingState];
        int8_t spriteType = spriteMap[current_time + 1];

        // 0x80 indicates that a complete swing has been
        // completed and the next swing can start
        if (spriteType != -128)
        {
            current_time++;
            if (static_cast<uint8_t>(spriteType) != flatRideAnimationFrame)
            {
                // Used to know which sprite to draw
                flatRideAnimationFrame = static_cast<uint8_t>(spriteType);
                invalidate();
            }
            return;
        }

        current_time = -1;
        NumSwings++;
        if (curRide->status != RideStatus::closed)
        {
            // It takes 3 swings to get into full swing
            // ride->rotations already takes this into account
            if (NumSwings + 3 < curRide->rotations)
            {
                // Go to the next swing state until we
                // are at full swing.
                if (sub_state != 3)
                {
                    sub_state++;
                }
                UpdateSwinging();
                return;
            }
        }

        // To get to this part of the code the
        // swing has to be in slowing down phase
        if (sub_state == 0)
        {
            SetState(Status::arriving);
            var_C0 = 0;
            return;
        }
        // Go towards first swing state
        sub_state--;
        UpdateSwinging();
    }

    /**
     *
     *  rct2: 0x006D9413
     */
    void Vehicle::UpdateFerrisWheelRotating()
    {
        if (_vehicleBreakdown == Breakdown::safetyCutOut)
            return;

        auto curRide = GetRide();
        if (curRide == nullptr)
            return;

        if ((ferris_wheel_var_1 -= 1) != 0)
            return;

        int8_t curFerrisWheelVar0 = ferris_wheel_var_0;

        if (curFerrisWheelVar0 == 3)
        {
            ferris_wheel_var_0 = curFerrisWheelVar0;
            ferris_wheel_var_1 = curFerrisWheelVar0;
        }
        else if (curFerrisWheelVar0 < 3)
        {
            if (curFerrisWheelVar0 != -8)
                curFerrisWheelVar0--;
            ferris_wheel_var_0 = curFerrisWheelVar0;
            ferris_wheel_var_1 = -curFerrisWheelVar0;
        }
        else
        {
            curFerrisWheelVar0--;
            ferris_wheel_var_0 = curFerrisWheelVar0;
            ferris_wheel_var_1 = curFerrisWheelVar0;
        }

        uint8_t rotation = flatRideAnimationFrame;
        if (curRide->mode == RideMode::forwardRotation)
            rotation++;
        else
            rotation--;

        rotation &= 0x7F;
        flatRideAnimationFrame = rotation;

        if (rotation == sub_state)
            NumRotations++;

        invalidate();

        uint8_t subState = sub_state;
        if (curRide->mode == RideMode::forwardRotation)
            subState++;
        else
            subState--;
        subState &= 0x7F;

        if (subState == flatRideAnimationFrame)
        {
            bool shouldStop = true;
            if (curRide->status != RideStatus::closed)
            {
                if (NumRotations < curRide->rotations)
                    shouldStop = false;
            }

            if (shouldStop)
            {
                curFerrisWheelVar0 = ferris_wheel_var_0;
                ferris_wheel_var_0 = -abs(curFerrisWheelVar0);
                ferris_wheel_var_1 = abs(curFerrisWheelVar0);
            }
        }

        if (ferris_wheel_var_0 != -8)
            return;

        subState = sub_state;
        if (curRide->mode == RideMode::forwardRotation)
            subState += 8;
        else
            subState -= 8;
        subState &= 0x7F;

        if (subState != flatRideAnimationFrame)
            return;

        SetState(Status::arriving);
        var_C0 = 0;
    }

    /**
     *
     *  rct2: 0x006D94F2
     */
    void Vehicle::UpdateSimulatorOperating()
    {
        if (_vehicleBreakdown == Breakdown::safetyCutOut)
            return;

        assert(current_time >= -1);
        assert(current_time < MotionSimulatorTimeToSpriteMapCount);
        uint8_t al = MotionSimulatorTimeToSpriteMap[current_time + 1];
        if (al != 0xFF)
        {
            current_time++;
            if (al == flatRideAnimationFrame)
                return;
            flatRideAnimationFrame = al;
            invalidate();
            return;
        }

        SetState(Status::arriving);
        var_C0 = 0;
    }

    void UpdateRotatingDefault(Vehicle& vehicle)
    {
        vehicle.sub_state = 1;
        vehicle.UpdateRotating();
    }

    void UpdateRotatingEnterprise(Vehicle& vehicle)
    {
        if (vehicle.sub_state == 2)
        {
            vehicle.SetState(Vehicle::Status::arriving);
            vehicle.var_C0 = 0;
            return;
        }

        UpdateRotatingDefault(vehicle);
    }

    /**
     *
     *  rct2: 0x006D92FF
     */
    void Vehicle::UpdateRotating()
    {
        if (_vehicleBreakdown == Breakdown::safetyCutOut)
            return;

        auto curRide = GetRide();
        if (curRide == nullptr)
            return;

        auto rideEntry = GetRideEntry();
        if (rideEntry == nullptr)
        {
            return;
        }

        const uint8_t* timeToSpriteMap;
        if (rideEntry->flags.has(RideEntryFlag::hasTwistRotationType))
        {
            timeToSpriteMap = kTwistTimeToSpriteMaps[sub_state];
        }
        else if (rideEntry->flags.has(RideEntryFlag::hasEnterpriseRotationType))
        {
            timeToSpriteMap = kEnterpriseTimeToSpriteMaps[sub_state];
        }
        else
        {
            timeToSpriteMap = kMerryGoRoundTimeToSpriteMaps[sub_state];
        }

        uint16_t time = current_time;
        if (_vehicleBreakdown == Breakdown::controlFailure)
        {
            time += (curRide->breakdownSoundModifier >> 6) + 1;
        }
        time++;

        uint8_t sprite = timeToSpriteMap[time];
        if (sprite != 0xFF)
        {
            current_time = time;
            if (sprite == flatRideAnimationFrame)
                return;
            flatRideAnimationFrame = sprite;
            invalidate();
            return;
        }

        current_time = -1;
        NumRotations++;
        if (_vehicleBreakdown != Breakdown::controlFailure)
        {
            bool shouldStop = true;
            if (curRide->status != RideStatus::closed)
            {
                sprite = NumRotations + 1;
                if (curRide->getRideTypeDescriptor().specialType == RtdSpecialType::enterprise)
                    sprite += 9;

                if (sprite < curRide->rotations)
                    shouldStop = false;
            }

            if (shouldStop)
            {
                if (sub_state == 2)
                {
                    SetState(Status::arriving);
                    var_C0 = 0;
                    return;
                }
                sub_state++;
                UpdateRotating();
                return;
            }
        }

        const auto& rtd = GetRideTypeDescriptor(curRide->type);
        rtd.UpdateRotating(*this);
    }

    /**
     *
     *  rct2: 0x006D97CB
     */
    void Vehicle::UpdateSpaceRingsOperating()
    {
        if (_vehicleBreakdown == Breakdown::safetyCutOut)
            return;

        uint8_t spriteType = kSpaceRingsTimeToSpriteMap[current_time + 1];
        if (spriteType != 255)
        {
            current_time++;
            if (spriteType != flatRideAnimationFrame)
            {
                flatRideAnimationFrame = spriteType;
                invalidate();
            }
        }
        else
        {
            SetState(Status::arriving);
            var_C0 = 0;
        }
    }

    /**
     *
     *  rct2: 0x006D9641
     */
    void Vehicle::UpdateHauntedHouseOperating()
    {
        if (_vehicleBreakdown == Breakdown::safetyCutOut)
            return;

        if (flatRideAnimationFrame != 0)
        {
            if (getGameState().currentTicks & 1)
            {
                flatRideAnimationFrame++;
                invalidate();

                if (flatRideAnimationFrame == 19)
                    flatRideAnimationFrame = 0;
            }
        }

        if (current_time + 1 > 1500)
        {
            SetState(Status::arriving);
            var_C0 = 0;
            return;
        }

        current_time++;
        switch (current_time)
        {
            case 45:
                Play3D(SoundId::hauntedHouseScare, getLocation());
                break;
            case 75:
                flatRideAnimationFrame = 1;
                invalidate();
                break;
            case 400:
                Play3D(SoundId::hauntedHouseScream1, getLocation());
                break;
            case 745:
                Play3D(SoundId::hauntedHouseScare, getLocation());
                break;
            case 775:
                flatRideAnimationFrame = 1;
                invalidate();
                break;
            case 1100:
                Play3D(SoundId::hauntedHouseScream2, getLocation());
                break;
        }
    }

    /**
     *
     *  rct2: 0x006d9781
     */
    void Vehicle::UpdateCrookedHouseOperating()
    {
        if (_vehicleBreakdown == Breakdown::safetyCutOut)
            return;

        // Originally used an array of size 1 at 0x009A0AC4 and passed the sub state into it.
        if (static_cast<uint16_t>(current_time + 1) > 600)
        {
            SetState(Status::arriving);
            var_C0 = 0;
            return;
        }

        current_time++;
    }

    /**
     *
     *  rct2: 0x006D9547
     */
    void Vehicle::UpdateTopSpinOperating()
    {
        if (_vehicleBreakdown == Breakdown::safetyCutOut)
            return;

        const TopSpinTimeToSpriteMap* sprite_map = kTopSpinTimeToSpriteMaps[sub_state];
        uint8_t rotation = sprite_map[current_time + 1].arm_rotation;
        if (rotation != 0xFF)
        {
            current_time = current_time + 1;
            if (rotation != flatRideAnimationFrame)
            {
                flatRideAnimationFrame = rotation;
                invalidate();
            }
            rotation = sprite_map[current_time].bank_rotation;
            if (rotation != flatRideSecondaryAnimationFrame)
            {
                flatRideSecondaryAnimationFrame = rotation;
                invalidate();
            }
            return;
        }

        SetState(Status::arriving);
        var_C0 = 0;
    }

    /**
     *
     *  rct2: 0x006D95AD
     */
    void Vehicle::UpdateShowingFilm()
    {
        if (_vehicleBreakdown == Breakdown::safetyCutOut)
            return;

        int32_t totalTime = kRideFilmLength[sub_state];
        int32_t currentTime = current_time + 1;
        if (currentTime <= totalTime)
        {
            current_time = currentTime;
        }
        else
        {
            SetState(Status::arriving);
            var_C0 = 0;
        }
    }

    /**
     *
     *  rct2: 0x006D95F7
     */
    void Vehicle::UpdateDoingCircusShow()
    {
        if (_vehicleBreakdown == Breakdown::safetyCutOut)
            return;

        int32_t currentTime = current_time + 1;
        if (currentTime <= 5000)
        {
            current_time = currentTime;
        }
        else
        {
            SetState(Status::arriving);
            var_C0 = 0;
        }
    }

    int32_t Vehicle::GetSwingAmount() const
    {
        switch (GetTrackType())
        {
            case TrackElemType::leftQuarterTurn5Tiles:
            case TrackElemType::bankedLeftQuarterTurn5Tiles:
            case TrackElemType::leftQuarterTurn5TilesUp25:
            case TrackElemType::leftQuarterTurn5TilesDown25:
            case TrackElemType::leftQuarterTurn5TilesCovered:
            case TrackElemType::leftHalfBankedHelixUpLarge:
            case TrackElemType::leftHalfBankedHelixDownLarge:
            case TrackElemType::leftQuarterBankedHelixLargeUp:
            case TrackElemType::leftQuarterBankedHelixLargeDown:
            case TrackElemType::leftQuarterHelixLargeUp:
            case TrackElemType::leftQuarterHelixLargeDown:
            case TrackElemType::leftBankedQuarterTurn5TileUp25:
            case TrackElemType::leftBankedQuarterTurn5TileDown25:
                // Loc6D67E1
                return 14;

            case TrackElemType::rightQuarterTurn5Tiles:
            case TrackElemType::bankedRightQuarterTurn5Tiles:
            case TrackElemType::rightQuarterTurn5TilesUp25:
            case TrackElemType::rightQuarterTurn5TilesDown25:
            case TrackElemType::rightQuarterTurn5TilesCovered:
            case TrackElemType::rightHalfBankedHelixUpLarge:
            case TrackElemType::rightHalfBankedHelixDownLarge:
            case TrackElemType::rightQuarterBankedHelixLargeUp:
            case TrackElemType::rightQuarterBankedHelixLargeDown:
            case TrackElemType::rightQuarterHelixLargeUp:
            case TrackElemType::rightQuarterHelixLargeDown:
            case TrackElemType::rightBankedQuarterTurn5TileUp25:
            case TrackElemType::rightBankedQuarterTurn5TileDown25:
                // Loc6D6804
                return -14;

            case TrackElemType::sBendLeft:
            case TrackElemType::sBendLeftCovered:
                // Loc6D67EF
                if (track_progress < 48)
                {
                    return 14;
                }
                return -15;

            case TrackElemType::sBendRight:
            case TrackElemType::sBendRightCovered:
                // Loc6D67CC
                if (track_progress < 48)
                {
                    return -14;
                }
                return 15;

            case TrackElemType::leftQuarterTurn3Tiles:
            case TrackElemType::leftBankedQuarterTurn3Tiles:
            case TrackElemType::leftQuarterTurn3TilesUp25:
            case TrackElemType::leftQuarterTurn3TilesDown25:
            case TrackElemType::leftQuarterTurn3TilesCovered:
            case TrackElemType::leftHalfBankedHelixUpSmall:
            case TrackElemType::leftHalfBankedHelixDownSmall:
            case TrackElemType::leftBankToLeftQuarterTurn3TilesUp25:
            case TrackElemType::leftQuarterTurn3TilesDown25ToLeftBank:
            case TrackElemType::leftCurvedLiftHill:
            case TrackElemType::leftBankedQuarterTurn3TileUp25:
            case TrackElemType::leftBankedQuarterTurn3TileDown25:
                // Loc6D67BE
                return 13;

            case TrackElemType::rightQuarterTurn3Tiles:
            case TrackElemType::rightBankedQuarterTurn3Tiles:
            case TrackElemType::rightQuarterTurn3TilesUp25:
            case TrackElemType::rightQuarterTurn3TilesDown25:
            case TrackElemType::rightQuarterTurn3TilesCovered:
            case TrackElemType::rightHalfBankedHelixUpSmall:
            case TrackElemType::rightHalfBankedHelixDownSmall:
            case TrackElemType::rightBankToRightQuarterTurn3TilesUp25:
            case TrackElemType::rightQuarterTurn3TilesDown25ToRightBank:
            case TrackElemType::rightCurvedLiftHill:
            case TrackElemType::rightBankedQuarterTurn3TileUp25:
            case TrackElemType::rightBankedQuarterTurn3TileDown25:
                // Loc6D67B0
                return -13;

            case TrackElemType::leftQuarterTurn1Tile:
            case TrackElemType::leftQuarterTurn1TileUp60:
            case TrackElemType::leftQuarterTurn1TileDown60:
                // Loc6D67A2
                return 12;

            case TrackElemType::rightQuarterTurn1Tile:
            case TrackElemType::rightQuarterTurn1TileUp60:
            case TrackElemType::rightQuarterTurn1TileDown60:
                // Loc6D6794
                return -12;

            case TrackElemType::leftEighthToDiag:
            case TrackElemType::leftEighthToOrthogonal:
            case TrackElemType::leftEighthBankToDiag:
            case TrackElemType::leftEighthBankToOrthogonal:
                // Loc6D67D3
                return 15;

            case TrackElemType::rightEighthToDiag:
            case TrackElemType::rightEighthToOrthogonal:
            case TrackElemType::rightEighthBankToDiag:
            case TrackElemType::rightEighthBankToOrthogonal:
                // Loc6D67F6
                return -15;
            default:
                return 0;
        }
    }

    static uint8_t GetSwingSprite(int16_t swingPosition)
    {
        if (swingPosition < -10010)
            return 11;
        if (swingPosition > 10010)
            return 12;

        if (swingPosition < -8190)
            return 9;
        if (swingPosition > 8190)
            return 10;

        if (swingPosition < -6370)
            return 7;
        if (swingPosition > 6370)
            return 8;

        if (swingPosition < -4550)
            return 5;
        if (swingPosition > 4550)
            return 6;

        if (swingPosition < -2730)
            return 3;
        if (swingPosition > 2730)
            return 4;

        if (swingPosition < -910)
            return 1;
        if (swingPosition > 910)
            return 2;

        return 0;
    }

    /**
     *
     *  rct2: 0x006D6776
     */
    void Vehicle::UpdateSwingingCar()
    {
        int32_t dword_F64E08 = abs(_vehicleVelocity);
        if (flags.has(VehicleFlag::carIsReversed))
        {
            dword_F64E08 *= -1;
        }
        SwingSpeed += (-SwingPosition) >> 6;
        int32_t swingAmount = GetSwingAmount();
        if (swingAmount < 0)
        {
            SwingSpeed -= dword_F64E08 >> (-swingAmount);
        }
        else if (swingAmount > 0)
        {
            SwingSpeed += dword_F64E08 >> swingAmount;
        }

        auto carEntry = Entry();
        if (carEntry == nullptr)
        {
            return;
        }
        int16_t dx = 3185;
        if (carEntry->flags.has(CarEntryFlag::useSuspendedSwing))
        {
            dx = 5006;
        }
        if (carEntry->flags.has(CarEntryFlag::useWoodenWildMouseSwing))
        {
            dx = 1820;
        }
        int16_t cx = -dx;

        if (carEntry->flags.has(CarEntryFlag::useSlideSwing))
        {
            dx = 5370;
            cx = -5370;

            auto trackType = GetTrackType();
            switch (trackType)
            {
                case TrackElemType::bankedLeftQuarterTurn5Tiles:
                case TrackElemType::leftBank:
                case TrackElemType::leftBankedQuarterTurn3Tiles:
                    dx = 10831;
                    cx = -819;
                    break;
                case TrackElemType::bankedRightQuarterTurn5Tiles:
                case TrackElemType::rightBank:
                case TrackElemType::rightBankedQuarterTurn3Tiles:
                    dx = 819;
                    cx = -10831;
                    break;
                default:
                    break;
            }

            if (trackTypeIsStation(trackType) || trackTypeIsBrakes(trackType) || trackTypeIsBlockBrakes(trackType))
            {
                dx = 0;
                cx = 0;
            }

            if (flags.has(VehicleFlag::onLiftHill))
            {
                dx = 0;
                cx = 0;
            }
        }

        SwingPosition += SwingSpeed;
        SwingSpeed -= SwingSpeed >> 5;

        if (SwingPosition > dx)
        {
            SwingPosition = dx;
            SwingSpeed = 0;
        }
        if (SwingPosition < cx)
        {
            SwingPosition = cx;
            SwingSpeed = 0;
        }

        uint8_t swingSprite = GetSwingSprite(SwingPosition);

        if (swingSprite != SwingSprite)
        {
            SwingSprite = swingSprite;
            invalidate();
        }
    }

    /**
     *
     *  rct2: 0x006D661F
     */
    void Vehicle::UpdateSpinningCar()
    {
        if (flags.has(VehicleFlag::spinningIsLocked))
        {
            spin_speed = 0;
            return;
        }

        auto carEntry = Entry();
        if (carEntry == nullptr)
        {
            return;
        }
        int32_t spinningInertia = carEntry->spinningInertia;
        auto trackType = GetTrackType();
        int32_t dword_F64E08 = _vehicleVelocity;
        int32_t spinSpeed{};
        // An L spin adds to the spin speed, R does the opposite
        // The number indicates how much right shift of the velocity will become spin
        // The bigger the number the less change in spin.

        const auto& ted = GetTrackElementDescriptor(trackType);
        switch (ted.spinFunction)
        {
            case SpinFunction::rc:
                // On a rotation control track element
                spinningInertia += 6;
                spinSpeed = dword_F64E08 >> spinningInertia;
                // Alternate the spin direction (roughly). Perhaps in future save a value to the track
                if (id.ToUnderlying() & 1)
                {
                    spin_speed -= spinSpeed;
                }
                else
                {
                    spin_speed += spinSpeed;
                }
                break;
            case SpinFunction::r5:
                // It looks like in the original there was going to be special code for whirlpool
                // this has been removed and just uses R5
                spinningInertia += 5;
                spin_speed -= dword_F64E08 >> spinningInertia;
                break;
            case SpinFunction::l5:
                spinningInertia += 5;
                spin_speed += dword_F64E08 >> spinningInertia;
                break;
            case SpinFunction::r7:
                spinningInertia += 7;
                spin_speed -= dword_F64E08 >> spinningInertia;
                break;
            case SpinFunction::l7:
                spinningInertia += 7;
                spin_speed += dword_F64E08 >> spinningInertia;
                break;
            case SpinFunction::rl:
                // Right Left Curve Track Piece
                if (track_progress < 48)
                {
                    // R8
                    spinningInertia += 8;
                    spin_speed -= dword_F64E08 >> spinningInertia;
                    break;
                }
                [[fallthrough]];
            case SpinFunction::l9:
                spinningInertia += 9;
                spin_speed += dword_F64E08 >> spinningInertia;
                break;
            case SpinFunction::l8:
                spinningInertia += 8;
                spin_speed += dword_F64E08 >> spinningInertia;
                break;
            case SpinFunction::sp:
                // On rapids spin after fully on them
                if (track_progress > 22)
                {
                    // L5
                    spinningInertia += 5;
                    spin_speed += dword_F64E08 >> spinningInertia;
                }
                break;
            case SpinFunction::lr:
                // Left Right Curve Track Piece
                if (track_progress < 48)
                {
                    // L8
                    spinningInertia += 8;
                    spin_speed += dword_F64E08 >> spinningInertia;
                    break;
                }
                [[fallthrough]];
            case SpinFunction::r9:
                spinningInertia += 9;
                spin_speed -= dword_F64E08 >> spinningInertia;
                break;
            case SpinFunction::r8:
                spinningInertia += 8;
                spin_speed -= dword_F64E08 >> spinningInertia;
                break;
            case SpinFunction::none:
                break;
        }

        spinSpeed = std::clamp(spin_speed, kVehicleMinSpinSpeed, kVehicleMaxSpinSpeed);
        spin_speed = spinSpeed;
        spin_sprite += spinSpeed >> 8;
        // Note this actually increases the spin speed if going right!
        spin_speed -= spinSpeed >> carEntry->spinningFriction;
        invalidate();
    }

    void Vehicle::UpdateAnimationAnimalFlying()
    {
        if (animationState > 0)
        {
            animationState--;
            return;
        }

        if (animation_frame == 0)
        {
            auto trackType = GetTrackType();
            TileElement* trackElement = MapGetTrackElementAtOfTypeSeq(TrackLocation, trackType, 0);
            if (trackElement != nullptr && trackElement->asTrack()->hasChain())
            {
                // start flapping, bird
                animation_frame = 1;
                animationState = 5;
                invalidate();
            }
        }
        else
        {
            // continue flapping until reaching frame 0
            animation_frame = (animation_frame + 1) % 4;
            invalidate();
        }
        // number of frames to skip before updating again
        constexpr std::array frameWaitTimes = { 5, 3, 5, 3 };
        animationState = frameWaitTimes[animation_frame];
    }

    /**
     * Get the frame of animation for the current animationState based on animation speed and animation frames
     */
    static uint8_t GetTargetFrame(const CarEntry& carEntry, uint32_t animationState)
    {
        if (carEntry.animationSpeed == 0)
            return 0;
        auto targetFrame = animationState / (carEntry.animationSpeed << 2);
        // mask of 0xFF
        targetFrame &= std::numeric_limits<uint8_t>::max();
        // multiply by number of frames. After the bitshift 8, the range will be 0 to AnimationFrames - 1
        targetFrame *= carEntry.animationFrames;
        return targetFrame >> std::numeric_limits<uint8_t>::digits;
    }

    /**
     * Compute the position that steam should be spawned
     */
    static constexpr CoordsXYZ ComputeSteamOffset(int32_t height, int32_t length, VehiclePitch pitch, uint8_t yaw)
    {
        uint8_t trueYaw = Entity::Yaw::YawTo64(yaw);
        auto offsets = Math::Trigonometry::kPitchToDirectionVectorFromGeometry[EnumValue(pitch)];
        int32_t projectedRun = (offsets.x * length - offsets.y * height) / 256;
        int32_t projectedHeight = (offsets.x * height + offsets.y * length) / 256;
        return { Math::Trigonometry::ComputeXYVector(projectedRun, trueYaw), projectedHeight };
    }

    /**
     * Decide based on current frame and number of frames if a steam particle should be generated on this frame
     */
    static bool ShouldMakeSteam(uint8_t targetFrame, uint8_t animationFrames)
    {
        if (animationFrames < 1)
            return false;
        // steam is produced twice per wheel revolution
        return targetFrame == 0 || targetFrame == animationFrames / 2;
    }

    /**
     * Dummy function
     */
    static void AnimateNone(Vehicle& vehicle, const CarEntry& carEntry)
    {
        return;
    }

    /**
     * Animate the vehicle based on its speed
     */
    static void AnimateSimpleVehicle(Vehicle& vehicle, const CarEntry& carEntry)
    {
        vehicle.animationState += _vehicleVelocity;
        uint8_t targetFrame = GetTargetFrame(carEntry, vehicle.animationState);
        if (vehicle.animation_frame != targetFrame)
        {
            vehicle.animation_frame = targetFrame;
            vehicle.invalidate();
        }
    }

    /**
     * Animate the vehicle based on its speed plus add steam particles
     */
    static void AnimateSteamLocomotive(Vehicle& vehicle, const CarEntry& carEntry)
    {
        vehicle.animationState += _vehicleVelocity;
        uint8_t targetFrame = GetTargetFrame(carEntry, vehicle.animationState);
        if (vehicle.animation_frame != targetFrame)
        {
            vehicle.animation_frame = targetFrame;
            if (ShouldMakeSteam(targetFrame, carEntry.animationFrames))
            {
                auto curRide = vehicle.GetRide();
                if (curRide != nullptr)
                {
                    if (!RideHasStationShelter(*curRide)
                        || (vehicle.status != Vehicle::Status::movingToEndOfStation
                            && vehicle.status != Vehicle::Status::arriving))
                    {
                        CoordsXYZ steamOffset = ComputeSteamOffset(
                            carEntry.steamEffect.vertical, carEntry.steamEffect.longitudinal, vehicle.pitch,
                            vehicle.orientation);
                        SteamParticle::create(CoordsXYZ(vehicle.x, vehicle.y, vehicle.z) + steamOffset);
                    }
                }
            }
            vehicle.invalidate();
        }
    }

    /**
     * Animate the vehicle based on its speed. Specialized animation with exactly 2 frames due to how peep animation works.
     */
    static void AnimateSwanBoat(Vehicle& vehicle, const CarEntry& carEntry)
    {
        // The animation of swan boats places frames at 0 and 2 instead of 0 and 1 like Water Tricycles due to the second
        // pair of peeps. The animation technically uses 4 frames, but ignores frames 1 and 3.
        vehicle.animationState += _vehicleVelocity;
        uint8_t targetFrame = GetTargetFrame(carEntry, vehicle.animationState) * 2;
        if (vehicle.animation_frame != targetFrame)
        {
            vehicle.animation_frame = targetFrame;
            vehicle.invalidate();
        }
    }

    /**
     * Monorail Cycle animation only animates when a peep is present
     */
    static void AnimateMonorailCycle(Vehicle& vehicle, const CarEntry& carEntry)
    {
        if (vehicle.num_peeps != 0)
        {
            AnimateSimpleVehicle(vehicle, carEntry);
        }
    }

    /**
     * Observation tower animates at a constant speed continuously
     */
    static void AnimateObservationTower(Vehicle& vehicle, const CarEntry& carEntry)
    {
        if (vehicle.animationState <= 0xCCCC)
        {
            vehicle.animationState += carEntry.animationSpeed;
        }
        else
        {
            vehicle.animationState = 0;
            vehicle.animation_frame += 1;
            vehicle.animation_frame %= carEntry.animationFrames;
            vehicle.invalidate();
        }
    }
    /**
     * seatRotation value of 4 translates to animationFrame value of 0. This function makes that true for any number of
     * animation frames
     */
    static int16_t MultiDimensionTargetAngle(int16_t seatRotation, int16_t animationFrames)
    {
        return ((seatRotation - 4) % animationFrames + animationFrames) % animationFrames;
    }

    /**
     * Multidimension targets a specific animation frame based on track
     */
    static void AnimateMultiDimension(Vehicle& vehicle, const CarEntry& carEntry)
    {
        if (vehicle.seat_rotation != vehicle.target_seat_rotation)
        {
            if (vehicle.animationState <= 0xCCCC)
            {
                vehicle.animationState += carEntry.animationSpeed;
            }
            else
            {
                vehicle.animationState = 0;

                if (vehicle.seat_rotation >= vehicle.target_seat_rotation)
                    vehicle.seat_rotation--;
                else
                    vehicle.seat_rotation++;

                int16_t targetSeatRotation = MultiDimensionTargetAngle(vehicle.seat_rotation, carEntry.animationFrames);
                if (targetSeatRotation != vehicle.animation_frame)
                {
                    vehicle.animation_frame = targetSeatRotation;
                    vehicle.invalidate();
                }
            }
        }
    }

    /**
     * Animal Flying animates only on chainlift and in an unusual way. Made by Spacek531
     */
    static void AnimateAnimalFlying(Vehicle& vehicle, const CarEntry& carEntry)
    {
        vehicle.UpdateAnimationAnimalFlying();
        // makes animation play faster with vehicle speed
        uint8_t targetFrame = abs(_vehicleVelocity) >> carEntry.animationSpeed;
        vehicle.animationState = std::max(vehicle.animationState - targetFrame, 0u);
    }

    using AnimateFunction = void (*)(Vehicle& vehicle, const CarEntry& carEntry);

    constexpr static const AnimateFunction AnimationFunctions[]{
        AnimateNone,          AnimateSimpleVehicle,  AnimateSteamLocomotive,  AnimateSwanBoat,
        AnimateMonorailCycle, AnimateMultiDimension, AnimateObservationTower, AnimateAnimalFlying,
    };
    static_assert(std::size(AnimationFunctions) == EnumValue(CarEntryAnimation::count));

    /**
     *
     *  rct2: 0x006D63D4
     */
    void Vehicle::UpdateAdditionalAnimation()
    {
        auto carEntry = Entry();
        if (carEntry == nullptr)
        {
            return;
        }
        if (carEntry->animationFrames == 0 || carEntry->animation >= CarEntryAnimation::count)
            return;
        AnimationFunctions[EnumValue(carEntry->animation)](*this, *carEntry);
    }
} // namespace OpenRCT2
