/***************************************************************************
 *   fheroes2: https://github.com/ihhub/fheroes2                           *
 *   Copyright (C) 2023 - 2026                                             *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

#include "map_format_helper.h"

#include <algorithm>
#include <array>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <map>
#include <ostream>
#include <set>
#include <string>
#include <utility>

#include "army.h"
#include "army_troop.h"
#include "castle.h"
#include "color.h"
#include "direction.h"
#include "ground.h"
#include "heroes.h"
#include "logging.h"
#include "map_format_info.h"
#include "map_object_info.h"
#include "maps.h"
#include "maps_fileinfo.h"
#include "maps_tiles.h"
#include "maps_tiles_helper.h"
#include "math_base.h"
#include "monster.h"
#include "mp2.h"
#include "players.h"
#include "race.h"
#include "rand.h"
#include "world.h"
#include "world_object_uid.h"

namespace
{
    void addObjectToTile( Maps::Map_Format::TileInfo & info, const Maps::ObjectGroup group, const uint32_t index, const uint32_t uid )
    {
        auto & object = info.objects.emplace_back();
        object.id = uid;
        object.group = group;
        object.index = index;
    }

    struct IndexedObjectInfo
    {
        int32_t tileIndex{ -1 };

        const Maps::Map_Format::TileObjectInfo * info{ nullptr };
    };

    void loadArmyFromMetadata( Army & army, const std::array<int32_t, 5> & unitType, const std::array<int32_t, 5> & unitCount )
    {
        std::vector<Troop> troops( unitType.size() );
        for ( size_t i = 0; i < troops.size(); ++i ) {
            assert( unitType[i] >= 0 && unitCount[i] >= 0 );
            troops[i] = Troop{ unitType[i], static_cast<uint32_t>( unitCount[i] ) };
        }

        army.Assign( troops.data(), troops.data() + troops.size() );
    }

    void saveArmyToMetadata( const Army & army, std::array<int32_t, 5> & unitType, std::array<int32_t, 5> & unitCount )
    {
        const size_t armySize = army.Size();
        assert( unitType.size() == armySize );

        // Update army metadata.
        for ( size_t i = 0; i < armySize; ++i ) {
            const Troop * troop = army.GetTroop( i );
            assert( troop != nullptr );

            unitType[i] = troop->GetID();
            unitCount[i] = static_cast<int32_t>( troop->GetCount() );
        }
    }

    // This function only checks for Streams and ignores River Deltas.
    bool isStreamPresent( const Maps::Map_Format::TileInfo & mapTile )
    {
        return std::any_of( mapTile.objects.cbegin(), mapTile.objects.cend(),
                            []( const Maps::Map_Format::TileObjectInfo & object ) { return object.group == Maps::ObjectGroup::STREAMS; } );
    }

    bool isStreamToDeltaConnectionNeeded( const Maps::Map_Format::MapFormat & map, const int32_t tileId, const int direction )
    {
        if ( direction != Direction::TOP && direction != Direction::BOTTOM && direction != Direction::RIGHT && direction != Direction::LEFT ) {
            return false;
        }

        if ( !Maps::isValidDirection( tileId, direction ) ) {
            return false;
        }

        // The center tile of river delta is located in the next tile.
        const int32_t nextTileId = Maps::GetDirectionIndex( tileId, direction );

        return std::any_of( map.tiles[nextTileId].objects.cbegin(), map.tiles[nextTileId].objects.cend(),
                            [&direction]( const Maps::Map_Format::TileObjectInfo & object ) {
                                return Maps::getRiverDeltaDirectionByIndex( object.group, static_cast<int32_t>( object.index ) ) == Direction::Reflect( direction );
                            } );
    }

    // Returns the direction vector bits from 'centerTileIndex' to the around tiles with streams.
    int getStreamDirecton( Maps::Map_Format::MapFormat & map, const int32_t currentTileId, const bool forceStreamOnTile )
    {
        assert( currentTileId >= 0 && map.tiles.size() > static_cast<size_t>( currentTileId ) );

        // Stream includes also the Deltas. For the current tile we need to check only streams excluding Deltas.
        if ( !forceStreamOnTile && !isStreamPresent( map.tiles[currentTileId] ) ) {
            // Current tile has no streams.
            return Direction::UNKNOWN;
        }

        int streamDirection = Direction::CENTER;

        // For streams we can check only the next four directions.
        for ( const int direction : { Direction::LEFT, Direction::TOP, Direction::RIGHT, Direction::BOTTOM } ) {
            if ( !Maps::isValidDirection( currentTileId, direction ) ) {
                continue;
            }

            const int32_t tileId = Maps::GetDirectionIndex( currentTileId, direction );
            assert( tileId >= 0 && map.tiles.size() > static_cast<size_t>( tileId ) );

            // Check also for Deltas connection.
            if ( isStreamPresent( map.tiles[tileId] ) || isStreamToDeltaConnectionNeeded( map, tileId, direction ) ) {
                streamDirection |= direction;
            }
        }

        return streamDirection;
    }

    // Returns the direction vector bits from 'centerTileIndex' where the ground is 'groundId'.
    int getGroundDirecton( const Maps::Map_Format::MapFormat & map, const int32_t centerTileIndex, const int groundId )
    {
        if ( centerTileIndex < 0 || centerTileIndex >= static_cast<int32_t>( map.tiles.size() ) ) {
            // Center tile is out of the map borders.
            return Direction::UNKNOWN;
        }

        const fheroes2::Point centerTile( centerTileIndex % map.width, centerTileIndex / map.width );
        const int32_t maxTilePos = map.width - 1;

        int32_t groundDirection = ( Maps::Ground::getGroundByImageIndex( map.tiles[centerTileIndex].terrainIndex ) == groundId ) ? Direction::CENTER : 0;

        for ( const int32_t direction : Direction::allNeighboringDirections ) {
            // We do not let 'tilePosition' to get out of the world borders, meaning that beyond the borders is the same tile type as the nearby one on the map.
            fheroes2::Point tilePosition = Maps::getDirectionPoint( centerTile, direction );
            tilePosition.x = std::min<int32_t>( maxTilePos, std::max<int32_t>( 0, tilePosition.x ) );
            tilePosition.y = std::min<int32_t>( maxTilePos, std::max<int32_t>( 0, tilePosition.y ) );

            if ( Maps::Ground::getGroundByImageIndex( map.tiles[tilePosition.y * map.width + tilePosition.x].terrainIndex ) == groundId ) {
                groundDirection |= direction;
            }
        }

        return groundDirection;
    }

    bool doesContainStreams( const Maps::Map_Format::TileInfo & tile )
    {
        return std::any_of( tile.objects.cbegin(), tile.objects.cend(),
                            []( const Maps::Map_Format::TileObjectInfo & object ) { return object.group == Maps::ObjectGroup::STREAMS; } );
    }

    void setTerrain( Maps::Map_Format::MapFormat & map, const int32_t tileId, const uint16_t imageIndex, const bool horizontalFlip, const bool verticalFlip )
    {
        const int newGround = Maps::Ground::getGroundByImageIndex( imageIndex );
        Maps::Map_Format::TileInfo & mapTile = map.tiles[tileId];

        mapTile.terrainFlags = ( verticalFlip ? 1 : 0 ) + ( horizontalFlip ? 2 : 0 );

        if ( ( newGround != Maps::Ground::WATER ) && ( Maps::doesContainRoad( mapTile ) || doesContainStreams( mapTile ) )
             && Maps::Ground::doesTerrainImageIndexContainEmbeddedObjects( imageIndex ) ) {
            // There cannot be extra objects under the roads and streams.
            mapTile.terrainIndex = Maps::Ground::getRandomTerrainImageIndex( Maps::Ground::getGroundByImageIndex( imageIndex ), false );
        }
        else {
            mapTile.terrainIndex = imageIndex;
        }

        world.getTile( tileId ).setTerrain( mapTile.terrainIndex, mapTile.terrainFlags );
    }

    constexpr bool hasBits( const int value, const int bits )
    {
        return ( value & bits ) == bits;
    }

    constexpr bool hasNoBits( const int value, const int bits )
    {
        return ( value & bits ) == 0;
    }

    // Returns true if terrain transition on tile was properly set or it is not needed.
    bool setTerrainBoundaries( Maps::Map_Format::MapFormat & map, const int groundDirection, const int beachDirection, const int32_t tileId, const uint16_t imageOffset )
    {
        if ( groundDirection == DIRECTION_ALL ) {
            // No transition is needed.
            return true;
        }

        if ( groundDirection == ( Direction::TOP_RIGHT | Direction::TOP | DIRECTION_BOTTOM_ROW | DIRECTION_CENTER_ROW ) ) {
            // All directions without the top-left corner.
            uint16_t imageIndex = imageOffset + 12U;
            imageIndex += hasNoBits( beachDirection, Direction::TOP_LEFT ) ? 0U : 16U;
            imageIndex += static_cast<uint16_t>( Rand::Get( 3 ) );
            setTerrain( map, tileId, imageIndex, true, false );
            return true;
        }
        if ( groundDirection == ( Direction::TOP_LEFT | Direction::TOP | DIRECTION_BOTTOM_ROW | DIRECTION_CENTER_ROW ) ) {
            // All directions without the top-right corner.
            uint16_t imageIndex = imageOffset + 12U;
            imageIndex += hasNoBits( beachDirection, Direction::TOP_RIGHT ) ? 0U : 16U;
            imageIndex += static_cast<uint16_t>( Rand::Get( 3 ) );
            setTerrain( map, tileId, imageIndex, false, false );
            return true;
        }
        if ( groundDirection == ( Direction::BOTTOM_LEFT | Direction::BOTTOM | DIRECTION_TOP_ROW | DIRECTION_CENTER_ROW ) ) {
            // All directions without the bottom-right corner.
            uint16_t imageIndex = imageOffset + 12U;
            imageIndex += hasNoBits( beachDirection, Direction::BOTTOM_RIGHT ) ? 0U : 16U;
            imageIndex += static_cast<uint16_t>( Rand::Get( 3 ) );
            setTerrain( map, tileId, imageIndex, false, true );
            return true;
        }
        if ( groundDirection == ( Direction::BOTTOM_RIGHT | Direction::BOTTOM | DIRECTION_TOP_ROW | DIRECTION_CENTER_ROW ) ) {
            // All directions without the bottom-left corner.
            uint16_t imageIndex = imageOffset + 12U;
            imageIndex += hasNoBits( beachDirection, Direction::BOTTOM_LEFT ) ? 0U : 16U;
            imageIndex += static_cast<uint16_t>( Rand::Get( 3 ) );
            setTerrain( map, tileId, imageIndex, true, true );
            return true;
        }

        auto tileIsNotWater = [&map, tileId]() { return Maps::Ground::getGroundByImageIndex( map.tiles[tileId].terrainIndex ) != Maps::Ground::WATER; };

        if ( hasBits( groundDirection, Direction::LEFT | Direction::TOP | Direction::BOTTOM )
             && ( tileIsNotWater() || hasBits( groundDirection, Direction::TOP_LEFT | Direction::BOTTOM_LEFT ) ) ) {
            // There is no ground direction to the right.
            // NOTICE: Initially the whole 'DIRECTION_LEFT_COL' should have direction bits.
            // If ground is not Water we  do not check TOP_LEFT and BOTTOM_LEFT as there are no tile images for that cases.
            // The absence of this "corner" transition is barely noticeable.
            // TODO: Design tile images for these cases.

            if ( hasBits( beachDirection, Direction::RIGHT ) ) {
                // To the right there is a beach (or beach transition to the water).
                setTerrain( map, tileId, imageOffset + 8U + 16U + static_cast<uint16_t>( Rand::Get( 3 ) ), false, false );
                return true;
            }

            if ( hasNoBits( groundDirection, Direction::RIGHT ) ) {
                // There is no beach and no current ground to the right.
                if ( hasBits( beachDirection, Direction::TOP_RIGHT ) ) {
                    // Top-right is beach transition and right is dirt transition.
                    setTerrain( map, tileId, imageOffset + 35U, false, false );
                }
                else if ( hasBits( beachDirection, Direction::BOTTOM_RIGHT ) ) {
                    // Bottom-right is beach transition and right is dirt transition.
                    setTerrain( map, tileId, imageOffset + 35U, false, true );
                }
                else {
                    // Transition to the dirt to the right.
                    setTerrain( map, tileId, imageOffset + 8U + static_cast<uint16_t>( Rand::Get( 3 ) ), false, false );
                }
                return true;
            }
        }

        if ( hasBits( groundDirection, Direction::RIGHT | Direction::TOP | Direction::BOTTOM )
             && ( tileIsNotWater() || hasBits( groundDirection, Direction::TOP_RIGHT | Direction::BOTTOM_RIGHT ) ) ) {
            // There is no ground direction to the left.
            // NOTICE: Initially the whole 'DIRECTION_RIGHT_COL' should have direction bits.
            // If ground is not Water we do not check TOP_RIGHT and BOTTOM_RIGHT as there are no tile images for that cases.
            // The absence of this "corner" transition is barely noticeable.
            // TODO: Design tile images for these cases.

            if ( hasBits( beachDirection, Direction::LEFT ) ) {
                // To the left there is a beach (or beach transition to the water).
                setTerrain( map, tileId, imageOffset + 8U + 16U + static_cast<uint16_t>( Rand::Get( 3 ) ), true, false );
                return true;
            }

            if ( hasNoBits( groundDirection, Direction::LEFT ) ) {
                // There is no beach and no current ground to the left.
                if ( hasBits( beachDirection, Direction::TOP_LEFT ) ) {
                    // Top-left is beach transition and left is dirt transition.
                    setTerrain( map, tileId, imageOffset + 35U, true, false );
                }
                else if ( hasBits( beachDirection, Direction::BOTTOM_LEFT ) ) {
                    // Bottom-left is beach transition and left is dirt transition.
                    setTerrain( map, tileId, imageOffset + 35U, true, true );
                }
                else {
                    // Transition to the dirt to the left.
                    setTerrain( map, tileId, imageOffset + 8U + static_cast<uint16_t>( Rand::Get( 3 ) ), true, false );
                }
                return true;
            }
        }

        if ( hasBits( groundDirection, Direction::BOTTOM | Direction::LEFT | Direction::RIGHT )
             && ( tileIsNotWater() || hasBits( groundDirection, Direction::BOTTOM_LEFT | Direction::BOTTOM_RIGHT ) ) ) {
            // There is no ground direction to the top.
            // NOTICE: Initially the whole 'DIRECTION_BOTTOM_ROW' should have direction bits.
            // If ground is not Water we  do not check BOTTOM_LEFT and BOTTOM_RIGHT as there are no tile images for that cases.
            // The absence of this "corner" transition is barely noticeable.
            // TODO: Design tile images for these cases.

            if ( hasBits( beachDirection, Direction::TOP ) ) {
                // To the top there is a beach (or beach transition to the water).
                setTerrain( map, tileId, imageOffset + 16U + static_cast<uint16_t>( Rand::Get( 3 ) ), false, false );
                return true;
            }

            if ( hasNoBits( groundDirection, Direction::TOP ) ) {
                // There is no beach and no current ground to the top.
                if ( hasBits( beachDirection, Direction::TOP_RIGHT ) ) {
                    // Top-right is beach transition and top is dirt transition.
                    setTerrain( map, tileId, imageOffset + 34U, false, false );
                }
                else if ( hasBits( beachDirection, Direction::TOP_LEFT ) ) {
                    // Top-left is beach transition and top is dirt transition.
                    setTerrain( map, tileId, imageOffset + 34U, true, false );
                }
                else {
                    // Transition to the dirt to the top.
                    setTerrain( map, tileId, imageOffset + static_cast<uint16_t>( Rand::Get( 3 ) ), false, false );
                }
                return true;
            }
        }

        if ( hasBits( groundDirection, Direction::TOP | Direction::LEFT | Direction::RIGHT )
             && ( tileIsNotWater() || hasBits( groundDirection, Direction::TOP_LEFT | Direction::TOP_RIGHT ) ) ) {
            // There is no ground direction to the bottom.
            // NOTICE: Initially the whole 'DIRECTION_TOP_ROW' should have direction bits.
            // If ground is not Water we  do not check TOP_LEFT and TOP_RIGHT as there are no tile images for that cases.
            // The absence of this "corner" transition is barely noticeable.
            // TODO: Design tile images for these cases.

            if ( hasBits( beachDirection, Direction::BOTTOM ) ) {
                // To the bottom there is a beach (or beach transition to the water).
                setTerrain( map, tileId, imageOffset + 16U + static_cast<uint16_t>( Rand::Get( 3 ) ), false, true );
                return true;
            }

            if ( hasNoBits( groundDirection, Direction::BOTTOM ) ) {
                // There is no beach and no current ground to the bottom.
                if ( hasBits( beachDirection, Direction::BOTTOM_RIGHT ) ) {
                    // Bottom-right is beach transition and bottom is dirt transition.
                    setTerrain( map, tileId, imageOffset + 34U, false, true );
                }
                else if ( hasBits( beachDirection, Direction::BOTTOM_LEFT ) ) {
                    // Bottom-left is beach transition and bottom is dirt transition.
                    setTerrain( map, tileId, imageOffset + 34U, true, true );
                }
                else {
                    // Transition to the dirt to the bottom.
                    setTerrain( map, tileId, imageOffset + static_cast<uint16_t>( Rand::Get( 3 ) ), false, true );
                }
                return true;
            }
        }

        if ( hasBits( groundDirection, Direction::RIGHT | Direction::BOTTOM_RIGHT | Direction::BOTTOM ) ) {
            // There is no ground direction to the top, top-left and left tiles.

            if ( hasBits( beachDirection, Direction::TOP | Direction::LEFT ) || hasBits( beachDirection, Direction::TOP | Direction::BOTTOM_LEFT )
                 || hasBits( beachDirection, Direction::TOP_RIGHT | Direction::LEFT ) ) {
                // To the top and left there is a beach/water.
                setTerrain( map, tileId, imageOffset + 4U + 16U + static_cast<uint16_t>( Rand::Get( 3 ) ), true, false );
                return true;
            }

            if ( hasNoBits( groundDirection, Direction::TOP | Direction::LEFT ) ) {
                // There is no beach and no current ground to the top and left.

                if ( hasBits( beachDirection, Direction::TOP ) ) {
                    // Top is beach transition and left is dirt transition.
                    setTerrain( map, tileId, imageOffset + 36U, true, false );
                }
                else if ( hasBits( beachDirection, Direction::LEFT ) ) {
                    // Left is beach transition and top is dirt transition.
                    setTerrain( map, tileId, imageOffset + 37U, true, false );
                }
                else if ( hasBits( beachDirection, Direction::TOP_RIGHT ) ) {
                    // Top-right is beach transition and left is dirt transition.
                    setTerrain( map, tileId, imageOffset + 33U, true, false );
                }
                else if ( hasBits( beachDirection, Direction::BOTTOM_LEFT ) ) {
                    // Bottom-left is beach transition and left is dirt transition.
                    setTerrain( map, tileId, imageOffset + 32U, true, false );
                }
                else {
                    // Transition to the dirt to the top and left.
                    setTerrain( map, tileId, imageOffset + 4U + static_cast<uint16_t>( Rand::Get( 3 ) ), true, false );
                }
                return true;
            }
        }

        if ( hasBits( groundDirection, Direction::LEFT | Direction::BOTTOM_LEFT | Direction::BOTTOM ) ) {
            // There is no ground direction to the top, top-right and right tiles.

            if ( hasBits( beachDirection, Direction::TOP | Direction::RIGHT ) || hasBits( beachDirection, Direction::TOP | Direction::BOTTOM_RIGHT )
                 || hasBits( beachDirection, Direction::TOP_LEFT | Direction::RIGHT ) ) {
                // To the top and right there is a beach/water.
                setTerrain( map, tileId, imageOffset + 4U + 16U + static_cast<uint16_t>( Rand::Get( 3 ) ), false, false );
                return true;
            }

            if ( hasNoBits( groundDirection, Direction::TOP | Direction::RIGHT ) ) {
                // There is no beach and no current ground to the top and right.

                if ( hasBits( beachDirection, Direction::TOP ) ) {
                    // Top is beach transition and right is dirt transition.
                    setTerrain( map, tileId, imageOffset + 36U, false, false );
                }
                else if ( hasBits( beachDirection, Direction::RIGHT ) ) {
                    // Right is beach transition and top is dirt transition.
                    setTerrain( map, tileId, imageOffset + 37U, false, false );
                }
                else if ( hasBits( beachDirection, Direction::TOP_LEFT ) ) {
                    // Top-left is beach transition and right is dirt transition.
                    setTerrain( map, tileId, imageOffset + 33U, false, false );
                }
                else if ( hasBits( beachDirection, Direction::BOTTOM_RIGHT ) ) {
                    // Bottom-right is beach transition and top is dirt transition.
                    setTerrain( map, tileId, imageOffset + 32U, false, false );
                }
                else {
                    // Transition to the dirt to the top and right.
                    setTerrain( map, tileId, imageOffset + 4U + static_cast<uint16_t>( Rand::Get( 3 ) ), false, false );
                }
                return true;
            }
        }

        if ( hasBits( groundDirection, Direction::TOP | Direction::TOP_LEFT | Direction::LEFT ) ) {
            // There is no ground direction to the right, bottom-right and bottom tiles.

            if ( hasBits( beachDirection, Direction::RIGHT | Direction::BOTTOM ) || hasBits( beachDirection, Direction::RIGHT | Direction::BOTTOM_LEFT )
                 || hasBits( beachDirection, Direction::TOP_RIGHT | Direction::BOTTOM ) ) {
                // To the bottom and right there is a beach/water.
                setTerrain( map, tileId, imageOffset + 4U + 16U + static_cast<uint16_t>( Rand::Get( 3 ) ), false, true );
                return true;
            }

            if ( hasNoBits( groundDirection, Direction::RIGHT | Direction::BOTTOM ) ) {
                // There is no beach and no current ground to the bottom and right.

                if ( hasBits( beachDirection, Direction::BOTTOM ) ) {
                    // Bottom is beach transition and right is dirt transition.
                    setTerrain( map, tileId, imageOffset + 36U, false, true );
                }
                else if ( hasBits( beachDirection, Direction::RIGHT ) ) {
                    // Right is beach transition and bottom is dirt transition.
                    setTerrain( map, tileId, imageOffset + 37U, false, true );
                }
                else if ( hasBits( beachDirection, Direction::BOTTOM_LEFT ) ) {
                    // Bottom-left is beach transition and right is dirt transition.
                    setTerrain( map, tileId, imageOffset + 33U, false, true );
                }
                else if ( hasBits( beachDirection, Direction::TOP_RIGHT ) ) {
                    // Top-right is beach transition and bottom is dirt transition.
                    setTerrain( map, tileId, imageOffset + 32U, false, true );
                }
                else {
                    // Transition to the dirt to the bottom and right.
                    setTerrain( map, tileId, imageOffset + 4U + static_cast<uint16_t>( Rand::Get( 3 ) ), false, true );
                }
                return true;
            }
        }

        if ( hasBits( groundDirection, Direction::TOP | Direction::TOP_RIGHT | Direction::RIGHT ) ) {
            // There is no ground direction to the left, bottom-left and bottom tiles.

            if ( hasBits( beachDirection, Direction::LEFT | Direction::BOTTOM ) || hasBits( beachDirection, Direction::LEFT | Direction::BOTTOM_RIGHT )
                 || hasBits( beachDirection, Direction::TOP_LEFT | Direction::BOTTOM ) ) {
                // To the bottom and left there is a beach/water. Or a narrow path to the other land.
                setTerrain( map, tileId, imageOffset + 4U + 16U + static_cast<uint16_t>( Rand::Get( 3 ) ), true, true );
                return true;
            }

            if ( hasNoBits( groundDirection, Direction::LEFT | Direction::BOTTOM ) ) {
                // There is no beach and no current ground to the bottom and left.

                if ( hasBits( beachDirection, Direction::BOTTOM ) ) {
                    // Bottom is beach transition and left is dirt transition.
                    setTerrain( map, tileId, imageOffset + 36U, true, true );
                }
                else if ( hasBits( beachDirection, Direction::LEFT ) ) {
                    // Left is beach transition and bottom is dirt transition.
                    setTerrain( map, tileId, imageOffset + 37U, true, true );
                }
                else if ( hasBits( beachDirection, Direction::BOTTOM_RIGHT ) ) {
                    // Bottom-right is beach transition and left is dirt transition.
                    setTerrain( map, tileId, imageOffset + 33U, true, true );
                }
                else if ( hasBits( beachDirection, Direction::TOP_LEFT ) ) {
                    // Top-left is beach transition and bottom is dirt transition.
                    setTerrain( map, tileId, imageOffset + 32U, true, true );
                }

                else {
                    // Transition to the dirt to the bottom and left.
                    setTerrain( map, tileId, imageOffset + 4U + static_cast<uint16_t>( Rand::Get( 3 ) ), true, true );
                }
                return true;
            }
        }

        if ( hasBits( groundDirection, Direction::TOP | Direction::RIGHT | Direction::BOTTOM | Direction::LEFT ) ) {
            const int ground = Maps::Ground::getGroundByImageIndex( map.tiles[tileId].terrainIndex );
            if ( ground != Maps::Ground::WATER ) {
                // More than one corner needs ground transition.
                // For these cases there is no extra tile image, but for now we can leave a tile with ground without transition as it is barely noticeable.
                // TODO: Design tile images for these cases.

                setTerrain( map, tileId, Maps::Ground::getRandomTerrainImageIndex( ground, true ), false, false );
                return true;
            }
        }

        // This terrain cannot be properly connected with the nearby terrains. There are no such ground images.
        DEBUG_LOG( DBG_DEVEL, DBG_WARN,
                   "No proper ground transition found for " << Maps::Ground::String( Maps::Ground::getGroundByImageIndex( imageOffset ) ) << " at " << tileId % map.width
                                                            << ',' << tileId / map.width << " (" << tileId << ").\nDirections: " << Direction::String( groundDirection ) )
        return false;
    }

    // Returns true if terrain transition was set or it is not needed.
    bool updateTerrainTransitionOnTile( Maps::Map_Format::MapFormat & map, const int32_t tileId )
    {
        const Maps::Map_Format::TileInfo & mapTile = map.tiles[tileId];
        const int ground = Maps::Ground::getGroundByImageIndex( mapTile.terrainIndex );

        if ( ground == Maps::Ground::BEACH ) {
            // Beach tile images do not have transition with the other terrains.
            return true;
        }

        // Check the tiles around for the need of ground transition.
        // Dirt has transitions only with Water and Beach, and these "Beach transitions" have image index offsets like "Dirt transitions" for all other terrains.
        const int tileGroundDirection
            = ( ground == Maps::Ground::DIRT )
                  ? ( DIRECTION_ALL - ( getGroundDirecton( map, tileId, Maps::Ground::WATER ) | getGroundDirecton( map, tileId, Maps::Ground::BEACH ) ) )
                  : ( getGroundDirecton( map, tileId, ground ) | Direction::CENTER );

        if ( tileGroundDirection == DIRECTION_ALL ) {
            // Current tile does not need a transition because there is no other terrain nearby.

            if ( Maps::Ground::isTerrainTransitionImage( mapTile.terrainIndex ) ) {
                // We change image with the transition to original terrain image without transition.
                setTerrain( map, tileId, Maps::Ground::getRandomTerrainImageIndex( ground, true ), false, false );
            }
            return true;
        }

        switch ( ground ) {
        case Maps::Ground::WATER:
        case Maps::Ground::DIRT:
            // Water has only "Beach transition" to all other terrains.
            // Dirt has only "Beach transition" only with Water and/or Beach.
            // TODO: Set waves on the water for 3 tiles from the ground with the wave direction to the center of the ground.
            return setTerrainBoundaries( map, tileGroundDirection, 0, tileId, Maps::Ground::getTerrainStartImageIndex( ground ) );
        case Maps::Ground::GRASS:
        case Maps::Ground::SNOW:
        case Maps::Ground::SWAMP:
        case Maps::Ground::LAVA:
        case Maps::Ground::DESERT:
        case Maps::Ground::WASTELAND: {
            // The transition to the Beach terrain is rendered when the near tile ground is Water or Beach.
            const int beachDirection = getGroundDirecton( map, tileId, Maps::Ground::WATER ) | getGroundDirecton( map, tileId, Maps::Ground::BEACH );

            return setTerrainBoundaries( map, tileGroundDirection, beachDirection, tileId, Maps::Ground::getTerrainStartImageIndex( ground ) );
        }
        default:
            // Have you added a new ground? Add the logic above!
            assert( 0 );
            return false;
        }
    }

    void updateTerrainTransitionOnArea( Maps::Map_Format::MapFormat & map, const int newGroundId, const int32_t tileStart, const int32_t tileEnd, const int32_t tileStep )
    {
        for ( int32_t tileId = tileStart; tileId <= tileEnd; tileId += tileStep ) {
            if ( updateTerrainTransitionOnTile( map, tileId ) ) {
                // The terrain transition was correctly set or transition was not needed.
                continue;
            }

            // Try to change the ground type to one of the others.
            // TODO: Change this algorithm to a more proper one. E.g. remember the previous ground and try to UNDO it here.
            const int groundOnTile = Maps::Ground::getGroundByImageIndex( map.tiles[tileId].terrainIndex );

            DEBUG_LOG( DBG_DEVEL, DBG_WARN,
                       "Ground " << Maps::Ground::String( groundOnTile ) << " at " << tileId % map.width << ',' << tileId / map.width << " (" << tileId
                                 << ") should be replaced by some other one." )

            std::vector<int> newGrounds;

            if ( groundOnTile != newGroundId ) {
                // Outside of filled area we can try to place the ground of this area.
                newGrounds.push_back( newGroundId );
            }

            bool isWater = ( groundOnTile == Maps::Ground::WATER );
            const Maps::Indexes around = Maps::getAroundIndexes( tileId );

            // Get ground types from all tiles around to try them.
            for ( const int32_t index : around ) {
                const int32_t ground = Maps::Ground::getGroundByImageIndex( map.tiles[index].terrainIndex );
                if ( ground != groundOnTile && std::find( newGrounds.begin(), newGrounds.end(), ground ) == newGrounds.end() ) {
                    newGrounds.push_back( ground );
                }

                isWater = isWater || ( ground == Maps::Ground::WATER );
            }

            if ( isWater ) {
                // As a last chance, we can try to place Beach to make a path on the water because Beach doesn't need to transition to the water.
                if ( std::find( newGrounds.begin(), newGrounds.end(), Maps::Ground::BEACH ) == newGrounds.end() ) {
                    newGrounds.push_back( Maps::Ground::BEACH );
                }
            }
            else {
                // As a last chance, we can try to connect terrains by placing Dirt, because Dirt doesn't need to transition to other non-water terrains.
                if ( std::find( newGrounds.begin(), newGrounds.end(), Maps::Ground::DIRT ) == newGrounds.end() ) {
                    newGrounds.push_back( Maps::Ground::DIRT );
                }
            }

            bool needRevert = true;

            for ( const int newGround : newGrounds ) {
                DEBUG_LOG( DBG_DEVEL, DBG_WARN,
                           "Trying ground " << Maps::Ground::String( newGround ) << " at " << tileId % map.width << ',' << tileId / map.width << " (" << tileId << ")." )

                setTerrain( map, tileId, Maps::Ground::getRandomTerrainImageIndex( newGround, true ), false, false );

                if ( !updateTerrainTransitionOnTile( map, tileId ) ) {
                    // The ground image has not been set properly. We move on to the next type of the ground.
                    continue;
                }

                DEBUG_LOG( DBG_DEVEL, DBG_WARN,
                           "Ground " << Maps::Ground::String( newGround ) << " was properly set to " << tileId % map.width << ',' << tileId / map.width << " (" << tileId
                                     << ")." )

                // The ground on the tile has been changed, so we need to update the transitions on all the tiles around.
                for ( const int32_t index : around ) {
                    if ( !updateTerrainTransitionOnTile( map, index ) ) {
                        // TODO: Find a better solution without using recursions. In example, undo the tiles in 1 tile radius.
                        DEBUG_LOG( DBG_DEVEL, DBG_WARN, "Recursive call for tile at " << tileId % map.width << ',' << tileId / map.width << " (" << tileId << ")." )

                        updateTerrainTransitionOnArea( map, newGroundId, index, index, 1 );
                    }
                }

                needRevert = false;

                break;
            }

            // If all ground replacements fail we revert the ground change to the initial ground type.
            if ( needRevert && !newGrounds.empty() ) {
                setTerrain( map, tileId, Maps::Ground::getRandomTerrainImageIndex( groundOnTile, true ), false, false );
                DEBUG_LOG( DBG_DEVEL, DBG_WARN,
                           "Reverting ground to " << Maps::Ground::String( groundOnTile ) << " at " << tileId % map.width << ',' << tileId / map.width << " (" << tileId
                                                  << ")." )
            }
        }
    }

    void updateTerrainTransitionOnAreaBoundaries( Maps::Map_Format::MapFormat & map, const int groundId, const int32_t startX, const int32_t endX, const int32_t startY,
                                                  const int32_t endY )
    {
        const int32_t mapWidth = map.width;
        const int32_t mapHeight = map.width;

        // First we update the boundaries inside the filled area.
        updateTerrainTransitionOnArea( map, groundId, startX + mapWidth * startY, endX + mapWidth * startY, 1 );
        if ( startY != endY ) {
            updateTerrainTransitionOnArea( map, groundId, startX + mapWidth * endY, endX + mapWidth * endY, 1 );
            if ( endY - startY > 1 ) {
                updateTerrainTransitionOnArea( map, groundId, startX + mapWidth * ( startY + 1 ), startX + mapWidth * ( endY - 1 ), mapWidth );
                if ( startX != endX ) {
                    updateTerrainTransitionOnArea( map, groundId, endX + mapWidth * ( startY + 1 ), endX + mapWidth * ( endY - 1 ), mapWidth );
                }
            }
        }

        // Then we update the boundaries outside the filled area, excluding the corners.
        if ( startY > 0 ) {
            const int32_t tileOffset = mapWidth * ( startY - 1 );
            updateTerrainTransitionOnArea( map, groundId, startX + tileOffset, endX + tileOffset, 1 );
        }
        if ( endY < mapHeight - 1 ) {
            const int32_t tileOffset = mapWidth * ( endY + 1 );
            updateTerrainTransitionOnArea( map, groundId, startX + tileOffset, endX + tileOffset, 1 );
        }
        if ( startX > 0 ) {
            const int32_t tileOffset = startX - 1;
            updateTerrainTransitionOnArea( map, groundId, tileOffset + mapWidth * startY, tileOffset + mapWidth * endY, mapWidth );
        }
        if ( endX < mapWidth - 1 ) {
            const int32_t tileOffset = endX + 1;
            updateTerrainTransitionOnArea( map, groundId, tileOffset + mapWidth * startY, tileOffset + mapWidth * endY, mapWidth );
        }

        // Update the corners outside of filled area.
        if ( startX > 0 && startY > 0 ) {
            const int32_t tileId = startX - 1 + mapWidth * ( startY - 1 );
            updateTerrainTransitionOnArea( map, groundId, tileId, tileId, 1 );
        }
        if ( startY > 0 && endX < mapWidth - 1 ) {
            const int32_t tileId = endX + 1 + mapWidth * ( startY - 1 );
            updateTerrainTransitionOnArea( map, groundId, tileId, tileId, 1 );
        }
        if ( startX > 0 && endY < mapHeight - 1 ) {
            const int32_t tileId = startX - 1 + mapWidth * ( endY + 1 );
            updateTerrainTransitionOnArea( map, groundId, tileId, tileId, 1 );
        }
        if ( endX < mapWidth - 1 && endY < mapHeight - 1 ) {
            const int32_t tileId = endX + 1 + mapWidth * ( endY + 1 );
            updateTerrainTransitionOnArea( map, groundId, tileId, tileId, 1 );
        }
    }

    uint8_t getStreamIndex( const int streamDirection )
    {
        if ( hasNoBits( streamDirection, Direction::CENTER ) ) {
            // This tile should not have a stream image.
            assert( 0 );
            return 255U;
        }

        if ( hasBits( streamDirection, Direction::LEFT | Direction::BOTTOM ) && hasNoBits( streamDirection, Direction::TOP | Direction::RIGHT ) ) {
            // \ - stream from the left to the bottom.
            return 0U;
        }
        if ( hasBits( streamDirection, Direction::RIGHT | Direction::BOTTOM ) && hasNoBits( streamDirection, Direction::TOP | Direction::LEFT ) ) {
            // / - stream from the right to the bottom.
            return 1U;
        }
        if ( hasBits( streamDirection, Direction::RIGHT | Direction::TOP ) && hasNoBits( streamDirection, Direction::BOTTOM | Direction::LEFT ) ) {
            // \ - stream from the top to the right.
            return 4U;
        }
        if ( hasBits( streamDirection, Direction::LEFT | Direction::TOP ) && hasNoBits( streamDirection, Direction::BOTTOM | Direction::RIGHT ) ) {
            // / - stream from the top to the left.
            return 7U;
        }
        if ( hasBits( streamDirection, Direction::LEFT | Direction::TOP | Direction::RIGHT ) && hasNoBits( streamDirection, Direction::BOTTOM ) ) {
            // _|_ - stream from the top to the left and right.
            return 8U;
        }
        if ( hasBits( streamDirection, Direction::BOTTOM | Direction::TOP | Direction::RIGHT ) && hasNoBits( streamDirection, Direction::LEFT ) ) {
            // |- - stream from the top to the right and bottom.
            return 9U;
        }
        if ( hasBits( streamDirection, Direction::BOTTOM | Direction::TOP | Direction::LEFT ) && hasNoBits( streamDirection, Direction::RIGHT ) ) {
            // -| - stream from the top to the left and bottom.
            return 10U;
        }
        if ( hasBits( streamDirection, Direction::BOTTOM | Direction::LEFT | Direction::RIGHT ) && hasNoBits( streamDirection, Direction::TOP ) ) {
            // \/ - stream from the left and right to the bottom.
            return 11U;
        }
        if ( hasBits( streamDirection, Direction::BOTTOM | Direction::LEFT | Direction::TOP | Direction::RIGHT ) ) {
            // -|- - streams are all around.
            return 6U;
        }
        if ( ( hasBits( streamDirection, Direction::LEFT ) || hasBits( streamDirection, Direction::RIGHT ) )
             && hasNoBits( streamDirection, Direction::TOP | Direction::BOTTOM ) ) {
            // - - horizontal stream.
            return Rand::Get( 1 ) ? 2U : 5U;
        }

        // | - in all other cases are the vertical stream sprite, including the case when there are no other streams around.
        return Rand::Get( 1 ) ? 3U : 12U;
    }

    void updateStreamObjectOnMapTile( Maps::Map_Format::MapFormat & map, const int32_t tileId, const bool forceStreamOnTile )
    {
        const int direction = getStreamDirecton( map, tileId, forceStreamOnTile );

        if ( hasNoBits( direction, Direction::CENTER ) ) {
            // There is no stream on this tile.
            return;
        }

        const uint8_t objectIndex = getStreamIndex( direction );

        if ( objectIndex == 255U ) {
            return;
        }

        assert( tileId >= 0 && map.tiles.size() > static_cast<size_t>( tileId ) );

        auto & tileObjects = map.tiles[tileId].objects;

        auto streamIter = std::find_if( tileObjects.begin(), tileObjects.end(),
                                        []( const Maps::Map_Format::TileObjectInfo & object ) { return object.group == Maps::ObjectGroup::STREAMS; } );

        if ( streamIter == tileObjects.end() ) {
            // Add stream to this tile.
            const auto & objectInfo = Maps::getObjectInfo( Maps::ObjectGroup::STREAMS, objectIndex );

            if ( !Maps::setObjectOnTile( world.getTile( tileId ), objectInfo, true ) ) {
                assert( 0 );
                return;
            }

            Maps::addObjectToMap( map, tileId, Maps::ObjectGroup::STREAMS, objectIndex );
        }
        else {
            streamIter->index = objectIndex;

            // Update image index for the `world` tile.
            Maps::Tile::updateTileObjectIcnIndex( world.getTile( tileId ), streamIter->id, objectIndex );
        }
    }

    bool doesContainCastleEntrance( const Maps::Map_Format::TileInfo & tile )
    {
        return std::any_of( tile.objects.cbegin(), tile.objects.cend(), []( const auto & object ) { return object.group == Maps::ObjectGroup::KINGDOM_TOWNS; } );
    }

    int getRoadObjectIndex( const Maps::Map_Format::MapFormat & map, const int32_t mainTileIndex )
    {
        if ( mainTileIndex > map.width && doesContainCastleEntrance( map.tiles[mainTileIndex - map.width] ) ) {
            // 512 is the index of castle entrance road object.
            return 512;
        }

        int roadDirection = 0;

        // Road objects are made to correspond the outer road objects direction to properly connect with them.
        const Maps::Indexes around = Maps::getAroundIndexes( mainTileIndex, map.width, map.width, 1 );

        for ( const int32_t tileIndex : around ) {
            assert( tileIndex >= 0 && tileIndex < map.width * map.width );

            if ( Maps::doesContainRoad( map.tiles[tileIndex] ) ) {
                roadDirection |= Maps::GetDirection( mainTileIndex, tileIndex );
            }
        }

        assert( ( roadDirection & Direction::CENTER ) == 0 );

        // There are object duplicates after 255 because some road sprites have 2 variants in assets.
        return roadDirection + static_cast<int>( Rand::Get( 1 ) ) * 256;
    }

    bool placeNewRoadObjectOnTile( Maps::Map_Format::MapFormat & map, const int32_t tileIndex )
    {
        assert( static_cast<size_t>( tileIndex ) < map.tiles.size() );

        if ( Maps::doesContainRoad( map.tiles[tileIndex] ) ) {
            return false;
        }

        const int roadObjectIndex = getRoadObjectIndex( map, tileIndex );

        const auto & objectInfo = Maps::getObjectInfo( Maps::ObjectGroup::ROADS, roadObjectIndex );
        if ( !setObjectOnTile( world.getTile( tileIndex ), objectInfo, false ) ) {
            assert( 0 );
            return false;
        }

        Maps::addObjectToMap( map, tileIndex, Maps::ObjectGroup::ROADS, static_cast<uint32_t>( roadObjectIndex ) );

        return true;
    }

    void updateRoadObjectsInAreaAround( Maps::Map_Format::MapFormat & map, const int32_t centerTileIndex, const int32_t centerToRectBorderDistance )
    {
        for ( const int32_t index : Maps::getAroundIndexes( centerTileIndex, centerToRectBorderDistance ) ) {
            Maps::updateRoadOnTile( map, index );
        }
    }

    void updateRoadObjectsAround( Maps::Map_Format::MapFormat & map, const int32_t centerTileIndex )
    {
        updateRoadObjectsInAreaAround( map, centerTileIndex, 1 );
    }
}

namespace Maps
{
    bool readMapInEditor( const Map_Format::MapFormat & map )
    {
        world.generateUninitializedMap( map.width );

        if ( !readAllTiles( map ) ) {
            return false;
        }

        world.updatePassabilities();

        updatePlayerRelatedObjects( map );

        return true;
    }

    bool readAllTiles( const Map_Format::MapFormat & map )
    {
        assert( map.width == world.w() && map.width == world.h() );

        const size_t tilesConut = map.tiles.size();

        for ( size_t i = 0; i < tilesConut; ++i ) {
            auto & worldTile = world.getTile( static_cast<int32_t>( i ) );

            worldTile.setIndex( static_cast<int32_t>( i ) );
            worldTile.setTerrain( map.tiles[i].terrainIndex, map.tiles[i].terrainFlags );
        }

        // Read objects from all tiles and place them based on their IDs.
        auto sortObjects = []( const IndexedObjectInfo & left, const IndexedObjectInfo & right ) { return left.info->id < right.info->id; };
        std::multiset<IndexedObjectInfo, decltype( sortObjects )> sortedObjects( sortObjects );

#if defined( WITH_DEBUG )
        std::map<uint32_t, IndexedObjectInfo> objectsUIDs;
        std::multiset<IndexedObjectInfo, decltype( sortObjects )> incorrectObjects( sortObjects );
#endif

        for ( size_t i = 0; i < tilesConut; ++i ) {
            for ( const auto & object : map.tiles[i].objects ) {
                IndexedObjectInfo info;
                info.tileIndex = static_cast<int32_t>( i );
                info.info = &object;
                sortedObjects.emplace( info );

#if defined( WITH_DEBUG )
                if ( object.group != ObjectGroup::LANDSCAPE_TOWN_BASEMENTS && object.group != ObjectGroup::LANDSCAPE_FLAGS ) {
                    const auto [iter, inserted] = objectsUIDs.try_emplace( object.id, info );
                    if ( !inserted ) {
                        incorrectObjects.emplace( iter->second );
                        incorrectObjects.emplace( info );
                    }
                }
#endif
            }
        }

#if defined( WITH_DEBUG )
        uint32_t uid = 0;
        for ( const IndexedObjectInfo & info : incorrectObjects ) {
            if ( info.info->id != uid ) {
                uid = info.info->id;
                if ( map.resourceMetadata.find( uid ) != map.resourceMetadata.end() ) {
                    VERBOSE_LOG( "`resourceMetadata` belongs to many objects with same UID: " << uid )
                }
                if ( map.castleMetadata.find( uid ) != map.castleMetadata.end() ) {
                    VERBOSE_LOG( "`castleMetadata` belongs to many objects with same UID: " << uid )
                }
                if ( map.heroMetadata.find( uid ) != map.heroMetadata.end() ) {
                    VERBOSE_LOG( "`heroMetadata` belongs to many objects with same UID: " << uid )
                }
                if ( map.sphinxMetadata.find( uid ) != map.sphinxMetadata.end() ) {
                    VERBOSE_LOG( "`sphinxMetadata` belongs to many objects with same UID: " << uid )
                }
                if ( map.signMetadata.find( uid ) != map.signMetadata.end() ) {
                    VERBOSE_LOG( "`signMetadata` belongs to many objects with same UID: " << uid )
                }
                if ( map.adventureMapEventMetadata.find( uid ) != map.adventureMapEventMetadata.end() ) {
                    VERBOSE_LOG( "`adventureMapEventMetadata` belongs to many objects with same UID: " << uid )
                }
                if ( map.selectionObjectMetadata.find( uid ) != map.selectionObjectMetadata.end() ) {
                    VERBOSE_LOG( "`selectionObjectMetadata` belongs to many objects with same UID: " << uid )
                }
                if ( map.capturableObjectsMetadata.find( uid ) != map.capturableObjectsMetadata.end() ) {
                    VERBOSE_LOG( "`capturableObjectsMetadata` belongs to many objects with same UID: " << uid )
                }
                if ( map.monsterMetadata.find( uid ) != map.monsterMetadata.end() ) {
                    VERBOSE_LOG( "`monsterMetadata` belongs to many objects with same UID: " << uid )
                }
                if ( map.artifactMetadata.find( uid ) != map.artifactMetadata.end() ) {
                    VERBOSE_LOG( "`artifactMetadata` belongs to many objects with same UID: " << uid )
                }
            }

            VERBOSE_LOG( "Non-unique UID " << info.info->id << " at " << info.tileIndex << " (" << info.tileIndex % map.width << ", " << info.tileIndex / map.width
                                           << ") tile for object type: " << MP2::StringObject( getObjectInfo( info.info->group, info.info->index ).objectType ) )
        }
#endif

        for ( const auto & info : sortedObjects ) {
            assert( info.info != nullptr );
            if ( !readTileObject( world.getTile( info.tileIndex ), *info.info ) ) {
                return false;
            }
        }

        return true;
    }

    bool readTileObject( Tile & tile, const Map_Format::TileObjectInfo & object )
    {
        const auto & objectInfos = getObjectsByGroup( object.group );
        if ( object.index >= objectInfos.size() ) {
            // This is a bad map format!
            assert( 0 );
            return false;
        }

        // Object UID is set through global object UID counter. Therefore, we need to update it before running the operation.
        if ( object.id == 0 ) {
            // This object UID is not set!
            assert( 0 );
            return false;
        }

        setLastObjectUID( object.id - 1 );
        // We don't update map passabilities as it is a very expensive process.
        // Let's do it once everything is being loaded.
        return setObjectOnTile( tile, objectInfos[object.index], false );
    }

    void setTerrainWithTransition( Map_Format::MapFormat & map, const int32_t startTileId, const int32_t endTileId, const int groundId )
    {
        assert( map.width == world.w() && map.width == world.h() );

        const int32_t maxTileId = static_cast<int32_t>( map.tiles.size() ) - 1;
        if ( startTileId < 0 || startTileId > maxTileId || endTileId < 0 || endTileId > maxTileId ) {
            return;
        }

        const fheroes2::Point startTileOffset( startTileId % map.width, startTileId / map.width );

        if ( startTileId == endTileId ) {
            // In original editor these tiles are never flipped.
            setTerrain( map, startTileId, Ground::getRandomTerrainImageIndex( groundId, true ), false, false );

            updateTerrainTransitionOnAreaBoundaries( map, groundId, startTileOffset.x, startTileOffset.x, startTileOffset.y, startTileOffset.y );

            return;
        }

        const fheroes2::Point endTileOffset( endTileId % map.width, endTileId / map.width );

        const int32_t startX = std::min( startTileOffset.x, endTileOffset.x );
        const int32_t startY = std::min( startTileOffset.y, endTileOffset.y );
        const int32_t endX = std::max( startTileOffset.x, endTileOffset.x );
        const int32_t endY = std::max( startTileOffset.y, endTileOffset.y );

        for ( int32_t y = startY; y <= endY; ++y ) {
            const int32_t tileOffset = y * map.width;
            for ( int32_t x = startX; x <= endX; ++x ) {
                // In original editor these tiles are never flipped.
                setTerrain( map, x + tileOffset, Ground::getRandomTerrainImageIndex( groundId, true ), false, false );
            }
        }

        // Set ground transitions on the boundaries of filled terrain area.
        updateTerrainTransitionOnAreaBoundaries( map, groundId, startX, endX, startY, endY );
    }

    void addObjectToMap( Map_Format::MapFormat & map, const int32_t tileId, const ObjectGroup group, const uint32_t index )
    {
        assert( tileId >= 0 && map.tiles.size() > static_cast<size_t>( tileId ) );

        // At this time it is assumed that object was added into world object to be rendered using Maps::setObjectOnTile() function.
        const uint32_t uid = getLastObjectUID();
        assert( uid > 0 );

        addObjectToTile( map.tiles[tileId], group, index, uid );

        // Towns and heroes have extra metadata.
        if ( group == ObjectGroup::KINGDOM_HEROES ) {
            auto [heroMetadata, isMetadataEmplaced] = map.heroMetadata.try_emplace( uid );
            assert( isMetadataEmplaced );

            const auto & objects = getObjectsByGroup( group );
            assert( index < objects.size() );
            // Set race according the object metadata.
            heroMetadata->second.race = Race::IndexToRace( static_cast<int>( objects[index].metadata[1] ) );
        }
        else if ( group == ObjectGroup::KINGDOM_TOWNS ) {
            auto [metadata, isMetadataEmplaced] = map.castleMetadata.try_emplace( uid );
            assert( isMetadataEmplaced );

            const auto & objects = getObjectsByGroup( group );
            assert( index < objects.size() );
            // Add town or castle main buildings.
            metadata->second.builtBuildings.push_back( objects[index].metadata[1] == 0 ? BUILD_TENT : BUILD_CASTLE );
        }
        else if ( isJailObject( group, index ) ) {
            auto [heroMetadata, isMetadataEmplaced] = map.heroMetadata.try_emplace( uid );
            assert( isMetadataEmplaced );

            // Set Random race for the jailed hero by default.
            heroMetadata->second.race = Race::RAND;
        }
        else if ( group == ObjectGroup::MONSTERS ) {
            const auto [dummy, isMetadataEmplaced] = map.monsterMetadata.try_emplace( uid );
            assert( isMetadataEmplaced );

#ifdef NDEBUG
            (void)isMetadataEmplaced;
#endif
        }
        else if ( group == ObjectGroup::ADVENTURE_MISCELLANEOUS ) {
            const auto & objects = getObjectsByGroup( group );

            assert( index < objects.size() );
            const auto objectType = objects[index].objectType;

            switch ( objectType ) {
            case MP2::OBJ_EVENT: {
                const auto [dummy, isMetadataEmplaced] = map.adventureMapEventMetadata.try_emplace( uid );
                assert( isMetadataEmplaced );

#ifdef NDEBUG
                (void)isMetadataEmplaced;
#endif
                break;
            }
            case MP2::OBJ_SIGN: {
                const auto [dummy, isMetadataEmplaced] = map.signMetadata.try_emplace( uid );
                assert( isMetadataEmplaced );

#ifdef NDEBUG
                (void)isMetadataEmplaced;
#endif
                break;
            }
            case MP2::OBJ_SPHINX: {
                const auto [dummy, isMetadataEmplaced] = map.sphinxMetadata.try_emplace( uid );
                assert( isMetadataEmplaced );

#ifdef NDEBUG
                (void)isMetadataEmplaced;
#endif
                break;
            }
            default:
                break;
            }
        }
        else if ( group == ObjectGroup::ADVENTURE_WATER ) {
            const auto & objects = getObjectsByGroup( group );

            assert( index < objects.size() );
            const auto objectType = objects[index].objectType;
            if ( objectType == MP2::OBJ_BOTTLE ) {
                const auto [dummy, isMetadataEmplaced] = map.signMetadata.try_emplace( uid );
                assert( isMetadataEmplaced );

#ifdef NDEBUG
                (void)isMetadataEmplaced;
#endif
            }
        }
        else if ( group == ObjectGroup::ADVENTURE_ARTIFACTS ) {
            assert( index < getObjectsByGroup( group ).size() );

            const auto [dummy, isMetadataEmplaced] = map.artifactMetadata.try_emplace( uid );
            assert( isMetadataEmplaced );

#ifdef NDEBUG
            (void)isMetadataEmplaced;
#endif
        }
    }

    void setTerrainOnTile( Map_Format::MapFormat & map, const int32_t tileId, const int groundId )
    {
        setTerrain( map, tileId, Ground::getRandomTerrainImageIndex( groundId, true ), false, false );
    }

    bool addStream( Map_Format::MapFormat & map, const int32_t tileId )
    {
        assert( tileId >= 0 && map.tiles.size() > static_cast<size_t>( tileId ) );

        Map_Format::TileInfo & thisTile = map.tiles[tileId];

        if ( isStreamPresent( thisTile ) || Ground::getGroundByImageIndex( thisTile.terrainIndex ) == Ground::WATER ) {
            // We cannot place streams on the water or on already placed streams.
            return false;
        }

        // Force set stream on this tile and update its sprite.
        updateStreamObjectOnMapTile( map, tileId, true );

        updateStreamsAround( map, tileId );

        if ( Ground::doesTerrainImageIndexContainEmbeddedObjects( thisTile.terrainIndex ) ) {
            // We need to set terrain image without extra objects under the stream.
            thisTile.terrainIndex = Ground::getRandomTerrainImageIndex( Ground::getGroundByImageIndex( thisTile.terrainIndex ), false );
        }

        return true;
    }

    void updateStreamsAround( Map_Format::MapFormat & map, const int32_t centerTileId )
    {
        // For streams we should update only the next four directions.
        for ( const int direction : { Direction::LEFT, Direction::TOP, Direction::RIGHT, Direction::BOTTOM } ) {
            if ( isValidDirection( centerTileId, direction ) ) {
                updateStreamObjectOnMapTile( map, GetDirectionIndex( centerTileId, direction ), false );
            }
        }
    }

    void updateStreamsToDeltaConnection( Map_Format::MapFormat & map, const int32_t tileId, const int deltaDirection )
    {
        if ( !isValidDirection( tileId, deltaDirection ) ) {
            return;
        }

        const int32_t nextTileId = GetDirectionIndex( tileId, deltaDirection );

        if ( !isValidDirection( nextTileId, deltaDirection ) ) {
            return;
        }

        updateStreamObjectOnMapTile( map, GetDirectionIndex( nextTileId, deltaDirection ), false );
    }

    int getRiverDeltaDirectionByIndex( const ObjectGroup group, const int32_t objectIndex )
    {
        if ( group != ObjectGroup::LANDSCAPE_MISCELLANEOUS ) {
            return Direction::UNKNOWN;
        }

        const auto & objectInfo = getObjectInfo( ObjectGroup::LANDSCAPE_MISCELLANEOUS, objectIndex );

        assert( !objectInfo.groundLevelParts.empty() );

        const auto & firstObjectPart = objectInfo.groundLevelParts.front();

        // Yes, the below code is very hacky but so far this is the best we can do.
        if ( firstObjectPart.icnType == MP2::OBJ_ICN_TYPE_OBJNMUL2 ) {
            switch ( firstObjectPart.icnIndex ) {
            case 2U:
                return Direction::TOP;
            case 11U:
                return Direction::BOTTOM;
            case 218U + 2U:
                return Direction::LEFT;
            case 218U + 11U:
                return Direction::RIGHT;
            default:
                break;
            }
        }

        return Direction::UNKNOWN;
    }

    bool isRiverDeltaObject( const ObjectGroup group, const int32_t objectIndex )
    {
        return getRiverDeltaDirectionByIndex( group, objectIndex ) != Direction::UNKNOWN;
    }

    void updatePlayerRelatedObjects( const Map_Format::MapFormat & map )
    {
        assert( map.width == world.w() && map.width == world.h() );

        const auto & townObjects = getObjectsByGroup( ObjectGroup::KINGDOM_TOWNS );
        const auto & heroObjects = getObjectsByGroup( ObjectGroup::KINGDOM_HEROES );

        // Capturable objects exist in Miscellaneous and Mines groups.
        const auto & miscObjects = getObjectsByGroup( ObjectGroup::ADVENTURE_MISCELLANEOUS );
        const auto & minesObjects = getObjectsByGroup( ObjectGroup::ADVENTURE_MINES );

        for ( size_t tileId = 0; tileId < map.tiles.size(); ++tileId ) {
            for ( const auto & object : map.tiles[tileId].objects ) {
                if ( object.group == ObjectGroup::KINGDOM_TOWNS ) {
                    const PlayerColor color = Color::IndexToColor( getTownColorIndex( map, tileId, object.id ) );
                    const uint8_t race = Race::IndexToRace( static_cast<int>( townObjects[object.index].metadata[0] ) );

                    world.addCastle( static_cast<int32_t>( tileId ), race, color );
                }
                else if ( object.group == ObjectGroup::KINGDOM_HEROES ) {
                    const auto & metadata = heroObjects[object.index].metadata;
                    const PlayerColor color = Color::IndexToColor( static_cast<int>( metadata[0] ) );

                    Heroes * hero = world.GetHeroForHire( static_cast<int>( metadata[1] ) );
                    if ( hero ) {
                        hero->SetCenter( { static_cast<int32_t>( tileId ) % world.w(), static_cast<int32_t>( tileId ) / world.w() } );
                        hero->SetColor( color );
                        world.getTile( static_cast<int32_t>( tileId ) ).setHero( hero );
                    }
                }
                else if ( object.group == ObjectGroup::ADVENTURE_MISCELLANEOUS ) {
                    assert( object.index < miscObjects.size() );

                    const MP2::MapObjectType objectType = miscObjects[object.index].objectType;

                    captureObject( map, static_cast<int32_t>( tileId ), object.id, objectType );
                }
                else if ( object.group == ObjectGroup::ADVENTURE_MINES ) {
                    assert( object.index < minesObjects.size() );

                    const MP2::MapObjectType objectType = minesObjects[object.index].objectType;

                    captureObject( map, static_cast<int32_t>( tileId ), object.id, objectType );
                }
            }
        }
    }

    bool updateMapPlayers( Map_Format::MapFormat & map )
    {
        static_assert( PlayerColor::BLUE == static_cast<PlayerColor>( 1 << 0 ), "The kingdom color values have changed. You are going to break map format!" );
        static_assert( PlayerColor::GREEN == static_cast<PlayerColor>( 1 << 1 ), "The kingdom color values have changed. You are going to break map format!" );
        static_assert( PlayerColor::RED == static_cast<PlayerColor>( 1 << 2 ), "The kingdom color values have changed. You are going to break map format!" );
        static_assert( PlayerColor::YELLOW == static_cast<PlayerColor>( 1 << 3 ), "The kingdom color values have changed. You are going to break map format!" );
        static_assert( PlayerColor::ORANGE == static_cast<PlayerColor>( 1 << 4 ), "The kingdom color values have changed. You are going to break map format!" );
        static_assert( PlayerColor::PURPLE == static_cast<PlayerColor>( 1 << 5 ), "The kingdom color values have changed. You are going to break map format!" );

        static_assert( Race::NONE == 0, "The race values have changed. You are going to break map format!" );
        static_assert( Race::KNGT == 1 << 0, "The race values have changed. You are going to break map format!" );
        static_assert( Race::BARB == 1 << 1, "The race values have changed. You are going to break map format!" );
        static_assert( Race::SORC == 1 << 2, "The race values have changed. You are going to break map format!" );
        static_assert( Race::WRLK == 1 << 3, "The race values have changed. You are going to break map format!" );
        static_assert( Race::WZRD == 1 << 4, "The race values have changed. You are going to break map format!" );
        static_assert( Race::NECR == 1 << 5, "The race values have changed. You are going to break map format!" );
        static_assert( Race::MULT == 1 << 6, "The race values have changed. You are going to break map format!" );
        static_assert( Race::RAND == 1 << 7, "The race values have changed. You are going to break map format!" );

        constexpr size_t mainColors{ maxNumOfPlayers };

        if ( map.playerRace.size() != mainColors ) {
            // Possibly corrupted map.
            assert( 0 );
            return false;
        }

        // Gather all information about all kingdom colors and races.
        std::array<bool, mainColors> heroColorsPresent{ false };
        // Towns can be neutral so 1 more color for them.
        std::array<bool, mainColors + 1> townColorsPresent{ false };
        std::array<uint8_t, mainColors> heroRacesPresent{ 0 };
        std::array<uint8_t, mainColors + 1> townRacesPresent{ 0 };

        const auto & heroObjects = getObjectsByGroup( ObjectGroup::KINGDOM_HEROES );
        const auto & townObjects = getObjectsByGroup( ObjectGroup::KINGDOM_TOWNS );

        for ( size_t tileIndex = 0; tileIndex < map.tiles.size(); ++tileIndex ) {
            const auto & mapTile = map.tiles[tileIndex];

            for ( const auto & object : mapTile.objects ) {
                if ( object.group == ObjectGroup::KINGDOM_HEROES ) {
                    if ( object.index >= heroObjects.size() ) {
                        assert( 0 );
                        return false;
                    }

                    const auto & metadata = heroObjects[object.index].metadata;

                    const uint32_t color = metadata[0];
                    if ( color >= heroColorsPresent.size() ) {
                        assert( 0 );
                        return false;
                    }

                    heroColorsPresent[color] = true;

                    const uint32_t race = metadata[1];
                    heroRacesPresent[color] |= ( 1 << race );
                }
                else if ( object.group == ObjectGroup::KINGDOM_TOWNS ) {
                    if ( object.index >= townObjects.size() ) {
                        assert( 0 );
                        return false;
                    }

                    // Towns and Castles have 2 flags on both sides on the entrance. Verify, that they exist and have the same color.
                    assert( tileIndex > 0 && tileIndex < map.tiles.size() - 1 );

                    const uint8_t color = getTownColorIndex( map, tileIndex, object.id );

                    if ( color >= townColorsPresent.size() ) {
                        assert( 0 );
                        return false;
                    }

                    townColorsPresent[color] = true;

                    const uint32_t race = townObjects[object.index].metadata[0];
                    townRacesPresent[color] |= ( 1 << race );
                }
            }
        }

        // Update map format settings based on the gathered information.
        map.availablePlayerColors = 0;
        for ( size_t i = 0; i < mainColors; ++i ) {
            map.playerRace[i] = ( heroRacesPresent[i] | townRacesPresent[i] );

            if ( map.playerRace[i] != 0 ) {
                map.availablePlayerColors |= static_cast<PlayerColor>( 1 << i );
            }

            // Only one race can be present.
            if ( ( map.playerRace[i] & Race::RAND ) != 0 ) {
                map.playerRace[i] = Race::RAND;
            }
            if ( ( map.playerRace[i] & Race::MULT ) != 0 ) {
                map.playerRace[i] = Race::MULT;
            }
            else {
                size_t raceCount = 0;
                for ( uint8_t raceIdx = 0; raceIdx < 6; ++raceIdx ) {
                    if ( ( map.playerRace[i] & ( 1 << raceIdx ) ) != 0 ) {
                        ++raceCount;
                    }
                }

                if ( raceCount > 1 ) {
                    map.playerRace[i] = Race::MULT;
                }
            }
        }

        map.computerPlayerColors = map.computerPlayerColors & map.availablePlayerColors;
        map.humanPlayerColors = map.humanPlayerColors & map.availablePlayerColors;

        if ( map.availablePlayerColors != 0 ) {
            // Human and computer player colors might be set previously. If they are, do not update them.
            for ( size_t i = 0; i < mainColors; ++i ) {
                const PlayerColor color = static_cast<PlayerColor>( 1 << i );
                if ( ( map.availablePlayerColors & color ) != 0 && ( map.computerPlayerColors & color ) == 0 && ( map.humanPlayerColors & color ) == 0 ) {
                    // This color was not set for anyone.
                    map.computerPlayerColors |= color;
                    map.humanPlayerColors |= color;
                }
            }

            // Make sure that at least one human color exist.
            if ( map.humanPlayerColors == 0 ) {
                for ( size_t i = 0; i < mainColors; ++i ) {
                    const PlayerColor color = static_cast<PlayerColor>( 1 << i );
                    if ( ( map.availablePlayerColors & color ) != 0 ) {
                        map.humanPlayerColors |= color;
                        break;
                    }
                }
            }

            if ( !map.alliances.empty() ) {
                // Verify that alliances are set correctly:
                // - each alliance has at least one color
                // - no color should be repeated more than once
                std::array<bool, mainColors> usedAllianceColors{ false };

                for ( auto iter = map.alliances.begin(); iter != map.alliances.end(); ) {
                    PlayerColorsSet & allianceColors = *iter;

                    // Only available players should be in the alliances.
                    allianceColors &= map.availablePlayerColors;

                    for ( size_t i = 0; i < mainColors; ++i ) {
                        const PlayerColor color = static_cast<PlayerColor>( 1 << i );
                        if ( ( allianceColors & color ) != 0 ) {
                            if ( usedAllianceColors[i] ) {
                                // This color is used in another alliance. Remove it from here.
                                allianceColors = allianceColors & ( ~color );
                            }
                            else {
                                usedAllianceColors[i] = true;
                            }
                        }
                    }

                    if ( allianceColors == 0 ) {
                        // This alliance is invalid.
                        iter = map.alliances.erase( iter );
                        continue;
                    }

                    ++iter;
                }

                if ( map.alliances.size() == 1 ) {
                    // Everyone cannot be in one alliance!
                    map.alliances.clear();
                }
                else {
                    // Check that all colors being used in alliances. If not then add missing colors to the last alliance.
                    for ( size_t i = 0; i < mainColors; ++i ) {
                        const PlayerColor color = static_cast<PlayerColor>( 1 << i );
                        if ( ( map.availablePlayerColors & color ) != 0 && !usedAllianceColors[i] ) {
                            if ( map.alliances.empty() ) {
                                map.alliances.push_back( static_cast<PlayerColorsSet>( color ) );
                            }
                            else {
                                map.alliances.back() |= color;
                            }
                        }
                    }
                }
            }
        }
        else {
            // No colors are set so no alliances should exist.
            map.alliances.clear();

            // No races are set.
            map.playerRace = { 0 };
        }

        if ( map.alliances.empty() && map.victoryConditionType == FileInfo::VictoryCondition::VICTORY_DEFEAT_OTHER_SIDE ) {
            // When there are no alliances there are no sides. Reset the victory condition to the default.
            map.victoryConditionType = FileInfo::VictoryCondition::VICTORY_DEFEAT_EVERYONE;
        }

        // Update events according to the possible changes in human and/or AI player colors.
        for ( auto & [dummy, eventMetadata] : map.adventureMapEventMetadata ) {
            eventMetadata.humanPlayerColors = eventMetadata.humanPlayerColors & map.humanPlayerColors;
            eventMetadata.computerPlayerColors = eventMetadata.computerPlayerColors & map.computerPlayerColors;
        }

        // Check and update the special victory and loss conditions that depend on player objects.

        // Returns true if all is OK.
        auto checkSpecialCondition = [&map, &heroObjects, &townObjects]( const std::vector<uint32_t> & conditionMetadata, const ObjectGroup objectGroup ) {
            if ( conditionMetadata.size() != 2 ) {
                return false;
            }

            // Verify that this is a valid map object.
            const uint32_t tileIndex = conditionMetadata[0];

            assert( tileIndex < map.tiles.size() );

            for ( const auto & object : map.tiles[tileIndex].objects ) {
                if ( object.group != objectGroup ) {
                    continue;
                }

                switch ( objectGroup ) {
                case ObjectGroup::KINGDOM_TOWNS: {
                    if ( object.index >= townObjects.size() ) {
                        assert( 0 );
                        continue;
                    }

                    const PlayerColor color = Color::IndexToColor( getTownColorIndex( map, tileIndex, object.id ) );
                    if ( color != static_cast<PlayerColor>( conditionMetadata[1] ) ) {
                        // Current town color is incorrect.
                        continue;
                    }

                    return true;
                }
                case ObjectGroup::KINGDOM_HEROES: {
                    if ( object.index >= heroObjects.size() ) {
                        assert( 0 );
                        continue;
                    }

                    const uint32_t color = 1 << heroObjects[object.index].metadata[0];
                    if ( color != conditionMetadata[1] ) {
                        // Current hero color is incorrect.
                        continue;
                    }

                    return true;
                }
                default:
                    // Have you added a new object type for victory or loss conditions? Update the logic!
                    assert( 0 );
                    break;
                }
            }

            return false;
        };

        switch ( map.victoryConditionType ) {
        case FileInfo::VICTORY_CAPTURE_TOWN:
            if ( !checkSpecialCondition( map.victoryConditionMetadata, ObjectGroup::KINGDOM_TOWNS ) ) {
                map.victoryConditionMetadata.clear();
                map.victoryConditionType = FileInfo::VICTORY_DEFEAT_EVERYONE;
            }

            break;
        case FileInfo::VICTORY_KILL_HERO:
            if ( !checkSpecialCondition( map.victoryConditionMetadata, ObjectGroup::KINGDOM_HEROES ) ) {
                map.victoryConditionMetadata.clear();
                map.victoryConditionType = FileInfo::VICTORY_DEFEAT_EVERYONE;
            }

            break;
        default:
            break;
        }

        switch ( map.lossConditionType ) {
        case FileInfo::LOSS_TOWN:
            if ( !checkSpecialCondition( map.lossConditionMetadata, ObjectGroup::KINGDOM_TOWNS ) ) {
                map.lossConditionMetadata.clear();
                map.lossConditionType = FileInfo::LOSS_EVERYTHING;
            }

            break;
        case FileInfo::LOSS_HERO:
            if ( !checkSpecialCondition( map.lossConditionMetadata, ObjectGroup::KINGDOM_HEROES ) ) {
                map.lossConditionMetadata.clear();
                map.lossConditionType = FileInfo::LOSS_EVERYTHING;
            }

            break;
        default:
            break;
        }

        // Check and update owner metadata to avoid non-used player color ownership.
        auto capturableIter = map.capturableObjectsMetadata.begin();
        while ( capturableIter != map.capturableObjectsMetadata.end() ) {
            if ( !( map.availablePlayerColors & capturableIter->second.ownerColor ) ) {
                // Reset the capture state in the `world` instance.
                for ( size_t tileIndex = 0; tileIndex < map.tiles.size(); ++tileIndex ) {
                    const auto & tileObjects = map.tiles[tileIndex].objects;

                    if ( std::any_of( tileObjects.cbegin(), tileObjects.cend(),
                                      [objectUid = capturableIter->first]( const Map_Format::TileObjectInfo & info ) { return info.id == objectUid; } ) ) {
                        world.CaptureObject( static_cast<int32_t>( tileIndex ), PlayerColor::NONE );

                        break;
                    }
                }

                // Remove the ownership metadata from map.
                capturableIter = map.capturableObjectsMetadata.erase( capturableIter );
            }
            else {
                ++capturableIter;
            }
        }

        return true;
    }

    uint8_t getTownColorIndex( const Map_Format::MapFormat & map, const size_t tileIndex, const uint32_t id )
    {
        if ( map.tiles.empty() ) {
            assert( 0 );
            return 0;
        }

        if ( tileIndex == 0 || tileIndex >= map.tiles.size() - 1 ) {
            assert( 0 );
            return 0;
        }

        const auto & flagObjects = getObjectsByGroup( ObjectGroup::LANDSCAPE_FLAGS );

        uint32_t leftFlagColor = 0;
        uint32_t rightFlagColor = 0;

        for ( const auto & tempObject : map.tiles[tileIndex - 1].objects ) {
            if ( tempObject.group == ObjectGroup::LANDSCAPE_FLAGS && tempObject.id == id ) {
                if ( tempObject.index >= flagObjects.size() ) {
                    assert( 0 );
                    return 0;
                }

                leftFlagColor = flagObjects[tempObject.index].metadata[0];
                break;
            }
        }

        for ( const auto & tempObject : map.tiles[tileIndex + 1].objects ) {
            if ( tempObject.group == ObjectGroup::LANDSCAPE_FLAGS && tempObject.id == id ) {
                if ( tempObject.index >= flagObjects.size() ) {
                    assert( 0 );
                    return 0;
                }

                rightFlagColor = flagObjects[tempObject.index].metadata[0];
                break;
            }
        }

        if ( leftFlagColor != rightFlagColor ) {
            assert( 0 );
            return 0;
        }

        return static_cast<uint8_t>( leftFlagColor );
    }

    bool isJailObject( const ObjectGroup group, const uint32_t index )
    {
        return ( group == ObjectGroup::ADVENTURE_MISCELLANEOUS && getObjectInfo( group, static_cast<int32_t>( index ) ).objectType == MP2::OBJ_JAIL );
    }

    bool isCapturableObject( const MP2::MapObjectType objectType )
    {
        switch ( objectType ) {
        case MP2::OBJ_ALCHEMIST_LAB:
        case MP2::OBJ_LIGHTHOUSE:
        case MP2::OBJ_MINE:
        case MP2::OBJ_SAWMILL:
            return true;
        default:
            break;
        }

        return false;
    }

    void captureObject( const Map_Format::MapFormat & map, const int32_t tileIndex, const uint32_t objectId, const MP2::MapObjectType objectType )
    {
        if ( isCapturableObject( objectType ) ) {
            auto ownershipMetadata = map.capturableObjectsMetadata.find( objectId );
            if ( ownershipMetadata != map.capturableObjectsMetadata.end() ) {
                world.CaptureObject( tileIndex, ownershipMetadata->second.ownerColor );
            }
        }
    }

    uint32_t getBuildingsFromVector( const std::vector<uint32_t> & buildingsVector )
    {
        uint32_t buildings{ BUILD_NOTHING };
        for ( const uint32_t building : buildingsVector ) {
            buildings |= building;
        }

        return buildings;
    }

    void setDefaultCastleDefenderArmy( Map_Format::CastleMetadata & metadata )
    {
        for ( int32_t & type : metadata.defenderMonsterType ) {
            type = -1;
        }
        for ( int32_t & count : metadata.defenderMonsterCount ) {
            count = 0;
        }
    }

    bool isDefaultCastleDefenderArmy( const Map_Format::CastleMetadata & metadata )
    {
        return std::all_of( metadata.defenderMonsterType.begin(), metadata.defenderMonsterType.end(), []( const int32_t type ) { return type < 0; } );
    }

    bool loadCastleArmy( Army & army, const Map_Format::CastleMetadata & metadata )
    {
        if ( isDefaultCastleDefenderArmy( metadata ) ) {
            return false;
        }

        loadArmyFromMetadata( army, metadata.defenderMonsterType, metadata.defenderMonsterCount );
        return true;
    }

    void saveCastleArmy( const Army & army, Map_Format::CastleMetadata & metadata )
    {
        saveArmyToMetadata( army, metadata.defenderMonsterType, metadata.defenderMonsterCount );
    }

    bool loadHeroArmy( Army & army, const Map_Format::HeroMetadata & metadata )
    {
        if ( std::all_of( metadata.armyMonsterType.begin(), metadata.armyMonsterType.end(), []( const int32_t type ) { return type <= 0; } ) ) {
            // There is no custom army.
            return false;
        }

        loadArmyFromMetadata( army, metadata.armyMonsterType, metadata.armyMonsterCount );
        return true;
    }

    void saveHeroArmy( const Army & army, Map_Format::HeroMetadata & metadata )
    {
        saveArmyToMetadata( army, metadata.armyMonsterType, metadata.armyMonsterCount );
    }

    bool setRoadOnTile( Map_Format::MapFormat & map, const int32_t tileIndex )
    {
        assert( static_cast<size_t>( tileIndex ) < map.tiles.size() );

        if ( !placeNewRoadObjectOnTile( map, tileIndex ) ) {
            return false;
        }

        updateRoadObjectsAround( map, tileIndex );

        const auto & tile = map.tiles[tileIndex];

        if ( Ground::doesTerrainImageIndexContainEmbeddedObjects( tile.terrainIndex ) ) {
            // We need to set terrain image without extra objects under the road.
            const int groundType = Ground::getGroundByImageIndex( tile.terrainIndex );
            setTerrain( map, tileIndex, Ground::getRandomTerrainImageIndex( groundType, false ), false, false );
        }

        return true;
    }

    bool removeRoadFromTile( Map_Format::MapFormat & map, const int32_t tileIndex )
    {
        assert( static_cast<size_t>( tileIndex ) < map.tiles.size() );

        auto & tile = map.tiles[tileIndex];

        const auto iter = std::find_if( tile.objects.cbegin(), tile.objects.cend(), []( const auto & object ) { return object.group == Maps::ObjectGroup::ROADS; } );

        if ( iter == tile.objects.cend() ) {
            // Nothing to do here.
            return false;
        }

        removeObjectFromMapByUID( tileIndex, iter->id );
        world.getTile( tileIndex ).updateRoadFlag();
        tile.objects.erase( iter );

        updateRoadObjectsAround( map, tileIndex );

        return true;
    }

    bool updateRoadOnTile( Map_Format::MapFormat & map, const int32_t tileIndex )
    {
        assert( static_cast<size_t>( tileIndex ) < map.tiles.size() );

        auto & tile = map.tiles[tileIndex];

        const auto iter = std::find_if( tile.objects.begin(), tile.objects.end(), []( const auto & object ) { return object.group == Maps::ObjectGroup::ROADS; } );

        if ( iter == tile.objects.end() ) {
            // Nothing to do here.
            return false;
        }

        const int roadObjectIndex = getRoadObjectIndex( map, tileIndex );

        if ( iter->index == static_cast<uint32_t>( roadObjectIndex ) ) {
            // Nothing to update here.
            return false;
        }

        removeObjectFromMapByUID( tileIndex, iter->id );

        // To replace the road on the `world` map we temporarily change the last object UID to the UID previous to the current road UID.
        const uint32_t lastUid = Maps::getLastObjectUID();
        setLastObjectUID( iter->id - 1 );

        const auto & objectInfo = Maps::getObjectInfo( Maps::ObjectGroup::ROADS, roadObjectIndex );
        if ( !setObjectOnTile( world.getTile( tileIndex ), objectInfo, false ) ) {
            assert( 0 );
            return false;
        }

        assert( Maps::getLastObjectUID() == iter->id );

        // Restore the last object UID.
        setLastObjectUID( lastUid );

        // Just update the road direction index.
        iter->index = static_cast<uint32_t>( roadObjectIndex );

        return true;
    }

    void updateAllRoads( Map_Format::MapFormat & map )
    {
        const int32_t size = map.width * map.width;
        for ( int32_t index = 0; index < size; ++index ) {
            updateRoadOnTile( map, index );
        }
    }

    bool doesContainRoad( const Map_Format::TileInfo & tile )
    {
        return std::any_of( tile.objects.cbegin(), tile.objects.cend(), []( const auto & object ) { return object.group == ObjectGroup::ROADS; } );
    }

    void changeLanguage( Map_Format::MapFormat & map, const fheroes2::SupportedLanguage language )
    {
        if ( language == map.mainLanguage ) {
            // Nothing to be done as this is the current language.
            return;
        }

        // Save the existing language as a translation.
        auto & translation = map.translations[map.mainLanguage];
        translation.name = map.name;
        translation.description = map.description;
        translation.creatorNotes = map.creatorNotes;

        auto & translationInfo = map.translationInfo[map.mainLanguage];
        translationInfo = {};

        translationInfo.dailyEvents.reserve( map.dailyEvents.size() );
        for ( const auto & event : map.dailyEvents ) {
            translationInfo.dailyEvents.emplace_back( event.message );
        }

        translationInfo.rumors.reserve( map.rumors.size() );
        for ( const auto & rumor : map.rumors ) {
            translationInfo.rumors.emplace_back( rumor );
        }

        for ( const auto & [tileId, castleInfo] : map.castleMetadata ) {
            translationInfo.castleMetadata.try_emplace( tileId, castleInfo.customName );
        }

        for ( const auto & [tileId, heroInfo] : map.heroMetadata ) {
            translationInfo.heroMetadata.try_emplace( tileId, heroInfo.customName );
        }

        for ( const auto & [tileId, sphinxInfo] : map.sphinxMetadata ) {
            auto & info = translationInfo.sphinxMetadata[tileId];
            info.riddle = sphinxInfo.riddle;
            info.answers = sphinxInfo.answers;
        }

        for ( const auto & [tileId, signInfo] : map.signMetadata ) {
            translationInfo.signMetadata.try_emplace( tileId, signInfo.message );
        }

        for ( const auto & [tileId, eventInfo] : map.adventureMapEventMetadata ) {
            translationInfo.adventureMapEventMetadata.try_emplace( tileId, eventInfo.message );
        }

        loadTranslation( map, language );

        // Remove the language from the translations.
        removeTranslation( map, language );

        map.mainLanguage = language;
    }

    bool setInGameLanguage( Map_Format::BaseMapFormat & map, const fheroes2::SupportedLanguage language )
    {
        if ( !loadTranslation( map, language ) ) {
            return false;
        }

        if ( map.mainLanguage != language ) {
            // Add just an empty entry to show that the main language is also being supported.
            map.translations.try_emplace( map.mainLanguage, Maps::Map_Format::TranslationBaseMapMetadata{} );
        }

        map.mainLanguage = language;
        map.translations.erase( language );

        return true;
    }

    bool loadTranslation( Map_Format::BaseMapFormat & map, const fheroes2::SupportedLanguage language )
    {
        if ( language == map.mainLanguage ) {
            // Nothing to be done as this is the current language.
            return true;
        }

        // Check whether the language exists in the list of translations.
        // If it doesn't then leave all texts intact.
        auto translationIter = map.translations.find( language );
        if ( translationIter == map.translations.end() ) {
            return false;
        }

        // The translation exists. Restore if possible the information.
        // Here is the tricky part: we have no idea whether an empty text was left intentionally.
        // Therefore, we assume that empty texts are intentional.
        auto & translation = translationIter->second;
        map.name = std::move( translation.name );
        map.description = std::move( translation.description );
        map.creatorNotes = std::move( translation.creatorNotes );

        return true;
    }

    bool loadTranslation( Map_Format::MapFormat & map, const fheroes2::SupportedLanguage language )
    {
        if ( language == map.mainLanguage ) {
            // Nothing to be done as this is the current language.
            return true;
        }

        if ( !loadTranslation( static_cast<Maps::Map_Format::BaseMapFormat &>( map ), language ) ) {
            return false;
        }

        auto translationInfoIter = map.translationInfo.find( language );
        if ( translationInfoIter == map.translationInfo.end() ) {
            // Object information doesn't exist.
            // Nothing we need to do here.
            return true;
        }

        auto & translationInfo = translationInfoIter->second;

        // Daily events should be in order. However, if some events are deleted or added the order is not preserved.
        // We assume that the map maker preserves the order.
        const size_t minDailyEvents = std::min( map.dailyEvents.size(), translationInfo.dailyEvents.size() );
        for ( size_t i = 0; i < minDailyEvents; ++i ) {
            map.dailyEvents[i].message = std::move( translationInfo.dailyEvents[i] );
        }

        map.rumors = std::move( translationInfo.rumors );

        // The below objects might not even exist on the map so we need to verify their presence before modifying them.
        for ( auto & [tileId, castleInfo] : translationInfo.castleMetadata ) {
            auto iter = map.castleMetadata.find( tileId );
            if ( iter == map.castleMetadata.end() ) {
                continue;
            }

            iter->second.customName = std::move( castleInfo );
        }

        for ( auto & [tileId, heroInfo] : translationInfo.heroMetadata ) {
            auto iter = map.heroMetadata.find( tileId );
            if ( iter == map.heroMetadata.end() ) {
                continue;
            }

            iter->second.customName = std::move( heroInfo );
        }

        for ( auto & [tileId, sphinxInfo] : translationInfo.sphinxMetadata ) {
            auto iter = map.sphinxMetadata.find( tileId );
            if ( iter == map.sphinxMetadata.end() ) {
                continue;
            }
            iter->second.riddle = std::move( sphinxInfo.riddle );
            iter->second.answers = std::move( sphinxInfo.answers );
        }

        for ( auto & [tileId, signInfo] : translationInfo.signMetadata ) {
            auto iter = map.signMetadata.find( tileId );
            if ( iter == map.signMetadata.end() ) {
                continue;
            }

            iter->second.message = std::move( signInfo );
        }

        for ( auto & [tileId, eventInfo] : translationInfo.adventureMapEventMetadata ) {
            auto iter = map.adventureMapEventMetadata.find( tileId );
            if ( iter == map.adventureMapEventMetadata.end() ) {
                continue;
            }

            iter->second.message = std::move( eventInfo );
        }

        return true;
    }

    void removeTranslation( Map_Format::MapFormat & map, const fheroes2::SupportedLanguage language )
    {
        map.translations.erase( language );
        map.translationInfo.erase( language );
    }
}
