/*****************************************************************************
 * 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 "Particle.h"

#include "../GameState.h"
#include "../SpriteIds.h"
#include "../audio/Audio.h"
#include "../core/DataSerialiser.h"
#include "../paint/Paint.h"
#include "../profiling/Profiling.h"
#include "../ride/VehicleColour.h"
#include "../scenario/Scenario.h"
#include "../world/Map.h"
#include "../world/tile_element/SurfaceElement.h"
#include "EntityRegistry.h"

#include <iterator>

using namespace OpenRCT2;

static constexpr uint32_t kVehicleCrashParticleSprites[kCrashedVehicleParticleNumberTypes] = {
    SPR_VEHICLE_CRASH_PARTICLE_1, SPR_VEHICLE_CRASH_PARTICLE_2, SPR_VEHICLE_CRASH_PARTICLE_3,
    SPR_VEHICLE_CRASH_PARTICLE_4, SPR_VEHICLE_CRASH_PARTICLE_5,
};

template<>
bool EntityBase::Is<SteamParticle>() const
{
    return Type == EntityType::steamParticle;
}

template<>
bool EntityBase::Is<ExplosionFlare>() const
{
    return Type == EntityType::explosionFlare;
}

template<>
bool EntityBase::Is<ExplosionCloud>() const
{
    return Type == EntityType::explosionCloud;
}

template<>
bool EntityBase::Is<VehicleCrashParticle>() const
{
    return Type == EntityType::crashedVehicleParticle;
}

template<>
bool EntityBase::Is<CrashSplashParticle>() const
{
    return Type == EntityType::crashSplash;
}

void VehicleCrashParticle::SetSpriteData()
{
    SpriteData.Width = 8;
    SpriteData.HeightMin = 8;
    SpriteData.HeightMax = 8;
}

void VehicleCrashParticle::Launch()
{
    frame = (ScenarioRand() & 0xFF) * kCrashedVehicleParticleNumberSprites;
    time_to_live = (ScenarioRand() & 0x7F) + 140;
    crashed_sprite_base = ScenarioRandMax(kCrashedVehicleParticleNumberTypes);
    acceleration_x = (static_cast<int16_t>(ScenarioRand() & 0xFFFF)) * 4;
    acceleration_y = (static_cast<int16_t>(ScenarioRand() & 0xFFFF)) * 4;
    acceleration_z = (ScenarioRand() & 0xFFFF) * 4 + 0x10000;
    velocity_x = 0;
    velocity_y = 0;
    velocity_z = 0;
}

/**
 *
 *  rct2: 0x006735A1
 */
void VehicleCrashParticle::Create(VehicleColour& colours, const CoordsXYZ& vehiclePos)
{
    VehicleCrashParticle* sprite = getGameState().entities.CreateEntity<VehicleCrashParticle>();
    if (sprite != nullptr)
    {
        sprite->MoveTo(vehiclePos);
        sprite->colour[0] = colours.Body;
        sprite->colour[1] = colours.Trim;
        sprite->SetSpriteData();
        sprite->Launch();
    }
}

/**
 *
 *  rct2: 0x00673298
 */
void VehicleCrashParticle::Update()
{
    Invalidate();
    time_to_live--;
    if (time_to_live == 0)
    {
        getGameState().entities.EntityRemove(this);
        return;
    }

    // Apply gravity
    acceleration_z -= 5041;

    // Apply air resistance
    acceleration_x -= (acceleration_x / 256);
    acceleration_y -= (acceleration_y / 256);
    acceleration_z -= (acceleration_z / 256);

    // Update velocity and position
    int32_t vx = velocity_x + acceleration_x;
    int32_t vy = velocity_y + acceleration_y;
    int32_t vz = velocity_z + acceleration_z;

    CoordsXYZ newLoc = { x + (vx >> 16), y + (vy >> 16), z + (vz >> 16) };

    velocity_x = vx & 0xFFFF;
    velocity_y = vy & 0xFFFF;
    velocity_z = vz & 0xFFFF;

    // Check collision with land / water
    int16_t landZ = TileElementHeight(newLoc);
    int16_t waterZ = TileElementWaterHeight(newLoc);

    if (waterZ != 0 && z >= waterZ && newLoc.z <= waterZ)
    {
        // Splash
        Audio::Play3D(Audio::SoundId::water2, { x, y, waterZ });
        CrashSplashParticle::Create({ x, y, waterZ });
        getGameState().entities.EntityRemove(this);
        return;
    }

    if (z >= landZ && newLoc.z <= landZ)
    {
        // Bounce
        acceleration_z *= -1;
        newLoc.z = landZ;
    }
    MoveTo(newLoc);

    frame += kCrashedVehicleParticleFrameIncrement;
    if (frame >= (kCrashedVehicleParticleNumberSprites * kCrashedVehicleParticleFrameToSprite))
    {
        frame = 0;
    }
}

void VehicleCrashParticle::Serialise(DataSerialiser& stream)
{
    EntityBase::Serialise(stream);
    stream << frame;
    stream << time_to_live;
    stream << colour;
    stream << crashed_sprite_base;
    stream << velocity_x;
    stream << velocity_y;
    stream << velocity_z;
    stream << acceleration_x;
    stream << acceleration_y;
    stream << acceleration_z;
}

void VehicleCrashParticle::Paint(PaintSession& session, int32_t imageDirection) const
{
    PROFILED_FUNCTION();

    auto& rt = session.rt;
    if (rt.zoom_level > ZoomLevel{ 0 })
    {
        return;
    }

    uint32_t imageId = kVehicleCrashParticleSprites[crashed_sprite_base] + frame / 256;
    auto image = ImageId(imageId, colour[0], colour[1]);
    PaintAddImageAsParent(session, image, { 0, 0, z }, { 1, 1, 0 });
}

/**
 *
 *  rct2: 0x00673699
 */
void CrashSplashParticle::Create(const CoordsXYZ& splashPos)
{
    auto* sprite = getGameState().entities.CreateEntity<CrashSplashParticle>();
    if (sprite != nullptr)
    {
        sprite->SpriteData.Width = 33;
        sprite->SpriteData.HeightMin = 51;
        sprite->SpriteData.HeightMax = 16;
        sprite->MoveTo(splashPos + CoordsXYZ{ 0, 0, 3 });
        sprite->frame = 0;
    }
}

/**
 *
 *  rct2: 0x0067339D
 */
void CrashSplashParticle::Update()
{
    Invalidate();
    frame += 85;
    if (frame >= 7168)
    {
        getGameState().entities.EntityRemove(this);
    }
}

void CrashSplashParticle::Serialise(DataSerialiser& stream)
{
    EntityBase::Serialise(stream);
    stream << frame;
}

void CrashSplashParticle::Paint(PaintSession& session, int32_t imageDirection) const
{
    PROFILED_FUNCTION();

    uint32_t imageId = SPR_CRASH_SPLASH_PARTICLE + (frame / 256);
    PaintAddImageAsParent(session, ImageId(imageId), { 0, 0, z }, { 1, 1, 0 });
}

/**
 *
 *  rct2: 0x006734B2
 */
void SteamParticle::Create(const CoordsXYZ& coords)
{
    auto surfaceElement = MapGetSurfaceElementAt(coords);
    if (surfaceElement != nullptr && coords.z > surfaceElement->GetBaseZ())
    {
        SteamParticle* steam = getGameState().entities.CreateEntity<SteamParticle>();
        if (steam == nullptr)
            return;

        steam->SpriteData.Width = 20;
        steam->SpriteData.HeightMin = 18;
        steam->SpriteData.HeightMax = 16;
        steam->frame = 256;
        steam->time_to_move = 0;
        steam->MoveTo(coords);
    }
}

/**
 *
 *  rct2: 0x00673200
 */
void SteamParticle::Update()
{
    // Move up 1 z every 3 ticks (Starts after 4 ticks)
    Invalidate();
    time_to_move++;
    if (time_to_move >= 4)
    {
        time_to_move = 1;
        MoveTo({ x, y, z + 1 });
    }
    frame += 64;
    if (frame >= (56 * 64))
    {
        getGameState().entities.EntityRemove(this);
    }
}

void SteamParticle::Serialise(DataSerialiser& stream)
{
    EntityBase::Serialise(stream);
    stream << frame;
    stream << time_to_move;
}

void SteamParticle::Paint(PaintSession& session, int32_t imageDirection) const
{
    PROFILED_FUNCTION();

    uint32_t imageId = SPR_STEAM_PARTICLE + (frame / 256);
    PaintAddImageAsParent(session, ImageId(imageId), { 0, 0, z }, { 1, 1, 0 });
}

/**
 *
 *  rct2: 0x0067363D
 */
void ExplosionCloud::Create(const CoordsXYZ& cloudPos)
{
    auto* entity = getGameState().entities.CreateEntity<ExplosionCloud>();
    if (entity != nullptr)
    {
        entity->SpriteData.Width = 44;
        entity->SpriteData.HeightMin = 32;
        entity->SpriteData.HeightMax = 34;
        entity->MoveTo(cloudPos + CoordsXYZ{ 0, 0, 4 });
        entity->frame = 0;
    }
}

/**
 *
 *  rct2: 0x00673385
 */
void ExplosionCloud::Update()
{
    Invalidate();
    frame += 128;
    if (frame >= (36 * 128))
    {
        getGameState().entities.EntityRemove(this);
    }
}

void ExplosionCloud::Serialise(DataSerialiser& stream)
{
    EntityBase::Serialise(stream);
    stream << frame;
}

void ExplosionCloud::Paint(PaintSession& session, int32_t imageDirection) const
{
    PROFILED_FUNCTION();

    uint32_t imageId = SPR_EXPLOSION_CLOUD + (frame / 256);
    PaintAddImageAsParent(session, ImageId(imageId), { 0, 0, z }, { 1, 1, 0 });
}

/**
 *
 *  rct2: 0x0067366B
 */
void ExplosionFlare::Create(const CoordsXYZ& flarePos)
{
    auto* entity = getGameState().entities.CreateEntity<ExplosionFlare>();
    if (entity != nullptr)
    {
        entity->SpriteData.Width = 25;
        entity->SpriteData.HeightMin = 85;
        entity->SpriteData.HeightMax = 8;
        entity->MoveTo(flarePos + CoordsXYZ{ 0, 0, 4 });
        entity->frame = 0;
    }
}

/**
 *
 *  rct2: 0x006733B4
 */
void ExplosionFlare::Update()
{
    Invalidate();
    frame += 64;
    if (frame >= (124 * 64))
    {
        getGameState().entities.EntityRemove(this);
    }
}

void ExplosionFlare::Serialise(DataSerialiser& stream)
{
    EntityBase::Serialise(stream);
    stream << frame;
}

void ExplosionFlare::Paint(PaintSession& session, int32_t imageDirection) const
{
    PROFILED_FUNCTION();

    uint32_t imageId = SPR_EXPLOSION_FLARE + (frame / 256);
    PaintAddImageAsParent(session, ImageId(imageId), { 0, 0, z }, { 1, 1, 0 });
}
