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

#include "../../Cheats.h"
#include "../../Diagnostic.h"
#include "../../GameState.h"
#include "../../OpenRCT2.h"
#include "../../localisation/StringIds.h"
#include "../../management/Finance.h"
#include "../../object/ObjectEntryManager.h"
#include "../../object/SmallSceneryEntry.h"
#include "../../ride/TrackDesign.h"
#include "../../world/ConstructionClearance.h"
#include "../../world/Footpath.h"
#include "../../world/Map.h"
#include "../../world/MapAnimation.h"
#include "../../world/QuarterTile.h"
#include "../../world/Scenery.h"
#include "../../world/Wall.h"
#include "../../world/tile_element/Slope.h"
#include "../../world/tile_element/SmallSceneryElement.h"
#include "../../world/tile_element/SurfaceElement.h"
#include "../GameAction.hpp"

namespace OpenRCT2::GameActions
{
    using namespace OpenRCT2::Numerics;

    SmallSceneryPlaceAction::SmallSceneryPlaceAction(
        const CoordsXYZD& loc, uint8_t quadrant, ObjectEntryIndex sceneryType, Drawing::Colour primaryColour,
        Drawing::Colour secondaryColour, Drawing::Colour tertiaryColour)
        : _loc(loc)
        , _quadrant(quadrant)
        , _sceneryType(sceneryType)
        , _primaryColour(primaryColour)
        , _secondaryColour(secondaryColour)
        , _tertiaryColour(tertiaryColour)
    {
    }

    void SmallSceneryPlaceAction::AcceptParameters(GameActionParameterVisitor& visitor)
    {
        visitor.Visit(_loc);
        visitor.Visit("quadrant", _quadrant);
        visitor.Visit("object", _sceneryType);
        visitor.Visit("primaryColour", _primaryColour);
        visitor.Visit("secondaryColour", _secondaryColour);
        visitor.Visit("tertiaryColour", _tertiaryColour);
    }

    uint32_t SmallSceneryPlaceAction::GetCooldownTime() const
    {
        return 20;
    }

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

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

        stream << DS_TAG(_loc) << DS_TAG(_quadrant) << DS_TAG(_sceneryType) << DS_TAG(_primaryColour)
               << DS_TAG(_secondaryColour) << DS_TAG(_tertiaryColour);
    }

    Result SmallSceneryPlaceAction::Query(GameState_t& gameState, Park::ParkData& park) const
    {
        bool isOnWater = false;
        bool supportsRequired = false;
        if (_loc.z != 0)
        {
            supportsRequired = true;
        }
        int32_t landHeight = TileElementHeight(_loc);
        int16_t waterHeight = TileElementWaterHeight(_loc);

        int32_t surfaceHeight = landHeight;
        // If on water
        if (waterHeight > 0)
        {
            surfaceHeight = waterHeight;
        }
        auto res = Result();
        auto centre = _loc.ToTileCentre();
        res.position.x = centre.x;
        res.position.y = centre.y;
        res.position.z = surfaceHeight;
        if (_loc.z != 0)
        {
            surfaceHeight = _loc.z;
        }

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

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

        auto maxSizeMax = GetMapSizeMaxXY();
        if (!_trackDesignDrawingPreview && (_loc.x > maxSizeMax.x || _loc.y > maxSizeMax.y))
        {
            return Result(Status::invalidParameters, STR_CANT_POSITION_THIS_HERE, STR_ERR_VALUE_OUT_OF_RANGE);
        }

        auto* sceneryEntry = ObjectEntryManager::GetObjectEntry<SmallSceneryEntry>(_sceneryType);
        if (sceneryEntry == nullptr)
        {
            LOG_ERROR("Small scenery object entry not found for sceneryType %u", _sceneryType);
            return Result(Status::invalidParameters, STR_CANT_POSITION_THIS_HERE, STR_UNKNOWN_OBJECT_TYPE);
        }

        auto quadrant = _quadrant;
        if (sceneryEntry->flags.has(SmallSceneryFlag::occupiesFullTile)
            || !sceneryEntry->flags.has(SmallSceneryFlag::isDiagonal))
        {
            if (sceneryEntry->flags.hasAny(
                    SmallSceneryFlag::isDiagonal, SmallSceneryFlag::occupiesHalfTile, SmallSceneryFlag::occupiesThreeQuarters))
            {
                quadrant = 0;
            }
        }

        // Check if sub tile height is any different compared to actual surface tile height
        auto loc2 = _loc;
        if (sceneryEntry->flags.has(SmallSceneryFlag::occupiesFullTile))
        {
            loc2 = loc2.ToTileCentre();
        }
        else
        {
            loc2.x += SceneryQuadrantOffsets[quadrant & 3].x;
            loc2.y += SceneryQuadrantOffsets[quadrant & 3].y;
        }
        landHeight = TileElementHeight(loc2);
        waterHeight = TileElementWaterHeight(loc2);

        surfaceHeight = landHeight;
        // If on water (land at this quadrant is below water level)
        if (waterHeight > 0 && landHeight < waterHeight)
        {
            // surfaceHeight is now the water height
            surfaceHeight = waterHeight;
            if (_loc.z == 0)
            {
                isOnWater = true;
            }
        }
        auto targetHeight = _loc.z;
        if (_loc.z == 0)
        {
            targetHeight = surfaceHeight;
            res.position.z = targetHeight;
        }

        if (gLegacyScene != LegacyScene::scenarioEditor && !gameState.cheats.sandboxMode
            && !MapIsLocationOwned({ _loc.x, _loc.y, targetHeight }))
        {
            return Result(Status::notOwned, STR_CANT_POSITION_THIS_HERE, STR_LAND_NOT_OWNED_BY_PARK);
        }

        auto* surfaceElement = MapGetSurfaceElementAt(_loc);

        if (surfaceElement != nullptr && !gameState.cheats.disableClearanceChecks && surfaceElement->getWaterHeight() > 0)
        {
            int32_t water_height = surfaceElement->getWaterHeight() - 1;
            if (water_height > targetHeight)
            {
                return Result(Status::disallowed, STR_CANT_POSITION_THIS_HERE, STR_CANT_BUILD_THIS_UNDERWATER);
            }
        }

        if (!gameState.cheats.disableClearanceChecks && !sceneryEntry->flags.has(SmallSceneryFlag::isStackable))
        {
            if (isOnWater)
            {
                return Result(Status::disallowed, STR_CANT_POSITION_THIS_HERE, STR_CAN_ONLY_BUILD_THIS_ON_LAND);
            }

            if (surfaceElement != nullptr && surfaceElement->getWaterHeight() > 0)
            {
                if (surfaceElement->getWaterHeight() > targetHeight)
                {
                    return Result(Status::disallowed, STR_CANT_POSITION_THIS_HERE, STR_CAN_ONLY_BUILD_THIS_ON_LAND);
                }
            }
        }

        if (!gameState.cheats.disableClearanceChecks && sceneryEntry->flags.has(SmallSceneryFlag::requiresFlatSurface)
            && !supportsRequired && surfaceElement != nullptr && surfaceElement->getWaterHeight() == 0
            && (surfaceElement->getSlope() != kTileSlopeFlat))
        {
            return Result(Status::disallowed, STR_CANT_POSITION_THIS_HERE, STR_LEVEL_LAND_REQUIRED);
        }

        if (!gameState.cheats.disableSupportLimits && !sceneryEntry->flags.has(SmallSceneryFlag::isStackable)
            && supportsRequired)
        {
            if (!isOnWater)
            {
                if (surfaceElement != nullptr)
                {
                    if (surfaceElement->getWaterHeight() > 0 || (surfaceElement->getBaseZ()) != targetHeight)
                    {
                        return Result(Status::disallowed, STR_CANT_POSITION_THIS_HERE, STR_LEVEL_LAND_REQUIRED);
                    }
                }
            }
            else
            {
                return Result(Status::disallowed, STR_CANT_POSITION_THIS_HERE, STR_CAN_ONLY_BUILD_THIS_ON_LAND);
            }
        }

        int32_t zLow = targetHeight;
        int32_t zHigh = zLow + ceil2(sceneryEntry->height, kCoordsZStep);
        uint8_t collisionQuadrants = 0b1111;
        auto quadRotation{ 0 };
        if (!sceneryEntry->flags.has(SmallSceneryFlag::occupiesFullTile))
        {
            quadRotation = (quadrant ^ 2);
            collisionQuadrants = 0b0001;
        }
        if (!sceneryEntry->flags.has(SmallSceneryFlag::occupiesHalfTile))
        {
            if (sceneryEntry->flags.hasAll(SmallSceneryFlag::isDiagonal, SmallSceneryFlag::occupiesFullTile))
            {
                if (sceneryEntry->flags.has(SmallSceneryFlag::occupiesThreeQuarters))
                {
                    quadRotation = ((quadrant ^ 2) + _loc.direction) & 3;
                    collisionQuadrants = 0b1011;
                }
                else
                {
                    quadRotation = (quadrant + _loc.direction) & 1;
                    collisionQuadrants = 0b1010;
                }
            }
        }
        else
        {
            quadRotation = ((quadrant ^ 2) + _loc.direction) & 3;
            collisionQuadrants = 0b0011;
        }
        uint8_t supports = 0;
        if (!supportsRequired)
        {
            supports = 0b1111;
        }

        QuarterTile quarterTile = QuarterTile{ collisionQuadrants, supports }.Rotate(quadRotation);
        const auto isTree = sceneryEntry->flags.has(SmallSceneryFlag::isTree);
        auto canBuild = MapCanConstructWithClearAt(
            { _loc, zLow, zHigh }, MapPlaceSceneryClearFunc, quarterTile, GetFlags(), { .isTree = isTree });
        if (canBuild.error != Status::ok)
        {
            canBuild.errorTitle = STR_CANT_POSITION_THIS_HERE;
            return canBuild;
        }

        const auto clearanceData = canBuild.getData<ConstructClearResult>();
        const uint8_t groundFlags = clearanceData.GroundFlags & (ELEMENT_IS_ABOVE_GROUND | ELEMENT_IS_UNDERGROUND);
        res.setData(SmallSceneryPlaceActionResult{ groundFlags, 0, 0 });

        res.expenditure = ExpenditureType::landscaping;
        res.cost = sceneryEntry->price + canBuild.cost;

        return res;
    }

    Result SmallSceneryPlaceAction::Execute(GameState_t& gameState, Park::ParkData& park) const
    {
        bool supportsRequired = false;
        if (_loc.z != 0)
        {
            supportsRequired = true;
        }
        int32_t landHeight = TileElementHeight(_loc);
        int16_t waterHeight = TileElementWaterHeight(_loc);

        int32_t surfaceHeight = landHeight;
        // If on water
        if (waterHeight > 0)
        {
            surfaceHeight = waterHeight;
        }
        auto res = Result();
        auto centre = _loc.ToTileCentre();
        res.position.x = centre.x;
        res.position.y = centre.y;
        res.position.z = surfaceHeight;
        if (_loc.z != 0)
        {
            surfaceHeight = _loc.z;
        }

        auto* sceneryEntry = ObjectEntryManager::GetObjectEntry<SmallSceneryEntry>(_sceneryType);
        if (sceneryEntry == nullptr)
        {
            LOG_ERROR("Small scenery object entry not found for sceneryType %u", _sceneryType);
            return Result(Status::invalidParameters, STR_CANT_POSITION_THIS_HERE, STR_UNKNOWN_OBJECT_TYPE);
        }

        auto quadrant = _quadrant;
        if (sceneryEntry->flags.has(SmallSceneryFlag::occupiesFullTile)
            || !sceneryEntry->flags.has(SmallSceneryFlag::isDiagonal))
        {
            if (sceneryEntry->flags.hasAny(
                    SmallSceneryFlag::isDiagonal, SmallSceneryFlag::occupiesHalfTile, SmallSceneryFlag::occupiesThreeQuarters))
            {
                quadrant = 0;
            }
        }

        // Check if sub tile height is any different compared to actual surface tile height
        int32_t x2 = _loc.x;
        int32_t y2 = _loc.y;
        if (sceneryEntry->flags.has(SmallSceneryFlag::occupiesFullTile))
        {
            x2 += 16;
            y2 += 16;
        }
        else
        {
            x2 += SceneryQuadrantOffsets[quadrant & 3].x;
            y2 += SceneryQuadrantOffsets[quadrant & 3].y;
        }
        landHeight = TileElementHeight({ x2, y2 });
        waterHeight = TileElementWaterHeight({ x2, y2 });

        surfaceHeight = landHeight;
        // If on water (land at this quadrant is below water level)
        if (waterHeight > 0 && landHeight < waterHeight)
        {
            // surfaceHeight is now the water height
            surfaceHeight = waterHeight;
        }
        auto targetHeight = _loc.z;
        if (_loc.z == 0)
        {
            targetHeight = surfaceHeight;
            res.position.z = targetHeight;
        }

        if (!GetFlags().has(CommandFlag::ghost))
        {
            FootpathRemoveLitter({ _loc, targetHeight });
            if (!gameState.cheats.disableClearanceChecks && sceneryEntry->flags.has(SmallSceneryFlag::prohibitWalls))
            {
                WallRemoveAt({ _loc, targetHeight, targetHeight + sceneryEntry->height });
            }
        }

        int32_t zLow = targetHeight;
        int32_t zHigh = zLow + ceil2(sceneryEntry->height, 8);
        uint8_t collisionQuadrants = 0b1111;
        auto quadRotation{ 0 };
        if (!sceneryEntry->flags.has(SmallSceneryFlag::occupiesFullTile))
        {
            quadRotation = (quadrant ^ 2);
            collisionQuadrants = 0b0001;
        }
        if (!sceneryEntry->flags.has(SmallSceneryFlag::occupiesHalfTile))
        {
            if (sceneryEntry->flags.hasAll(SmallSceneryFlag::isDiagonal, SmallSceneryFlag::occupiesFullTile))
            {
                if (sceneryEntry->flags.has(SmallSceneryFlag::occupiesThreeQuarters))
                {
                    quadRotation = ((quadrant ^ 2) + _loc.direction) & 3;
                    collisionQuadrants = 0b1011;
                }
                else
                {
                    quadRotation = (quadrant + _loc.direction) & 1;
                    collisionQuadrants = 0b1010;
                }
            }
        }
        else
        {
            quadRotation = ((quadrant ^ 2) + _loc.direction) & 3;
            collisionQuadrants = 0b0011;
        }
        uint8_t supports = 0;
        if (!supportsRequired)
        {
            supports = 0b1111;
        }

        QuarterTile quarterTile = QuarterTile{ collisionQuadrants, supports }.Rotate(quadRotation);
        const auto isTree = sceneryEntry->flags.has(SmallSceneryFlag::isTree);
        auto canBuild = MapCanConstructWithClearAt(
            { _loc, zLow, zHigh }, MapPlaceSceneryClearFunc, quarterTile, GetFlags().with(CommandFlag::apply),
            { .isTree = isTree });
        if (canBuild.error != Status::ok)
        {
            canBuild.errorTitle = STR_CANT_POSITION_THIS_HERE;
            return canBuild;
        }

        res.expenditure = ExpenditureType::landscaping;
        res.cost = sceneryEntry->price + canBuild.cost;

        auto* sceneryElement = TileElementInsert<SmallSceneryElement>(
            CoordsXYZ{ _loc, zLow }, quarterTile.GetBaseQuarterOccupied());
        if (sceneryElement == nullptr)
        {
            return Result(Status::noFreeElements, STR_CANT_POSITION_THIS_HERE, STR_TILE_ELEMENT_LIMIT_REACHED);
        }

        sceneryElement->setDirection(_loc.direction);
        sceneryElement->setSceneryQuadrant(quadrant);
        sceneryElement->setEntryIndex(_sceneryType);
        sceneryElement->setAge(0);
        sceneryElement->setPrimaryColour(_primaryColour);
        sceneryElement->setSecondaryColour(_secondaryColour);
        sceneryElement->setTertiaryColour(_tertiaryColour);
        sceneryElement->setClearanceZ(sceneryElement->getBaseZ() + sceneryEntry->height + 7);
        sceneryElement->setGhost(GetFlags().has(CommandFlag::ghost));
        if (supportsRequired)
        {
            sceneryElement->setNeedsSupports();
        }

        const auto clearanceData = canBuild.getData<ConstructClearResult>();
        const uint8_t groundFlags = clearanceData.GroundFlags & (ELEMENT_IS_ABOVE_GROUND | ELEMENT_IS_UNDERGROUND);
        res.setData(
            SmallSceneryPlaceActionResult{ groundFlags, sceneryElement->getBaseZ(), sceneryElement->getSceneryQuadrant() });

        MapInvalidateTileFull(_loc);
        if (sceneryEntry->flags.has(SmallSceneryFlag::isClock))
        {
            MapAnimations::MarkTileForUpdate(TileCoordsXY(_loc));
        }
        else if (sceneryEntry->flags.has(SmallSceneryFlag::isAnimated))
        {
            MapAnimations::MarkTileForInvalidation(TileCoordsXY(_loc));
        }

        return res;
    }
} // namespace OpenRCT2::GameActions
