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

#include "../GameState.h"
#include "../SpriteIds.h"
#include "../audio/Audio.h"
#include "../core/DataSerialiser.h"
#include "../entity/EntityList.h"
#include "../scenario/Scenario.h"
#include "../world/Map.h"
#include "../world/tile_element/SurfaceElement.h"
#include "EntityRegistry.h"

#include <iterator>
#include <limits>

namespace OpenRCT2
{
    constexpr int32_t kDuckMaxStates = 5;

    // clang-format off
static constexpr CoordsXY kDuckMoveOffset[] =
{
    { -1,  0 },
    {  0,  1 },
    {  1,  0 },
    {  0, -1 },
};

static constexpr uint8_t kDuckAnimationFlyToWater[] =
{
    8, 9, 10, 11, 12, 13
};

static constexpr uint8_t kDuckAnimationSwim[] =
{
    0
};

static constexpr uint8_t kDuckAnimationDrink[] =
{
    1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0xFF
};

static constexpr uint8_t kDuckAnimationDoubleDrink[] =
{
    4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6,
    6, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 0, 0, 0, 0, 0xFF
};

static constexpr uint8_t kDuckAnimationFlyAway[] =
{
    8, 9, 10, 11, 12, 13
};
    // clang-format on

    static constexpr const uint8_t* kDuckAnimations[] = {
        kDuckAnimationFlyToWater,  //  DuckState::FlyToWater,
        kDuckAnimationSwim,        //  DuckState::Swim,
        kDuckAnimationDrink,       //  DuckState::Drink,
        kDuckAnimationDoubleDrink, //  DuckState::DoubleDrink,
        kDuckAnimationFlyAway,     //  DuckState::FlyAway,
    };

    template<>
    bool EntityBase::is<Duck>() const
    {
        return type == EntityType::duck;
    }

    bool Duck::isFlying()
    {
        return this->state == DuckState::flyAway || this->state == DuckState::flyToWater;
    }

    void Duck::remove()
    {
        invalidate();
        getGameState().entities.entityRemove(this);
    }

    void Duck::updateFlyToWater()
    {
        const auto currentTicks = getGameState().currentTicks;

        if ((currentTicks & 3) != 0)
            return;

        frame++;
        if (frame >= std::size(kDuckAnimationFlyToWater))
        {
            frame = 0;
        }

        invalidate();
        int32_t manhattanDistance = abs(targetX - x) + abs(targetY - y);
        int32_t direction = orientation >> 3;
        auto destination = CoordsXYZ{ CoordsXY{ x, y } + kDuckMoveOffset[direction], 0 };
        int32_t manhattanDistanceN = abs(targetX - destination.x) + abs(targetY - destination.y);

        auto surfaceElement = MapGetSurfaceElementAt(CoordsXY{ targetX, targetY });
        int32_t waterHeight = surfaceElement != nullptr ? surfaceElement->getWaterHeight() : 0;
        if (waterHeight == 0)
        {
            state = DuckState::flyAway;
            updateFlyAway();
        }
        else
        {
            destination.z = abs(z - waterHeight);

            if (manhattanDistanceN <= manhattanDistance)
            {
                if (destination.z > manhattanDistanceN)
                {
                    destination.z = z - 2;
                    if (waterHeight >= z)
                    {
                        destination.z += 4;
                    }
                    frame = 1;
                }
                else
                {
                    destination.z = z;
                }
                moveTo(destination);
            }
            else
            {
                if (destination.z > 4)
                {
                    state = DuckState::flyAway;
                    updateFlyAway();
                }
                else
                {
                    state = DuckState::swim;
                    frame = 0;
                    updateSwim();
                }
            }
        }
    }

    void Duck::updateSwim()
    {
        const auto currentTicks = getGameState().currentTicks;

        if (((currentTicks + id.ToUnderlying()) & 3) != 0)
            return;

        uint32_t randomNumber = ScenarioRand();
        if ((randomNumber & 0xFFFF) < 0x666)
        {
            if (randomNumber & 0x80000000)
            {
                state = DuckState::doubleDrink;
                frame = std::numeric_limits<uint16_t>::max();
                updateDoubleDrink();
            }
            else
            {
                state = DuckState::drink;
                frame = std::numeric_limits<uint16_t>::max();
                updateDrink();
            }
        }
        else
        {
            int32_t currentMonth = GetDate().GetMonth();
            if (currentMonth >= MONTH_SEPTEMBER && (randomNumber >> 16) < 218)
            {
                state = DuckState::flyAway;
                updateFlyAway();
            }
            else
            {
                invalidate();
                int16_t landZ = TileElementHeight({ x, y });
                int16_t waterZ = TileElementWaterHeight({ x, y });

                if (z < landZ || waterZ == 0)
                {
                    state = DuckState::flyAway;
                    updateFlyAway();
                }
                else
                {
                    z = waterZ;
                    randomNumber = ScenarioRand();
                    if ((randomNumber & 0xFFFF) <= 0xAAA)
                    {
                        randomNumber >>= 16;
                        orientation = randomNumber & 0x18;
                    }

                    int32_t direction = orientation >> 3;
                    auto destination = CoordsXYZ{ CoordsXY{ x, y } + kDuckMoveOffset[direction], 0 };
                    landZ = TileElementHeight(destination);
                    waterZ = TileElementWaterHeight(destination);

                    if (z > landZ && z == waterZ)
                    {
                        destination.z = waterZ;
                        moveTo(destination);
                        invalidate();
                    }
                }
            }
        }
    }

    void Duck::updateDrink()
    {
        frame++;
        if (kDuckAnimationDrink[frame] == 0xFF)
        {
            state = DuckState::swim;
            frame = 0;
            updateSwim();
        }
        else
        {
            invalidate();
        }
    }

    void Duck::updateDoubleDrink()
    {
        frame++;
        if (kDuckAnimationDoubleDrink[frame] == 0xFF)
        {
            state = DuckState::swim;
            frame = 0;
            updateSwim();
        }
        else
        {
            invalidate();
        }
    }

    void Duck::updateFlyAway()
    {
        if ((getGameState().currentTicks & 3) == 0)
        {
            frame++;
            if (frame >= std::size(kDuckAnimationFlyAway))
            {
                frame = 0;
            }

            invalidate();

            int32_t direction = orientation >> 3;
            auto destination = CoordsXYZ{ x + (kDuckMoveOffset[direction].x * 2), y + (kDuckMoveOffset[direction].y * 2),
                                          std::min<int32_t>(z + 2, 496) };
            if (MapIsLocationValid(destination))
            {
                moveTo(destination);
            }
            else
            {
                remove();
            }
        }
    }

    uint32_t Duck::getFrameImage(int32_t direction) const
    {
        uint32_t imageId = 0;
        if (EnumValue(state) < kDuckMaxStates)
        {
            // TODO: Check frame is in range
            uint8_t imageOffset = kDuckAnimations[EnumValue(state)][frame];
            imageId = SPR_DUCK + (imageOffset * 4) + (direction / 8);
        }
        return imageId;
    }

    void Duck::create(const CoordsXY& pos)
    {
        auto* duck = getGameState().entities.createEntity<Duck>();
        if (duck == nullptr)
            return;

        CoordsXY targetPos = pos;

        int32_t offsetXY = ScenarioRand() & 0x1E;
        targetPos.x += offsetXY;
        targetPos.y += offsetXY;

        duck->spriteData.width = 9;
        duck->spriteData.heightMin = 12;
        duck->spriteData.heightMax = 9;
        duck->targetX = targetPos.x;
        duck->targetY = targetPos.y;
        uint8_t direction = ScenarioRand() & 3;
        switch (direction)
        {
            case 0:
                targetPos.x = GetMapSizeMaxXY().x - (ScenarioRand() & 0x3F);
                break;
            case 1:
                targetPos.y = ScenarioRand() & 0x3F;
                break;
            case 2:
                targetPos.x = ScenarioRand() & 0x3F;
                break;
            case 3:
                targetPos.y = GetMapSizeMaxXY().y - (ScenarioRand() & 0x3F);
                break;
        }
        duck->orientation = direction << 3;
        duck->moveTo({ targetPos.x, targetPos.y, 496 });
        duck->state = DuckState::flyToWater;
        duck->frame = 0;
    }

    void Duck::update()
    {
        switch (state)
        {
            case DuckState::flyToWater:
                updateFlyToWater();
                break;
            case DuckState::swim:
                updateSwim();
                break;
            case DuckState::drink:
                updateDrink();
                break;
            case DuckState::doubleDrink:
                updateDoubleDrink();
                break;
            case DuckState::flyAway:
                updateFlyAway();
                break;
        }
    }

    void Duck::press()
    {
        OpenRCT2::Audio::Play3D(Audio::SoundId::quack, { x, y, z });
    }

    void Duck::removeAll()
    {
        for (auto duck : EntityList<Duck>())
        {
            duck->remove();
        }
    }

    void Duck::serialise(DataSerialiser& stream)
    {
        EntityBase::serialise(stream);
        stream << frame;
        stream << targetX;
        stream << targetY;
        stream << state;
    }
} // namespace OpenRCT2
