/*****************************************************************************
 * Copyright (c) 2014-2026 OpenRCT2 developers
 *
 * For a complete list of all authors, please refer to contributors.md
 * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
 *
 * OpenRCT2 is licensed under the GNU General Public License version 3.
 *****************************************************************************/

#include "Map.h"

#include "../Cheats.h"
#include "../Context.h"
#include "../Diagnostic.h"
#include "../GameState.h"
#include "../OpenRCT2.h"
#include "../actions/GameActionRunner.h"
#include "../actions/park/ParkEntranceRemoveAction.h"
#include "../actions/scenery/BannerRemoveAction.h"
#include "../actions/scenery/LargeSceneryRemoveAction.h"
#include "../actions/scenery/WallRemoveAction.h"
#include "../core/EnumUtils.hpp"
#include "../entity/Duck.h"
#include "../entity/EntityTweener.h"
#include "../entity/JumpingFountain.h"
#include "../entity/PatrolArea.h"
#include "../entity/Staff.h"
#include "../interface/Cursors.h"
#include "../interface/Viewport.h"
#include "../object/LargeSceneryEntry.h"
#include "../object/SmallSceneryEntry.h"
#include "../profiling/Profiling.h"
#include "../ride/RideManager.hpp"
#include "../ride/Vehicle.h"
#include "../windows/Intent.h"
#include "../world/MapRangeView.hpp"
#include "../world/TilePointerIndex.hpp"
#include "Banner.h"
#include "Footpath.h"
#include "MapAnimation.h"
#include "Park.h"
#include "Scenery.h"
#include "TileElementsView.h"
#include "tile_element/BannerElement.h"
#include "tile_element/EntranceElement.h"
#include "tile_element/LargeSceneryElement.h"
#include "tile_element/PathElement.h"
#include "tile_element/Slope.h"
#include "tile_element/SmallSceneryElement.h"
#include "tile_element/SurfaceElement.h"
#include "tile_element/TrackElement.h"

namespace OpenRCT2
{
    /**
     * Replaces 0x00993CCC, 0x00993CCE
     */
    // clang-format off
    const std::array<CoordsXY, 8> CoordsDirectionDelta = {
        CoordsXY{ -kCoordsXYStep, 0 },
        CoordsXY{               0, +kCoordsXYStep },
        CoordsXY{ +kCoordsXYStep, 0 },
        CoordsXY{               0, -kCoordsXYStep },
        CoordsXY{ -kCoordsXYStep, +kCoordsXYStep },
        CoordsXY{ +kCoordsXYStep, +kCoordsXYStep },
        CoordsXY{ +kCoordsXYStep, -kCoordsXYStep },
        CoordsXY{ -kCoordsXYStep, -kCoordsXYStep }
    };
    // clang-format on

    const TileCoordsXY TileDirectionDelta[] = {
        { -1, 0 }, { 0, +1 }, { +1, 0 }, { 0, -1 }, { -1, +1 }, { +1, +1 }, { +1, -1 }, { -1, -1 },
    };

    static constexpr size_t kMinTileElements = 1024;

    uint32_t gLandRemainingOwnershipSales;
    uint32_t gLandRemainingConstructionSales;

    bool gMapLandRightsUpdateSuccess;

    static TilePointerIndex<TileElement> _tileIndex;
    static TilePointerIndex<TileElement> _tileIndexStash;
    static std::vector<TileElement> _tileElementsStash;
    static size_t _tileElementsInUse;
    static size_t _tileElementsInUseStash;
    static TileCoordsXY _mapSizeStash;

    void StashMap()
    {
        auto& gameState = getGameState();
        _tileIndexStash = std::move(_tileIndex);
        _tileElementsStash = std::move(gameState.tileElements);
        _mapSizeStash = gameState.mapSize;
        _tileElementsInUseStash = _tileElementsInUse;
    }

    void UnstashMap()
    {
        auto& gameState = getGameState();
        _tileIndex = std::move(_tileIndexStash);
        gameState.tileElements = std::move(_tileElementsStash);
        gameState.mapSize = _mapSizeStash;
        _tileElementsInUse = _tileElementsInUseStash;
    }

    CoordsXY GetMapSizeUnits()
    {
        auto& gameState = getGameState();
        return { (gameState.mapSize.x - 1) * kCoordsXYStep, (gameState.mapSize.y - 1) * kCoordsXYStep };
    }
    CoordsXY GetMapSizeMinus2()
    {
        auto& gameState = getGameState();
        return { (gameState.mapSize.x * kCoordsXYStep) + (8 * kCoordsXYStep - 2),
                 (gameState.mapSize.y * kCoordsXYStep) + (8 * kCoordsXYStep - 2) };
    }
    CoordsXY GetMapSizeMaxXY()
    {
        return GetMapSizeUnits() - CoordsXY{ 1, 1 };
    }

    const std::vector<TileElement>& GetTileElements()
    {
        return getGameState().tileElements;
    }

    void SetTileElements(GameState_t& gameState, std::vector<TileElement>&& tileElements)
    {
        gameState.tileElements = std::move(tileElements);
        _tileIndex = TilePointerIndex<TileElement>(
            kMaximumMapSizeTechnical, gameState.tileElements.data(), gameState.tileElements.size());
        _tileElementsInUse = gameState.tileElements.size();
    }

    static TileElement GetDefaultSurfaceElement()
    {
        TileElement el;
        el.clearAs(TileElementType::surface);
        el.setLastForTile(true);
        el.baseHeight = 14;
        el.clearanceHeight = 14;
        el.asSurface()->setWaterHeight(0);
        el.asSurface()->setSlope(kTileSlopeFlat);
        el.asSurface()->setGrassLength(GRASS_LENGTH_CLEAR_0);
        el.asSurface()->setOwnership(kUnowned);
        el.asSurface()->setParkFences(0);
        el.asSurface()->setSurfaceObjectIndex(0);
        el.asSurface()->setEdgeObjectIndex(0);
        return el;
    }

    std::vector<TileElement> GetReorganisedTileElementsWithoutGhosts()
    {
        std::vector<TileElement> newElements;
        newElements.reserve(std::max(kMinTileElements, getGameState().tileElements.size()));
        for (int32_t y = 0; y < kMaximumMapSizeTechnical; y++)
        {
            for (int32_t x = 0; x < kMaximumMapSizeTechnical; x++)
            {
                auto oldSize = newElements.size();

                // Add all non-ghost elements
                const auto* element = MapGetFirstElementAt(TileCoordsXY{ x, y });
                if (element != nullptr)
                {
                    do
                    {
                        if (!element->isGhost())
                        {
                            newElements.push_back(*element);
                        }
                    } while (!(element++)->isLastForTile());
                }

                // Insert default surface element if no elements were added
                auto newSize = newElements.size();
                if (oldSize == newSize)
                {
                    newElements.push_back(GetDefaultSurfaceElement());
                }

                // Ensure last element of tile has last flag set
                auto& lastEl = newElements.back();
                lastEl.setLastForTile(true);
            }
        }
        return newElements;
    }

    static void ReorganiseTileElements(GameState_t& gameState, size_t capacity)
    {
        ContextSetCurrentCursor(CursorID::zzz);

        std::vector<TileElement> newElements;
        newElements.reserve(std::max(kMinTileElements, capacity));
        for (int32_t y = 0; y < kMaximumMapSizeTechnical; y++)
        {
            for (int32_t x = 0; x < kMaximumMapSizeTechnical; x++)
            {
                const auto* element = MapGetFirstElementAt(TileCoordsXY{ x, y });
                if (element == nullptr)
                {
                    newElements.push_back(GetDefaultSurfaceElement());
                }
                else
                {
                    do
                    {
                        newElements.push_back(*element);
                    } while (!(element++)->isLastForTile());
                }
            }
        }

        SetTileElements(gameState, std::move(newElements));
    }

    static void ReorganiseTileElements(size_t capacity)
    {
        auto& gameState = getGameState();
        ReorganiseTileElements(gameState, capacity);
    }

    void ReorganiseTileElements()
    {
        auto& gameState = getGameState();
        ReorganiseTileElements(gameState, gameState.tileElements.size());
    }

    static bool MapCheckFreeElementsAndReorganise(size_t numElementsOnTile, size_t numNewElements)
    {
        // Check hard cap on num in use tiles (this would be the size of _tileElements immediately after a reorg)
        if (_tileElementsInUse + numNewElements > kMaxTileElements)
        {
            return false;
        }

        auto& gameState = getGameState();
        auto totalElementsRequired = numElementsOnTile + numNewElements;
        auto freeElements = gameState.tileElements.capacity() - gameState.tileElements.size();
        if (freeElements >= totalElementsRequired)
        {
            return true;
        }

        // if space issue is due to fragmentation then Reorg Tiles without increasing capacity
        if (gameState.tileElements.size() > totalElementsRequired + _tileElementsInUse)
        {
            ReorganiseTileElements();
            // This check is not expected to fail
            freeElements = gameState.tileElements.capacity() - gameState.tileElements.size();
            if (freeElements >= totalElementsRequired)
            {
                return true;
            }
        }

        // Capacity must increase to handle the space (Note capacity can go above kMaxTileElements)
        auto newCapacity = gameState.tileElements.capacity() * 2;
        ReorganiseTileElements(newCapacity);
        return true;
    }

    static size_t CountElementsOnTile(const CoordsXY& loc);

    bool MapCheckCapacityAndReorganise(const CoordsXY& loc, size_t numElements)
    {
        auto numElementsOnTile = CountElementsOnTile(loc);
        return MapCheckFreeElementsAndReorganise(numElementsOnTile, numElements);
    }

    static void ClearElementsAt(const CoordsXY& loc);

    void TileElementIteratorBegin(TileElementIterator* it)
    {
        it->x = 1;
        it->y = 1;
        it->element = MapGetFirstElementAt(TileCoordsXY{ 1, 1 });
    }

    int32_t TileElementIteratorNext(TileElementIterator* it)
    {
        if (it->element == nullptr)
        {
            it->element = MapGetFirstElementAt(TileCoordsXY{ it->x, it->y });
            return it->element == nullptr ? 0 : 1;
        }

        if (!it->element->isLastForTile())
        {
            it->element++;
            return 1;
        }

        auto& gameState = getGameState();
        if (it->y < (gameState.mapSize.y - 2))
        {
            it->y++;
            it->element = MapGetFirstElementAt(TileCoordsXY{ it->x, it->y });
            return it->element == nullptr ? 0 : 1;
        }

        if (it->x < (gameState.mapSize.x - 2))
        {
            it->y = 1;
            it->x++;
            it->element = MapGetFirstElementAt(TileCoordsXY{ it->x, it->y });
            return it->element == nullptr ? 0 : 1;
        }

        return 0;
    }

    void TileElementIteratorRestartForTile(TileElementIterator* it)
    {
        it->element = nullptr;
    }

    static bool IsTileLocationValid(const TileCoordsXY& coords)
    {
        const bool is_x_valid = coords.x < kMaximumMapSizeTechnical && coords.x >= 0;
        const bool is_y_valid = coords.y < kMaximumMapSizeTechnical && coords.y >= 0;
        return is_x_valid && is_y_valid;
    }

    TileElement* MapGetFirstElementAt(const TileCoordsXY& tilePos)
    {
        if (!IsTileLocationValid(tilePos))
        {
            LOG_VERBOSE("Trying to access element outside of range");
            return nullptr;
        }
        return _tileIndex.GetFirstElementAt(tilePos);
    }

    TileElement* MapGetFirstElementAt(const CoordsXY& elementPos)
    {
        return MapGetFirstElementAt(TileCoordsXY{ elementPos });
    }

    TileElement* MapGetNthElementAt(const CoordsXY& coords, int32_t n)
    {
        TileElement* tileElement = MapGetFirstElementAt(coords);
        if (tileElement == nullptr)
        {
            return nullptr;
        }
        // Iterate through elements on this tile. This has to be walked, rather than
        // jumped directly to, because n may exceed element count for given tile,
        // and the order of tiles (unlike elements) is not synced over multiplayer.
        while (n >= 0)
        {
            if (n == 0)
            {
                return tileElement;
            }
            if (tileElement->isLastForTile())
            {
                break;
            }
            tileElement++;
            n--;
        }
        // The element sought for is not within given tile.
        return nullptr;
    }

    TileElement* MapGetFirstTileElementWithBaseHeightBetween(const TileCoordsXYRangedZ& loc, TileElementType type)
    {
        TileElement* tileElement = MapGetFirstElementAt(loc);
        if (tileElement == nullptr)
            return nullptr;
        do
        {
            if (tileElement->getType() != type)
                continue;
            if (tileElement->baseHeight >= loc.baseZ && tileElement->baseHeight <= loc.clearanceZ)
                return tileElement;
        } while (!(tileElement++)->isLastForTile());

        return nullptr;
    }

    void MapSetTileElement(const TileCoordsXY& tilePos, TileElement* elements)
    {
        if (!MapIsLocationValid(tilePos.ToCoordsXY()))
        {
            LOG_ERROR("Trying to access element outside of range");
            return;
        }
        _tileIndex.SetTile(tilePos, elements);
    }

    SurfaceElement* MapGetSurfaceElementAt(const TileCoordsXY& coords)
    {
        auto view = TileElementsView<SurfaceElement>(coords);

        return *view.begin();
    }

    SurfaceElement* MapGetSurfaceElementAt(const CoordsXY& coords)
    {
        return MapGetSurfaceElementAt(TileCoordsXY{ coords });
    }

    PathElement* MapGetPathElementAt(const TileCoordsXYZ& loc)
    {
        for (auto* element : TileElementsView<PathElement>(loc.ToCoordsXY()))
        {
            if (element->isGhost())
                continue;
            if (element->baseHeight != loc.z)
                continue;
            return element;
        }
        return nullptr;
    }

    BannerElement* MapGetBannerElementAt(const CoordsXYZ& bannerPos, uint8_t position)
    {
        const auto bannerTilePos = TileCoordsXYZ{ bannerPos };
        for (auto* element : TileElementsView<BannerElement>(bannerPos))
        {
            if (element->baseHeight != bannerTilePos.z)
                continue;
            if (element->getPosition() != position)
                continue;
            return element;
        }
        return nullptr;
    }

    /**
     *
     *  rct2: 0x0068AB4C
     */
    void MapInit(const TileCoordsXY& size)
    {
        auto numTiles = kMaximumMapSizeTechnical * kMaximumMapSizeTechnical;

        auto& gameState = getGameState();
        SetTileElements(gameState, std::vector<TileElement>(numTiles, GetDefaultSurfaceElement()));

        gameState.grassSceneryTileLoopPosition = 0;
        gameState.widePathTileLoopPosition = {};
        gameState.mapSize = size;
        MapRemoveOutOfRangeElements();
        MapAnimations::ClearAll();

        auto intent = Intent(INTENT_ACTION_MAP);
        ContextBroadcastIntent(&intent);
    }

    /**
     * Counts the number of surface tiles that offer land ownership rights for sale,
     * but haven't been bought yet. It updates gLandRemainingOwnershipSales and
     * gLandRemainingConstructionSales.
     */
    void MapCountRemainingLandRights()
    {
        gLandRemainingOwnershipSales = 0;
        gLandRemainingConstructionSales = 0;
        auto& gameState = getGameState();

        for (int32_t y = 0; y < gameState.mapSize.y; y++)
        {
            for (int32_t x = 0; x < gameState.mapSize.x; x++)
            {
                auto* surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
                // Surface elements are sometimes hacked out to save some space for other map elements
                if (surfaceElement == nullptr)
                {
                    continue;
                }

                auto flags = surfaceElement->getOwnership();

                // Do not combine this condition with (flags & OwnershipFlag::forSale)
                // As some RCT1 parks have owned tiles with the 'construction rights available' flag also set
                if (!flags.has(OwnershipFlag::landOwned))
                {
                    if (flags.has(OwnershipFlag::landForSale))
                    {
                        gLandRemainingOwnershipSales++;
                    }
                    else if (
                        flags.has(OwnershipFlag::constructionRightsForSale)
                        && !flags.has(OwnershipFlag::constructionRightsOwned))
                    {
                        gLandRemainingConstructionSales++;
                    }
                }
            }
        }
    }

    /**
     * This is meant to strip TILE_ELEMENT_FLAG_GHOST flag from all elements when
     * importing a park.
     *
     * This can only exist in hacked parks, as we remove ghost elements while saving.
     *
     * This is less invasive than removing ghost elements themselves, as they can
     * contain valid data.
     */
    void MapStripGhostFlagFromElements()
    {
        auto& gameState = getGameState();
        for (auto& element : gameState.tileElements)
        {
            element.setGhost(false);
        }
    }

    /**
     * Return the absolute height of an element, given its (x,y) coordinates
     *
     * ax: x
     * cx: y
     * dx: return remember to & with 0xFFFF if you don't want water affecting results
     *  rct2: 0x00662783
     */
    int16_t TileElementHeight(const CoordsXY& loc)
    {
        // Off the map
        if (!MapIsLocationValid(loc))
            return kMinimumLandZ;

        // Get the surface element for the tile
        auto surfaceElement = MapGetSurfaceElementAt(loc);

        if (surfaceElement == nullptr)
        {
            return kMinimumLandZ;
        }

        auto height = surfaceElement->getBaseZ();
        auto slope = surfaceElement->getSlope();

        return TileElementHeight(CoordsXYZ{ loc, height }, slope);
    }

    int16_t TileElementHeight(const CoordsXYZ& loc, uint8_t slope)
    {
        // Off the map
        if (!MapIsLocationValid(loc))
            return kMinimumLandZ;

        auto height = loc.z;

        uint8_t extra_height = (slope & kTileSlopeDiagonalFlag) >> 4; // 0x10 is the 5th bit - sets slope to double height
        // Remove the extra height bit
        slope &= kTileSlopeRaisedCornersMask;

        int8_t quad = 0, quad_extra = 0; // which quadrant the element is in?
                                         // quad_extra is for extra height tiles

        // coordinates across this tile
        uint8_t xl = loc.x & 0x1f;
        uint8_t yl = loc.y & 0x1f;

        // Slope logic:
        // Each of the four bits in slope represents that corner being raised
        // slope == 15 (all four bits) is not used and slope == 0 is flat
        // If the extra_height bit is set, then the slope goes up two z-levels

        // We arbitrarily take the SW corner to be closest to the viewer

        // One corner up
        if (slope == kTileSlopeNCornerUp || slope == kTileSlopeECornerUp || slope == kTileSlopeSCornerUp
            || slope == kTileSlopeWCornerUp)
        {
            switch (slope)
            {
                case kTileSlopeNCornerUp:
                    quad = xl + yl - kCoordsXYStep;
                    break;
                case kTileSlopeECornerUp:
                    quad = xl - yl;
                    break;
                case kTileSlopeSCornerUp:
                    quad = kCoordsXYStep - yl - xl;
                    break;
                case kTileSlopeWCornerUp:
                    quad = yl - xl;
                    break;
            }
            // If the element is in the quadrant with the slope, raise its height
            if (quad > 0)
            {
                height += quad / 2;
            }
        }

        // One side up
        switch (slope)
        {
            case kTileSlopeNESideUp:
                height += xl / 2;
                break;
            case kTileSlopeSESideUp:
                height += (kCoordsXYStep - yl) / 2;
                break;
            case kTileSlopeNWSideUp:
                height += yl / 2;
                break;
            case kTileSlopeSWSideUp:
                height += (kCoordsXYStep - xl) / 2;
                break;
        }

        // One corner down
        if ((slope == kTileSlopeWCornerDown) || (slope == kTileSlopeSCornerDown) || (slope == kTileSlopeECornerDown)
            || (slope == kTileSlopeNCornerDown))
        {
            switch (slope)
            {
                case kTileSlopeWCornerDown:
                    quad_extra = xl + kCoordsXYStep - yl;
                    quad = xl - yl;
                    break;
                case kTileSlopeSCornerDown:
                    quad_extra = xl + yl;
                    quad = xl + yl - kCoordsXYStep;
                    break;
                case kTileSlopeECornerDown:
                    quad_extra = kCoordsXYStep - xl + yl;
                    quad = yl - xl;
                    break;
                case kTileSlopeNCornerDown:
                    quad_extra = (kCoordsXYStep - xl) + (kCoordsXYStep - yl);
                    quad = kCoordsXYStep - yl - xl;
                    break;
            }

            if (extra_height)
            {
                height += quad_extra / 2;
                return height;
            }
            // This tile is essentially at the next height level
            height += kLandHeightStep;
            // so we move *down* the slope
            if (quad < 0)
            {
                height += quad / 2;
            }
        }

        // Valleys
        if ((slope == kTileSlopeWEValley) || (slope == kTileSlopeNSValley))
        {
            switch (slope)
            {
                case kTileSlopeWEValley:
                    quad = std::abs(xl - yl);
                    break;
                case kTileSlopeNSValley:
                    quad = std::abs(xl + yl - kCoordsXYStep);
                    break;
            }
            height += quad / 2;
        }

        return height;
    }

    int16_t TileElementWaterHeight(const CoordsXY& loc)
    {
        // Off the map
        if (!MapIsLocationValid(loc))
            return 0;

        // Get the surface element for the tile
        auto surfaceElement = MapGetSurfaceElementAt(loc);

        if (surfaceElement == nullptr)
        {
            return 0;
        }

        return surfaceElement->getWaterHeight();
    }

    /**
     * Checks if the tile at coordinate at height counts as connected.
     * @return 1 if connected, 0 otherwise
     */
    bool MapCoordIsConnected(const TileCoordsXYZ& loc, uint8_t faceDirection)
    {
        TileElement* tileElement = MapGetFirstElementAt(loc);

        if (tileElement == nullptr)
            return false;

        do
        {
            if (tileElement->getType() != TileElementType::path)
                continue;

            uint8_t slopeDirection = tileElement->asPath()->getSlopeDirection();

            if (tileElement->asPath()->isSloped())
            {
                if (slopeDirection == faceDirection)
                {
                    if (loc.z == tileElement->baseHeight + 2)
                        return true;
                }
                else if (DirectionReverse(slopeDirection) == faceDirection && loc.z == tileElement->baseHeight)
                {
                    return true;
                }
            }
            else
            {
                if (loc.z == tileElement->baseHeight && (tileElement->asPath()->getEdges() & (1 << faceDirection)))
                    return true;
            }
        } while (!(tileElement++)->isLastForTile());

        return false;
    }

    /**
     *
     *  rct2: 0x006A876D
     */
    void MapUpdatePathWideFlags()
    {
        PROFILED_FUNCTION();

        if (isInTrackDesignerOrManager())
        {
            return;
        }

        const int32_t kPracticalMapSizeBigX = getGameState().mapSize.x * kCoordsXYStep;
        const int32_t kPracticalMapSizeBigY = getGameState().mapSize.y * kCoordsXYStep;

        // Presumably update_path_wide_flags is too computationally expensive to call for every
        // tile every update, so gWidePathTileLoopX and gWidePathTileLoopY store the x and y
        // progress. A maximum of 128 calls is done per update.
        CoordsXY& loopPosition = getGameState().widePathTileLoopPosition;
        for (int32_t i = 0; i < 128; i++)
        {
            FootpathUpdatePathWideFlags(loopPosition);

            // Next x, y tile
            loopPosition.x += kCoordsXYStep;
            if (loopPosition.x >= kPracticalMapSizeBigX)
            {
                loopPosition.x = 0;
                loopPosition.y += kCoordsXYStep;
                if (loopPosition.y >= kPracticalMapSizeBigY)
                {
                    loopPosition.y = 0;
                }
            }
        }
    }

    /**
     *
     *  rct2: 0x006A7B84
     */
    int32_t MapHeightFromSlope(const CoordsXY& coords, int32_t slopeDirection, bool isSloped)
    {
        if (!isSloped)
            return 0;

        switch (slopeDirection % kNumOrthogonalDirections)
        {
            case TILE_ELEMENT_DIRECTION_WEST:
                return (31 - (coords.x & 31)) / 2;
            case TILE_ELEMENT_DIRECTION_NORTH:
                return (coords.y & 31) / 2;
            case TILE_ELEMENT_DIRECTION_EAST:
                return (coords.x & 31) / 2;
            case TILE_ELEMENT_DIRECTION_SOUTH:
                return (31 - (coords.y & 31)) / 2;
        }
        return 0;
    }

    bool MapIsLocationValid(const CoordsXY& coords)
    {
        const bool is_x_valid = coords.x < kMaximumMapSizeBig && coords.x >= 0;
        const bool is_y_valid = coords.y < kMaximumMapSizeBig && coords.y >= 0;
        return is_x_valid && is_y_valid;
    }

    bool MapIsEdge(const CoordsXY& coords)
    {
        auto mapSizeUnits = GetMapSizeUnits();
        return (coords.x < 32 || coords.y < 32 || coords.x >= mapSizeUnits.x || coords.y >= mapSizeUnits.y);
    }

    bool MapCanBuildAt(const CoordsXYZ& loc)
    {
        if (gLegacyScene == LegacyScene::scenarioEditor)
            return true;
        if (getGameState().cheats.sandboxMode)
            return true;
        if (MapIsLocationOwned(loc))
            return true;
        return false;
    }

    /**
     *
     *  rct2: 0x00664F72
     */
    bool MapIsLocationOwned(const CoordsXYZ& loc)
    {
        // This check is to avoid throwing lots of messages in logs.
        if (MapIsLocationValid(loc))
        {
            auto* surfaceElement = MapGetSurfaceElementAt(loc);
            if (surfaceElement != nullptr)
            {
                if (surfaceElement->hasOwnership(OwnershipFlag::landOwned))
                    return true;

                if (surfaceElement->hasOwnership(OwnershipFlag::constructionRightsOwned))
                {
                    if (loc.z < surfaceElement->getBaseZ()
                        || loc.z >= surfaceElement->getBaseZ() + kConstructionRightsClearanceBig)
                        return true;
                }
            }
        }
        return false;
    }

    /**
     *
     *  rct2: 0x00664F2C
     */
    bool MapIsLocationInPark(const CoordsXY& coords)
    {
        if (MapIsLocationValid(coords))
        {
            auto surfaceElement = MapGetSurfaceElementAt(coords);
            if (surfaceElement == nullptr)
                return false;
            if (surfaceElement->hasOwnership(OwnershipFlag::landOwned))
                return true;
        }
        return false;
    }

    bool MapIsLocationOwnedOrHasRights(const CoordsXY& loc)
    {
        if (MapIsLocationValid(loc))
        {
            auto surfaceElement = MapGetSurfaceElementAt(loc);
            if (surfaceElement == nullptr)
            {
                return false;
            }
            if (surfaceElement->hasOwnership(OwnershipFlag::landOwned))
                return true;
            if (surfaceElement->hasOwnership(OwnershipFlag::constructionRightsOwned))
                return true;
        }
        return false;
    }

    bool MapIsLocationUnderground(const CoordsXYZ& loc)
    {
        const auto* surfaceElement = MapGetSurfaceElementAt(loc);
        if (surfaceElement == nullptr)
            return false;

        return surfaceElement->getClearanceZ() > loc.z;
    }

    int32_t MapGetCornerHeight(int32_t z, int32_t slope, int32_t direction)
    {
        switch (direction)
        {
            case 0:
                if (slope & kTileSlopeNCornerUp)
                {
                    z += 2;
                    if (slope == (kTileSlopeSCornerDown | kTileSlopeDiagonalFlag))
                    {
                        z += 2;
                    }
                }
                break;
            case 1:
                if (slope & kTileSlopeECornerUp)
                {
                    z += 2;
                    if (slope == (kTileSlopeWCornerDown | kTileSlopeDiagonalFlag))
                    {
                        z += 2;
                    }
                }
                break;
            case 2:
                if (slope & kTileSlopeSCornerUp)
                {
                    z += 2;
                    if (slope == (kTileSlopeNCornerDown | kTileSlopeDiagonalFlag))
                    {
                        z += 2;
                    }
                }
                break;
            case 3:
                if (slope & kTileSlopeWCornerUp)
                {
                    z += 2;
                    if (slope == (kTileSlopeECornerDown | kTileSlopeDiagonalFlag))
                    {
                        z += 2;
                    }
                }
                break;
        }
        return z;
    }

    int32_t TileElementGetCornerHeight(const SurfaceElement* surfaceElement, int32_t direction)
    {
        int32_t z = surfaceElement->baseHeight;
        int32_t slope = surfaceElement->getSlope();
        return MapGetCornerHeight(z, slope, direction);
    }

    uint8_t MapGetLowestLandHeight(const MapRange& range)
    {
        uint8_t minHeight = 0xFF;
        for (auto tileCoords : Map::getClampedRange(range))
        {
            auto* surfaceElement = MapGetSurfaceElementAt(tileCoords);
            if (surfaceElement == nullptr || minHeight <= surfaceElement->baseHeight)
                continue;

            if (gLegacyScene != LegacyScene::scenarioEditor && !getGameState().cheats.sandboxMode)
            {
                if (!MapIsLocationInPark(tileCoords.ToCoordsXY()))
                    continue;
            }

            minHeight = surfaceElement->baseHeight;
        }
        return minHeight;
    }

    uint8_t MapGetHighestLandHeight(const MapRange& range)
    {
        uint8_t maxHeight = 0;
        for (auto tileCoords : Map::getClampedRange(range))
        {
            auto* surfaceElement = MapGetSurfaceElementAt(tileCoords);
            if (surfaceElement == nullptr)
                continue;

            if (gLegacyScene != LegacyScene::scenarioEditor && !getGameState().cheats.sandboxMode)
            {
                if (!MapIsLocationInPark(tileCoords.ToCoordsXY()))
                    continue;
            }

            uint8_t height = surfaceElement->baseHeight;
            if (surfaceElement->getSlope() & kTileSlopeRaisedCornersMask)
                height += 2;
            if (surfaceElement->getSlope() & kTileSlopeDiagonalFlag)
                height += 2;

            if (maxHeight < height)
                maxHeight = height;
        }
        return maxHeight;
    }

    /**
     *
     *  rct2: 0x0068B280
     */
    void TileElementRemove(TileElement* tileElement)
    {
        // Replace Nth element by (N+1)th element.
        // This loop will make tileElement point to the old last element position,
        // after copy it to it's new position
        if (!tileElement->isLastForTile())
        {
            do
            {
                *tileElement = *(tileElement + 1);
            } while (!(++tileElement)->isLastForTile());
        }

        // Mark the latest element with the last element flag.
        (tileElement - 1)->setLastForTile(true);
        tileElement->baseHeight = kMaxTileElementHeight;
        _tileElementsInUse--;
        auto& gameState = getGameState();
        if (tileElement == &gameState.tileElements.back())
        {
            gameState.tileElements.pop_back();
        }
    }

    /**
     *
     *  rct2: 0x00675A8E
     */
    void MapRemoveAllRides()
    {
        TileElementIterator it;

        TileElementIteratorBegin(&it);
        do
        {
            switch (it.element->getType())
            {
                case TileElementType::path:
                    if (it.element->asPath()->isQueue())
                    {
                        it.element->asPath()->setHasQueueBanner(false);
                        it.element->asPath()->setRideIndex(RideId::GetNull());
                    }
                    break;
                case TileElementType::entrance:
                    if (it.element->asEntrance()->getEntranceType() == EntranceType::parkEntrance)
                        break;
                    [[fallthrough]];
                case TileElementType::track:
                    FootpathQueueChainReset();
                    FootpathRemoveEdgesAt(TileCoordsXY{ it.x, it.y }.ToCoordsXY(), it.element);
                    TileElementRemove(it.element);
                    TileElementIteratorRestartForTile(&it);
                    break;
                default:
                    break;
            }
        } while (TileElementIteratorNext(&it));
    }

    void MapGetBoundingBox(const MapRange& _range, int32_t* left, int32_t* top, int32_t* right, int32_t* bottom)
    {
        uint32_t rotation = GetCurrentRotation();
        const std::array corners{
            CoordsXY{ _range.GetX1(), _range.GetY1() },
            CoordsXY{ _range.GetX2(), _range.GetY1() },
            CoordsXY{ _range.GetX2(), _range.GetY2() },
            CoordsXY{ _range.GetX1(), _range.GetY2() },
        };

        *left = std::numeric_limits<int32_t>::max();
        *top = std::numeric_limits<int32_t>::max();
        *right = std::numeric_limits<int32_t>::min();
        *bottom = std::numeric_limits<int32_t>::min();

        for (const auto& corner : corners)
        {
            auto screenCoord = Translate3DTo2DWithZ(rotation, CoordsXYZ{ corner, 0 });
            if (screenCoord.x < *left)
                *left = screenCoord.x;
            if (screenCoord.x > *right)
                *right = screenCoord.x;
            if (screenCoord.y > *bottom)
                *bottom = screenCoord.y;
            if (screenCoord.y < *top)
                *top = screenCoord.y;
        }
    }

    static size_t CountElementsOnTile(const CoordsXY& loc)
    {
        size_t count = 0;
        auto* element = _tileIndex.GetFirstElementAt(TileCoordsXY(loc));
        do
        {
            count++;
        } while (!(element++)->isLastForTile());
        return count;
    }

    static TileElement* AllocateTileElements(size_t numElementsOnTile, size_t numNewElements)
    {
        if (!MapCheckFreeElementsAndReorganise(numElementsOnTile, numNewElements))
        {
            LOG_ERROR("Cannot insert new element");
            return nullptr;
        }

        auto& gameState = getGameState();
        auto oldSize = gameState.tileElements.size();
        gameState.tileElements.resize(gameState.tileElements.size() + numElementsOnTile + numNewElements);
        _tileElementsInUse += numNewElements;
        return &gameState.tileElements[oldSize];
    }

    /**
     *
     *  rct2: 0x0068B1F6
     */
    TileElement* TileElementInsert(const CoordsXYZ& loc, int32_t occupiedQuadrants, TileElementType type)
    {
        const auto& tileLoc = TileCoordsXYZ(loc);

        auto numElementsOnTileOld = CountElementsOnTile(loc);
        auto* newTileElement = AllocateTileElements(numElementsOnTileOld, 1);
        auto* originalTileElement = _tileIndex.GetFirstElementAt(tileLoc);
        if (newTileElement == nullptr)
        {
            return nullptr;
        }

        // Set tile index pointer to point to new element block
        _tileIndex.SetTile(tileLoc, newTileElement);

        bool isLastForTile = false;
        if (originalTileElement == nullptr)
        {
            isLastForTile = true;
        }
        else
        {
            // Copy all elements that are below the insert height
            while (loc.z >= originalTileElement->getBaseZ())
            {
                // Copy over map element
                *newTileElement = *originalTileElement;
                originalTileElement->baseHeight = kMaxTileElementHeight;
                originalTileElement++;
                newTileElement++;

                if ((newTileElement - 1)->isLastForTile())
                {
                    // No more elements above the insert element
                    (newTileElement - 1)->setLastForTile(false);
                    isLastForTile = true;
                    break;
                }
            }
        }

        // Insert new map element
        auto* insertedElement = newTileElement;
        newTileElement->type = 0;
        newTileElement->setType(type);
        newTileElement->setBaseZ(loc.z);
        newTileElement->flags = 0;
        newTileElement->setLastForTile(isLastForTile);
        newTileElement->setOccupiedQuadrants(occupiedQuadrants);
        newTileElement->setClearanceZ(loc.z);
        newTileElement->owner = 0;
        std::memset(&newTileElement->pad05, 0, sizeof(newTileElement->pad05));
        std::memset(&newTileElement->pad08, 0, sizeof(newTileElement->pad08));
        newTileElement++;

        // Insert rest of map elements above insert height
        if (!isLastForTile)
        {
            do
            {
                // Copy over map element
                *newTileElement = *originalTileElement;
                originalTileElement->baseHeight = kMaxTileElementHeight;
                originalTileElement++;
                newTileElement++;
            } while (!((newTileElement - 1)->isLastForTile()));
        }

        return insertedElement;
    }

    /**
     * Updates grass length, scenery age and jumping fountains.
     *
     *  rct2: 0x006646E1
     */
    void MapUpdateTiles()
    {
        PROFILED_FUNCTION();

        if (isInEditorMode())
            return;

        auto& gameState = getGameState();

        // Update 43 more tiles (for each 256x256 block)
        for (int32_t j = 0; j < 43; j++)
        {
            int32_t x = 0;
            int32_t y = 0;

            uint16_t interleaved_xy = gameState.grassSceneryTileLoopPosition;
            for (int32_t i = 0; i < 8; i++)
            {
                x = (x << 1) | (interleaved_xy & 1);
                interleaved_xy >>= 1;
                y = (y << 1) | (interleaved_xy & 1);
                interleaved_xy >>= 1;
            }

            // Repeat for each 256x256 block on the map
            for (int32_t blockY = 0; blockY < gameState.mapSize.y; blockY += 256)
            {
                for (int32_t blockX = 0; blockX < gameState.mapSize.x; blockX += 256)
                {
                    auto mapPos = TileCoordsXY{ blockX + x, blockY + y }.ToCoordsXY();
                    if (MapIsEdge(mapPos))
                        continue;

                    auto* surfaceElement = MapGetSurfaceElementAt(mapPos);
                    if (surfaceElement != nullptr)
                    {
                        surfaceElement->updateGrassLength(mapPos);
                        SceneryUpdateTile(mapPos);
                    }
                }
            }

            gameState.grassSceneryTileLoopPosition++;
        }
    }

    /**
     * Removes elements that are out of the map size range and crops the park perimeter.
     *  rct2: 0x0068ADBC
     */
    void MapRemoveOutOfRangeElements()
    {
        auto mapSizeMax = GetMapSizeMaxXY();

        // Ensure that we can remove elements
        //
        // NOTE: This is only a workaround for non-networked games.
        // Map resize has to become its own Game Action to properly solve this issue.
        //
        auto& gameState = getGameState();
        bool buildState = gameState.cheats.buildInPauseMode;
        gameState.cheats.buildInPauseMode = true;

        for (int32_t y = kMaximumMapSizeBig - kCoordsXYStep; y >= 0; y -= kCoordsXYStep)
        {
            for (int32_t x = kMaximumMapSizeBig - kCoordsXYStep; x >= 0; x -= kCoordsXYStep)
            {
                if (x == 0 || y == 0 || x >= mapSizeMax.x || y >= mapSizeMax.y)
                {
                    // Note this purposely does not use LandSetRightsAction as X Y coordinates are outside of normal range.
                    auto surfaceElement = MapGetSurfaceElementAt(CoordsXY{ x, y });
                    if (surfaceElement != nullptr)
                    {
                        surfaceElement->setOwnership(kUnowned);
                        Park::UpdateFencesAroundTile({ x, y });
                    }
                    ClearElementsAt({ x, y });
                }
            }
        }

        // Reset cheat state
        gameState.cheats.buildInPauseMode = buildState;
    }

    static void MapExtendBoundarySurfaceExtendTile(
        const SurfaceElement& sourceTile, SurfaceElement& destTile, const Direction direction)
    {
        destTile.setSurfaceObjectIndex(sourceTile.getSurfaceObjectIndex());
        destTile.setEdgeObjectIndex(sourceTile.getEdgeObjectIndex());
        destTile.setGrassLength(sourceTile.getGrassLength());
        destTile.setOwnership(kUnowned);
        destTile.setWaterHeight(sourceTile.getWaterHeight());

        auto z = sourceTile.baseHeight;
        const auto originalSlope = Numerics::rol4(sourceTile.getSlope(), direction)
            | (sourceTile.getSlope() & kTileSlopeDiagonalFlag);
        auto slope = originalSlope & kTileSlopeNWSideUp;
        if (slope == kTileSlopeNWSideUp)
        {
            z += 2;
            slope = kTileSlopeFlat;
            if (originalSlope & kTileSlopeDiagonalFlag)
            {
                slope = kTileSlopeNCornerUp;
                if (originalSlope & kTileSlopeSCornerUp)
                {
                    slope = kTileSlopeWCornerUp;
                    if (originalSlope & kTileSlopeECornerUp)
                    {
                        slope = kTileSlopeFlat;
                    }
                }
            }
        }
        if (slope & kTileSlopeNCornerUp)
            slope |= kTileSlopeECornerUp;
        if (slope & kTileSlopeWCornerUp)
            slope |= kTileSlopeSCornerUp;

        destTile.setSlope(Numerics::ror4(slope, direction));
        destTile.baseHeight = z;
        destTile.clearanceHeight = z;
    }

    /**
     * Copies the terrain and slope from the Y edge of the map to the new tiles. Used when increasing the size of the map.
     */
    void MapExtendBoundarySurfaceY()
    {
        auto y = getGameState().mapSize.y - 2;
        for (auto x = 0; x < kMaximumMapSizeTechnical; x++)
        {
            auto existingTileElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y - 1 });
            auto newTileElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });

            if (existingTileElement != nullptr && newTileElement != nullptr)
            {
                MapExtendBoundarySurfaceExtendTile(*existingTileElement, *newTileElement, 0);
            }

            Park::UpdateFences({ x << 5, y << 5 });
        }
    }

    /**
     * Copies the terrain and slope from the X edge of the map to the new tiles. Used when increasing the size of the map.
     */
    void MapExtendBoundarySurfaceX()
    {
        auto x = getGameState().mapSize.x - 2;
        for (auto y = 0; y < kMaximumMapSizeTechnical; y++)
        {
            auto existingTileElement = MapGetSurfaceElementAt(TileCoordsXY{ x - 1, y });
            auto newTileElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
            if (existingTileElement != nullptr && newTileElement != nullptr)
            {
                MapExtendBoundarySurfaceExtendTile(*existingTileElement, *newTileElement, 3);
            }
            Park::UpdateFences({ x << 5, y << 5 });
        }
    }

    static void MapExtendBoundarySurfaceShiftX(const int32_t amount, const int32_t mapSizeY)
    {
        for (int32_t y = 1; y < mapSizeY - 1; y++)
        {
            for (int32_t x = amount; x > 0; x--)
            {
                const auto* const existingTileElement = MapGetSurfaceElementAt(TileCoordsXY{ x + 1, y });
                auto* const newTileElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
                if (existingTileElement != nullptr && newTileElement != nullptr)
                {
                    MapExtendBoundarySurfaceExtendTile(*existingTileElement, *newTileElement, 1);
                }
                Park::UpdateFences(TileCoordsXY{ x, y }.ToCoordsXY());
            }
        }
    }

    static void MapExtendBoundarySurfaceShiftY(const int32_t amount, const int32_t mapSizeX)
    {
        for (int32_t x = 1; x < mapSizeX - 1; x++)
        {
            for (int32_t y = amount; y > 0; y--)
            {
                const auto* const existingTileElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y + 1 });
                auto* const newTileElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
                if (existingTileElement != nullptr && newTileElement != nullptr)
                {
                    MapExtendBoundarySurfaceExtendTile(*existingTileElement, *newTileElement, 2);
                }
                Park::UpdateFences(TileCoordsXY{ x, y }.ToCoordsXY());
            }
        }
    }

    /**
     * Clears the provided element properly from a certain tile, and updates
     * the pointer (when needed) passed to this function to point to the next element.
     */
    void ClearElementAt(const CoordsXY& loc, TileElement** elementPtr)
    {
        auto& gameState = getGameState();

        TileElement* element = *elementPtr;
        switch (element->getType())
        {
            case TileElementType::surface:
                element->baseHeight = kMinimumLandHeight;
                element->clearanceHeight = kMinimumLandHeight;
                element->owner = 0;
                element->asSurface()->setSlope(kTileSlopeFlat);
                element->asSurface()->setSurfaceObjectIndex(0);
                element->asSurface()->setEdgeObjectIndex(0);
                element->asSurface()->setGrassLength(GRASS_LENGTH_CLEAR_0);
                element->asSurface()->setOwnership(kUnowned);
                element->asSurface()->setParkFences(0);
                element->asSurface()->setWaterHeight(0);
                // Because this element is not completely removed, the pointer must be updated manually
                // The rest of the elements are removed from the array, so the pointer doesn't need to be updated.
                (*elementPtr)++;
                break;
            case TileElementType::entrance:
            {
                int32_t rotation = element->getDirectionWithOffset(1);
                auto seqLoc = loc;
                switch (element->asEntrance()->getSequenceIndex())
                {
                    case ParkEntranceSequence::left:
                        seqLoc += CoordsDirectionDelta[rotation];
                        break;
                    case ParkEntranceSequence::right:
                        seqLoc -= CoordsDirectionDelta[rotation];
                        break;
                    default:
                        break;
                }
                auto parkEntranceRemoveAction = GameActions::ParkEntranceRemoveAction(CoordsXYZ{ seqLoc, element->getBaseZ() });
                auto result = GameActions::ExecuteNested(&parkEntranceRemoveAction, gameState);
                // If asking nicely did not work, forcibly remove this to avoid an infinite loop.
                if (result.error != GameActions::Status::ok)
                {
                    TileElementRemove(element);
                }
                break;
            }
            case TileElementType::wall:
            {
                CoordsXYZD wallLocation = { loc.x, loc.y, element->getBaseZ(), element->getDirection() };
                auto wallRemoveAction = GameActions::WallRemoveAction(wallLocation);
                auto result = GameActions::ExecuteNested(&wallRemoveAction, gameState);
                // If asking nicely did not work, forcibly remove this to avoid an infinite loop.
                if (result.error != GameActions::Status::ok)
                {
                    TileElementRemove(element);
                }
            }
            break;
            case TileElementType::largeScenery:
            {
                auto removeSceneryAction = GameActions::LargeSceneryRemoveAction(
                    { loc.x, loc.y, element->getBaseZ(), element->getDirection() },
                    element->asLargeScenery()->getSequenceIndex());
                auto result = GameActions::ExecuteNested(&removeSceneryAction, gameState);
                // If asking nicely did not work, forcibly remove this to avoid an infinite loop.
                if (result.error != GameActions::Status::ok)
                {
                    TileElementRemove(element);
                }
            }
            break;
            case TileElementType::banner:
            {
                auto bannerRemoveAction = GameActions::BannerRemoveAction(
                    { loc.x, loc.y, element->getBaseZ(), element->asBanner()->getPosition() });
                auto result = GameActions::ExecuteNested(&bannerRemoveAction, gameState);
                // If asking nicely did not work, forcibly remove this to avoid an infinite loop.
                if (result.error != GameActions::Status::ok)
                {
                    TileElementRemove(element);
                }
                break;
            }
            default:
                TileElementRemove(element);
                break;
        }
    }

    /**
     * Clears all elements properly from a certain tile.
     *  rct2: 0x0068AE2A
     */
    static void ClearElementsAt(const CoordsXY& loc)
    {
        auto& gameState = getGameState();
        // Remove the spawn point (if there is one in the current tile)
        gameState.peepSpawns.erase(
            std::remove_if(
                gameState.peepSpawns.begin(), gameState.peepSpawns.end(),
                [loc](const CoordsXY& spawn) { return spawn.ToTileStart() == loc.ToTileStart(); }),
            gameState.peepSpawns.end());

        TileElement* tileElement = MapGetFirstElementAt(loc);
        if (tileElement == nullptr)
            return;

        // Remove all elements except the last one
        while (!tileElement->isLastForTile())
            ClearElementAt(loc, &tileElement);

        // Remove the last element
        ClearElementAt(loc, &tileElement);
    }

    int32_t MapGetHighestZ(const CoordsXY& loc)
    {
        auto surfaceElement = MapGetSurfaceElementAt(loc);
        if (surfaceElement == nullptr)
            return -1;

        auto z = surfaceElement->getBaseZ();

        // Raise z so that is above highest point of land and water on tile
        if ((surfaceElement->getSlope() & kTileSlopeRaisedCornersMask) != kTileSlopeFlat)
            z += kLandHeightStep;
        if ((surfaceElement->getSlope() & kTileSlopeDiagonalFlag) != 0)
            z += kLandHeightStep;

        z = std::max(z, surfaceElement->getWaterHeight());
        return z;
    }

    LargeSceneryElement* MapGetLargeScenerySegment(const CoordsXYZD& sceneryPos, int32_t sequence)
    {
        TileElement* tileElement = MapGetFirstElementAt(sceneryPos);
        if (tileElement == nullptr)
        {
            return nullptr;
        }
        auto sceneryTilePos = TileCoordsXYZ{ sceneryPos };
        do
        {
            if (tileElement->getType() != TileElementType::largeScenery)
                continue;
            if (tileElement->baseHeight != sceneryTilePos.z)
                continue;
            if (tileElement->asLargeScenery()->getSequenceIndex() != sequence)
                continue;
            if ((tileElement->getDirection()) != sceneryPos.direction)
                continue;

            return tileElement->asLargeScenery();
        } while (!(tileElement++)->isLastForTile());
        return nullptr;
    }

    EntranceElement* MapGetParkEntranceElementAt(const CoordsXYZ& entranceCoords, bool ghost)
    {
        auto entranceTileCoords = TileCoordsXYZ(entranceCoords);
        TileElement* tileElement = MapGetFirstElementAt(entranceCoords);
        if (tileElement != nullptr)
        {
            do
            {
                if (tileElement->getType() != TileElementType::entrance)
                    continue;

                if (tileElement->baseHeight != entranceTileCoords.z)
                    continue;

                if (tileElement->asEntrance()->getEntranceType() != EntranceType::parkEntrance)
                    continue;

                if (!ghost && tileElement->isGhost())
                    continue;

                return tileElement->asEntrance();
            } while (!(tileElement++)->isLastForTile());
        }
        return nullptr;
    }

    EntranceElement* MapGetRideEntranceElementAt(const CoordsXYZ& entranceCoords, bool ghost)
    {
        auto entranceTileCoords = TileCoordsXYZ{ entranceCoords };
        TileElement* tileElement = MapGetFirstElementAt(entranceCoords);
        if (tileElement != nullptr)
        {
            do
            {
                if (tileElement->getType() != TileElementType::entrance)
                    continue;

                if (tileElement->baseHeight != entranceTileCoords.z)
                    continue;

                if (tileElement->asEntrance()->getEntranceType() != EntranceType::rideEntrance)
                    continue;

                if (!ghost && tileElement->isGhost())
                    continue;

                return tileElement->asEntrance();
            } while (!(tileElement++)->isLastForTile());
        }
        return nullptr;
    }

    EntranceElement* MapGetRideExitElementAt(const CoordsXYZ& exitCoords, bool ghost)
    {
        auto exitTileCoords = TileCoordsXYZ{ exitCoords };
        TileElement* tileElement = MapGetFirstElementAt(exitCoords);
        if (tileElement != nullptr)
        {
            do
            {
                if (tileElement->getType() != TileElementType::entrance)
                    continue;

                if (tileElement->baseHeight != exitTileCoords.z)
                    continue;

                if (tileElement->asEntrance()->getEntranceType() != EntranceType::rideExit)
                    continue;

                if (!ghost && tileElement->isGhost())
                    continue;

                return tileElement->asEntrance();
            } while (!(tileElement++)->isLastForTile());
        }
        return nullptr;
    }

    SmallSceneryElement* MapGetSmallSceneryElementAt(const CoordsXYZ& sceneryCoords, int32_t type, uint8_t quadrant)
    {
        auto sceneryTileCoords = TileCoordsXYZ{ sceneryCoords };
        TileElement* tileElement = MapGetFirstElementAt(sceneryCoords);
        if (tileElement != nullptr)
        {
            do
            {
                if (tileElement->getType() != TileElementType::smallScenery)
                    continue;
                if (tileElement->asSmallScenery()->getSceneryQuadrant() != quadrant)
                    continue;
                if (tileElement->baseHeight != sceneryTileCoords.z)
                    continue;
                if (tileElement->asSmallScenery()->getEntryIndex() != type)
                    continue;

                return tileElement->asSmallScenery();
            } while (!(tileElement++)->isLastForTile());
        }
        return nullptr;
    }

    std::optional<CoordsXYZ> MapLargeSceneryGetOrigin(
        const CoordsXYZD& sceneryPos, int32_t sequence, LargeSceneryElement** outElement)
    {
        auto tileElement = MapGetLargeScenerySegment(sceneryPos, sequence);
        if (tileElement == nullptr)
            return std::nullopt;

        auto* sceneryEntry = tileElement->getEntry();
        auto& tile = sceneryEntry->tiles[sequence];

        CoordsXY offsetPos{ tile.offset };
        auto rotatedOffsetPos = offsetPos.Rotate(sceneryPos.direction);

        auto origin = CoordsXYZ{ sceneryPos.x - rotatedOffsetPos.x, sceneryPos.y - rotatedOffsetPos.y,
                                 sceneryPos.z - tile.offset.z };
        if (outElement != nullptr)
            *outElement = tileElement;
        return origin;
    }

    /**
     *
     *  rct2: 0x006B9B05
     */
    bool MapLargeScenerySignSetColour(
        const CoordsXYZD& signPos, int32_t sequence, Drawing::Colour mainColour, Drawing::Colour textColour)
    {
        LargeSceneryElement* tileElement;

        auto sceneryOrigin = MapLargeSceneryGetOrigin(signPos, sequence, &tileElement);
        if (!sceneryOrigin)
        {
            return false;
        }

        auto* sceneryEntry = tileElement->getEntry();

        // Iterate through each tile of the large scenery element
        for (auto& tile : sceneryEntry->tiles)
        {
            CoordsXY offsetPos{ tile.offset };
            auto rotatedOffsetPos = offsetPos.Rotate(signPos.direction);

            auto tmpSignPos = CoordsXYZD{ sceneryOrigin->x + rotatedOffsetPos.x, sceneryOrigin->y + rotatedOffsetPos.y,
                                          sceneryOrigin->z + tile.offset.z, signPos.direction };
            tileElement = MapGetLargeScenerySegment(tmpSignPos, tile.index);
            if (tileElement != nullptr)
            {
                tileElement->setPrimaryColour(mainColour);
                tileElement->setSecondaryColour(textColour);

                MapInvalidateTile({ tmpSignPos, tileElement->getBaseZ(), tileElement->getClearanceZ() });
            }
        }

        return true;
    }

    static void MapInvalidateTileUnderZoom(int32_t x, int32_t y, int32_t z0, int32_t z1, ZoomLevel maxZoom)
    {
        if (gOpenRCT2Headless)
            return;

        ViewportsInvalidate(x, y, z0, z1, maxZoom);
    }

    /**
     *
     *  rct2: 0x006EC847
     */
    void MapInvalidateTile(const CoordsXYRangedZ& tilePos)
    {
        MapInvalidateTileUnderZoom(tilePos.x, tilePos.y, tilePos.baseZ, tilePos.clearanceZ, ZoomLevel{ -1 });
    }

    /**
     *
     *  rct2: 0x006ECB60
     */
    void MapInvalidateTileZoom1(const CoordsXYRangedZ& tilePos)
    {
        MapInvalidateTileUnderZoom(tilePos.x, tilePos.y, tilePos.baseZ, tilePos.clearanceZ, ZoomLevel{ 1 });
    }

    /**
     *
     *  rct2: 0x006EC9CE
     */
    void MapInvalidateTileZoom0(const CoordsXYRangedZ& tilePos)
    {
        MapInvalidateTileUnderZoom(tilePos.x, tilePos.y, tilePos.baseZ, tilePos.clearanceZ, ZoomLevel{ 0 });
    }

    /**
     *
     *  rct2: 0x006EC6D7
     */
    void MapInvalidateTileFull(const CoordsXY& tilePos)
    {
        MapInvalidateTile({ tilePos, 0, 2080 });
    }

    void MapInvalidateElement(const CoordsXY& elementPos, TileElement* tileElement)
    {
        MapInvalidateTile({ elementPos, tileElement->getBaseZ(), tileElement->getClearanceZ() });
    }

    void MapInvalidateRegion(const CoordsXY& mins, const CoordsXY& maxs)
    {
        int32_t x0 = mins.x + 16;
        int32_t y0 = mins.y + 16;
        int32_t x1 = maxs.x + 16;
        int32_t y1 = maxs.y + 16;
        int32_t left, right, top, bottom;

        MapGetBoundingBox({ x0, y0, x1, y1 }, &left, &top, &right, &bottom);

        left -= 32;
        right += 32;
        bottom += 32;
        top -= 32 + 2080;

        ViewportsInvalidate({ { left, top }, { right, bottom } });
    }

    int32_t MapGetTileSide(const CoordsXY& mapPos)
    {
        int32_t subMapX = mapPos.x & (32 - 1);
        int32_t subMapY = mapPos.y & (32 - 1);
        return (subMapX < subMapY) ? ((subMapX + subMapY) < 32 ? 0 : 1) : ((subMapX + subMapY) < 32 ? 3 : 2);
    }

    int32_t MapGetTileQuadrant(const CoordsXY& mapPos)
    {
        int32_t subMapX = mapPos.x & (32 - 1);
        int32_t subMapY = mapPos.y & (32 - 1);
        return (subMapX > 16) ? (subMapY < 16 ? 1 : 0) : (subMapY < 16 ? 2 : 3);
    }

    /**
     *
     *  rct2: 0x00693BFF
     */
    bool MapSurfaceIsBlocked(const CoordsXY& mapCoords)
    {
        if (!MapIsLocationValid(mapCoords))
            return true;

        auto surfaceElement = MapGetSurfaceElementAt(mapCoords);

        if (surfaceElement == nullptr)
        {
            return true;
        }

        if (surfaceElement->getWaterHeight() > surfaceElement->getBaseZ())
            return true;

        int16_t base_z = surfaceElement->baseHeight;
        int16_t clear_z = surfaceElement->baseHeight + 2;
        if (surfaceElement->getSlope() & kTileSlopeDiagonalFlag)
            clear_z += 2;

        auto tileElement = reinterpret_cast<TileElement*>(surfaceElement);
        while (!(tileElement++)->isLastForTile())
        {
            if (clear_z >= tileElement->clearanceHeight)
                continue;

            if (base_z < tileElement->baseHeight)
                continue;

            if (tileElement->getType() == TileElementType::path || tileElement->getType() == TileElementType::wall)
                continue;

            if (tileElement->getType() != TileElementType::smallScenery)
                return true;

            auto* sceneryEntry = tileElement->asSmallScenery()->getEntry();
            if (sceneryEntry == nullptr)
            {
                return false;
            }
            if (sceneryEntry->flags.has(SmallSceneryFlag::occupiesFullTile))
                return true;
        }
        return false;
    }

    /* Clears all map elements, to be used before generating a new map */
    void MapClearAllElements()
    {
        for (int32_t y = 0; y < kMaximumMapSizeBig; y += kCoordsXYStep)
        {
            for (int32_t x = 0; x < kMaximumMapSizeBig; x += kCoordsXYStep)
            {
                ClearElementsAt({ x, y });
            }
        }
    }

    /**
     * Gets the track element at x, y, z.
     * @param x x units, not tiles.
     * @param y y units, not tiles.
     * @param z Base height.
     */
    TrackElement* MapGetTrackElementAt(const CoordsXYZ& trackPos)
    {
        TileElement* tileElement = MapGetFirstElementAt(trackPos);
        if (tileElement == nullptr)
            return nullptr;
        do
        {
            if (tileElement->getType() != TileElementType::track)
                continue;
            if (tileElement->getBaseZ() != trackPos.z)
                continue;

            return tileElement->asTrack();
        } while (!(tileElement++)->isLastForTile());

        return nullptr;
    }

    /**
     * Gets the track element at x, y, z that is the given track type.
     * @param x x units, not tiles.
     * @param y y units, not tiles.
     * @param z Base height.
     */
    TileElement* MapGetTrackElementAtOfType(const CoordsXYZ& trackPos, TrackElemType trackType)
    {
        TileElement* tileElement = MapGetFirstElementAt(trackPos);
        if (tileElement == nullptr)
            return nullptr;
        auto trackTilePos = TileCoordsXYZ{ trackPos };
        do
        {
            if (tileElement->getType() != TileElementType::track)
                continue;
            if (tileElement->baseHeight != trackTilePos.z)
                continue;
            if (tileElement->asTrack()->getTrackType() != trackType)
                continue;

            return tileElement;
        } while (!(tileElement++)->isLastForTile());

        return nullptr;
    }

    /**
     * Gets the track element at x, y, z that is the given track type and sequence.
     * @param x x units, not tiles.
     * @param y y units, not tiles.
     * @param z Base height.
     */
    TileElement* MapGetTrackElementAtOfTypeSeq(const CoordsXYZ& trackPos, TrackElemType trackType, int32_t sequence)
    {
        TileElement* tileElement = MapGetFirstElementAt(trackPos);
        auto trackTilePos = TileCoordsXYZ{ trackPos };
        do
        {
            if (tileElement == nullptr)
                break;
            if (tileElement->getType() != TileElementType::track)
                continue;
            if (tileElement->baseHeight != trackTilePos.z)
                continue;
            if (tileElement->asTrack()->getTrackType() != trackType)
                continue;
            if (tileElement->asTrack()->getSequenceIndex() != sequence)
                continue;

            return tileElement;
        } while (!(tileElement++)->isLastForTile());

        return nullptr;
    }

    TrackElement* MapGetTrackElementAtOfType(const CoordsXYZD& location, TrackElemType trackType)
    {
        auto tileElement = MapGetFirstElementAt(location);
        if (tileElement != nullptr)
        {
            do
            {
                auto trackElement = tileElement->asTrack();
                if (trackElement != nullptr)
                {
                    if (trackElement->getBaseZ() != location.z)
                        continue;
                    if (trackElement->getDirection() != location.direction)
                        continue;
                    if (trackElement->getTrackType() != trackType)
                        continue;
                    return trackElement;
                }
            } while (!(tileElement++)->isLastForTile());
        }
        return nullptr;
    }

    TrackElement* MapGetTrackElementAtOfTypeSeq(const CoordsXYZD& location, TrackElemType trackType, int32_t sequence)
    {
        auto tileElement = MapGetFirstElementAt(location);
        if (tileElement != nullptr)
        {
            do
            {
                auto trackElement = tileElement->asTrack();
                if (trackElement != nullptr)
                {
                    if (trackElement->getBaseZ() != location.z)
                        continue;
                    if (trackElement->getDirection() != location.direction)
                        continue;
                    if (trackElement->getTrackType() != trackType)
                        continue;
                    if (trackElement->getSequenceIndex() != sequence)
                        continue;
                    return trackElement;
                }
            } while (!(tileElement++)->isLastForTile());
        }
        return nullptr;
    }

    /**
     * Gets the track element at x, y, z that is the given track type and sequence.
     * @param x x units, not tiles.
     * @param y y units, not tiles.
     * @param z Base height.
     */
    TileElement* MapGetTrackElementAtOfTypeFromRide(const CoordsXYZ& trackPos, TrackElemType trackType, RideId rideIndex)
    {
        TileElement* tileElement = MapGetFirstElementAt(trackPos);
        if (tileElement == nullptr)
            return nullptr;
        auto trackTilePos = TileCoordsXYZ{ trackPos };
        do
        {
            if (tileElement->getType() != TileElementType::track)
                continue;
            if (tileElement->baseHeight != trackTilePos.z)
                continue;
            if (tileElement->asTrack()->getRideIndex() != rideIndex)
                continue;
            if (tileElement->asTrack()->getTrackType() != trackType)
                continue;

            return tileElement;
        } while (!(tileElement++)->isLastForTile());

        return nullptr;
    }

    /**
     * Gets the track element at x, y, z that is the given track type and sequence.
     * @param x x units, not tiles.
     * @param y y units, not tiles.
     * @param z Base height.
     */
    TileElement* MapGetTrackElementAtFromRide(const CoordsXYZ& trackPos, RideId rideIndex)
    {
        TileElement* tileElement = MapGetFirstElementAt(trackPos);
        if (tileElement == nullptr)
            return nullptr;
        auto trackTilePos = TileCoordsXYZ{ trackPos };
        do
        {
            if (tileElement->getType() != TileElementType::track)
                continue;
            if (tileElement->baseHeight != trackTilePos.z)
                continue;
            if (tileElement->asTrack()->getRideIndex() != rideIndex)
                continue;

            return tileElement;
        } while (!(tileElement++)->isLastForTile());

        return nullptr;
    }

    TileElement* MapGetTrackElementAtBeforeSurfaceFromRide(const CoordsXYZ& trackPos, const RideId rideIndex)
    {
        TileElement* tileElement = MapGetFirstElementAt(trackPos);
        if (tileElement == nullptr)
            return nullptr;

        do
        {
            if (tileElement->getType() == TileElementType::surface)
            {
                return nullptr;
            }

            if (tileElement->getType() == TileElementType::track && tileElement->getBaseZ() == trackPos.z
                && tileElement->asTrack()->getRideIndex() == rideIndex)
            {
                return tileElement;
            }
        } while (!(tileElement++)->isLastForTile());

        return nullptr;
    }

    /**
     * Gets the track element at x, y, z that is the given track type and sequence.
     * @param x x units, not tiles.
     * @param y y units, not tiles.
     * @param z Base height.
     * @param direction The direction (0 - 3).
     */
    TileElement* MapGetTrackElementAtWithDirectionFromRide(const CoordsXYZD& trackPos, RideId rideIndex)
    {
        TileElement* tileElement = MapGetFirstElementAt(trackPos);
        if (tileElement == nullptr)
            return nullptr;
        auto trackTilePos = TileCoordsXYZ{ trackPos };
        do
        {
            if (tileElement->getType() != TileElementType::track)
                continue;
            if (tileElement->baseHeight != trackTilePos.z)
                continue;
            if (tileElement->asTrack()->getRideIndex() != rideIndex)
                continue;
            if (tileElement->getDirection() != trackPos.direction)
                continue;

            return tileElement;
        } while (!(tileElement++)->isLastForTile());

        return nullptr;
    }

    WallElement* MapGetWallElementAt(const CoordsXYRangedZ& coords)
    {
        auto tileElement = MapGetFirstElementAt(coords);

        if (tileElement != nullptr)
        {
            do
            {
                if (tileElement->getType() == TileElementType::wall && coords.baseZ < tileElement->getClearanceZ()
                    && coords.clearanceZ > tileElement->getBaseZ())
                {
                    return tileElement->asWall();
                }
            } while (!(tileElement++)->isLastForTile());
        }

        return nullptr;
    }

    WallElement* MapGetWallElementAt(const CoordsXYZD& wallCoords)
    {
        auto tileWallCoords = TileCoordsXYZ(wallCoords);
        TileElement* tileElement = MapGetFirstElementAt(wallCoords);
        if (tileElement == nullptr)
            return nullptr;
        do
        {
            if (tileElement->getType() != TileElementType::wall)
                continue;
            if (tileElement->baseHeight != tileWallCoords.z)
                continue;
            if (tileElement->getDirection() != wallCoords.direction)
                continue;

            return tileElement->asWall();
        } while (!(tileElement++)->isLastForTile());
        return nullptr;
    }

    MapRange ClampRangeWithinMap(const MapRange& range)
    {
        auto mapSizeMax = GetMapSizeMaxXY();
        auto aX = std::max<decltype(range.GetX1())>(kCoordsXYStep, range.GetX1());
        auto bX = std::min<decltype(range.GetX2())>(mapSizeMax.x, range.GetX2());
        auto aY = std::max<decltype(range.GetY1())>(kCoordsXYStep, range.GetY1());
        auto bY = std::min<decltype(range.GetY2())>(mapSizeMax.y, range.GetY2());
        MapRange validRange = MapRange{ aX, aY, bX, bY };
        return validRange;
    }

    static inline void shiftIfNotNull(TileCoordsXY& coords, const TileCoordsXY& amount)
    {
        if (!coords.IsNull())
            coords += amount;
    }

    static inline void shiftIfNotNull(CoordsXY& coords, const CoordsXY& amount)
    {
        if (!coords.IsNull())
            coords += amount;
    }

    void ShiftMap(const TileCoordsXY& amount)
    {
        if (amount.x == 0 && amount.y == 0)
            return;

        auto amountToMove = amount.ToCoordsXY();
        auto& gameState = getGameState();

        // Tile elements
        auto newElements = std::vector<TileElement>();
        for (int32_t y = 0; y < kMaximumMapSizeTechnical; y++)
        {
            for (int32_t x = 0; x < kMaximumMapSizeTechnical; x++)
            {
                auto srcX = x - amount.x;
                auto srcY = y - amount.y;
                if (x > 0 && y > 0 && x < gameState.mapSize.x - 1 && y < gameState.mapSize.y - 1 && srcX > 0 && srcY > 0
                    && srcX < gameState.mapSize.x - 1 && srcY < gameState.mapSize.y - 1)
                {
                    auto srcTile = _tileIndex.GetFirstElementAt(TileCoordsXY(srcX, srcY));
                    do
                    {
                        newElements.push_back(*srcTile);
                    } while (!(srcTile++)->isLastForTile());
                }
                else if (x == 0 || y == 0 || x == gameState.mapSize.x - 1 || y == gameState.mapSize.y - 1)
                {
                    auto surface = GetDefaultSurfaceElement();
                    surface.setBaseZ(kMinimumLandZ);
                    surface.setClearanceZ(kMinimumLandZ);
                    surface.asSurface()->setSlope(0);
                    surface.asSurface()->setWaterHeight(0);
                    newElements.push_back(surface);
                }
                else
                {
                    newElements.push_back(GetDefaultSurfaceElement());
                }
            }
        }

        SetTileElements(gameState, std::move(newElements));
        MapRemoveOutOfRangeElements();

        MapExtendBoundarySurfaceShiftX(amount.x, gameState.mapSize.y);
        MapExtendBoundarySurfaceShiftY(amount.y, gameState.mapSize.x);

        for (auto& spawn : gameState.peepSpawns)
            shiftIfNotNull(spawn, amountToMove);

        for (auto& entrance : gameState.park.entrances)
            shiftIfNotNull(entrance, amountToMove);

        // Entities
        auto& entityTweener = EntityTweener::get();
        for (auto i = 0; i < EnumValue(EntityType::count); i++)
        {
            auto entityType = static_cast<EntityType>(i);
            auto& list = getGameState().entities.getEntityList(entityType);
            for (const auto& entityId : list)
            {
                auto entity = getGameState().entities.getEntity(entityId);

                // Do not tween the entity
                entityTweener.removeEntity(entity);

                auto location = entity->getLocation();
                shiftIfNotNull(location, amountToMove);
                entity->moveTo(location);

                switch (entityType)
                {
                    case EntityType::guest:
                    case EntityType::staff:
                    {
                        auto peep = entity->as<Peep>();
                        if (peep != nullptr)
                        {
                            shiftIfNotNull(peep->nextLoc, amountToMove);
                            peep->destinationX += amountToMove.x;
                            peep->destinationY += amountToMove.y;
                            shiftIfNotNull(peep->pathfindGoal, amount);
                            for (auto& h : peep->pathfindHistory)
                                shiftIfNotNull(h, amount);
                        }
                        break;
                    }
                    case EntityType::vehicle:
                    {
                        auto vehicle = entity->as<Vehicle>();
                        if (vehicle != nullptr)
                        {
                            shiftIfNotNull(vehicle->TrackLocation, amountToMove);
                            shiftIfNotNull(vehicle->BoatLocation, amountToMove);
                        }
                        break;
                    }
                    case EntityType::duck:
                    {
                        auto duck = entity->as<Duck>();
                        if (duck != nullptr)
                        {
                            duck->targetX += amountToMove.x;
                            duck->targetY += amountToMove.y;
                        }
                        break;
                    }
                    case EntityType::jumpingFountain:
                    {
                        auto fountain = entity->as<JumpingFountain>();
                        if (fountain != nullptr)
                        {
                            fountain->targetX += amountToMove.x;
                            fountain->targetY += amountToMove.y;
                        }
                        break;
                    }
                    default:
                        break;
                }
                if (entityType == EntityType::staff)
                {
                    auto staff = entity->as<Staff>();
                    if (staff != nullptr)
                    {
                        auto patrol = staff->patrolInfo;
                        if (patrol != nullptr)
                        {
                            auto positions = patrol->ToVector();
                            for (auto& p : positions)
                                shiftIfNotNull(p, amount);
                            patrol->Clear();
                            patrol->Union(positions);
                        }
                    }
                }
            }
        }
        UpdateConsolidatedPatrolAreas();

        // Rides
        for (auto& ride : RideManager(gameState))
        {
            auto stations = ride.getStations();
            for (auto& station : stations)
            {
                shiftIfNotNull(station.Start, amountToMove);
                shiftIfNotNull(station.Entrance, amount);
                shiftIfNotNull(station.Exit, amount);
            }

            shiftIfNotNull(ride.overallView, amountToMove);
            shiftIfNotNull(ride.boatHireReturnPosition, amount);
            shiftIfNotNull(ride.curTestTrackLocation, amount);
            shiftIfNotNull(ride.chairliftBullwheelLocation[0], amount);
            shiftIfNotNull(ride.chairliftBullwheelLocation[1], amount);
            shiftIfNotNull(ride.cableLiftLoc, amountToMove);
        }

        // Banners
        auto numBanners = GetNumBanners();
        auto id = BannerIndex::FromUnderlying(0);
        size_t count = 0;
        while (count < numBanners)
        {
            auto* banner = GetBanner(id);
            if (banner != nullptr)
            {
                shiftIfNotNull(banner->position, amount);
                count++;
            }
            id = BannerIndex::FromUnderlying(id.ToUnderlying() + 1);
        }

        MapAnimations::ShiftAll(amount);
    }
} // namespace OpenRCT2
