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

#include "../GameState.h"
#include "../core/DataSerialiser.h"
#include "../object/PathAdditionEntry.h"
#include "../scenario/Scenario.h"
#include "../world/Location.hpp"
#include "../world/Map.h"
#include "../world/tile_element/PathElement.h"
#include "EntityRegistry.h"

namespace OpenRCT2
{
    enum class Pattern
    {
        cyclicSquares,
        continuousChasers,
        bouncingPairs,
        sproutingBlooms,
        racingPairs,
        splittingChasers,
        dopeyJumpers,
        fastRandomChasers,
    };

    static constexpr std::array _fountainDirectionsNegative = {
        CoordsXY{ -kCoordsXYStep, 0 },
        CoordsXY{ -kCoordsXYStep, -kCoordsXYStep },
        CoordsXY{ 0, 0 },
        CoordsXY{ -kCoordsXYStep, 0 },
        CoordsXY{ 0, 0 },
        CoordsXY{ 0, -kCoordsXYStep },
        CoordsXY{ 0, -kCoordsXYStep },
        CoordsXY{ -kCoordsXYStep, -kCoordsXYStep },
    };

    static constexpr std::array _fountainDirectionsPositive = {
        CoordsXY{ kCoordsXYStep, 0 },
        CoordsXY{ 0, 0 },
        CoordsXY{ 0, kCoordsXYStep },
        CoordsXY{ kCoordsXYStep, kCoordsXYStep },
        CoordsXY{ kCoordsXYStep, kCoordsXYStep },
        CoordsXY{ kCoordsXYStep, 0 },
        CoordsXY{ 0, 0 },
        CoordsXY{ 0, kCoordsXYStep },
    };
    static constexpr auto kFountainChanceOfStoppingEdgeMode = 0x3333;   // 0.200
    static constexpr auto kFountainChanceOfStoppingRandomMode = 0x2000; // 0.125

    // rct2: 0x0097F040
    const uint8_t _fountainDirections[] = {
        0, 1, 2, 3, 0, 1, 2, 3,
    };

    // rct2: 0x0097F048
    const FountainFlags _fountainDirectionFlags[] = {
        {}, {}, FountainFlag::direction, FountainFlag::direction, FountainFlag::direction, FountainFlag::direction, {}, {},
    };

    // rct2: 0x0097F050
    const FountainFlags _fountainPatternFlags[] = {
        { FountainFlag::terminate },                                         // cyclicSquares
        { FountainFlag::fast, FountainFlag::goToEdge },                      // continuousChasers
        { FountainFlag::bounce },                                            // bouncingPairs
        { FountainFlag::fast, FountainFlag::split },                         // sproutingBlooms
        { FountainFlag::goToEdge },                                          // racingPairs
        { FountainFlag::fast, FountainFlag::goToEdge, FountainFlag::split }, // splittingChasers
        {},                                                                  // dopeyJumpers
        { FountainFlag::fast },                                              // fastRandomChasers
    };

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

    void JumpingFountain::startAnimation(
        const JumpingFountainType newType, const CoordsXY& newLoc, const TileElement* tileElement)
    {
        const auto currentTicks = getGameState().currentTicks;

        int32_t randomIndex;
        auto newZ = tileElement->getBaseZ();

        // Change pattern approximately every 51 seconds
        uint32_t pattern = (currentTicks >> 11) & 7;
        switch (static_cast<Pattern>(pattern))
        {
            case Pattern::cyclicSquares:
                // 0, 1, 2, 3
                for (int32_t i = 0; i < kNumOrthogonalDirections; i++)
                {
                    create(
                        newType, { newLoc + _fountainDirectionsPositive[i], newZ }, _fountainDirections[i],
                        _fountainDirectionFlags[i] | _fountainPatternFlags[pattern], 0);
                }
                break;
            case Pattern::bouncingPairs:
                // random [0, 2 or 1, 3]
                randomIndex = ScenarioRand() & 1;
                for (int32_t i = randomIndex; i < kNumOrthogonalDirections; i += 2)
                {
                    create(
                        newType, { newLoc + _fountainDirectionsPositive[i], newZ }, _fountainDirections[i],
                        _fountainDirectionFlags[i] | _fountainPatternFlags[pattern], 0);
                }
                break;
            case Pattern::racingPairs:
                // random [0 - 3 and 4 - 7]
                randomIndex = ScenarioRand() & 3;
                create(
                    newType, { newLoc + _fountainDirectionsPositive[randomIndex], newZ }, _fountainDirections[randomIndex],
                    _fountainDirectionFlags[randomIndex] | _fountainPatternFlags[pattern], 0);
                randomIndex += 4;
                create(
                    newType, { newLoc + _fountainDirectionsPositive[randomIndex], newZ }, _fountainDirections[randomIndex],
                    _fountainDirectionFlags[randomIndex] | _fountainPatternFlags[pattern], 0);
                break;
            default:
                // random [0 - 7]
                randomIndex = ScenarioRand() & 7;
                create(
                    newType, { newLoc + _fountainDirectionsPositive[randomIndex], newZ }, _fountainDirections[randomIndex],
                    _fountainDirectionFlags[randomIndex] | _fountainPatternFlags[pattern], 0);
                break;
        }
    }

    void JumpingFountain::create(
        const JumpingFountainType newType, const CoordsXYZ& newLoc, const int32_t direction, const FountainFlags newFlags,
        const int32_t iteration)
    {
        auto* jumpingFountain = getGameState().entities.createEntity<JumpingFountain>();
        if (jumpingFountain != nullptr)
        {
            jumpingFountain->iteration = iteration;
            jumpingFountain->fountainFlags = newFlags;
            jumpingFountain->orientation = direction << 3;
            jumpingFountain->spriteData.width = 33;
            jumpingFountain->spriteData.heightMin = 36;
            jumpingFountain->spriteData.heightMax = 12;
            jumpingFountain->moveTo(newLoc);
            jumpingFountain->fountainType = newType;
            jumpingFountain->numTicksAlive = 0;
            jumpingFountain->frame = 0;
        }
    }

    void JumpingFountain::update()
    {
        numTicksAlive++;
        // Originally this would not update the frame on the following
        // ticks: 1, 3, 6, 9, 11, 14, 17, 19, 22, 25
        // This change was to simplify the code base. There is a small increase
        // in speed of the fountain jump because of this change.
        if (numTicksAlive % 3 == 0)
        {
            return;
        }

        invalidate();
        frame++;

        switch (fountainType)
        {
            case JumpingFountainType::water:
                if (frame == 11 && fountainFlags.has(FountainFlag::fast))
                {
                    advanceAnimation();
                }
                if (frame == 16 && !fountainFlags.has(FountainFlag::fast))
                {
                    advanceAnimation();
                }
                break;
            case JumpingFountainType::snow:
                if (frame == 16)
                {
                    advanceAnimation();
                }
                break;
            default:
                break;
        }

        if (frame == 16)
        {
            getGameState().entities.entityRemove(this);
        }
    }

    JumpingFountainType JumpingFountain::getType() const
    {
        return fountainType;
    }

    void JumpingFountain::advanceAnimation()
    {
        const JumpingFountainType newType = getType();
        const int32_t direction = (orientation >> 3) & 7;
        const CoordsXY newLoc = CoordsXY{ x, y } + CoordsDirectionDelta[direction];

        int32_t availableDirections = 0;
        for (uint32_t i = 0; i < _fountainDirectionsNegative.size(); i++)
        {
            if (isJumpingFountain(newType, { newLoc + _fountainDirectionsNegative[i], z }))
            {
                availableDirections |= 1 << i;
            }
        }

        if (availableDirections == 0)
        {
            return;
        }

        if (fountainFlags.has(FountainFlag::terminate))
        {
            return;
        }

        if (fountainFlags.has(FountainFlag::goToEdge))
        {
            goToEdge({ newLoc, z }, availableDirections);
            return;
        }

        if (fountainFlags.has(FountainFlag::bounce))
        {
            bounce({ newLoc, z }, availableDirections);
            return;
        }

        if (fountainFlags.has(FountainFlag::split))
        {
            split({ newLoc, z }, availableDirections);
            return;
        }

        random({ newLoc, z }, availableDirections);
    }

    bool JumpingFountain::isJumpingFountain(const JumpingFountainType newType, const CoordsXYZ& newLoc)
    {
        const int32_t pathAdditionFlagMask = newType == JumpingFountainType::snow ? PATH_ADDITION_FLAG_JUMPING_FOUNTAIN_SNOW
                                                                                  : PATH_ADDITION_FLAG_JUMPING_FOUNTAIN_WATER;

        TileElement* tileElement = MapGetFirstElementAt(newLoc);
        if (tileElement == nullptr)
            return false;
        do
        {
            if (tileElement->getType() != TileElementType::path)
                continue;
            if (tileElement->getBaseZ() != newLoc.z)
                continue;
            if (tileElement->asPath()->additionIsGhost())
                continue;
            if (!tileElement->asPath()->hasAddition())
                continue;

            auto* pathAdditionEntry = tileElement->asPath()->getAdditionEntry();
            if (pathAdditionEntry != nullptr && pathAdditionEntry->flags & pathAdditionFlagMask)
            {
                return true;
            }
        } while (!(tileElement++)->isLastForTile());

        return false;
    }

    void JumpingFountain::goToEdge(const CoordsXYZ& newLoc, const int32_t availableDirections) const
    {
        int32_t direction = (orientation >> 3) << 1;
        if (availableDirections & (1 << direction))
        {
            createNext(newLoc, direction);
            return;
        }

        direction++;
        if (availableDirections & (1 << direction))
        {
            createNext(newLoc, direction);
            return;
        }

        const uint32_t randomIndex = ScenarioRand();
        if ((randomIndex & 0xFFFF) < kFountainChanceOfStoppingEdgeMode)
        {
            return;
        }

        if (fountainFlags.has(FountainFlag::split))
        {
            split(newLoc, availableDirections);
            return;
        }

        direction = randomIndex & 7;
        while (!(availableDirections & (1 << direction)))
        {
            direction = (direction + 1) & 7;
        }

        createNext(newLoc, direction);
    }

    void JumpingFountain::bounce(const CoordsXYZ& newLoc, const int32_t availableDirections)
    {
        iteration++;
        if (iteration < 8)
        {
            int32_t direction = ((orientation >> 3) ^ 2) << 1;
            if (availableDirections & (1 << direction))
            {
                createNext(newLoc, direction);
            }
            else
            {
                direction++;
                if (availableDirections & (1 << direction))
                {
                    createNext(newLoc, direction);
                }
            }
        }
    }

    void JumpingFountain::split(const CoordsXYZ& newLoc, int32_t availableDirections) const
    {
        if (iteration < 3)
        {
            const auto newType = getType();
            int32_t direction = ((orientation >> 3) ^ 2) << 1;
            availableDirections &= ~(1 << direction);
            availableDirections &= ~(1 << (direction + 1));

            for (direction = 0; direction < 8; direction++)
            {
                if (availableDirections & (1 << direction))
                {
                    auto copiedFlags = fountainFlags;
                    copiedFlags.unset(FountainFlag::direction);
                    create(newType, newLoc, direction >> 1, copiedFlags, iteration + 1);
                }
                direction++;
                if (availableDirections & (1 << direction))
                {
                    auto copiedFlags = fountainFlags;
                    copiedFlags.set(FountainFlag::direction);
                    create(newType, newLoc, direction >> 1, copiedFlags, iteration + 1);
                }
            }
        }
    }

    void JumpingFountain::random(const CoordsXYZ& newLoc, int32_t availableDirections) const
    {
        const uint32_t randomIndex = ScenarioRand();
        if ((randomIndex & 0xFFFF) >= kFountainChanceOfStoppingRandomMode)
        {
            int32_t direction = randomIndex & 7;
            while (!(availableDirections & (1 << direction)))
            {
                direction = (direction + 1) & 7;
            }
            createNext(newLoc, direction);
        }
    }

    void JumpingFountain::createNext(const CoordsXYZ& newLoc, int32_t direction) const
    {
        const auto newType = getType();
        auto newFlags = fountainFlags;
        newFlags.set(FountainFlag::direction, !!(direction & 1));
        create(newType, newLoc, direction >> 1, newFlags, iteration);
    }

    void JumpingFountain::serialise(DataSerialiser& stream)
    {
        EntityBase::serialise(stream);
        stream << frame;
        stream << fountainType;
        stream << numTicksAlive;
        stream << fountainFlags.holder;
        stream << targetX;
        stream << targetY;
        stream << iteration;
    }
} // namespace OpenRCT2
