/*****************************************************************************
 * Copyright (c) 2014-2025 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 "LandSmoothAction.h"

#include "../Context.h"
#include "../Diagnostic.h"
#include "../OpenRCT2.h"
#include "../actions/LandLowerAction.h"
#include "../actions/LandRaiseAction.h"
#include "../actions/LandSetHeightAction.h"
#include "../audio/Audio.h"
#include "../localisation/StringIds.h"
#include "../management/Finance.h"
#include "../ride/RideData.h"
#include "../windows/Intent.h"
#include "../world/Park.h"
#include "../world/Scenery.h"
#include "../world/SurfaceData.h"
#include "../world/tile_element/Slope.h"
#include "../world/tile_element/SurfaceElement.h"

#include <algorithm>

namespace OpenRCT2::GameActions
{
    LandSmoothAction::LandSmoothAction(const CoordsXY& coords, MapRange range, uint8_t selectionType, bool isLowering)
        : _coords(coords)
        , _range(range)
        , _selectionType(selectionType)
        , _isLowering(isLowering)
    {
    }

    void LandSmoothAction::AcceptParameters(GameActionParameterVisitor& visitor)
    {
        visitor.Visit(_coords);
        visitor.Visit(_range);
        visitor.Visit("selectionType", _selectionType);
        visitor.Visit("isLowering", _isLowering);
    }

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

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

        stream << DS_TAG(_coords) << DS_TAG(_range) << DS_TAG(_selectionType) << DS_TAG(_isLowering);
    }

    Result LandSmoothAction::Query() const
    {
        return SmoothLand(false);
    }

    Result LandSmoothAction::Execute() const
    {
        return SmoothLand(true);
    }

    Result LandSmoothAction::SmoothLandTile(
        int32_t direction, bool isExecuting, const CoordsXY& loc, SurfaceElement* surfaceElement) const
    {
        int32_t targetBaseZ = surfaceElement->BaseHeight;
        int32_t slope = surfaceElement->GetSlope();
        if (_isLowering)
        {
            slope = LowerSurfaceCornerFlags(direction, slope);
            if (slope & kTileSlopeRaiseOrLowerBaseHeight)
            {
                targetBaseZ -= 2;
                slope &= ~kTileSlopeRaiseOrLowerBaseHeight;
            }
        }
        else
        {
            slope = RaiseSurfaceCornerFlags(direction, slope);
            if (slope & kTileSlopeRaiseOrLowerBaseHeight)
            {
                targetBaseZ += 2;
                slope &= ~kTileSlopeRaiseOrLowerBaseHeight;
            }
        }

        auto landSetHeightAction = LandSetHeightAction(loc, targetBaseZ, slope);
        landSetHeightAction.SetFlags(GetFlags());
        auto res = isExecuting ? ExecuteNested(&landSetHeightAction) : QueryNested(&landSetHeightAction);

        return res;
    }

    money64 LandSmoothAction::SmoothLandRowByEdge(
        bool isExecuting, const CoordsXY& loc, int32_t expectedLandHeight1, int32_t expectedLandHeight2, int32_t stepX,
        int32_t stepY, int32_t direction1, int32_t direction2, int32_t checkDirection1, int32_t checkDirection2) const
    {
        uint8_t shouldContinue = 0xF;
        int32_t landChangePerTile = _isLowering ? 2 : -2;
        money64 totalCost = 0;

        // check if we need to start at all
        if (!LocationValid(loc) || !LocationValid({ loc.x + stepX, loc.y + stepY }))
        {
            return 0;
        }
        auto surfaceElement = MapGetSurfaceElementAt(loc);
        auto nextSurfaceElement = MapGetSurfaceElementAt(CoordsXY{ loc.x + stepX, loc.y + stepY });
        if (surfaceElement == nullptr || nextSurfaceElement == nullptr)
        {
            return 0;
        }
        if (TileElementGetCornerHeight(surfaceElement, checkDirection1) != expectedLandHeight1 + landChangePerTile)
        {
            shouldContinue &= ~0x1;
        }
        if (TileElementGetCornerHeight(surfaceElement, checkDirection2) != expectedLandHeight2 + landChangePerTile)
        {
            shouldContinue &= ~0x2;
        }
        if (TileElementGetCornerHeight(surfaceElement, checkDirection1)
            != TileElementGetCornerHeight(nextSurfaceElement, direction1))
        {
            shouldContinue &= ~0x1;
        }
        if (TileElementGetCornerHeight(surfaceElement, checkDirection2)
            != TileElementGetCornerHeight(nextSurfaceElement, direction2))
        {
            shouldContinue &= ~0x2;
        }
        auto nextLoc = loc;
        while ((shouldContinue & 0x3) != 0)
        {
            shouldContinue = ((shouldContinue << 2) | 0x3) & shouldContinue;
            nextLoc.x += stepX;
            nextLoc.y += stepY;
            // check if we need to continue after raising the current tile
            // this needs to be checked before the tile is changed
            if (!LocationValid({ nextLoc.x + stepX, nextLoc.y + stepY }))
            {
                shouldContinue &= ~0x3;
            }
            else
            {
                surfaceElement = nextSurfaceElement;
                nextSurfaceElement = MapGetSurfaceElementAt(CoordsXY{ nextLoc.x + stepX, nextLoc.y + stepY });
                if (nextSurfaceElement == nullptr)
                {
                    shouldContinue &= ~0x3;
                }
                if (TileElementGetCornerHeight(surfaceElement, direction1) + landChangePerTile
                    != TileElementGetCornerHeight(surfaceElement, checkDirection1))
                {
                    shouldContinue &= ~0x1;
                }
                if (TileElementGetCornerHeight(surfaceElement, direction2) + landChangePerTile
                    != TileElementGetCornerHeight(surfaceElement, checkDirection2))
                {
                    shouldContinue &= ~0x2;
                }
                if ((shouldContinue & 0x1)
                    && TileElementGetCornerHeight(surfaceElement, checkDirection1)
                        != TileElementGetCornerHeight(nextSurfaceElement, direction1))
                {
                    shouldContinue &= ~0x1;
                }
                if ((shouldContinue & 0x2)
                    && TileElementGetCornerHeight(surfaceElement, checkDirection2)
                        != TileElementGetCornerHeight(nextSurfaceElement, direction2))
                {
                    shouldContinue &= ~0x2;
                }
            }
            expectedLandHeight1 += landChangePerTile;

            // change land of current tile
            int32_t targetBaseZ = surfaceElement->BaseHeight;
            int32_t slope = surfaceElement->GetSlope();
            int32_t oldSlope = slope;
            if (_isLowering)
            {
                if (shouldContinue & 0x4)
                {
                    slope = LowerSurfaceCornerFlags(direction1, slope);
                    if (slope & kTileSlopeRaiseOrLowerBaseHeight)
                    {
                        targetBaseZ -= 2;
                        slope &= ~kTileSlopeRaiseOrLowerBaseHeight;
                    }
                }
                if ((shouldContinue & 0x8)
                    && MapGetCornerHeight(surfaceElement->BaseHeight, oldSlope, direction2)
                        == MapGetCornerHeight(targetBaseZ, slope, direction2))
                {
                    slope = LowerSurfaceCornerFlags(direction2, slope);
                    if (slope & kTileSlopeRaiseOrLowerBaseHeight)
                    {
                        targetBaseZ -= 2;
                        slope &= ~kTileSlopeRaiseOrLowerBaseHeight;
                    }
                }
            }
            else
            {
                if (shouldContinue & 0x4)
                {
                    slope = RaiseSurfaceCornerFlags(direction1, slope);
                    if (slope & kTileSlopeRaiseOrLowerBaseHeight)
                    {
                        targetBaseZ += 2;
                        slope &= ~kTileSlopeRaiseOrLowerBaseHeight;
                    }
                }
                if ((shouldContinue & 0x8)
                    && MapGetCornerHeight(surfaceElement->BaseHeight, oldSlope, direction2)
                        == MapGetCornerHeight(targetBaseZ, slope, direction2))
                {
                    slope = RaiseSurfaceCornerFlags(direction2, slope);
                    if (slope & kTileSlopeRaiseOrLowerBaseHeight)
                    {
                        targetBaseZ += 2;
                        slope &= ~kTileSlopeRaiseOrLowerBaseHeight;
                    }
                }
            }
            auto landSetHeightAction = LandSetHeightAction(nextLoc, targetBaseZ, slope);
            landSetHeightAction.SetFlags(GetFlags());
            auto res = isExecuting ? ExecuteNested(&landSetHeightAction) : QueryNested(&landSetHeightAction);
            if (res.Error == Status::Ok)
            {
                totalCost += res.Cost;
            }
        }
        return totalCost;
    }

    money64 LandSmoothAction::SmoothLandRowByCorner(
        bool isExecuting, const CoordsXY& loc, int32_t expectedLandHeight, int32_t stepX, int32_t stepY, int32_t direction,
        int32_t checkDirection) const
    {
        bool shouldContinue = true;
        money64 totalCost = 0;
        int32_t landChangePerTile;
        if (stepX == 0 || stepY == 0)
        {
            landChangePerTile = _isLowering ? 2 : -2;
        }
        else
        {
            landChangePerTile = _isLowering ? 4 : -4;
        }

        // check if we need to start at all
        if (!LocationValid(loc) || !LocationValid({ loc.x + stepX, loc.y + stepY }))
        {
            return 0;
        }
        auto surfaceElement = MapGetSurfaceElementAt(loc);
        auto nextSurfaceElement = MapGetSurfaceElementAt(CoordsXY{ loc.x + stepX, loc.y + stepY });
        if (surfaceElement == nullptr || nextSurfaceElement == nullptr)
        {
            return 0;
        }
        if (TileElementGetCornerHeight(surfaceElement, checkDirection) != expectedLandHeight + (_isLowering ? 2 : -2))
        {
            return 0;
        }
        if (TileElementGetCornerHeight(surfaceElement, checkDirection)
            != TileElementGetCornerHeight(nextSurfaceElement, direction))
        {
            return 0;
        }

        auto nextLoc = loc;
        while (shouldContinue)
        {
            nextLoc.x += stepX;
            nextLoc.y += stepY;
            // check if we need to continue after raising the current tile
            // this needs to be checked before the tile is changed
            if (!LocationValid({ nextLoc.x + stepX, nextLoc.y + stepY }))
            {
                shouldContinue = false;
            }
            else
            {
                surfaceElement = nextSurfaceElement;
                nextSurfaceElement = MapGetSurfaceElementAt(CoordsXY{ nextLoc.x + stepX, nextLoc.y + stepY });
                if (nextSurfaceElement == nullptr)
                {
                    shouldContinue = false;
                }
                if (TileElementGetCornerHeight(surfaceElement, direction) + landChangePerTile
                    != TileElementGetCornerHeight(surfaceElement, checkDirection))
                {
                    shouldContinue = false;
                }
                if (shouldContinue
                    && TileElementGetCornerHeight(surfaceElement, checkDirection)
                        != TileElementGetCornerHeight(nextSurfaceElement, direction))
                {
                    shouldContinue = false;
                }
            }
            if (stepX * stepY != 0)
            {
                totalCost += SmoothLandRowByCorner(
                    isExecuting, nextLoc, expectedLandHeight + (landChangePerTile / 2), 0, stepY, direction,
                    checkDirection ^ 3);
                totalCost += SmoothLandRowByCorner(
                    isExecuting, nextLoc, expectedLandHeight + (landChangePerTile / 2), stepX, 0, direction,
                    checkDirection ^ 1);
            }
            expectedLandHeight += landChangePerTile;
            // change land of current tile
            auto result = SmoothLandTile(direction, isExecuting, nextLoc, surfaceElement);
            if (result.Error == Status::Ok)
            {
                totalCost += result.Cost;
            }
        }
        return totalCost;
    }

    Result LandSmoothAction::SmoothLand(bool isExecuting) const
    {
        const bool raiseLand = !_isLowering;
        const int32_t selectionType = _selectionType;
        const int32_t heightOffset = raiseLand ? 2 : -2;

        auto normRange = _range.Normalise();
        // Cap bounds to map
        auto l = std::max(normRange.GetLeft(), 32);
        auto t = std::max(normRange.GetTop(), 32);
        auto r = std::clamp(normRange.GetRight(), 0, kMaximumTileStartXY);
        auto b = std::clamp(normRange.GetBottom(), 0, kMaximumTileStartXY);
        auto validRange = MapRange{ l, t, r, b };

        int32_t centreZ = TileElementHeight(_coords);

        auto res = Result();
        res.ErrorTitle = kErrorTitles[_isLowering ? 0 : 1];
        res.Expenditure = ExpenditureType::landscaping;
        res.Position = { _coords.x, _coords.y, centreZ };

        // Do the smoothing
        switch (selectionType)
        {
            case MAP_SELECT_TYPE_FULL:
            {
                uint8_t minHeight = heightOffset + MapGetLowestLandHeight(validRange);
                uint8_t maxHeight = heightOffset + MapGetHighestLandHeight(validRange);

                // Smooth the 4 corners
                { // top-left
                    auto surfaceElement = MapGetSurfaceElementAt(CoordsXY{ validRange.GetLeft(), validRange.GetTop() });
                    if (surfaceElement != nullptr)
                    {
                        int32_t z = std::clamp(
                            static_cast<uint8_t>(TileElementGetCornerHeight(surfaceElement, 2)), minHeight, maxHeight);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, -32, 0, 2);
                    }
                }
                { // bottom-left
                    auto surfaceElement = MapGetSurfaceElementAt(CoordsXY{ validRange.GetLeft(), validRange.GetBottom() });
                    if (surfaceElement != nullptr)
                    {
                        int32_t z = std::clamp(
                            static_cast<uint8_t>(TileElementGetCornerHeight(surfaceElement, 3)), minHeight, maxHeight);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetBottom() }, z, -32, 32, 1, 3);
                    }
                }
                { // bottom-right
                    auto surfaceElement = MapGetSurfaceElementAt(CoordsXY{ validRange.GetRight(), validRange.GetBottom() });
                    if (surfaceElement != nullptr)
                    {
                        int32_t z = std::clamp(
                            static_cast<uint8_t>(TileElementGetCornerHeight(surfaceElement, 0)), minHeight, maxHeight);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetRight(), validRange.GetBottom() }, z, 32, 32, 2, 0);
                    }
                }
                { // top-right
                    auto surfaceElement = MapGetSurfaceElementAt(CoordsXY{ validRange.GetRight(), validRange.GetTop() });
                    if (surfaceElement != nullptr)
                    {
                        int32_t z = std::clamp(
                            static_cast<uint8_t>(TileElementGetCornerHeight(surfaceElement, 1)), minHeight, maxHeight);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetRight(), validRange.GetTop() }, z, 32, -32, 3, 1);
                    }
                }

                // Smooth the edges
                int32_t z1, z2;
                for (int32_t y = validRange.GetTop(); y <= validRange.GetBottom(); y += kCoordsXYStep)
                {
                    auto surfaceElement = MapGetSurfaceElementAt(CoordsXY{ validRange.GetLeft(), y });
                    if (surfaceElement != nullptr)
                    {
                        z1 = std::clamp(
                            static_cast<uint8_t>(TileElementGetCornerHeight(surfaceElement, 3)), minHeight, maxHeight);
                        z2 = std::clamp(
                            static_cast<uint8_t>(TileElementGetCornerHeight(surfaceElement, 2)), minHeight, maxHeight);
                        res.Cost += SmoothLandRowByEdge(isExecuting, { validRange.GetLeft(), y }, z1, z2, -32, 0, 0, 1, 3, 2);
                    }

                    surfaceElement = MapGetSurfaceElementAt(CoordsXY{ validRange.GetRight(), y });
                    if (surfaceElement != nullptr)
                    {
                        z1 = std::clamp(
                            static_cast<uint8_t>(TileElementGetCornerHeight(surfaceElement, 1)), minHeight, maxHeight);
                        z2 = std::clamp(
                            static_cast<uint8_t>(TileElementGetCornerHeight(surfaceElement, 0)), minHeight, maxHeight);
                        res.Cost += SmoothLandRowByEdge(isExecuting, { validRange.GetRight(), y }, z1, z2, 32, 0, 2, 3, 1, 0);
                    }
                }

                for (int32_t x = validRange.GetLeft(); x <= validRange.GetRight(); x += kCoordsXYStep)
                {
                    auto surfaceElement = MapGetSurfaceElementAt(CoordsXY{ x, validRange.GetTop() });
                    if (surfaceElement != nullptr)
                    {
                        z1 = std::clamp(
                            static_cast<uint8_t>(TileElementGetCornerHeight(surfaceElement, 1)), minHeight, maxHeight);
                        z2 = std::clamp(
                            static_cast<uint8_t>(TileElementGetCornerHeight(surfaceElement, 2)), minHeight, maxHeight);
                        res.Cost += SmoothLandRowByEdge(isExecuting, { x, validRange.GetTop() }, z1, z2, 0, -32, 0, 3, 1, 2);
                    }

                    surfaceElement = MapGetSurfaceElementAt(CoordsXY{ x, validRange.GetBottom() });
                    if (surfaceElement != nullptr)
                    {
                        z1 = std::clamp(
                            static_cast<uint8_t>(TileElementGetCornerHeight(surfaceElement, 0)), minHeight, maxHeight);
                        z2 = std::clamp(
                            static_cast<uint8_t>(TileElementGetCornerHeight(surfaceElement, 3)), minHeight, maxHeight);
                        res.Cost += SmoothLandRowByEdge(isExecuting, { x, validRange.GetBottom() }, z1, z2, 0, 32, 1, 2, 0, 3);
                    }
                }
                break;
            }
            case MAP_SELECT_TYPE_CORNER_0:
            case MAP_SELECT_TYPE_CORNER_1:
            case MAP_SELECT_TYPE_CORNER_2:
            case MAP_SELECT_TYPE_CORNER_3:
            {
                auto surfaceElement = MapGetSurfaceElementAt(CoordsXY{ validRange.GetLeft(), validRange.GetTop() });
                if (surfaceElement == nullptr)
                    break;
                uint8_t newBaseZ = surfaceElement->BaseHeight;
                uint8_t newSlope = surfaceElement->GetSlope();

                if (raiseLand)
                {
                    newSlope = RaiseSurfaceCornerFlags(selectionType, newSlope);
                }
                else
                {
                    newSlope = LowerSurfaceCornerFlags(selectionType, newSlope);
                }

                if (newSlope & kTileSlopeRaiseOrLowerBaseHeight)
                {
                    newBaseZ += heightOffset;
                    newSlope &= ~kTileSlopeRaiseOrLowerBaseHeight;
                }

                // Smooth the corners
                int32_t z = MapGetCornerHeight(newBaseZ, newSlope, 2);
                res.Cost += SmoothLandRowByCorner(
                    isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, -32, 0, 2);
                z = MapGetCornerHeight(newBaseZ, newSlope, 0);
                res.Cost += SmoothLandRowByCorner(isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 32, 32, 2, 0);
                z = MapGetCornerHeight(newBaseZ, newSlope, 3);
                res.Cost += SmoothLandRowByCorner(isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, 32, 1, 3);
                z = MapGetCornerHeight(newBaseZ, newSlope, 1);
                res.Cost += SmoothLandRowByCorner(isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 32, -32, 3, 1);

                // Smooth the edges
                switch (selectionType)
                {
                    case MAP_SELECT_TYPE_CORNER_0:
                        z = MapGetCornerHeight(newBaseZ, newSlope, 0);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 32, 0, 3, 0);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 0, 32, 1, 0);
                        z = MapGetCornerHeight(newBaseZ, newSlope, 3);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, 0, 0, 3);
                        z = MapGetCornerHeight(newBaseZ, newSlope, 1);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 0, -32, 0, 1);
                        break;
                    case MAP_SELECT_TYPE_CORNER_1:
                        z = MapGetCornerHeight(newBaseZ, newSlope, 1);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 32, 0, 2, 1);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 0, -32, 0, 1);
                        z = MapGetCornerHeight(newBaseZ, newSlope, 2);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, 0, 1, 2);
                        z = MapGetCornerHeight(newBaseZ, newSlope, 0);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 0, 32, 1, 0);
                        break;
                    case MAP_SELECT_TYPE_CORNER_2:
                        z = MapGetCornerHeight(newBaseZ, newSlope, 2);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, 0, 1, 2);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 0, -32, 3, 2);
                        z = MapGetCornerHeight(newBaseZ, newSlope, 1);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 32, 0, 2, 1);
                        z = MapGetCornerHeight(newBaseZ, newSlope, 3);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 0, 32, 2, 3);
                        break;
                    case MAP_SELECT_TYPE_CORNER_3:
                        z = MapGetCornerHeight(newBaseZ, newSlope, 3);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, 0, 0, 3);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 0, 32, 2, 3);
                        z = MapGetCornerHeight(newBaseZ, newSlope, 0);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 32, 0, 3, 0);
                        z = MapGetCornerHeight(newBaseZ, newSlope, 2);
                        res.Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 0, -32, 3, 2);
                        break;
                }
                break;
            }
            case MAP_SELECT_TYPE_EDGE_0:
            case MAP_SELECT_TYPE_EDGE_1:
            case MAP_SELECT_TYPE_EDGE_2:
            case MAP_SELECT_TYPE_EDGE_3:
            {
                // TODO: Handle smoothing by edge
                // Get the two corners to raise
                auto surfaceElement = MapGetSurfaceElementAt(CoordsXY{ validRange.GetLeft(), validRange.GetTop() });
                if (surfaceElement == nullptr)
                    break;
                uint8_t newBaseZ = surfaceElement->BaseHeight;
                uint8_t oldSlope = surfaceElement->GetSlope();
                int32_t rowIndex = selectionType - (MAP_SELECT_TYPE_EDGE_0 - MAP_SELECT_TYPE_FULL - 1);
                uint8_t newSlope = raiseLand ? RaiseSurfaceCornerFlags(rowIndex, oldSlope)
                                             : LowerSurfaceCornerFlags(rowIndex, oldSlope);

                const bool changeBaseHeight = newSlope & kTileSlopeRaiseOrLowerBaseHeight;
                if (changeBaseHeight)
                {
                    newBaseZ += heightOffset;
                    newSlope &= ~kTileSlopeRaiseOrLowerBaseHeight;
                }

                const uint8_t edge = selectionType - MAP_SELECT_TYPE_EDGE_0;

                // Table with corners for each edge selection. The first two are the selected corners, the latter
                // two are the opposites
                static constexpr uint8_t cornerIndices[][4] = {
                    { 2, 3, 1, 0 }, // MAP_SELECT_TYPE_EDGE_0
                    { 3, 0, 2, 1 }, // MAP_SELECT_TYPE_EDGE_1
                    { 0, 1, 3, 2 }, // MAP_SELECT_TYPE_EDGE_2
                    { 1, 2, 0, 3 }, // MAP_SELECT_TYPE_EDGE_3
                };
                // Big coordinate offsets for the neighbouring tile for the given edge selection
                static constexpr CoordsXY stepOffsets[] = {
                    { -32, 0 },
                    { 0, 32 },
                    { 32, 0 },
                    { 0, -32 },
                };

                // Smooth higher and lower edges
                uint8_t c1 = cornerIndices[edge][0];
                uint8_t c2 = cornerIndices[edge][1];
                uint8_t c3 = cornerIndices[edge][2];
                uint8_t c4 = cornerIndices[edge][3];
                uint8_t z1 = MapGetCornerHeight(newBaseZ, newSlope, c1);
                uint8_t z2 = MapGetCornerHeight(newBaseZ, newSlope, c2);
                uint8_t z3 = MapGetCornerHeight(newBaseZ, newSlope, c3);
                uint8_t z4 = MapGetCornerHeight(newBaseZ, newSlope, c4);
                // Smooth the edge at the top of the new slope
                res.Cost += SmoothLandRowByEdge(
                    isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z1, z2, stepOffsets[edge].x,
                    stepOffsets[edge].y, c3, c4, c1, c2);
                // Smooth the edge at the bottom of the new slope
                res.Cost += SmoothLandRowByEdge(
                    isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z3, z4, -stepOffsets[edge].x,
                    -stepOffsets[edge].y, c1, c2, c3, c4);

                // Smooth corners
                res.Cost += SmoothLandRowByCorner(
                    isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z1, -stepOffsets[edge].y, stepOffsets[edge].x,
                    c2, c1);
                res.Cost += SmoothLandRowByCorner(
                    isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z2, stepOffsets[edge].y, -stepOffsets[edge].x,
                    c1, c2);
                int32_t z = MapGetCornerHeight(newBaseZ, newSlope, 2);
                res.Cost += SmoothLandRowByCorner(
                    isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, -32, 0, 2);
                z = MapGetCornerHeight(newBaseZ, newSlope, 0);
                res.Cost += SmoothLandRowByCorner(isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 32, 32, 2, 0);
                z = MapGetCornerHeight(newBaseZ, newSlope, 3);
                res.Cost += SmoothLandRowByCorner(isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, 32, 1, 3);
                z = MapGetCornerHeight(newBaseZ, newSlope, 1);
                res.Cost += SmoothLandRowByCorner(isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 32, -32, 3, 1);
                break;
            }
            default:
                LOG_ERROR("Invalid map selection type %u", _selectionType);
                return Result(Status::InvalidParameters, std::get<StringId>(res.ErrorTitle), STR_ERR_VALUE_OUT_OF_RANGE);
        } // switch selectionType

        // Raise / lower the land tool selection area
        Result result;
        if (raiseLand)
        {
            auto raiseLandAction = LandRaiseAction({ _coords.x, _coords.y }, validRange, selectionType);
            raiseLandAction.SetFlags(GetFlags());
            result = isExecuting ? ExecuteNested(&raiseLandAction) : QueryNested(&raiseLandAction);
        }
        else
        {
            auto lowerLandAction = LandLowerAction({ _coords.x, _coords.y }, validRange, selectionType);
            lowerLandAction.SetFlags(GetFlags());
            result = isExecuting ? ExecuteNested(&lowerLandAction) : QueryNested(&lowerLandAction);
        }
        if (result.Error != Status::Ok)
        {
            return result;
        }

        if (isExecuting)
        {
            Audio::Play3D(Audio::SoundId::PlaceItem, { _coords.x, _coords.y, centreZ });
        }
        res.Cost += result.Cost;
        return res;
    }
} // namespace OpenRCT2::GameActions
