/*****************************************************************************
 * Copyright (c) 2014-2019 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.
 *****************************************************************************/

#pragma once

#include "../Context.h"
#include "../OpenRCT2.h"
#include "../actions/LandLowerAction.hpp"
#include "../actions/LandRaiseAction.hpp"
#include "../actions/LandSetHeightAction.hpp"
#include "../audio/audio.h"
#include "../interface/Window.h"
#include "../localisation/Localisation.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/Sprite.h"
#include "../world/Surface.h"
#include "GameAction.h"

DEFINE_GAME_ACTION(LandSmoothAction, GAME_COMMAND_EDIT_LAND_SMOOTH, GameActionResult)
{
private:
    CoordsXY _coords;
    MapRange _range;
    uint8_t _selectionType{ 0 };
    bool _isLowering{ false };

    constexpr static rct_string_id _ErrorTitles[] = { STR_CANT_LOWER_LAND_HERE, STR_CANT_RAISE_LAND_HERE };

public:
    LandSmoothAction()
    {
    }
    LandSmoothAction(CoordsXY coords, MapRange range, uint8_t selectionType, bool isLowering)
        : _coords(coords)
        , _range(range)
        , _selectionType(selectionType)
        , _isLowering(isLowering)
    {
    }

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

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

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

    GameActionResult::Ptr Query() const override
    {
        return SmoothLand(false);
    }

    GameActionResult::Ptr Execute() const override
    {
        return SmoothLand(true);
    }

private:
    GameActionResult::Ptr SmoothLandTile(
        int32_t direction, bool isExecuting, int32_t x, int32_t y, SurfaceElement* surfaceElement) const
    {
        int32_t targetBaseZ = surfaceElement->base_height;
        int32_t slope = surfaceElement->GetSlope();
        if (_isLowering)
        {
            slope = tile_element_lower_styles[direction][slope];
            if (slope & SURFACE_STYLE_FLAG_RAISE_OR_LOWER_BASE_HEIGHT)
            {
                targetBaseZ -= 2;
                slope &= ~SURFACE_STYLE_FLAG_RAISE_OR_LOWER_BASE_HEIGHT;
            }
        }
        else
        {
            slope = tile_element_raise_styles[direction][slope];
            if (slope & SURFACE_STYLE_FLAG_RAISE_OR_LOWER_BASE_HEIGHT)
            {
                targetBaseZ += 2;
                slope &= ~SURFACE_STYLE_FLAG_RAISE_OR_LOWER_BASE_HEIGHT;
            }
        }

        auto landSetHeightAction = LandSetHeightAction({ x, y }, targetBaseZ, slope);
        landSetHeightAction.SetFlags(GetFlags());
        auto res = isExecuting ? GameActions::ExecuteNested(&landSetHeightAction)
                               : GameActions::QueryNested(&landSetHeightAction);

        return res;
    }

    money32 SmoothLandRowByEdge(
        bool isExecuting, 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;
        money32 totalCost = 0;

        // check if we need to start at all
        if (!map_is_location_valid(loc) || !map_is_location_valid({ loc.x + stepX, loc.y + stepY }))
        {
            return 0;
        }
        auto surfaceElement = map_get_surface_element_at(loc);
        auto nextSurfaceElement = map_get_surface_element_at({ loc.x + stepX, loc.y + stepY });
        if (surfaceElement == nullptr || nextSurfaceElement == nullptr)
        {
            return 0;
        }
        if (tile_element_get_corner_height(surfaceElement, checkDirection1) != expectedLandHeight1 + landChangePerTile)
        {
            shouldContinue &= ~0x1;
        }
        if (tile_element_get_corner_height(surfaceElement, checkDirection2) != expectedLandHeight2 + landChangePerTile)
        {
            shouldContinue &= ~0x2;
        }
        if (tile_element_get_corner_height(surfaceElement, checkDirection1)
            != tile_element_get_corner_height(nextSurfaceElement, direction1))
        {
            shouldContinue &= ~0x1;
        }
        if (tile_element_get_corner_height(surfaceElement, checkDirection2)
            != tile_element_get_corner_height(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 (!map_is_location_valid({ nextLoc.x + stepX, nextLoc.y + stepY }))
            {
                shouldContinue &= ~0x3;
            }
            else
            {
                surfaceElement = nextSurfaceElement;
                nextSurfaceElement = map_get_surface_element_at({ nextLoc.x + stepX, nextLoc.y + stepY });
                if (nextSurfaceElement == nullptr)
                {
                    shouldContinue &= ~0x3;
                }
                if (tile_element_get_corner_height(surfaceElement, direction1) + landChangePerTile
                    != tile_element_get_corner_height(surfaceElement, checkDirection1))
                {
                    shouldContinue &= ~0x1;
                }
                if (tile_element_get_corner_height(surfaceElement, direction2) + landChangePerTile
                    != tile_element_get_corner_height(surfaceElement, checkDirection2))
                {
                    shouldContinue &= ~0x2;
                }
                if ((shouldContinue & 0x1)
                    && tile_element_get_corner_height(surfaceElement, checkDirection1)
                        != tile_element_get_corner_height(nextSurfaceElement, direction1))
                {
                    shouldContinue &= ~0x1;
                }
                if ((shouldContinue & 0x2)
                    && tile_element_get_corner_height(surfaceElement, checkDirection2)
                        != tile_element_get_corner_height(nextSurfaceElement, direction2))
                {
                    shouldContinue &= ~0x2;
                }
            }
            expectedLandHeight1 += landChangePerTile;

            // change land of current tile
            int32_t targetBaseZ = surfaceElement->base_height;
            int32_t slope = surfaceElement->GetSlope();
            int32_t oldSlope = slope;
            if (_isLowering)
            {
                if (shouldContinue & 0x4)
                {
                    slope = tile_element_lower_styles[direction1][slope];
                    if (slope & SURFACE_STYLE_FLAG_RAISE_OR_LOWER_BASE_HEIGHT)
                    {
                        targetBaseZ -= 2;
                        slope &= ~SURFACE_STYLE_FLAG_RAISE_OR_LOWER_BASE_HEIGHT;
                    }
                }
                if ((shouldContinue & 0x8)
                    && map_get_corner_height(surfaceElement->base_height, oldSlope, direction2)
                        == map_get_corner_height(targetBaseZ, slope, direction2))
                {
                    slope = tile_element_lower_styles[direction2][slope];
                    if (slope & SURFACE_STYLE_FLAG_RAISE_OR_LOWER_BASE_HEIGHT)
                    {
                        targetBaseZ -= 2;
                        slope &= ~SURFACE_STYLE_FLAG_RAISE_OR_LOWER_BASE_HEIGHT;
                    }
                }
            }
            else
            {
                if (shouldContinue & 0x4)
                {
                    slope = tile_element_raise_styles[direction1][slope];
                    if (slope & SURFACE_STYLE_FLAG_RAISE_OR_LOWER_BASE_HEIGHT)
                    {
                        targetBaseZ += 2;
                        slope &= ~SURFACE_STYLE_FLAG_RAISE_OR_LOWER_BASE_HEIGHT;
                    }
                }
                if ((shouldContinue & 0x8)
                    && map_get_corner_height(surfaceElement->base_height, oldSlope, direction2)
                        == map_get_corner_height(targetBaseZ, slope, direction2))
                {
                    slope = tile_element_raise_styles[direction2][slope];
                    if (slope & SURFACE_STYLE_FLAG_RAISE_OR_LOWER_BASE_HEIGHT)
                    {
                        targetBaseZ += 2;
                        slope &= ~SURFACE_STYLE_FLAG_RAISE_OR_LOWER_BASE_HEIGHT;
                    }
                }
            }
            auto landSetHeightAction = LandSetHeightAction(nextLoc, targetBaseZ, slope);
            landSetHeightAction.SetFlags(GetFlags());
            auto res = isExecuting ? GameActions::ExecuteNested(&landSetHeightAction)
                                   : GameActions::QueryNested(&landSetHeightAction);
            if (res->Error == GA_ERROR::OK)
            {
                totalCost += res->Cost;
            }
        }
        return totalCost;
    }

    money32 SmoothLandRowByCorner(
        bool isExecuting, CoordsXY loc, int32_t expectedLandHeight, int32_t stepX, int32_t stepY, int32_t direction,
        int32_t checkDirection) const
    {
        bool shouldContinue = true;
        money32 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 (!map_is_location_valid(loc) || !map_is_location_valid({ loc.x + stepX, loc.y + stepY }))
        {
            return 0;
        }
        auto surfaceElement = map_get_surface_element_at(loc);
        auto nextSurfaceElement = map_get_surface_element_at({ loc.x + stepX, loc.y + stepY });
        if (surfaceElement == nullptr || nextSurfaceElement == nullptr)
        {
            return 0;
        }
        if (tile_element_get_corner_height(surfaceElement, checkDirection) != expectedLandHeight + (_isLowering ? 2 : -2))
        {
            return 0;
        }
        if (tile_element_get_corner_height(surfaceElement, checkDirection)
            != tile_element_get_corner_height(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 (!map_is_location_valid({ nextLoc.x + stepX, nextLoc.y + stepY }))
            {
                shouldContinue = false;
            }
            else
            {
                surfaceElement = nextSurfaceElement;
                nextSurfaceElement = map_get_surface_element_at({ nextLoc.x + stepX, nextLoc.y + stepY });
                if (nextSurfaceElement == nullptr)
                {
                    shouldContinue = false;
                }
                if (tile_element_get_corner_height(surfaceElement, direction) + landChangePerTile
                    != tile_element_get_corner_height(surfaceElement, checkDirection))
                {
                    shouldContinue = false;
                }
                if (shouldContinue
                    && tile_element_get_corner_height(surfaceElement, checkDirection)
                        != tile_element_get_corner_height(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.x, nextLoc.y, surfaceElement);
            if (result->Error == GA_ERROR::OK)
            {
                totalCost += result->Cost;
            }
        }
        return totalCost;
    }

    GameActionResult::Ptr 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, (MAXIMUM_MAP_SIZE_TECHNICAL - 1) * 32);
        auto b = std::clamp(normRange.GetBottom(), 0, (MAXIMUM_MAP_SIZE_TECHNICAL - 1) * 32);
        auto validRange = MapRange{ l, t, r, b };

        int32_t centreZ = tile_element_height(_coords);

        auto res = MakeResult();
        res->ErrorTitle = _ErrorTitles[_isLowering ? 0 : 1];
        res->ExpenditureType = RCT_EXPENDITURE_TYPE_LANDSCAPING;
        res->Position = { _coords.x, _coords.y, centreZ };

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

                // Smooth the 4 corners
                { // top-left
                    auto surfaceElement = map_get_surface_element_at({ validRange.GetLeft(), validRange.GetTop() });
                    int32_t z = std::clamp((uint8_t)tile_element_get_corner_height(surfaceElement, 2), minHeight, maxHeight);
                    res->Cost += SmoothLandRowByCorner(
                        isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, -32, 0, 2);
                }
                { // bottom-left
                    auto surfaceElement = map_get_surface_element_at({ validRange.GetLeft(), validRange.GetBottom() });
                    int32_t z = std::clamp((uint8_t)tile_element_get_corner_height(surfaceElement, 3), minHeight, maxHeight);
                    res->Cost += SmoothLandRowByCorner(
                        isExecuting, { validRange.GetLeft(), validRange.GetBottom() }, z, -32, 32, 1, 3);
                }
                { // bottom-right
                    auto surfaceElement = map_get_surface_element_at({ validRange.GetRight(), validRange.GetBottom() });
                    int32_t z = std::clamp((uint8_t)tile_element_get_corner_height(surfaceElement, 0), minHeight, maxHeight);
                    res->Cost += SmoothLandRowByCorner(
                        isExecuting, { validRange.GetRight(), validRange.GetBottom() }, z, 32, 32, 2, 0);
                }
                { // top-right
                    auto surfaceElement = map_get_surface_element_at({ validRange.GetRight(), validRange.GetTop() });
                    int32_t z = std::clamp((uint8_t)tile_element_get_corner_height(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 += 32)
                {
                    auto surfaceElement = map_get_surface_element_at({ validRange.GetLeft(), y });
                    z1 = std::clamp((uint8_t)tile_element_get_corner_height(surfaceElement, 3), minHeight, maxHeight);
                    z2 = std::clamp((uint8_t)tile_element_get_corner_height(surfaceElement, 2), minHeight, maxHeight);
                    res->Cost += SmoothLandRowByEdge(isExecuting, { validRange.GetLeft(), y }, z1, z2, -32, 0, 0, 1, 3, 2);

                    surfaceElement = map_get_surface_element_at({ validRange.GetRight(), y });
                    z1 = std::clamp((uint8_t)tile_element_get_corner_height(surfaceElement, 1), minHeight, maxHeight);
                    z2 = std::clamp((uint8_t)tile_element_get_corner_height(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 += 32)
                {
                    auto surfaceElement = map_get_surface_element_at({ x, validRange.GetTop() });
                    z1 = std::clamp((uint8_t)tile_element_get_corner_height(surfaceElement, 1), minHeight, maxHeight);
                    z2 = std::clamp((uint8_t)tile_element_get_corner_height(surfaceElement, 2), minHeight, maxHeight);
                    res->Cost += SmoothLandRowByEdge(isExecuting, { x, validRange.GetTop() }, z1, z2, 0, -32, 0, 3, 1, 2);

                    surfaceElement = map_get_surface_element_at({ x, validRange.GetBottom() });
                    z1 = std::clamp((uint8_t)tile_element_get_corner_height(surfaceElement, 0), minHeight, maxHeight);
                    z2 = std::clamp((uint8_t)tile_element_get_corner_height(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 = map_get_surface_element_at({ validRange.GetLeft(), validRange.GetTop() });
                uint8_t newBaseZ = surfaceElement->base_height;
                uint8_t newSlope = surfaceElement->GetSlope();

                if (raiseLand)
                {
                    newSlope = tile_element_raise_styles[selectionType][newSlope];
                }
                else
                {
                    newSlope = tile_element_lower_styles[selectionType][newSlope];
                }

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

                // Smooth the corners
                int32_t z = map_get_corner_height(newBaseZ, newSlope, 2);
                res->Cost += SmoothLandRowByCorner(
                    isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, -32, 0, 2);
                z = map_get_corner_height(newBaseZ, newSlope, 0);
                res->Cost += SmoothLandRowByCorner(isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 32, 32, 2, 0);
                z = map_get_corner_height(newBaseZ, newSlope, 3);
                res->Cost += SmoothLandRowByCorner(
                    isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, 32, 1, 3);
                z = map_get_corner_height(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 = map_get_corner_height(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 = map_get_corner_height(newBaseZ, newSlope, 3);
                        res->Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, 0, 0, 3);
                        z = map_get_corner_height(newBaseZ, newSlope, 1);
                        res->Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 0, -32, 0, 1);
                        break;
                    case MAP_SELECT_TYPE_CORNER_1:
                        z = map_get_corner_height(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 = map_get_corner_height(newBaseZ, newSlope, 2);
                        res->Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, 0, 1, 2);
                        z = map_get_corner_height(newBaseZ, newSlope, 0);
                        res->Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 0, 32, 1, 0);
                        break;
                    case MAP_SELECT_TYPE_CORNER_2:
                        z = map_get_corner_height(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 = map_get_corner_height(newBaseZ, newSlope, 1);
                        res->Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 32, 0, 2, 1);
                        z = map_get_corner_height(newBaseZ, newSlope, 3);
                        res->Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 0, 32, 2, 3);
                        break;
                    case MAP_SELECT_TYPE_CORNER_3:
                        z = map_get_corner_height(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 = map_get_corner_height(newBaseZ, newSlope, 0);
                        res->Cost += SmoothLandRowByCorner(
                            isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 32, 0, 3, 0);
                        z = map_get_corner_height(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 = map_get_surface_element_at({ validRange.GetLeft(), validRange.GetTop() });
                uint8_t newBaseZ = surfaceElement->base_height;
                uint8_t oldSlope = surfaceElement->GetSlope();
                uint8_t newSlope = oldSlope;
                int32_t rowIndex = selectionType - (MAP_SELECT_TYPE_EDGE_0 - MAP_SELECT_TYPE_FULL - 1);

                if (raiseLand)
                {
                    newSlope = tile_element_raise_styles[rowIndex][oldSlope];
                }
                else
                {
                    newSlope = tile_element_lower_styles[rowIndex][oldSlope];
                }

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

                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 neigbouring tile for the given edge selection
                static constexpr sLocationXY8 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 = map_get_corner_height(newBaseZ, newSlope, c1);
                uint8_t z2 = map_get_corner_height(newBaseZ, newSlope, c2);
                uint8_t z3 = map_get_corner_height(newBaseZ, newSlope, c3);
                uint8_t z4 = map_get_corner_height(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 = map_get_corner_height(newBaseZ, newSlope, 2);
                res->Cost += SmoothLandRowByCorner(
                    isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, -32, 0, 2);
                z = map_get_corner_height(newBaseZ, newSlope, 0);
                res->Cost += SmoothLandRowByCorner(isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 32, 32, 2, 0);
                z = map_get_corner_height(newBaseZ, newSlope, 3);
                res->Cost += SmoothLandRowByCorner(
                    isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, -32, 32, 1, 3);
                z = map_get_corner_height(newBaseZ, newSlope, 1);
                res->Cost += SmoothLandRowByCorner(
                    isExecuting, { validRange.GetLeft(), validRange.GetTop() }, z, 32, -32, 3, 1);
                break;
            }
            default:
                log_error("Invalid map selection %u", _selectionType);
                return MakeResult(GA_ERROR::INVALID_PARAMETERS, res->ErrorTitle);
        } // switch selectionType

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

        if (isExecuting)
        {
            audio_play_sound_at_location(SoundId::PlaceItem, { _coords.x, _coords.y, centreZ });
        }
        res->Cost += result->Cost;
        return res;
    }
};
