/*****************************************************************************
 * 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.
 *****************************************************************************/

#include "TileInspector.h"

#include "../Context.h"
#include "../Game.h"
#include "../actions/GameAction.h"
#include "../common.h"
#include "../core/Guard.hpp"
#include "../interface/Window.h"
#include "../interface/Window_internal.h"
#include "../localisation/Localisation.h"
#include "../ride/Station.h"
#include "../ride/Track.h"
#include "../windows/Intent.h"
#include "../windows/tile_inspector.h"
#include "Banner.h"
#include "Footpath.h"
#include "LargeScenery.h"
#include "Map.h"
#include "Park.h"
#include "Scenery.h"
#include "Surface.h"

using namespace OpenRCT2;

uint32_t windowTileInspectorTileX;
uint32_t windowTileInspectorTileY;
int32_t windowTileInspectorElementCount = 0;
int32_t windowTileInspectorSelectedIndex;

static bool map_swap_elements_at(CoordsXY loc, int16_t first, int16_t second)
{
    TileElement* const firstElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, first);
    TileElement* const secondElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, second);

    if (firstElement == nullptr)
    {
        log_error("First element is out of range for the tile");
        return false;
    }
    if (secondElement == nullptr)
    {
        log_error("Second element is out of range for the tile");
        return false;
    }
    if (firstElement == secondElement)
    {
        log_error("Can't swap the element with itself");
        return false;
    }

    // Swap their memory
    TileElement temp = *firstElement;
    *firstElement = *secondElement;
    *secondElement = temp;

    // Swap the 'last map element for tile' flag if either one of them was last
    if ((firstElement)->IsLastForTile() || (secondElement)->IsLastForTile())
    {
        firstElement->SetLastForTile(!firstElement->IsLastForTile());
        secondElement->SetLastForTile(!secondElement->IsLastForTile());
    }

    return true;
}

/**
 * Inserts a corrupt element under a given element on a given tile
 * @param x The x coordinate of the tile
 * @param y The y coordinate of the tile
 * @param elementIndex The nth element on this tile
 * Returns 0 on success, MONEY_UNDEFINED otherwise.
 */
GameActionResult::Ptr tile_inspector_insert_corrupt_at(CoordsXY loc, int16_t elementIndex, bool isExecuting)
{
    // Make sure there is enough space for the new element
    if (!map_check_free_elements_and_reorganise(1))
        return std::make_unique<GameActionResult>(GA_ERROR::NO_FREE_ELEMENTS, STR_NONE);

    if (isExecuting)
    {
        // Create new corrupt element
        TileElement* corruptElement = tile_element_insert(
            { loc.x / 32, loc.y / 32, -1 }, 0b0000); // Ugly hack: -1 guarantees this to be placed first
        if (corruptElement == nullptr)
        {
            log_warning("Failed to insert corrupt element.");
            return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);
        }
        corruptElement->SetType(TILE_ELEMENT_TYPE_CORRUPT);

        // Set the base height to be the same as the selected element
        TileElement* const selectedElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex + 1);
        if (selectedElement == nullptr)
        {
            return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);
        }
        corruptElement->base_height = corruptElement->clearance_height = selectedElement->base_height;

        // Move the corrupt element up until the selected list item is reached
        // this way it's placed under the selected element, even when there are multiple elements with the same base height
        for (int16_t i = 0; i < elementIndex; i++)
        {
            if (!map_swap_elements_at(loc, i, i + 1))
            {
                // don't return error here, we've already inserted an element
                // and moved it as far as we could, the only sensible thing left
                // to do is to invalidate the window.
                break;
            }
        }

        map_invalidate_tile_full(loc.x, loc.y);

        // Update the tile inspector's list for everyone who has the tile selected
        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            windowTileInspectorElementCount++;

            // Keep other elements (that are not being hidden) selected
            if (windowTileInspectorSelectedIndex > elementIndex)
            {
                windowTileInspectorSelectedIndex++;
            }

            tileInspectorWindow->Invalidate();
        }
    }

    // Nothing went wrong
    return std::make_unique<GameActionResult>();
}

/**
 * Forcefully removes an element for a given tile
 * @param x The x coordinate of the tile
 * @param y The y coordinate of the tile
 * @param elementIndex The nth element on this tile
 */
GameActionResult::Ptr tile_inspector_remove_element_at(CoordsXY loc, int16_t elementIndex, bool isExecuting)
{
    if (isExecuting)
    {
        // Forcefully remove the element
        TileElement* const tileElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);
        if (tileElement == nullptr)
        {
            return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);
        }
        tile_element_remove(tileElement);
        map_invalidate_tile_full(loc.x, loc.y);

        // Update the window
        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            windowTileInspectorElementCount--;

            if (windowTileInspectorSelectedIndex > elementIndex)
            {
                windowTileInspectorSelectedIndex--;
            }
            else if (windowTileInspectorSelectedIndex == elementIndex)
            {
                windowTileInspectorSelectedIndex = -1;
            }

            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_swap_elements_at(CoordsXY loc, int16_t first, int16_t second, bool isExecuting)
{
    if (isExecuting)
    {
        if (!map_swap_elements_at(loc, first, second))
        {
            return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);
        }
        map_invalidate_tile_full(loc.x, loc.y);

        // Update the window
        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            // If one of them was selected, update selected list item
            if (windowTileInspectorSelectedIndex == first)
                windowTileInspectorSelectedIndex = second;
            else if (windowTileInspectorSelectedIndex == second)
                windowTileInspectorSelectedIndex = first;

            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_rotate_element_at(CoordsXY loc, int32_t elementIndex, bool isExecuting)
{
    if (isExecuting)
    {
        uint8_t newRotation, pathEdges, pathCorners;

        TileElement* const tileElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);
        if (tileElement == nullptr)
        {
            return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);
        }
        switch (tileElement->GetType())
        {
            case TILE_ELEMENT_TYPE_PATH:
                if (tileElement->AsPath()->IsSloped())
                {
                    newRotation = (tileElement->AsPath()->GetSlopeDirection() + 1) & TILE_ELEMENT_DIRECTION_MASK;
                    tileElement->AsPath()->SetSlopeDirection(newRotation);
                }
                pathEdges = tileElement->AsPath()->GetEdges();
                pathCorners = tileElement->AsPath()->GetCorners();
                tileElement->AsPath()->SetEdges((pathEdges << 1) | (pathEdges >> 3));
                tileElement->AsPath()->SetCorners((pathCorners << 1) | (pathCorners >> 3));
                break;
            case TILE_ELEMENT_TYPE_ENTRANCE:
            {
                // Update element rotation
                newRotation = tileElement->GetDirectionWithOffset(1);
                tileElement->SetDirection(newRotation);

                // Update ride's known entrance/exit rotation
                auto ride = get_ride(tileElement->AsEntrance()->GetRideIndex());
                if (ride != nullptr)
                {
                    auto stationIndex = tileElement->AsEntrance()->GetStationIndex();
                    auto entrance = ride_get_entrance_location(ride, stationIndex);
                    auto exit = ride_get_exit_location(ride, stationIndex);
                    uint8_t entranceType = tileElement->AsEntrance()->GetEntranceType();
                    uint8_t z = tileElement->base_height;

                    // Make sure this is the correct entrance or exit
                    if (entranceType == ENTRANCE_TYPE_RIDE_ENTRANCE && entrance.x == loc.x / 32 && entrance.y == loc.y / 32
                        && entrance.z == z)
                    {
                        ride_set_entrance_location(ride, stationIndex, { entrance.x, entrance.y, entrance.z, newRotation });
                    }
                    else if (
                        entranceType == ENTRANCE_TYPE_RIDE_EXIT && exit.x == loc.x / 32 && exit.y == loc.y / 32 && exit.z == z)
                    {
                        ride_set_exit_location(ride, stationIndex, { exit.x, exit.y, exit.z, newRotation });
                    }
                }
                break;
            }
            case TILE_ELEMENT_TYPE_TRACK:
            case TILE_ELEMENT_TYPE_SMALL_SCENERY:
            case TILE_ELEMENT_TYPE_WALL:
                newRotation = tileElement->GetDirectionWithOffset(1);
                tileElement->SetDirection(newRotation);
                break;
            case TILE_ELEMENT_TYPE_BANNER:
            {
                uint8_t unblockedEdges = tileElement->AsBanner()->GetAllowedEdges();
                unblockedEdges = (unblockedEdges << 1 | unblockedEdges >> 3) & 0xF;
                tileElement->AsBanner()->SetAllowedEdges(unblockedEdges);
                tileElement->AsBanner()->SetPosition((tileElement->AsBanner()->GetPosition() + 1) & 3);
                break;
            }
        }

        map_invalidate_tile_full(loc.x, loc.y);

        if ((uint32_t)(loc.x / 32) == windowTileInspectorTileX && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            window_invalidate_by_class(WC_TILE_INSPECTOR);
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_paste_element_at(CoordsXY loc, TileElement element, bool isExecuting)
{
    // Make sure there is enough space for the new element
    if (!map_check_free_elements_and_reorganise(1))
    {
        return std::make_unique<GameActionResult>(GA_ERROR::NO_FREE_ELEMENTS, STR_NONE);
    }

    if (isExecuting)
    {
        // Check if the element to be pasted refers to a banner index
        BannerIndex bannerIndex = tile_element_get_banner_index(&element);
        if (bannerIndex != BANNER_INDEX_NULL)
        {
            // The element to be pasted refers to a banner index - make a copy of it
            BannerIndex newBannerIndex = create_new_banner(GAME_COMMAND_FLAG_APPLY);
            if (newBannerIndex == BANNER_INDEX_NULL)
            {
                return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);
            }
            auto& newBanner = *GetBanner(newBannerIndex);
            newBanner = *GetBanner(bannerIndex);
            newBanner.position = TileCoordsXY(loc);

            // Use the new banner index
            tile_element_set_banner_index(&element, newBannerIndex);
        }

        // The occupiedQuadrants will be automatically set when the element is copied over, so it's not necessary to set them
        // correctly _here_.
        TileElement* const pastedElement = tile_element_insert({ loc.x / 32, loc.y / 32, element.base_height }, 0b0000);

        bool lastForTile = pastedElement->IsLastForTile();
        *pastedElement = element;
        pastedElement->SetLastForTile(lastForTile);

        map_invalidate_tile_full(loc.x, loc.y);

        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            windowTileInspectorElementCount++;

            // Select new element if there was none selected already
            int16_t newIndex = (int16_t)(pastedElement - map_get_first_element_at(loc.x / 32, loc.y / 32));
            if (windowTileInspectorSelectedIndex == -1)
                windowTileInspectorSelectedIndex = newIndex;
            else if (windowTileInspectorSelectedIndex >= newIndex)
                windowTileInspectorSelectedIndex++;

            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_sort_elements_at(CoordsXY loc, bool isExecuting)
{
    if (isExecuting)
    {
        const TileElement* const firstElement = map_get_first_element_at(loc.x / 32, loc.y / 32);
        if (firstElement == nullptr)
            return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

        // Count elements on tile
        int32_t numElement = 0;
        const TileElement* elementIterator = firstElement;
        do
        {
            numElement++;
        } while (!(elementIterator++)->IsLastForTile());

        // Bubble sort
        for (int32_t loopStart = 1; loopStart < numElement; loopStart++)
        {
            int32_t currentId = loopStart;
            const TileElement* currentElement = firstElement + currentId;
            const TileElement* otherElement = currentElement - 1;

            // While current element's base height is lower, or (when their baseheight is the same) the other map element's
            // clearance height is lower...
            while (currentId > 0
                   && (otherElement->base_height > currentElement->base_height
                       || (otherElement->base_height == currentElement->base_height
                           && otherElement->clearance_height > currentElement->clearance_height)))
            {
                if (!map_swap_elements_at(loc, currentId - 1, currentId))
                {
                    // don't return error here, we've already ran some actions
                    // and moved things as far as we could, the only sensible
                    // thing left to do is to invalidate the window.
                    loopStart = numElement;
                    break;
                }
                currentId--;
                currentElement--;
                otherElement--;
            }
        }

        map_invalidate_tile_full(loc.x, loc.y);

        // Deselect tile for clients who had it selected
        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            windowTileInspectorSelectedIndex = -1;
            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_any_base_height_offset(
    CoordsXY loc, int16_t elementIndex, int8_t heightOffset, bool isExecuting)
{
    TileElement* const tileElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);
    if (tileElement == nullptr)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    int16_t newBaseHeight = (int16_t)tileElement->base_height + heightOffset;
    int16_t newClearanceHeight = (int16_t)tileElement->clearance_height + heightOffset;
    if (newBaseHeight < 0 || newBaseHeight > 0xff || newClearanceHeight < 0 || newClearanceHeight > 0xff)
    {
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);
    }

    if (isExecuting)
    {
        if (tileElement->GetType() == TILE_ELEMENT_TYPE_ENTRANCE)
        {
            uint8_t entranceType = tileElement->AsEntrance()->GetEntranceType();
            if (entranceType != ENTRANCE_TYPE_PARK_ENTRANCE)
            {
                // Update the ride's known entrance or exit height
                auto ride = get_ride(tileElement->AsEntrance()->GetRideIndex());
                if (ride != nullptr)
                {
                    auto entranceIndex = tileElement->AsEntrance()->GetStationIndex();
                    auto entrance = ride_get_entrance_location(ride, entranceIndex);
                    auto exit = ride_get_exit_location(ride, entranceIndex);
                    uint8_t z = tileElement->base_height;

                    // Make sure this is the correct entrance or exit
                    if (entranceType == ENTRANCE_TYPE_RIDE_ENTRANCE && entrance.x == loc.x / 32 && entrance.y == loc.y / 32
                        && entrance.z == z)
                        ride_set_entrance_location(
                            ride, entranceIndex, { entrance.x, entrance.y, z + heightOffset, entrance.direction });
                    else if (
                        entranceType == ENTRANCE_TYPE_RIDE_EXIT && exit.x == loc.x / 32 && exit.y == loc.y / 32 && exit.z == z)
                        ride_set_exit_location(ride, entranceIndex, { exit.x, exit.y, z + heightOffset, exit.direction });
                }
            }
        }

        tileElement->base_height += heightOffset;
        tileElement->clearance_height += heightOffset;

        map_invalidate_tile_full(loc.x, loc.y);

        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_surface_show_park_fences(CoordsXY loc, bool showFences, bool isExecuting)
{
    auto* const surfaceelement = map_get_surface_element_at(loc);

    // No surface element on tile
    if (surfaceelement == nullptr)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        if (!showFences)
            surfaceelement->SetParkFences(0);
        else
            update_park_fences(loc);

        map_invalidate_tile_full(loc.x, loc.y);

        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_surface_toggle_corner(CoordsXY loc, int32_t cornerIndex, bool isExecuting)
{
    auto* const surfaceElement = map_get_surface_element_at(loc);

    // No surface element on tile
    if (surfaceElement == nullptr)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        const uint8_t originalSlope = surfaceElement->GetSlope();
        const bool diagonal = (originalSlope & TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT) >> 4;

        uint8_t newSlope = surfaceElement->GetSlope() ^ (1 << cornerIndex);
        surfaceElement->SetSlope(newSlope);
        if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_ALL_CORNERS_UP)
        {
            surfaceElement->clearance_height = surfaceElement->base_height + 2;
        }
        else
        {
            surfaceElement->clearance_height = surfaceElement->base_height;
        }

        // All corners are raised
        if ((surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_ALL_CORNERS_UP) == TILE_ELEMENT_SLOPE_ALL_CORNERS_UP)
        {
            uint8_t slope = TILE_ELEMENT_SLOPE_FLAT;

            if (diagonal)
            {
                switch (originalSlope & TILE_ELEMENT_SLOPE_ALL_CORNERS_UP)
                {
                    case TILE_ELEMENT_SLOPE_S_CORNER_DN:
                        slope |= TILE_ELEMENT_SLOPE_N_CORNER_UP;
                        break;
                    case TILE_ELEMENT_SLOPE_W_CORNER_DN:
                        slope |= TILE_ELEMENT_SLOPE_E_CORNER_UP;
                        break;
                    case TILE_ELEMENT_SLOPE_N_CORNER_DN:
                        slope |= TILE_ELEMENT_SLOPE_S_CORNER_UP;
                        break;
                    case TILE_ELEMENT_SLOPE_E_CORNER_DN:
                        slope |= TILE_ELEMENT_SLOPE_W_CORNER_UP;
                        break;
                }
            }
            surfaceElement->SetSlope(slope);

            // Update base and clearance heights
            surfaceElement->base_height += 2;
            surfaceElement->clearance_height = surfaceElement->base_height + (diagonal ? 2 : 0);
        }

        map_invalidate_tile_full(loc.x, loc.y);

        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_surface_toggle_diagonal(CoordsXY loc, bool isExecuting)
{
    auto* const surfaceElement = map_get_surface_element_at(loc);

    // No surface element on tile
    if (surfaceElement == nullptr)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        uint8_t newSlope = surfaceElement->GetSlope() ^ TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT;
        surfaceElement->SetSlope(newSlope);
        if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT)
        {
            surfaceElement->clearance_height = surfaceElement->base_height + 4;
        }
        else if (surfaceElement->GetSlope() & TILE_ELEMENT_SLOPE_ALL_CORNERS_UP)
        {
            surfaceElement->clearance_height = surfaceElement->base_height + 2;
        }
        else
        {
            surfaceElement->clearance_height = surfaceElement->base_height;
        }

        map_invalidate_tile_full(loc.x, loc.y);

        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_path_set_sloped(CoordsXY loc, int32_t elementIndex, bool sloped, bool isExecuting)
{
    TileElement* const pathElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);

    if (pathElement == nullptr || pathElement->GetType() != TILE_ELEMENT_TYPE_PATH)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        pathElement->AsPath()->SetSloped(sloped);

        map_invalidate_tile_full(loc.x, loc.y);

        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_path_set_broken(CoordsXY loc, int32_t elementIndex, bool broken, bool isExecuting)
{
    TileElement* const pathElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);

    if (pathElement == nullptr || pathElement->GetType() != TILE_ELEMENT_TYPE_PATH)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        pathElement->AsPath()->SetIsBroken(broken);

        map_invalidate_tile_full(loc.x, loc.y);

        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_path_toggle_edge(CoordsXY loc, int32_t elementIndex, int32_t edgeIndex, bool isExecuting)
{
    TileElement* const pathElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);

    if (pathElement == nullptr || pathElement->GetType() != TILE_ELEMENT_TYPE_PATH)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        uint8_t newEdges = pathElement->AsPath()->GetEdgesAndCorners() ^ (1 << edgeIndex);
        pathElement->AsPath()->SetEdgesAndCorners(newEdges);

        map_invalidate_tile_full(loc.x, loc.y);

        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_entrance_make_usable(CoordsXY loc, int32_t elementIndex, bool isExecuting)
{
    TileElement* const entranceElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);

    if (entranceElement == nullptr || entranceElement->GetType() != TILE_ELEMENT_TYPE_ENTRANCE)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    auto ride = get_ride(entranceElement->AsEntrance()->GetRideIndex());
    if (ride == nullptr)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        uint8_t stationIndex = entranceElement->AsEntrance()->GetStationIndex();

        switch (entranceElement->AsEntrance()->GetEntranceType())
        {
            case ENTRANCE_TYPE_RIDE_ENTRANCE:
                ride_set_entrance_location(
                    ride, stationIndex,
                    { loc.x / 32, loc.y / 32, entranceElement->base_height, (uint8_t)entranceElement->GetDirection() });
                break;
            case ENTRANCE_TYPE_RIDE_EXIT:
                ride_set_exit_location(
                    ride, stationIndex,
                    { loc.x / 32, loc.y / 32, entranceElement->base_height, (uint8_t)entranceElement->GetDirection() });
                break;
        }

        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_wall_set_slope(CoordsXY loc, int32_t elementIndex, int32_t slopeValue, bool isExecuting)
{
    TileElement* const wallElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);

    if (wallElement == nullptr || wallElement->GetType() != TILE_ELEMENT_TYPE_WALL)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        // Set new slope value
        wallElement->AsWall()->SetSlope(slopeValue);

        map_invalidate_tile_full(loc.x, loc.y);

        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

// Changes the height of all track elements that belong to the same track piece
// Broxzier: Copied from track_remove and stripped of unneeded code, but I think this should be smaller
GameActionResult::Ptr tile_inspector_track_base_height_offset(
    CoordsXY loc, int32_t elementIndex, int8_t offset, bool isExecuting)
{
    if (offset == 0)
        return std::make_unique<GameActionResult>();

    TileElement* const trackElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);

    if (trackElement == nullptr || trackElement->GetType() != TILE_ELEMENT_TYPE_TRACK)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        uint8_t type = trackElement->AsTrack()->GetTrackType();
        int16_t originX = loc.x;
        int16_t originY = loc.y;
        int16_t originZ = trackElement->base_height * 8;
        uint8_t rotation = trackElement->GetDirection();
        auto rideIndex = trackElement->AsTrack()->GetRideIndex();
        auto ride = get_ride(rideIndex);
        if (ride == nullptr)
            return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

        auto trackBlock = get_track_def_from_ride(ride, type);
        trackBlock += trackElement->AsTrack()->GetSequenceIndex();

        uint8_t originDirection = trackElement->GetDirection();
        CoordsXY offsets = { trackBlock->x, trackBlock->y };
        CoordsXY coords = { originX, originY };
        coords += offsets.Rotate(direction_reverse(originDirection));

        originX = (int16_t)coords.x;
        originY = (int16_t)coords.y;
        originZ -= trackBlock->z;

        trackBlock = get_track_def_from_ride(ride, type);
        for (; trackBlock->index != 255; trackBlock++)
        {
            CoordsXY elem = { originX, originY };
            int16_t elemZ = originZ;
            offsets.x = trackBlock->x;
            offsets.y = trackBlock->y;
            elem += offsets.Rotate(originDirection);
            elemZ += trackBlock->z;

            map_invalidate_tile_full(elem.x, elem.y);

            bool found = false;
            TileElement* tileElement = map_get_first_element_at(elem.x >> 5, elem.y >> 5);
            do
            {
                if (tileElement == nullptr)
                    break;

                if (tileElement->base_height != elemZ / 8)
                    continue;

                if (tileElement->GetType() != TILE_ELEMENT_TYPE_TRACK)
                    continue;

                if (tileElement->GetDirection() != rotation)
                    continue;

                if (tileElement->AsTrack()->GetSequenceIndex() != trackBlock->index)
                    continue;

                if (tileElement->AsTrack()->GetTrackType() != type)
                    continue;

                found = true;
                break;
            } while (!(tileElement++)->IsLastForTile());

            if (!found)
            {
                log_error("Track map element part not found!");
                return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);
            }

            // track_remove returns here on failure, not sure when this would ever be hit. Only thing I can think of is for when
            // you decrease the map size.
            openrct2_assert(map_get_surface_element_at(elem) != nullptr, "No surface at %d,%d", elem.x >> 5, elem.y >> 5);

            // Keep?
            // invalidate_test_results(ride);

            tileElement->base_height += offset;
            tileElement->clearance_height += offset;
        }
    }

    // TODO: Only invalidate when one of the affected tiles is selected
    window_invalidate_by_class(WC_TILE_INSPECTOR);

    return std::make_unique<GameActionResult>();
}

// Sets chainlift, optionally for an entire track block
// Broxzier: Basically a copy of the above function, with just two different lines... should probably be combined somehow
GameActionResult::Ptr tile_inspector_track_set_chain(
    CoordsXY loc, int32_t elementIndex, bool entireTrackBlock, bool setChain, bool isExecuting)
{
    TileElement* const trackElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);

    if (trackElement == nullptr || trackElement->GetType() != TILE_ELEMENT_TYPE_TRACK)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        if (!entireTrackBlock)
        {
            // Set chain for only the selected piece
            if (trackElement->AsTrack()->HasChain() != setChain)
            {
                trackElement->AsTrack()->SetHasChain(setChain);
            }

            return std::make_unique<GameActionResult>();
        }

        uint8_t type = trackElement->AsTrack()->GetTrackType();
        int16_t originX = loc.x;
        int16_t originY = loc.y;
        int16_t originZ = trackElement->base_height * 8;
        uint8_t rotation = trackElement->GetDirection();
        auto rideIndex = trackElement->AsTrack()->GetRideIndex();
        auto ride = get_ride(rideIndex);
        if (ride == nullptr)
            return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

        auto trackBlock = get_track_def_from_ride(ride, type);
        trackBlock += trackElement->AsTrack()->GetSequenceIndex();

        uint8_t originDirection = trackElement->GetDirection();
        CoordsXY offsets = { trackBlock->x, trackBlock->y };
        CoordsXY coords = { originX, originY };
        coords += offsets.Rotate(direction_reverse(originDirection));

        originX = (int16_t)coords.x;
        originY = (int16_t)coords.y;
        originZ -= trackBlock->z;

        trackBlock = get_track_def_from_ride(ride, type);
        for (; trackBlock->index != 255; trackBlock++)
        {
            CoordsXY elem = { originX, originY };
            int16_t elemZ = originZ;
            offsets.x = trackBlock->x;
            offsets.y = trackBlock->y;
            elem += offsets.Rotate(originDirection);
            elemZ += trackBlock->z;

            map_invalidate_tile_full(elem.x, elem.y);

            bool found = false;
            TileElement* tileElement = map_get_first_element_at(elem.x >> 5, elem.y >> 5);
            do
            {
                if (tileElement == nullptr)
                    break;

                if (tileElement->base_height != elemZ / 8)
                    continue;

                if (tileElement->GetType() != TILE_ELEMENT_TYPE_TRACK)
                    continue;

                if (tileElement->GetDirection() != rotation)
                    continue;

                if (tileElement->AsTrack()->GetSequenceIndex() != trackBlock->index)
                    continue;

                if (tileElement->AsTrack()->GetTrackType() != type)
                    continue;

                found = true;
                break;
            } while (!(tileElement++)->IsLastForTile());

            if (!found)
            {
                log_error("Track map element part not found!");
                return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);
            }

            // track_remove returns here on failure, not sure when this would ever be hit. Only thing I can think of is for when
            // you decrease the map size.
            openrct2_assert(map_get_surface_element_at(elem) != nullptr, "No surface at %d,%d", elem.x >> 5, elem.y >> 5);

            // Keep?
            // invalidate_test_results(ride);

            if (tileElement->AsTrack()->HasChain() != setChain)
            {
                tileElement->AsTrack()->SetHasChain(setChain);
            }
        }
    }

    // TODO: Only invalidate when one of the affected tiles is selected
    window_invalidate_by_class(WC_TILE_INSPECTOR);

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_track_set_block_brake(
    CoordsXY loc, int32_t elementIndex, bool blockBrake, bool isExecuting)
{
    TileElement* const trackElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);

    if (trackElement == nullptr || trackElement->GetType() != TILE_ELEMENT_TYPE_TRACK)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        trackElement->AsTrack()->SetBlockBrakeClosed(blockBrake);

        map_invalidate_tile_full(loc.x, loc.y);

        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_track_set_indestructible(
    CoordsXY loc, int32_t elementIndex, bool isIndestructible, bool isExecuting)
{
    TileElement* const trackElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);

    if (trackElement == nullptr || trackElement->GetType() != TILE_ELEMENT_TYPE_TRACK)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        trackElement->AsTrack()->SetIsIndestructible(isIndestructible);

        map_invalidate_tile_full(loc.x, loc.y);

        rct_window* const tileInspectorWindow = window_find_by_class(WC_TILE_INSPECTOR);
        if (tileInspectorWindow != nullptr && (uint32_t)(loc.x / 32) == windowTileInspectorTileX
            && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            tileInspectorWindow->Invalidate();
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_scenery_set_quarter_location(
    CoordsXY loc, int32_t elementIndex, int32_t quarterIndex, bool isExecuting)
{
    TileElement* const tileElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);

    if (tileElement == nullptr || tileElement->GetType() != TILE_ELEMENT_TYPE_SMALL_SCENERY)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        // Set quadrant index
        tileElement->AsSmallScenery()->SetSceneryQuadrant(quarterIndex);

        // Update collision
        tileElement->SetOccupiedQuadrants(1 << ((quarterIndex + 2) & 3));

        map_invalidate_tile_full(loc.x, loc.y);
        if ((uint32_t)(loc.x / 32) == windowTileInspectorTileX && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            window_invalidate_by_class(WC_TILE_INSPECTOR);
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_scenery_set_quarter_collision(
    CoordsXY loc, int32_t elementIndex, int32_t quarterIndex, bool isExecuting)
{
    TileElement* const tileElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);

    if (tileElement == nullptr || tileElement->GetType() != TILE_ELEMENT_TYPE_SMALL_SCENERY)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        auto occupiedQuadrants = tileElement->GetOccupiedQuadrants();
        occupiedQuadrants ^= 1 << quarterIndex;
        tileElement->SetOccupiedQuadrants(occupiedQuadrants);

        map_invalidate_tile_full(loc.x, loc.y);
        if ((uint32_t)(loc.x / 32) == windowTileInspectorTileX && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            window_invalidate_by_class(WC_TILE_INSPECTOR);
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_banner_toggle_blocking_edge(
    CoordsXY loc, int32_t elementIndex, int32_t edgeIndex, bool isExecuting)
{
    TileElement* const bannerElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);

    if (bannerElement == nullptr || bannerElement->GetType() != TILE_ELEMENT_TYPE_BANNER)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        uint8_t edges = bannerElement->AsBanner()->GetAllowedEdges();
        edges ^= (1 << edgeIndex);
        bannerElement->AsBanner()->SetAllowedEdges(edges);

        if ((uint32_t)(loc.x / 32) == windowTileInspectorTileX && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            window_invalidate_by_class(WC_TILE_INSPECTOR);
        }
    }

    return std::make_unique<GameActionResult>();
}

GameActionResult::Ptr tile_inspector_corrupt_clamp(CoordsXY loc, int32_t elementIndex, bool isExecuting)
{
    TileElement* const corruptElement = map_get_nth_element_at(loc.x / 32, loc.y / 32, elementIndex);

    if (corruptElement == nullptr || corruptElement->GetType() != TILE_ELEMENT_TYPE_CORRUPT)
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (corruptElement->IsLastForTile())
        return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);

    if (isExecuting)
    {
        TileElement* const nextElement = corruptElement + 1;
        corruptElement->base_height = corruptElement->clearance_height = nextElement->base_height;

        if ((uint32_t)(loc.x / 32) == windowTileInspectorTileX && (uint32_t)(loc.y / 32) == windowTileInspectorTileY)
        {
            window_invalidate_by_class(WC_TILE_INSPECTOR);
        }
    }

    return std::make_unique<GameActionResult>();
}
