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

#include "../../Diagnostic.h"
#include "../../GameState.h"
#include "../../OpenRCT2.h"
#include "../../drawing/TextColour.h"
#include "../../management/Finance.h"
#include "../../object/LargeSceneryEntry.h"
#include "../../object/ObjectEntryManager.h"
#include "../../object/SmallSceneryEntry.h"
#include "../../object/WallSceneryEntry.h"
#include "../../ride/RideData.h"
#include "../../ride/TrackData.h"
#include "../../ride/TrackDesign.h"
#include "../../ride/ted/TrackElementDescriptor.h"
#include "../../world/Banner.h"
#include "../../world/ConstructionClearance.h"
#include "../../world/Map.h"
#include "../../world/MapAnimation.h"
#include "../../world/TileElementsView.h"
#include "../../world/Wall.h"
#include "../../world/tile_element/LargeSceneryElement.h"
#include "../../world/tile_element/PathElement.h"
#include "../../world/tile_element/Slope.h"
#include "../../world/tile_element/SmallSceneryElement.h"
#include "../../world/tile_element/SurfaceElement.h"
#include "../../world/tile_element/TrackElement.h"
#include "../../world/tile_element/WallElement.h"

namespace OpenRCT2::GameActions
{
    using namespace OpenRCT2::TrackMetadata;

    WallPlaceAction::WallPlaceAction(
        ObjectEntryIndex wallType, const CoordsXYZ& loc, uint8_t edge, Drawing::Colour primaryColour,
        Drawing::Colour secondaryColour, Drawing::Colour tertiaryColour)
        : _wallType(wallType)
        , _loc(loc)
        , _edge(edge)
        , _primaryColour(primaryColour)
        , _secondaryColour(secondaryColour)
        , _tertiaryColour(tertiaryColour)
    {
    }

    void WallPlaceAction::AcceptParameters(GameActionParameterVisitor& visitor)
    {
        visitor.Visit(_loc);
        visitor.Visit("object", _wallType);
        visitor.Visit("edge", _edge);
        visitor.Visit("primaryColour", _primaryColour);
        visitor.Visit("secondaryColour", _secondaryColour);
        visitor.Visit("tertiaryColour", _tertiaryColour);
    }

    uint16_t WallPlaceAction::GetActionFlags() const
    {
        return GameAction::GetActionFlags();
    }

    void WallPlaceAction::Serialise(DataSerialiser& stream)
    {
        GameAction::Serialise(stream);

        stream << DS_TAG(_wallType) << DS_TAG(_loc) << DS_TAG(_edge) << DS_TAG(_primaryColour) << DS_TAG(_secondaryColour)
               << DS_TAG(_tertiaryColour);
    }

    Result WallPlaceAction::Query(GameState_t& gameState, Park::ParkData& park) const
    {
        auto res = Result();
        res.errorTitle = STR_CANT_BUILD_THIS_HERE;
        res.position = _loc;

        res.expenditure = ExpenditureType::landscaping;
        res.position.x += 16;
        res.position.y += 16;

        if (!LocationValid(_loc))
        {
            return Result(Status::invalidParameters, STR_CANT_BUILD_THIS_HERE, STR_OFF_EDGE_OF_MAP);
        }

        auto mapSizeMax = GetMapSizeMaxXY();
        if (gLegacyScene != LegacyScene::scenarioEditor && !GetFlags().has(CommandFlag::trackDesign)
            && !gameState.cheats.sandboxMode)
        {
            if (_loc.z == 0)
            {
                if (!MapIsLocationInPark(_loc))
                {
                    return Result(Status::notOwned, STR_CANT_BUILD_THIS_HERE, STR_LAND_NOT_OWNED_BY_PARK);
                }
            }
            else if (!MapIsLocationOwned(_loc))
            {
                return Result(Status::notOwned, STR_CANT_BUILD_THIS_HERE, STR_LAND_NOT_OWNED_BY_PARK);
            }
        }
        else if (!_trackDesignDrawingPreview && (_loc.x > mapSizeMax.x || _loc.y > mapSizeMax.y))
        {
            LOG_ERROR("Invalid x/y coordinates. x = %d y = %d", _loc.x, _loc.y);
            return Result(Status::invalidParameters, STR_CANT_BUILD_THIS_HERE, STR_OFF_EDGE_OF_MAP);
        }

        if (_edge > 3)
        {
            return Result(Status::invalidParameters, STR_CANT_BUILD_THIS_HERE, kStringIdNone);
        }

        uint8_t edgeSlope = 0;
        auto targetHeight = _loc.z;
        if (targetHeight == 0)
        {
            auto* surfaceElement = MapGetSurfaceElementAt(_loc);
            if (surfaceElement == nullptr)
            {
                LOG_ERROR("Surface element not found at %d, %d.", _loc.x, _loc.y);
                return Result(Status::invalidParameters, STR_CANT_BUILD_THIS_HERE, STR_ERR_SURFACE_ELEMENT_NOT_FOUND);
            }
            targetHeight = surfaceElement->getBaseZ();

            uint8_t slope = surfaceElement->getSlope();
            edgeSlope = GetWallSlopeFromEdgeSlope(slope, _edge & 3);
            if (edgeSlope & EDGE_SLOPE_ELEVATED)
            {
                targetHeight += 16;
                edgeSlope &= ~EDGE_SLOPE_ELEVATED;
            }
        }
        res.position.z = targetHeight;

        auto* surfaceElement = MapGetSurfaceElementAt(_loc);
        if (surfaceElement == nullptr)
        {
            LOG_ERROR("Surface element not found at %d, %d.", _loc.x, _loc.y);
            return Result(Status::invalidParameters, STR_CANT_BUILD_THIS_HERE, STR_ERR_SURFACE_ELEMENT_NOT_FOUND);
        }

        if (surfaceElement->getWaterHeight() > 0)
        {
            uint16_t waterHeight = surfaceElement->getWaterHeight();

            if (targetHeight < waterHeight && !gameState.cheats.disableClearanceChecks)
            {
                return Result(Status::disallowed, STR_CANT_BUILD_THIS_HERE, STR_CANT_BUILD_THIS_UNDERWATER);
            }
        }

        if (targetHeight < surfaceElement->getBaseZ() && !gameState.cheats.disableClearanceChecks)
        {
            return Result(Status::disallowed, STR_CANT_BUILD_THIS_HERE, STR_CAN_ONLY_BUILD_THIS_ABOVE_GROUND);
        }

        if (!(edgeSlope & (EDGE_SLOPE_UPWARDS | EDGE_SLOPE_DOWNWARDS)))
        {
            uint8_t newEdge = (_edge + 2) & 3;
            uint8_t newBaseHeight = surfaceElement->baseHeight;
            newBaseHeight += 2;
            if (surfaceElement->getSlope() & (1 << newEdge))
            {
                if (targetHeight / 8 < newBaseHeight && !gameState.cheats.disableClearanceChecks)
                {
                    return Result(Status::disallowed, STR_CANT_BUILD_THIS_HERE, STR_CAN_ONLY_BUILD_THIS_ABOVE_GROUND);
                }

                if (surfaceElement->getSlope() & kTileSlopeDiagonalFlag)
                {
                    newEdge = (newEdge - 1) & 3;

                    if (surfaceElement->getSlope() & (1 << newEdge))
                    {
                        newEdge = (newEdge + 2) & 3;
                        if (surfaceElement->getSlope() & (1 << newEdge))
                        {
                            newBaseHeight += 2;
                            if (targetHeight / 8 < newBaseHeight && !gameState.cheats.disableClearanceChecks)
                            {
                                return Result(
                                    Status::disallowed, STR_CANT_BUILD_THIS_HERE, STR_CAN_ONLY_BUILD_THIS_ABOVE_GROUND);
                            }
                            newBaseHeight -= 2;
                        }
                    }
                }
            }

            newEdge = (_edge + 3) & 3;
            if (surfaceElement->getSlope() & (1 << newEdge))
            {
                if (targetHeight / 8 < newBaseHeight && !gameState.cheats.disableClearanceChecks)
                {
                    return Result(Status::disallowed, STR_CANT_BUILD_THIS_HERE, STR_CAN_ONLY_BUILD_THIS_ABOVE_GROUND);
                }

                if (surfaceElement->getSlope() & kTileSlopeDiagonalFlag)
                {
                    newEdge = (newEdge - 1) & 3;

                    if (surfaceElement->getSlope() & (1 << newEdge))
                    {
                        newEdge = (newEdge + 2) & 3;
                        if (surfaceElement->getSlope() & (1 << newEdge))
                        {
                            newBaseHeight += 2;
                            if (targetHeight / 8 < newBaseHeight && !gameState.cheats.disableClearanceChecks)
                            {
                                return Result(
                                    Status::disallowed, STR_CANT_BUILD_THIS_HERE, STR_CAN_ONLY_BUILD_THIS_ABOVE_GROUND);
                            }
                        }
                    }
                }
            }
        }

        auto* wallEntry = ObjectEntryManager::GetObjectEntry<WallSceneryEntry>(_wallType);

        if (wallEntry == nullptr)
        {
            LOG_ERROR("Wall Type not found %d", _wallType);
            return Result(Status::invalidParameters, STR_CANT_BUILD_THIS_HERE, STR_UNKNOWN_OBJECT_TYPE);
        }

        if (wallEntry->scrolling_mode != kScrollingModeNone)
        {
            if (HasReachedBannerLimit())
            {
                LOG_ERROR("No free banners available");
                return Result(Status::invalidParameters, STR_CANT_BUILD_THIS_HERE, STR_TOO_MANY_BANNERS_IN_GAME);
            }
        }

        uint8_t clearanceHeight = targetHeight / 8;
        if (edgeSlope & (EDGE_SLOPE_UPWARDS | EDGE_SLOPE_DOWNWARDS))
        {
            if (wallEntry->flags.has(WallSceneryFlag::cannotBuildOnSlope))
            {
                return Result(Status::disallowed, STR_CANT_BUILD_THIS_HERE, STR_ERR_UNABLE_TO_BUILD_THIS_ON_SLOPE);
            }
            clearanceHeight += 2;
        }
        clearanceHeight += wallEntry->height;

        bool wallAcrossTrack = false;
        if (!GetFlags().has(CommandFlag::trackDesign) && !gameState.cheats.disableClearanceChecks)
        {
            auto result = WallCheckObstruction(wallEntry, targetHeight / 8, clearanceHeight, &wallAcrossTrack);
            if (result.error != Status::ok)
            {
                return result;
            }
        }

        if (!MapCheckCapacityAndReorganise(_loc))
        {
            return Result(Status::noFreeElements, STR_CANT_BUILD_THIS_HERE, STR_TILE_ELEMENT_LIMIT_REACHED);
        }

        res.cost = wallEntry->price;

        res.setData(WallPlaceActionResult{ targetHeight });

        return res;
    }

    Result WallPlaceAction::Execute(GameState_t& gameState, Park::ParkData& park) const
    {
        auto res = Result();
        res.errorTitle = STR_CANT_BUILD_THIS_HERE;
        res.position = _loc;

        res.expenditure = ExpenditureType::landscaping;
        res.position.x += 16;
        res.position.y += 16;

        uint8_t edgeSlope = 0;
        auto targetHeight = _loc.z;
        if (targetHeight == 0)
        {
            auto* surfaceElement = MapGetSurfaceElementAt(_loc);
            if (surfaceElement == nullptr)
            {
                LOG_ERROR("Surface element not found at %d, %d.", _loc.x, _loc.y);
                return Result(Status::invalidParameters, STR_CANT_BUILD_THIS_HERE, STR_ERR_SURFACE_ELEMENT_NOT_FOUND);
            }
            targetHeight = surfaceElement->getBaseZ();

            uint8_t slope = surfaceElement->getSlope();
            edgeSlope = GetWallSlopeFromEdgeSlope(slope, _edge & 3);
            if (edgeSlope & EDGE_SLOPE_ELEVATED)
            {
                targetHeight += 16;
                edgeSlope &= ~EDGE_SLOPE_ELEVATED;
            }
        }
        res.position.z = targetHeight;
        auto targetLoc = CoordsXYZ(_loc, targetHeight);

        auto* wallEntry = ObjectEntryManager::GetObjectEntry<WallSceneryEntry>(_wallType);

        if (wallEntry == nullptr)
        {
            LOG_ERROR("Wall Type not found %d", _wallType);
            return Result(Status::invalidParameters, STR_CANT_BUILD_THIS_HERE, STR_UNKNOWN_OBJECT_TYPE);
        }

        uint8_t clearanceHeight = targetHeight / kCoordsZStep;
        if (edgeSlope & (EDGE_SLOPE_UPWARDS | EDGE_SLOPE_DOWNWARDS))
        {
            clearanceHeight += 2;
        }
        clearanceHeight += wallEntry->height;

        bool wallAcrossTrack = false;
        if (!GetFlags().has(CommandFlag::trackDesign) && !gameState.cheats.disableClearanceChecks)
        {
            auto result = WallCheckObstruction(wallEntry, targetHeight / kCoordsZStep, clearanceHeight, &wallAcrossTrack);
            if (result.error != Status::ok)
            {
                return result;
            }
        }

        Banner* banner = nullptr;
        if (wallEntry->scrolling_mode != kScrollingModeNone)
        {
            banner = CreateBanner();
            if (banner == nullptr)
            {
                LOG_ERROR("No free banners available");
                return Result(Status::invalidParameters, STR_CANT_BUILD_THIS_HERE, STR_TOO_MANY_BANNERS_IN_GAME);
            }

            banner->text = {};
            banner->colour = Drawing::Colour::white;
            banner->textColour = Drawing::TextColour::white;
            banner->flags = { BannerFlag::isWall };
            banner->type = 0; // Banner must be deleted after this point in an early return
            banner->position = TileCoordsXY(_loc);

            RideId rideIndex = BannerGetClosestRideIndex(targetLoc);
            if (!rideIndex.IsNull())
            {
                banner->rideIndex = rideIndex;
                banner->flags.set(BannerFlag::linkedToRide);
            }
        }

        auto* wallElement = TileElementInsert<WallElement>(targetLoc, 0b0000);
        if (wallElement == nullptr)
        {
            return Result(Status::noFreeElements, STR_CANT_POSITION_THIS_HERE, STR_TILE_ELEMENT_LIMIT_REACHED);
        }

        wallElement->clearanceHeight = clearanceHeight;
        wallElement->setDirection(_edge);
        wallElement->setSlope(edgeSlope);

        wallElement->setPrimaryColour(_primaryColour);
        wallElement->setSecondaryColour(_secondaryColour);
        wallElement->setAcrossTrack(wallAcrossTrack);

        wallElement->setEntryIndex(_wallType);
        wallElement->setBannerIndex(banner != nullptr ? banner->id : BannerIndex::GetNull());

        if (wallEntry->flags.has(WallSceneryFlag::hasTertiaryColour))
        {
            wallElement->setTertiaryColour(_tertiaryColour);
        }

        wallElement->setGhost(GetFlags().has(CommandFlag::ghost));

        MapAnimations::MarkTileForInvalidation(TileCoordsXY(targetLoc));
        MapInvalidateTileZoom1({ _loc, wallElement->getBaseZ(), wallElement->getBaseZ() + 72 });

        res.cost = wallEntry->price;

        const auto bannerId = banner != nullptr ? banner->id : BannerIndex::GetNull();
        res.setData(WallPlaceActionResult{ wallElement->getBaseZ(), bannerId });

        return res;
    }

    /**
     *
     *  rct2: 0x006E5CBA
     */
    bool WallPlaceAction::WallCheckObstructionWithTrack(
        const WallSceneryEntry* wall, int32_t z0, TrackElement* trackElement, bool* wallAcrossTrack) const
    {
        TrackElemType trackType = trackElement->getTrackType();

        using namespace OpenRCT2::TrackMetadata;
        const auto& ted = GetTrackElementDescriptor(trackType);
        int32_t sequence = trackElement->getSequenceIndex();
        int32_t direction = (_edge - trackElement->getDirection()) & kTileElementDirectionMask;
        auto ride = GetRide(trackElement->getRideIndex());
        if (ride == nullptr)
        {
            return false;
        }

        if (TrackIsAllowedWallEdges(ride->type, trackType, sequence, direction))
        {
            return true;
        }

        if (!(wall->flags.has(WallSceneryFlag::isDoor)))
        {
            return false;
        }

        if (!ride->getRideTypeDescriptor().flags.has(RtdFlag::allowDoorsOnTrack))
        {
            return false;
        }

        *wallAcrossTrack = true;
        if (z0 & 1)
        {
            return false;
        }

        int32_t z;
        if (sequence == 0)
        {
            if (ted.sequenceData.sequences[0].flags.has(SequenceFlag::disallowDoors))
            {
                return false;
            }

            if (ted.definition.rollStart == TrackRoll::none)
            {
                if (!(ted.coordinates.rotationBegin & 4))
                {
                    direction = DirectionReverse(trackElement->getDirection());
                    if (direction == _edge)
                    {
                        z = ted.coordinates.zBegin;
                        z = trackElement->baseHeight + ((z - ted.sequenceData.sequences[sequence].clearance.z) * 8);
                        if (z == z0)
                        {
                            return true;
                        }
                    }
                }
            }
        }

        bool isLastInSequence = (sequence + 1) == ted.sequenceData.numSequences;
        if (!isLastInSequence)
        {
            return false;
        }

        if (ted.definition.rollEnd != TrackRoll::none)
        {
            return false;
        }

        direction = ted.coordinates.rotationEnd;
        if (direction & 4)
        {
            return false;
        }

        direction = (trackElement->getDirection() + ted.coordinates.rotationEnd) & kTileElementDirectionMask;
        if (direction != _edge)
        {
            return false;
        }

        z = ted.coordinates.zEnd;
        z = trackElement->baseHeight + ((z - ted.sequenceData.sequences[sequence].clearance.z) * kCoordsZStep);
        return z == z0;
    }

    /**
     *
     *  rct2: 0x006E5C1A
     */
    Result WallPlaceAction::WallCheckObstruction(
        const WallSceneryEntry* wall, int32_t z0, int32_t z1, bool* wallAcrossTrack) const
    {
        *wallAcrossTrack = false;
        if (MapIsEdge(_loc))
        {
            return Result(Status::invalidParameters, STR_CANT_BUILD_THIS_HERE, STR_OFF_EDGE_OF_MAP);
        }

        for (auto* tileElement : TileElementsView(_loc))
        {
            if (tileElement == nullptr)
                break;
            auto elementType = tileElement->getType();
            if (elementType == TileElementType::surface)
                continue;
            if (tileElement->isGhost())
                continue;
            if (z0 >= tileElement->clearanceHeight)
                continue;
            if (z1 <= tileElement->baseHeight)
                continue;
            if (elementType == TileElementType::wall)
            {
                int32_t direction = tileElement->getDirection();
                if (_edge == direction)
                {
                    auto res = Result(Status::noClearance, STR_CANT_BUILD_THIS_HERE, kStringIdNone);
                    MapGetObstructionErrorText(tileElement, res);
                    return res;
                }
                continue;
            }
            if (tileElement->getOccupiedQuadrants() == 0)
                continue;
            auto res = Result(Status::noClearance, STR_CANT_BUILD_THIS_HERE, kStringIdNone);
            switch (elementType)
            {
                case TileElementType::entrance:
                    MapGetObstructionErrorText(tileElement, res);
                    return res;
                case TileElementType::path:
                    if (tileElement->asPath()->getEdges() & (1 << _edge))
                    {
                        MapGetObstructionErrorText(tileElement, res);
                        return res;
                    }
                    break;
                case TileElementType::largeScenery:
                {
                    const auto* largeSceneryElement = tileElement->asLargeScenery();
                    const auto* sceneryEntry = largeSceneryElement->getEntry();

                    // If there is no entry, assume the object is not in the way.
                    if (sceneryEntry == nullptr)
                        break;

                    auto sequence = largeSceneryElement->getSequenceIndex();
                    const LargeSceneryTile& tile = sceneryEntry->tiles[sequence];

                    int32_t direction = ((_edge - tileElement->getDirection()) & kTileElementDirectionMask);
                    if (!(tile.walls & (1 << direction)))
                    {
                        MapGetObstructionErrorText(tileElement, res);
                        return res;
                    }
                    break;
                }
                case TileElementType::smallScenery:
                {
                    auto sceneryEntry = tileElement->asSmallScenery()->getEntry();
                    if (sceneryEntry != nullptr && sceneryEntry->flags.has(SmallSceneryFlag::prohibitWalls))
                    {
                        MapGetObstructionErrorText(tileElement, res);
                        return res;
                    }
                    break;
                }
                case TileElementType::track:
                    if (!WallCheckObstructionWithTrack(wall, z0, tileElement->asTrack(), wallAcrossTrack))
                    {
                        MapGetObstructionErrorText(tileElement, res);
                        return res;
                    }
                    break;
                default:
                    break;
            }
        }

        return Result();
    }

    bool WallPlaceAction::TrackIsAllowedWallEdges(
        ride_type_t rideType, TrackElemType trackType, uint8_t trackSequence, uint8_t direction)
    {
        if (!GetRideTypeDescriptor(rideType).flags.has(RtdFlag::noWallsAroundTrack))
        {
            const auto& ted = GetTrackElementDescriptor(trackType);
            if (ted.sequenceData.sequences[trackSequence].allowedWallEdges & (1 << direction))
            {
                return true;
            }
        }
        return false;
    }
} // namespace OpenRCT2::GameActions
