/***************************************************************************
 *   fheroes2: https://github.com/ihhub/fheroes2                           *
 *   Copyright (C) 2019 - 2025                                             *
 *                                                                         *
 *   Free Heroes2 Engine: http://sourceforge.net/projects/fheroes2         *
 *   Copyright (C) 2009 by Andrey Afletdinov <fheroes2@gmail.com>          *
 *                                                                         *
 *   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 "maps_tiles.h"

#include <algorithm>
#include <array>
#include <bitset>
#include <cassert>
#include <cstdint>
#include <cstdlib>
#include <limits>
#include <set>
#include <sstream>
#include <type_traits>
#include <utility>

#include "army_troop.h"
#include "castle.h"
#include "game_io.h"
#include "heroes.h"
#include "icn.h"
#include "logging.h"
#include "map_object_info.h"
#include "maps.h"
#include "maps_tiles_helper.h" // TODO: This file should not be included
#include "mp2.h"
#include "pairs.h"
#include "save_format_version.h"
#include "serialize.h"
#include "tools.h"
#include "world.h"
#include "world_object_uid.h"

namespace
{
    bool isValidShadowSprite( const int icn, const uint8_t icnIndex )
    {
        if ( icn == ICN::UNKNOWN ) {
            // Special case when no objects exist.
            return false;
        }

        switch ( icn ) {
        case ICN::MTNDSRT:
        case ICN::MTNGRAS:
        case ICN::MTNLAVA:
        case ICN::MTNMULT:
        case ICN::MTNSNOW:
        case ICN::MTNSWMP: {
            static const std::bitset<256> objMnts1ShadowBitset
                = fheroes2::makeBitsetFromVector<256>( { 0, 5, 11, 17, 21, 26, 32, 38, 42, 45, 49, 52, 55, 59, 62, 65, 68, 71, 74, 75, 79, 80 } );
            return objMnts1ShadowBitset[icnIndex];
        }
        case ICN::MTNCRCK:
        case ICN::MTNDIRT: {
            static const std::bitset<256> objMnts2ShadowBitset = fheroes2::makeBitsetFromVector<256>(
                { 0, 5, 11, 17, 21, 26, 32, 38, 42, 47, 53, 62, 68, 72, 75, 79, 82, 85, 89, 92, 95, 98, 101, 104, 105, 109, 110 } );
            return objMnts2ShadowBitset[icnIndex];
        }
        case ICN::TREDECI:
        case ICN::TREEVIL:
        case ICN::TREFALL:
        case ICN::TREFIR:
        case ICN::TREJNGL:
        case ICN::TRESNOW: {
            static const std::bitset<256> objTreeShadowBitset = fheroes2::makeBitsetFromVector<256>( { 0, 3, 7, 10, 13, 17, 20, 23, 26, 29, 32, 34 } );
            return objTreeShadowBitset[icnIndex];
        }
        case ICN::OBJNCRCK: {
            static const std::bitset<256> objCrckShadowBitset
                = fheroes2::makeBitsetFromVector<256>( { 2, 9, 13, 15, 20, 23, 28, 33, 36, 39, 45, 48, 51, 54, 56, 73, 75, 79, 200, 201, 207, 237 } );
            return objCrckShadowBitset[icnIndex];
        }
        case ICN::OBJNDIRT: {
            static const std::bitset<256> objDirtShadowBitset = fheroes2::makeBitsetFromVector<256>(
                { 0,   1,   5,   6,   14,  47,  52,  59,  62,  65,  68,  70,  72,  75,  78,  81,  84,  87,  91,  94,  97,  100, 103, 111, 114, 117,
                  126, 128, 136, 149, 150, 158, 161, 162, 163, 164, 165, 166, 167, 168, 177, 178, 179, 180, 181, 182, 183, 184, 193, 196, 200 } );
            return objDirtShadowBitset[icnIndex];
        }
        case ICN::OBJNDSRT: {
            static const std::bitset<256> objDsrtShadowBitset = fheroes2::makeBitsetFromVector<256>(
                { 11, 13, 16, 19, 23, 25, 27, 29, 33, 35, 38, 41, 44, 47, 50, 52, 54, 55, 56, 57, 58, 59, 60, 71, 75, 77, 80, 86, 103, 115, 118 } );
            return objDsrtShadowBitset[icnIndex];
        }
        case ICN::OBJNGRA2: {
            static const std::bitset<256> objGra2ShadowBitset = fheroes2::makeBitsetFromVector<256>(
                { 5,  14, 19, 20, 28, 31, 32, 33, 34, 35,  36,  37,  38,  47,  48,  49,  50,  51,  52,  53,  54,  70,  71,  72,  73,  74, 75,
                  76, 77, 78, 79, 80, 81, 82, 83, 91, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 121, 124, 128 } );
            return objGra2ShadowBitset[icnIndex];
        }
        case ICN::OBJNGRAS: {
            static const std::bitset<256> objGrasShadowBitset = fheroes2::makeBitsetFromVector<256>(
                { 0, 4, 29, 32, 36, 39, 42, 44, 46, 48, 76, 82, 88, 92, 94, 98, 102, 105, 108, 111, 113, 120, 124, 128, 134, 138, 141, 143, 145, 147 } );
            return objGrasShadowBitset[icnIndex];
        }
        case ICN::OBJNMUL2: {
            static const std::bitset<256> objMul2ShadowBitset = fheroes2::makeBitsetFromVector<256>(
                { 14,  17,  20,  24,  42,  43,  49,  50,  60,  71,  72,  113, 115, 118, 121, 123, 127, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147,
                  148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 164, 180, 181, 182, 183, 184, 185, 186, 189, 199, 200, 202, 206 } );
            return objMul2ShadowBitset[icnIndex];
        }
        case ICN::OBJNMULT: {
            static const std::bitset<256> objMultShadowBitset = fheroes2::makeBitsetFromVector<256>(
                { 1,  3,  15, 16, 17, 18, 19, 20, 21, 22, 23, 24,  25,  26,  27,  28,  29,  30,  31,  32,  33,  34,  45,  46,  47,  48,  49,  50,  51,  52,  53,  54, 57,
                  61, 67, 68, 75, 77, 79, 81, 83, 97, 98, 99, 100, 101, 102, 103, 105, 106, 107, 108, 109, 110, 113, 115, 121, 122, 124, 125, 126, 127, 128, 129, 130 } );
            return objMultShadowBitset[icnIndex];
        }
        case ICN::OBJNSNOW: {
            static const std::bitset<256> objSnowShadowBitset
                = fheroes2::makeBitsetFromVector<256>( { 21,  25,  29,  31,  33,  36,  40,  48,  54,  59,  63,  67,  70,  73,  76,  79,  101, 104, 105, 106, 107,
                                                         108, 109, 110, 111, 120, 121, 122, 123, 124, 125, 126, 127, 137, 140, 142, 144, 148, 193, 203, 207 } );
            return objSnowShadowBitset[icnIndex];
        }
        case ICN::OBJNSWMP: {
            static const std::bitset<256> objSwmpShadowBitset
                = fheroes2::makeBitsetFromVector<256>( { 2,  3,   14,  15,  16,  17,  18,  19,  20,  21,  31,  43,  44,  45,  46,  47,  48,  49, 66,
                                                         83, 125, 127, 130, 132, 136, 141, 163, 170, 175, 178, 195, 197, 202, 204, 207, 211, 215 } );
            return objSwmpShadowBitset[icnIndex];
        }
        case ICN::OBJNWAT2: {
            return icnIndex == 1;
        }
        case ICN::OBJNWATR: {
            static const std::bitset<256> objWatrShadowBitset = fheroes2::makeBitsetFromVector<256>(
                { 12,  13,  14,  15,  16,  17,  18,  26,  27,  28,  29,  30,  31,  32,  33,  34,  35,  36,  37,  38,  39,  40,  41,
                  42,  43,  44,  52,  55,  118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 166, 167,
                  168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 184, 188, 189, 190, 191, 192, 193, 194, 240 } );
            return objWatrShadowBitset[icnIndex];
        }
        case ICN::OBJNARTI:
        case ICN::OBJNRSRC:
            return 0 == ( icnIndex % 2 );
        case ICN::OBJNTWRD:
            return icnIndex > 31;
        case ICN::X_LOC1: {
            static const std::bitset<256> objXlc1ShadowBitset = fheroes2::makeBitsetFromVector<256>(
                { 1, 2, 32, 33, 34, 35, 36, 37, 38, 39, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 72, 78, 79, 83, 84, 112, 116, 120, 124, 125, 129, 133 } );
            return objXlc1ShadowBitset[icnIndex];
        }
        case ICN::X_LOC2: {
            static const std::bitset<256> objXlc2ShadowBitset = fheroes2::makeBitsetFromVector<256>(
                { 2, 7, 10, 11, 12, 13, 14, 15, 16, 17, 18, 47, 48, 49, 50, 51, 52, 53, 54, 55, 83, 84, 85, 86, 87, 88, 89, 90, 91 } );
            return objXlc2ShadowBitset[icnIndex];
        }
        case ICN::X_LOC3: {
            static const std::bitset<256> objXlc3ShadowBitset = fheroes2::makeBitsetFromVector<256>(
                { 0,  1,  2,  3,  4,  5,  6,  7,  8,  9,   20,  21,  22,  23,  24,  25,  26,  27,  28,  29,  41,  42,  43,  44,  45,  46, 47,
                  48, 49, 59, 65, 71, 77, 83, 89, 95, 101, 108, 109, 112, 113, 116, 117, 120, 121, 124, 125, 128, 129, 132, 133, 136, 137 } );
            return objXlc3ShadowBitset[icnIndex];
        }
        case ICN::OBJNLAVA: {
            static const std::bitset<256> objLavaShadowBitset = fheroes2::makeBitsetFromVector<256>( { 45, 49, 79, 80, 81, 82, 109, 113, 116 } );
            return objLavaShadowBitset[icnIndex];
        }
        case ICN::OBJNLAV2: {
            static const std::bitset<256> objLav2ShadowBitset
                = fheroes2::makeBitsetFromVector<256>( { 7,  8,  9,  10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 29, 34, 38, 39, 43, 44, 45, 46,
                                                         47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 72, 77, 78 } );
            return objLav2ShadowBitset[icnIndex];
        }
        case ICN::OBJNLAV3: {
            static const std::bitset<256> objLav3ShadowBitset = fheroes2::makeBitsetFromVector<256>(
                { 1,   2,   3,   4,   16,  17,  18,  19,  31,  32,  33,  34,  38,  46,  47,  48,  49,  50,  57,  58,  59,  61,  62,  63,  64,  76,  77,
                  91,  92,  93,  106, 107, 108, 109, 110, 111, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133,
                  134, 136, 137, 138, 139, 142, 143, 144, 145, 146, 147, 148, 149, 166, 167, 168, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186,
                  187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213,
                  214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 243 } );
            return objLav3ShadowBitset[icnIndex];
        }
        case ICN::OBJNTWSH:
            return true;
        case ICN::BOAT32:
        case ICN::EXTRAOVR:
        case ICN::FLAG32:
        case ICN::MINIHERO:
        case ICN::MONS32:
        case ICN::OBJNTOWN:
        case ICN::OBJNTWBA:
        case ICN::OBJNXTRA:
        case ICN::ROAD:
        case ICN::STREAM:
            return false;
        default:
            // Did you add a new ICN group of objects into the game?
            assert( 0 );
            break;
        }

        return false;
    }

    bool isReefs( const uint8_t index )
    {
        return index >= 111 && index <= 135;
    }

    bool isValidReefsSprite( const MP2::ObjectIcnType objectIcnType, const uint8_t icnIndex )
    {
        return objectIcnType == MP2::OBJ_ICN_TYPE_X_LOC2 && isReefs( icnIndex );
    }

    bool isShortObject( const MP2::MapObjectType objectType )
    {
        // Some objects allow middle moves even being attached to the bottom.
        // These object actually don't have any sprites on tiles above them within top layer object parts.
        // TODO: find a better way to do not hardcode values here.

        switch ( objectType ) {
        case MP2::OBJ_HALFLING_HOLE:
        case MP2::OBJ_NON_ACTION_HALFLING_HOLE:
        case MP2::OBJ_LEAN_TO:
        case MP2::OBJ_WATER_LAKE:
        case MP2::OBJ_TAR_PIT:
        case MP2::OBJ_MERCENARY_CAMP:
        case MP2::OBJ_NON_ACTION_MERCENARY_CAMP:
        case MP2::OBJ_STANDING_STONES:
        case MP2::OBJ_SHRINE_FIRST_CIRCLE:
        case MP2::OBJ_SHRINE_SECOND_CIRCLE:
        case MP2::OBJ_SHRINE_THIRD_CIRCLE:
        case MP2::OBJ_MAGIC_GARDEN:
        case MP2::OBJ_RUINS:
        case MP2::OBJ_NON_ACTION_RUINS:
        case MP2::OBJ_SIGN:
        case MP2::OBJ_IDOL:
        case MP2::OBJ_STONE_LITHS:
        case MP2::OBJ_NON_ACTION_STONE_LITHS:
        case MP2::OBJ_WAGON:
        case MP2::OBJ_WAGON_CAMP:
        case MP2::OBJ_NON_ACTION_WAGON_CAMP:
        case MP2::OBJ_GOBLIN_HUT:
        case MP2::OBJ_FAERIE_RING:
        case MP2::OBJ_NON_ACTION_FAERIE_RING:
        case MP2::OBJ_BARRIER:
        case MP2::OBJ_MAGIC_WELL:
        case MP2::OBJ_NOTHING_SPECIAL:
            return true;
        default:
            break;
        }

        return false;
    }

    bool isDetachedObjectType( const MP2::MapObjectType objectType )
    {
        // Some objects do not take into account other objects below them.
        switch ( objectType ) {
        case MP2::OBJ_CASTLE:
        case MP2::OBJ_WAGON_CAMP:
        case MP2::OBJ_FAERIE_RING:
        case MP2::OBJ_MINE:
        case MP2::OBJ_SAWMILL:
        case MP2::OBJ_WATER_ALTAR:
        case MP2::OBJ_AIR_ALTAR:
        case MP2::OBJ_FIRE_ALTAR:
        case MP2::OBJ_EARTH_ALTAR:
            return true;
        default:
            break;
        }

        return false;
    }

    bool isCombinedObject( const MP2::MapObjectType objectType )
    {
        // Trees allow bottom and top movements but they don't allow the same for other trees.
        switch ( objectType ) {
        case MP2::OBJ_TREES:
        case MP2::OBJ_CRATER:
            return true;
        default:
            break;
        }

        return false;
    }

    const char * getObjectLayerName( const uint8_t level )
    {
        switch ( level ) {
        case Maps::OBJECT_LAYER:
            return "Object layer";
        case Maps::BACKGROUND_LAYER:
            return "Background layer";
        case Maps::SHADOW_LAYER:
            return "Shadow layer";
        case Maps::TERRAIN_LAYER:
            return "Terrain layer";
        default:
            assert( 0 );
            break;
        }

        return "Unknown layer";
    }

    bool updatePriceOfLoyaltyObjectType( const Maps::ObjectPart & part, Maps::Tile & tile )
    {
        // The Price of Loyalty' object should belong to a certain ICN type.
        switch ( part.icnType ) {
        case MP2::OBJ_ICN_TYPE_X_LOC1:
        case MP2::OBJ_ICN_TYPE_X_LOC2:
        case MP2::OBJ_ICN_TYPE_X_LOC3:
            break;
        default:
            // This is not an original POL object.
            return false;
        }

        const auto * objectPart = Maps::getObjectPartByIcn( part.icnType, part.icnIndex );
        if ( objectPart == nullptr ) {
            // This could be a hacked map or an object part which we ignored in our list of objects (for example, an empty object part).
            return false;
        }

        if ( objectPart->objectType == MP2::OBJ_NONE ) {
            // It looks like the object is not present in the list or the object is marked incorrectly.
            // Let's update the tile based on the object parts it has.
            tile.updateObjectType();
            return true;
        }

        tile.setMainObjectType( objectPart->objectType );
        return true;
    }

    bool isSpriteRoad( const MP2::ObjectIcnType objectIcnType, const uint8_t imageIndex )
    {
        switch ( objectIcnType ) {
        case MP2::OBJ_ICN_TYPE_ROAD: {
            static const std::set<uint8_t> allowedIndecies{ 0, 2, 3, 4, 5, 6, 7, 9, 12, 13, 14, 16, 17, 18, 19, 20, 21, 26, 28, 29, 30, 31 };
            return ( allowedIndecies.count( imageIndex ) == 1 );
        }
        case MP2::OBJ_ICN_TYPE_OBJNTOWN: {
            static const std::set<uint8_t> allowedIndecies{ 13, 29, 45, 61, 77, 93, 109, 125, 141, 157, 173, 189 };
            return ( allowedIndecies.count( imageIndex ) == 1 );
        }
        case MP2::OBJ_ICN_TYPE_OBJNTWRD: {
            static const std::set<uint8_t> allowedIndecies{ 13, 29 };
            return ( allowedIndecies.count( imageIndex ) == 1 );
        }
        default:
            break;
        }

        return false;
    }

    bool isObjectPartShadow( const Maps::ObjectPart & ta )
    {
        return isValidShadowSprite( MP2::getIcnIdFromObjectIcnType( ta.icnType ), ta.icnIndex );
    }

    void getObjectPartInfo( const Maps::ObjectPart & part, std::ostringstream & os, const bool isGroundLayer )
    {
        os << "UID             : " << part._uid << std::endl
           << "ICN object type : " << static_cast<int>( part.icnType ) << " (" << ICN::getIcnFileName( MP2::getIcnIdFromObjectIcnType( part.icnType ) ) << ")"
           << std::endl
           << "image index     : " << static_cast<int>( part.icnIndex ) << std::endl;

        if ( isGroundLayer || ( part.layerType == Maps::OBJECT_LAYER ) ) {
            os << "layer type      : " << static_cast<int>( part.layerType ) << " - " << getObjectLayerName( part.layerType ) << std::endl;
        }
        else {
            os << "!!! INVALID !!! layer type : " << static_cast<int>( part.layerType ) << " - " << getObjectLayerName( part.layerType ) << std::endl;
        }

        os << "is shadow       : " << ( isObjectPartShadow( part ) ? "yes" : "no" ) << std::endl;

        if ( part == Maps::ObjectPart{} ) {
            return;
        }

        const auto * partInfo = Maps::getObjectPartByIcn( part.icnType, part.icnIndex );
        if ( partInfo == nullptr ) {
            os << "!!! UNKNOWN OBJECT PART !!!" << std::endl;
            return;
        }

        os << "MP2 object type : " << static_cast<int>( partInfo->objectType ) << " (" << MP2::StringObject( partInfo->objectType ) << ")" << std::endl;
    }

    std::string getObjectPartInfo( const Maps::ObjectPart & part, const bool isGroundLayer )
    {
        std::ostringstream os;
        os << "--------- " << ( isGroundLayer ? "Ground" : "Top" ) << " layer --------" << std::endl;
        getObjectPartInfo( part, os, isGroundLayer );
        return os.str();
    }
}

void Maps::Tile::Init( const MP2::MP2TileInfo & mp2 )
{
    _metadata[0] = ( ( ( mp2.quantity2 << 8 ) + mp2.quantity1 ) >> 3 );

    setMainObjectType( static_cast<MP2::MapObjectType>( mp2.mapObjectType ) );

    if ( !MP2::doesObjectContainMetadata( _mainObjectType ) && ( _metadata[0] != 0 ) ) {
        // No metadata should exist for non-action objects.
        // Some maps have invalid format. Even if this metadata is set here, it will later be reset during world map loading.
        DEBUG_LOG( DBG_GAME, DBG_WARN,
                   "Metadata present for non action object " << MP2::StringObject( _mainObjectType ) << " at tile " << _index << ". Metadata value " << _metadata[0] )
    }

    // In the original Editor the road bit is set even if no road exist.
    // It is important to verify the existence of a road without relying on this bit.

    const MP2::ObjectIcnType bottomObjectIcnType = static_cast<MP2::ObjectIcnType>( mp2.objectName1 >> 2 );
    if ( bottomObjectIcnType != MP2::ObjectIcnType::OBJ_ICN_TYPE_UNKNOWN ) {
        pushGroundObjectPart( { static_cast<ObjectLayerType>( mp2.quantity1 & 0x03 ), mp2.level1ObjectUID, bottomObjectIcnType, mp2.bottomIcnImageIndex } );
    }

    const MP2::ObjectIcnType topObjectIcnType = static_cast<MP2::ObjectIcnType>( mp2.objectName2 >> 2 );
    if ( topObjectIcnType != MP2::ObjectIcnType::OBJ_ICN_TYPE_UNKNOWN ) {
        // Top layer objects do not have any internal structure (layers) so all of them should have the same internal layer.
        // TODO: remove layer type for top layer objects.

        // One object part for a crack object has invalid layer.
        // This causes a rendering bug which is also present in the original game.
        if ( topObjectIcnType == MP2::OBJ_ICN_TYPE_OBJNCRCK && mp2.topIcnImageIndex == 226 ) {
            pushGroundObjectPart( { ObjectLayerType::TERRAIN_LAYER, mp2.level2ObjectUID, topObjectIcnType, mp2.topIcnImageIndex } );
        }
        else {
            pushTopObjectPart( { OBJECT_LAYER, mp2.level2ObjectUID, topObjectIcnType, mp2.topIcnImageIndex } );
        }
    }
}

Heroes * Maps::Tile::getHero() const
{
    return ( MP2::OBJ_HERO == _mainObjectType ) && Heroes::isValidId( _occupantHeroId ) ? world.GetHeroes( _occupantHeroId ) : nullptr;
}

void Maps::Tile::setHero( Heroes * hero )
{
    if ( hero ) {
        using OccupantHeroIdType = decltype( _occupantHeroId );
        static_assert( std::is_same_v<OccupantHeroIdType, uint8_t> );

        hero->setObjectTypeUnderHero( _mainObjectType );

        assert( hero->GetID() >= std::numeric_limits<OccupantHeroIdType>::min() && hero->GetID() < std::numeric_limits<OccupantHeroIdType>::max() );
        _occupantHeroId = static_cast<OccupantHeroIdType>( hero->GetID() );

        setMainObjectType( MP2::OBJ_HERO );
    }
    else {
        hero = getHero();

        if ( hero ) {
            setMainObjectType( hero->getObjectTypeUnderHero() );
            hero->setObjectTypeUnderHero( MP2::OBJ_NONE );
        }
        else {
            updateObjectType();
        }

        _occupantHeroId = Heroes::UNKNOWN;
    }
}

fheroes2::Point Maps::Tile::GetCenter() const
{
    return GetPoint( _index );
}

MP2::MapObjectType Maps::Tile::_getMainObjectTypeUnderHero() const
{
    if ( _mainObjectType != MP2::OBJ_HERO ) {
        return _mainObjectType;
    }

    const Heroes * hero = getHero();
    return hero ? hero->getObjectTypeUnderHero() : MP2::OBJ_NONE;
}

void Maps::Tile::setMainObjectType( const MP2::MapObjectType objectType )
{
    _mainObjectType = objectType;

    world.resetPathfinder();
}

void Maps::Tile::setBoat( const int direction, const PlayerColor color )
{
    if ( _mainObjectPart.icnType != MP2::OBJ_ICN_TYPE_UNKNOWN ) {
        // It is important to preserve the order of objects for rendering purposes. Therefore, the main object should go to the front of objects.
        _groundObjectPart.emplace_front( _mainObjectPart );
    }

    // If this assertion blows up then you are trying to put a boat on land!
    assert( isWater() );

    setMainObjectType( MP2::OBJ_BOAT );

    // Make sure that the object part is being cleared before setting a new one.
    _mainObjectPart = {};

    // A boat is an object so it must be on the object layer.
    _mainObjectPart.layerType = ObjectLayerType::OBJECT_LAYER;
    _mainObjectPart.icnType = MP2::OBJ_ICN_TYPE_BOAT32;

    switch ( direction ) {
    case Direction::TOP:
        _mainObjectPart.icnIndex = 0;
        break;
    case Direction::TOP_RIGHT:
        _mainObjectPart.icnIndex = 9;
        break;
    case Direction::RIGHT:
        _mainObjectPart.icnIndex = 18;
        break;
    case Direction::BOTTOM_RIGHT:
        _mainObjectPart.icnIndex = 27;
        break;
    case Direction::BOTTOM:
        _mainObjectPart.icnIndex = 36;
        break;
    // Left-side sprites have to be flipped, add 128 to index.
    case Direction::BOTTOM_LEFT:
        _mainObjectPart.icnIndex = 27 + 128;
        break;
    case Direction::LEFT:
        _mainObjectPart.icnIndex = 18 + 128;
        break;
    case Direction::TOP_LEFT:
        _mainObjectPart.icnIndex = 9 + 128;
        break;
    default:
        _mainObjectPart.icnIndex = 18;
        break;
    }

    // Boats are not passable.
    assert( !_mainObjectPart.isPassabilityTransparent() );

#ifdef WITH_DEBUG
    const uint32_t newUid = getNewObjectUID();

    // Check that this ID is not used for some other object.
    for ( uint32_t tileIndex = 0; tileIndex < world.getSize(); ++tileIndex ) {
        assert( !world.getTile( tileIndex ).doesObjectExist( newUid ) );
    }
    _mainObjectPart._uid = newUid;
#else
    _mainObjectPart._uid = getNewObjectUID();
#endif // WITH_DEBUG

    _boatOwnerColor = color;
}

int Maps::Tile::getBoatDirection() const
{
    // Check if it really is a boat
    if ( _mainObjectPart.icnType != MP2::OBJ_ICN_TYPE_BOAT32 )
        return Direction::UNKNOWN;

    // Left-side sprites have to flipped, add 128 to index
    switch ( _mainObjectPart.icnIndex ) {
    case 0:
        return Direction::TOP;
    case 9:
        return Direction::TOP_RIGHT;
    case 18:
        return Direction::RIGHT;
    case 27:
        return Direction::BOTTOM_RIGHT;
    case 36:
        return Direction::BOTTOM;
    case 27 + 128:
        return Direction::BOTTOM_LEFT;
    case 18 + 128:
        return Direction::LEFT;
    case 9 + 128:
        return Direction::TOP_LEFT;
    default:
        break;
    }

    return Direction::UNKNOWN;
}

int Maps::Tile::getTileIndependentPassability() const
{
    // Tile-independent passability is based purely on object parts located on this tile.
    // We need to run through all object parts on the ground level to calculate the resulting passability.
    //
    // The total passability is a combination of all object parts on the tile.
    // If we encounter any action object we have to stop calculations.
    //
    // All object parts should be verified in the following order:
    // - main object part
    // - from bottom object part from the end of the object list to the start
    // In other words, we have to go through object parts in the reverse order as they are being rendered.
    //
    // Top object parts do not affect passability.
    int passability = DIRECTION_ALL;

    const auto getObjectPartPassability = []( const Maps::ObjectPart & part, bool & isActionObject ) {
        if ( part.icnType == MP2::OBJ_ICN_TYPE_ROAD || part.icnType == MP2::OBJ_ICN_TYPE_STREAM || part.icnType == MP2::OBJ_ICN_TYPE_FLAG32 ) {
            // Rivers, streams and flags are completely passable.
            return DIRECTION_ALL;
        }

        // The tile has a valid object part. Check its passability.
        const MP2::MapObjectType type = getObjectTypeByIcn( part.icnType, part.icnIndex );
        if ( MP2::isOffGameActionObject( type ) ) {
            // This is an action object part.
            isActionObject = true;
            return MP2::getActionObjectDirection( type );
        }

        if ( type == MP2::OBJ_REEFS ) {
            // Reefs are inaccessible.
            return 0;
        }

        if ( !part.isPassabilityTransparent() && !isObjectPartShadow( part ) ) {
            // This is a ground object part which is not a shadow or a part of terrain.
            return ( DIRECTION_CENTER_ROW | DIRECTION_BOTTOM_ROW );
        }

        // It looks like this object part is fully passable.
        return DIRECTION_ALL;
    };

    bool isActionObject = false;

    if ( _mainObjectPart.icnType != MP2::OBJ_ICN_TYPE_UNKNOWN ) {
        passability = passability & getObjectPartPassability( _mainObjectPart, isActionObject );
        if ( isActionObject ) {
            return passability;
        }
    }

    for ( auto iter = _groundObjectPart.crbegin(); iter != _groundObjectPart.crend(); ++iter ) {
        passability = passability & getObjectPartPassability( *iter, isActionObject );
        if ( isActionObject ) {
            return passability;
        }
    }

    return passability;
}

void Maps::Tile::setInitialPassability()
{
    using TilePassabilityDirectionsType = decltype( _tilePassabilityDirections );
    static_assert( std::is_same_v<TilePassabilityDirectionsType, uint16_t> );

    const int passability = getTileIndependentPassability();
    assert( passability >= std::numeric_limits<TilePassabilityDirectionsType>::min() && passability <= std::numeric_limits<TilePassabilityDirectionsType>::max() );

    _tilePassabilityDirections = static_cast<TilePassabilityDirectionsType>( passability );
}

void Maps::Tile::updatePassability()
{
    // If the passability is already 0 nothing we need to do.
    if ( _tilePassabilityDirections == 0 ) {
        // This tile is impassable.
        return;
    }

    // If this assertion blows up then you are calling this method more than once!
    assert( _tilePassabilityDirections == getTileIndependentPassability() );

    // Verify the neighboring tiles.
    // If a tile contains a tall object then it affects the passability of diagonal moves to the top from the current tile.
    if ( ( _tilePassabilityDirections & Direction::TOP_LEFT ) && isValidDirection( _index, Direction::LEFT ) ) {
        const Tile & leftTile = world.getTile( GetDirectionIndex( _index, Direction::LEFT ) );

        if ( leftTile.isAnyTallObjectOnTile() && ( leftTile.getTileIndependentPassability() & Direction::TOP ) == 0 ) {
            _tilePassabilityDirections &= ~Direction::TOP_LEFT;
        }
    }

    if ( ( _tilePassabilityDirections & Direction::TOP_RIGHT ) && isValidDirection( _index, Direction::RIGHT ) ) {
        const Tile & rightTile = world.getTile( GetDirectionIndex( _index, Direction::RIGHT ) );

        if ( rightTile.isAnyTallObjectOnTile() && ( rightTile.getTileIndependentPassability() & Direction::TOP ) == 0 ) {
            _tilePassabilityDirections &= ~Direction::TOP_RIGHT;
        }
    }

    // Get object type but ignore heroes as they are "temporary" objects.
    const MP2::MapObjectType objectType = getMainObjectType( false );
    if ( MP2::isOffGameActionObject( objectType ) ) {
        // This is an action object. Action object passability is not affected by other objects.
        return;
    }

    if ( isShadow() ) {
        // The whole tile contains only shadow object parts. All shadows do not affect passability.
        return;
    }

    if ( _mainObjectPart.icnType == MP2::OBJ_ICN_TYPE_UNKNOWN ) {
        // The main object part is not set. Ignore the tile.
        // TODO: this is wrong as tiles can have object parts at the ground layer. Fix it!
        return;
    }

    if ( _mainObjectPart.isPassabilityTransparent() ) {
        // This object does not affect passability.
        return;
    }

    // Check the tile located below the object, which may affect the passability.
    if ( !isValidDirection( _index, Direction::BOTTOM ) ) {
        // This object "touches" the bottom part of the map. Mark is as inaccessible.
        _tilePassabilityDirections = 0;
        return;
    }

    const Tile & bottomTile = world.getTile( GetDirectionIndex( _index, Direction::BOTTOM ) );
    // If an object is located on land and the tile below it is a water tile, then mark the current tile as impassable.
    // It's done for cases when a hero won't be able to disembark on the tile.
    if ( !isWater() && bottomTile.isWater() ) {
        _tilePassabilityDirections = 0;
        return;
    }

    // If a bottom tile has the same object ID then this tile must be marked as inaccessible because it is a continuation of the same object.
    std::vector<uint32_t> tileUIDs;

    // If this assertion blows up then the object is not set properly. An object must have a valid UID!
    assert( _mainObjectPart._uid != 0 );
    tileUIDs.emplace_back( _mainObjectPart._uid );

    for ( const auto & part : _groundObjectPart ) {
        if ( !part.isPassabilityTransparent() ) {
            // If this assertion blows up then the object is not set properly. An object must have a valid UID!
            assert( part._uid != 0 );
            tileUIDs.emplace_back( part._uid );
        }
    }

    for ( const uint32_t objectId : tileUIDs ) {
        if ( bottomTile.doesObjectExist( objectId ) ) {
            _tilePassabilityDirections = 0;
            return;
        }
    }

    // If the tile below contains an action object and it allows access from top, nothing we need to change for passability
    // as the object below does not affect it.
    const MP2::MapObjectType bottomTileObjectType = bottomTile.getMainObjectType( false );
    if ( MP2::isOffGameActionObject( bottomTileObjectType ) && ( bottomTile.getTileIndependentPassability() & Direction::TOP ) != 0 ) {
        return;
    }

    // Count how many objects are there excluding shadows, roads, river streams and flags.
    const std::ptrdiff_t validBottomLayerObjects = std::count_if( _groundObjectPart.begin(), _groundObjectPart.end(), []( const auto & part ) {
        if ( isObjectPartShadow( part ) ) {
            return false;
        }

        return part.icnType != MP2::OBJ_ICN_TYPE_ROAD && part.icnType != MP2::OBJ_ICN_TYPE_STREAM && part.icnType != MP2::OBJ_ICN_TYPE_FLAG32;
    } );

    const bool singleObjectTile = ( validBottomLayerObjects == 0 ) && _topObjectPart.empty() && ( bottomTile._mainObjectPart.icnType != _mainObjectPart.icnType );

    // TODO: we might need to simplify the logic below as singleObjectTile might cover most of it.
    if ( !singleObjectTile && !isDetachedObject() && ( bottomTile._mainObjectPart.icnType != MP2::OBJ_ICN_TYPE_UNKNOWN )
         && !bottomTile._mainObjectPart.isPassabilityTransparent() ) {
        const MP2::MapObjectType correctedObjectType = MP2::getBaseActionObjectType( bottomTileObjectType );

        if ( MP2::isOffGameActionObject( bottomTileObjectType ) || MP2::isOffGameActionObject( correctedObjectType ) ) {
            if ( !isShortObject( bottomTileObjectType ) && !isShortObject( correctedObjectType ) ) {
                // Since the object on the tile below is considered as tall we must mark this tile as impassable.
                _tilePassabilityDirections = 0;
            }

            return;
        }

        if ( isShortObject( bottomTileObjectType )
             || ( !bottomTile.containsAnyObjectIcnType( getValidObjectIcnTypes() ) && ( isCombinedObject( objectType ) || isCombinedObject( bottomTileObjectType ) ) ) ) {
            // Checking the main object type of a tile is not entirely the correct idea
            // as some tiles can be marked as valid objects even though nothing was set on them.
            // As an example, the Necroking map contains a tile with rock shadow which is marked as object.
            // Most likely these maps are hacked but since we are reading from the old map format we cannot assume things and assert them.
        }
        else {
            _tilePassabilityDirections = 0;
        }
    }
}

bool Maps::Tile::doesObjectExist( const uint32_t uid ) const
{
    if ( _mainObjectPart._uid == uid && !_mainObjectPart.isPassabilityTransparent() ) {
        return true;
    }

    return std::any_of( _groundObjectPart.cbegin(), _groundObjectPart.cend(),
                        [uid]( const auto & part ) { return part._uid == uid && !part.isPassabilityTransparent(); } );
}

void Maps::Tile::pushGroundObjectPart( ObjectPart part )
{
    if ( isSpriteRoad( part.icnType, part.icnIndex ) ) {
        _isTileMarkedAsRoad = true;
    }

    _groundObjectPart.emplace_back( part );
}

void Maps::Tile::sortObjectParts()
{
    if ( _groundObjectPart.empty() ) {
        // Nothing to sort.
        return;
    }

    // Push everything to the container and sort it by level.
    if ( _mainObjectPart.icnType != MP2::OBJ_ICN_TYPE_UNKNOWN ) {
        _groundObjectPart.emplace_front( _mainObjectPart );
    }

    // Sort by internal layers.
    _groundObjectPart.sort( []( const auto & left, const auto & right ) { return ( left.layerType > right.layerType ); } );

    if ( !_groundObjectPart.empty() ) {
        auto highestPriorityPartIter = _groundObjectPart.end();

        while ( highestPriorityPartIter != _groundObjectPart.begin() ) {
            --highestPriorityPartIter;

            // Flags might be on the same layer as other objects but they cannot be the main object because they are a part of another object.
            if ( highestPriorityPartIter->icnType != MP2::OBJ_ICN_TYPE_FLAG32 ) {
                std::swap( *highestPriorityPartIter, _mainObjectPart );

                // If this assertion blows up then you are not storing correct values for layer type!
                assert( _mainObjectPart.layerType <= TERRAIN_LAYER );

                _groundObjectPart.erase( highestPriorityPartIter );

                break;
            }
        }
    }

    // Top layer objects don't have any rendering priorities so they should be rendered first in queue first to render.
}

Maps::ObjectPart * Maps::Tile::getGroundObjectPart( const uint32_t uid )
{
    auto it = std::find_if( _groundObjectPart.begin(), _groundObjectPart.end(), [uid]( const auto & v ) { return v._uid == uid; } );

    return it != _groundObjectPart.end() ? &( *it ) : nullptr;
}

Maps::ObjectPart * Maps::Tile::getTopObjectPart( const uint32_t uid )
{
    auto it = std::find_if( _topObjectPart.begin(), _topObjectPart.end(), [uid]( const auto & v ) { return v._uid == uid; } );

    return it != _topObjectPart.end() ? &( *it ) : nullptr;
}

std::string Maps::Tile::String() const
{
    std::ostringstream os;

    const MP2::MapObjectType objectType = getMainObjectType();

    os << "******* Tile info *******" << std::endl
       << "index           : " << _index << ", "
       << "point: (" << GetCenter().x << ", " << GetCenter().y << ")" << std::endl
       << "MP2 object type : " << static_cast<int>( objectType ) << " (" << MP2::StringObject( objectType ) << ")" << std::endl
       << "region Id       : " << _region << std::endl
       << "ground type     : " << Ground::String( GetGround() ) << " (isRoad: " << _isTileMarkedAsRoad << ")" << std::endl
       << "ground img index: " << _terrainImageIndex << ", image flags: " << static_cast<int>( _terrainFlags ) << std::endl
       << "passable from   : " << ( _tilePassabilityDirections ? Direction::String( _tilePassabilityDirections ) : "nowhere" ) << std::endl
       << "metadata value 1: " << _metadata[0] << std::endl
       << "metadata value 2: " << _metadata[1] << std::endl
       << "metadata value 3: " << _metadata[2] << std::endl;

    if ( objectType == MP2::OBJ_BOAT ) {
        os << "boat owner color: " << Color::String( _boatOwnerColor ) << std::endl;
    }

    os << "--------- Main object part --------" << std::endl;
    getObjectPartInfo( _mainObjectPart, os, true );

    for ( const auto & part : _groundObjectPart ) {
        os << getObjectPartInfo( part, true );
    }

    for ( const auto & part : _topObjectPart ) {
        os << getObjectPartInfo( part, false );
    }

    bool isExtraInfoPresent = false;

    const auto addExtraInfoLine = [&isExtraInfoPresent, &os]() {
        if ( !isExtraInfoPresent ) {
            isExtraInfoPresent = true;
            os << "--- Extra information ---" << std::endl;
        }
    };

    switch ( objectType ) {
    case MP2::OBJ_ABANDONED_MINE:
    case MP2::OBJ_AIR_ALTAR:
    case MP2::OBJ_ARCHER_HOUSE:
    case MP2::OBJ_BARROW_MOUNDS:
    case MP2::OBJ_CAVE:
    case MP2::OBJ_CITY_OF_DEAD:
    case MP2::OBJ_DESERT_TENT:
    case MP2::OBJ_DRAGON_CITY:
    case MP2::OBJ_DWARF_COTTAGE:
    case MP2::OBJ_EARTH_ALTAR:
    case MP2::OBJ_EXCAVATION:
    case MP2::OBJ_FIRE_ALTAR:
    case MP2::OBJ_GENIE_LAMP:
    case MP2::OBJ_GOBLIN_HUT:
    case MP2::OBJ_HALFLING_HOLE:
    case MP2::OBJ_MONSTER:
    case MP2::OBJ_PEASANT_HUT:
    case MP2::OBJ_RUINS:
    case MP2::OBJ_TREE_CITY:
    case MP2::OBJ_TREE_HOUSE:
    case MP2::OBJ_TROLL_BRIDGE:
    case MP2::OBJ_WAGON_CAMP:
    case MP2::OBJ_WATCH_TOWER:
    case MP2::OBJ_WATER_ALTAR:
        addExtraInfoLine();
        os << "monster count   : " << getMonsterCountFromTile( *this ) << std::endl;
        break;
    case MP2::OBJ_HERO: {
        addExtraInfoLine();
        const Heroes * hero = getHero();
        if ( hero ) {
            os << hero->String();
        }
        else {
            os << "!!! INVALID OBJECT TYPE !!!";
        }
        break;
    }
    case MP2::OBJ_NON_ACTION_CASTLE:
    case MP2::OBJ_CASTLE: {
        addExtraInfoLine();
        const Castle * castle = world.getCastle( GetCenter() );
        if ( castle ) {
            os << castle->String();
        }
        else {
            os << "!!! INVALID OBJECT TYPE !!!";
        }
        break;
    }
    default: {
        const MapsIndexes & v = getMonstersProtectingTile( _index );
        if ( !v.empty() ) {
            addExtraInfoLine();
            os << "protection      : ";
            for ( const int32_t index : v ) {
                os << index << ", ";
            }
            os << std::endl;
        }
        break;
    }
    }

    if ( MP2::isCaptureObject( getMainObjectType( false ) ) ) {
        addExtraInfoLine();
        const CapturedObject & co = world.GetCapturedObject( _index );

        os << "capture color   : " << Color::String( co.objCol.second ) << std::endl;
        if ( co.guardians.isValid() ) {
            os << "capture guard   : " << co.guardians.GetName() << std::endl << "capture count   : " << co.guardians.GetCount() << std::endl;
        }
    }

    os << "*************************" << std::endl;

    return os.str();
}

bool Maps::Tile::isSuitableForUltimateArtifact() const
{
    if ( isWater() || !isPassableFrom( Direction::CENTER, false, true, PlayerColor::NONE ) ) {
        return false;
    }

    if ( _mainObjectPart.icnType != MP2::OBJ_ICN_TYPE_UNKNOWN && !isObjectPartShadow( _mainObjectPart ) ) {
        return false;
    }

    if ( static_cast<size_t>( std::count_if( _groundObjectPart.begin(), _groundObjectPart.end(), isObjectPartShadow ) ) != _groundObjectPart.size() ) {
        return false;
    }

    if ( static_cast<size_t>( std::count_if( _topObjectPart.begin(), _topObjectPart.end(), isObjectPartShadow ) ) != _topObjectPart.size() ) {
        return false;
    }

    return true;
}

bool Maps::Tile::isSuitableForDisembarkation() const
{
    // Tiles with OBJ_COAST are always suitable for disembarkation
    if ( _mainObjectType == MP2::OBJ_COAST ) {
        return true;
    }

    // Tiles with heroes are a special case because heroes are moving objects and, strictly speaking, are not part of the map itself, so the checks below do not work with
    // them
    if ( _mainObjectType == MP2::OBJ_HERO ) {
        return false;
    }

    // Tiles with events are not suitable for disembarkation (at least for now)
    if ( _mainObjectType == MP2::OBJ_EVENT ) {
        return false;
    }

    // It is impossible to disembark on water tiles
    if ( isWater() ) {
        return false;
    }

    const auto isObjectPartPassable = []( const Maps::ObjectPart & part ) {
        // Roads, streams and flags are completely passable.
        if ( part.icnType == MP2::OBJ_ICN_TYPE_ROAD || part.icnType == MP2::OBJ_ICN_TYPE_STREAM || part.icnType == MP2::OBJ_ICN_TYPE_FLAG32 ) {
            return true;
        }

        return part.isPassabilityTransparent() || isObjectPartShadow( part );
    };

    return ( _mainObjectPart.icnType == MP2::OBJ_ICN_TYPE_UNKNOWN || isObjectPartPassable( _mainObjectPart ) )
           && std::all_of( _groundObjectPart.begin(), _groundObjectPart.end(), isObjectPartPassable );
}

bool Maps::Tile::isSuitableForSummoningBoat() const
{
    // Tiles with heroes are a special case because heroes are moving objects and, strictly speaking, are not part of the map itself, so the checks below do not work with
    // them
    if ( _mainObjectType == MP2::OBJ_HERO ) {
        return false;
    }

    // Tiles with events are not suitable for summoning a boat (at least for now)
    if ( _mainObjectType == MP2::OBJ_EVENT ) {
        return false;
    }

    // It is impossible to summon a boat on land
    if ( !isWater() ) {
        return false;
    }

    const auto isObjectPartPassable = []( const Maps::ObjectPart & part ) {
        // The part of the Lighthouse flag that is located on the adjacent tile doesn't affect the passability
        return part.icnType == MP2::OBJ_ICN_TYPE_FLAG32 || part.isPassabilityTransparent() || isObjectPartShadow( part );
    };

    return ( _mainObjectPart.icnType == MP2::OBJ_ICN_TYPE_UNKNOWN || isObjectPartPassable( _mainObjectPart ) )
           && std::all_of( _groundObjectPart.begin(), _groundObjectPart.end(), isObjectPartPassable );
}

bool Maps::Tile::isPassabilityTransparent() const
{
    for ( const auto & part : _groundObjectPart ) {
        if ( !part.isPassabilityTransparent() ) {
            return false;
        }
    }

    return _mainObjectPart.isPassabilityTransparent();
}

bool Maps::Tile::isPassableFrom( const int direction, const bool fromWater, const bool ignoreFog, const PlayerColor heroColor ) const
{
    if ( !ignoreFog && isFog( heroColor ) ) {
        return false;
    }

    const bool tileIsWater = isWater();

    // From the water we can get either to a water tile (provided that there is no boat on that tile) or to a shore tile suitable for disembarkation.
    if ( fromWater ) {
        if ( tileIsWater ) {
            if ( _mainObjectType == MP2::OBJ_BOAT ) {
                return false;
            }
        }
        else if ( !isSuitableForDisembarkation() ) {
            return false;
        }
    }
    // From the ground we can get to a water tile only if this tile contains a certain object.
    else if ( tileIsWater && _mainObjectType != MP2::OBJ_SHIPWRECK && _mainObjectType != MP2::OBJ_HERO && _mainObjectType != MP2::OBJ_BOAT ) {
        return false;
    }

    // Tiles on which allied heroes are located are inaccessible
    if ( _mainObjectType == MP2::OBJ_HERO ) {
        const Heroes * hero = getHero();
        assert( hero != nullptr );

        if ( hero->GetColor() != heroColor && hero->isFriends( heroColor ) ) {
            return false;
        }
    }

    // Tiles on which the entrances to the allied castles are located are inaccessible
    if ( _mainObjectType == MP2::OBJ_CASTLE ) {
        const Castle * castle = world.getCastleEntrance( GetCenter() );
        assert( castle != nullptr );

        if ( castle->GetColor() != heroColor && castle->isFriends( heroColor ) ) {
            return false;
        }
    }

    return ( direction & _tilePassabilityDirections ) != 0;
}

bool Maps::Tile::isStream() const
{
    for ( const auto & part : _groundObjectPart ) {
        if ( part.icnType == MP2::OBJ_ICN_TYPE_STREAM
             || ( part.icnType == MP2::OBJ_ICN_TYPE_OBJNMUL2 && ( part.icnIndex < 14 || ( part.icnIndex > 217 && part.icnIndex < ( 218 + 14 ) ) ) ) ) {
            return true;
        }
    }

    return _mainObjectPart.icnType == MP2::OBJ_ICN_TYPE_STREAM
           || ( _mainObjectPart.icnType == MP2::OBJ_ICN_TYPE_OBJNMUL2
                && ( _mainObjectPart.icnIndex < 14 || ( _mainObjectPart.icnIndex > 217 && _mainObjectPart.icnIndex < ( 218 + 14 ) ) ) );
}

bool Maps::Tile::isShadow() const
{
    return isObjectPartShadow( _mainObjectPart ) && std::all_of( _groundObjectPart.begin(), _groundObjectPart.end(), isObjectPartShadow );
}

Maps::ObjectPart * Maps::Tile::getObjectPartWithFlag( const uint32_t uid )
{
    const auto isFlag = [uid]( const auto & part ) { return part._uid == uid && part.icnType == MP2::OBJ_ICN_TYPE_FLAG32; };

    auto iter = std::find_if( _groundObjectPart.begin(), _groundObjectPart.end(), isFlag );
    if ( iter != _groundObjectPart.end() ) {
        return &( *iter );
    }

    iter = std::find_if( _topObjectPart.begin(), _topObjectPart.end(), isFlag );
    if ( iter != _topObjectPart.end() ) {
        return &( *iter );
    }

    return nullptr;
}

void Maps::Tile::setOwnershipFlag( const MP2::MapObjectType objectType, PlayerColor color )
{
    // All flags in FLAG32.ICN are actually the same except the fact of having different offset.
    // Set the default value for the UNUSED color.
    uint8_t objectSpriteIndex = 6;

    switch ( color ) {
    case PlayerColor::NONE:
        // No flag. Just ignore it.
        break;
    case PlayerColor::BLUE:
        objectSpriteIndex = 0;
        break;
    case PlayerColor::GREEN:
        objectSpriteIndex = 1;
        break;
    case PlayerColor::RED:
        objectSpriteIndex = 2;
        break;
    case PlayerColor::YELLOW:
        objectSpriteIndex = 3;
        break;
    case PlayerColor::ORANGE:
        objectSpriteIndex = 4;
        break;
    case PlayerColor::PURPLE:
        objectSpriteIndex = 5;
        break;
    case PlayerColor::UNUSED:
        // Should never be called using this color as an argument.
        assert( 0 );
        break;
    default:
        // Did you add a new color type? Add logic above!
        assert( 0 );
        break;
    }

    switch ( objectType ) {
    case MP2::OBJ_MINE: {
        objectSpriteIndex += 14;
        Tile & tile = world.getTile( _index );
        tile.updateFlag( color, objectSpriteIndex, _mainObjectPart._uid, true );
        break;
    }

    case MP2::OBJ_LIGHTHOUSE:
        objectSpriteIndex += 49;
        if ( isValidDirection( _index, Direction::LEFT ) ) {
            Tile & tile = world.getTile( GetDirectionIndex( _index, Direction::LEFT ) );
            tile.updateFlag( color, objectSpriteIndex, _mainObjectPart._uid, false );
        }

        objectSpriteIndex += 7;
        updateFlag( color, objectSpriteIndex, _mainObjectPart._uid, false );
        break;

    case MP2::OBJ_ALCHEMIST_LAB:
        objectSpriteIndex += 21;
        if ( isValidDirection( _index, Direction::TOP ) ) {
            Tile & tile = world.getTile( GetDirectionIndex( _index, Direction::TOP ) );
            tile.updateFlag( color, objectSpriteIndex, _mainObjectPart._uid, true );
        }
        break;

    case MP2::OBJ_SAWMILL:
        objectSpriteIndex += 28;
        if ( isValidDirection( _index, Direction::TOP_RIGHT ) ) {
            Tile & tile = world.getTile( GetDirectionIndex( _index, Direction::TOP_RIGHT ) );
            tile.updateFlag( color, objectSpriteIndex, _mainObjectPart._uid, true );
        }
        break;

    case MP2::OBJ_CASTLE:
        // Neutral castles always have flags on both sides of the gate, they should not be completely removed.
        if ( color == PlayerColor::NONE ) {
            color = PlayerColor::UNUSED;
        }

        objectSpriteIndex *= 2;
        if ( isValidDirection( _index, Direction::LEFT ) ) {
            Tile & tile = world.getTile( GetDirectionIndex( _index, Direction::LEFT ) );
            tile.updateFlag( color, objectSpriteIndex, _mainObjectPart._uid, true );
        }

        objectSpriteIndex += 1;
        if ( isValidDirection( _index, Direction::RIGHT ) ) {
            Tile & tile = world.getTile( GetDirectionIndex( _index, Direction::RIGHT ) );
            tile.updateFlag( color, objectSpriteIndex, _mainObjectPart._uid, true );
        }
        break;

    default:
        break;
    }
}

void Maps::Tile::updateFlag( const PlayerColor color, const uint8_t objectSpriteIndex, const uint32_t uid, const bool setOnUpperLayer )
{
    // Flag deletion or installation must be done in relation to object UID as flag is attached to the object.
    if ( color == PlayerColor::NONE ) {
        const auto isFlag = [uid]( const auto & part ) { return part._uid == uid && part.icnType == MP2::OBJ_ICN_TYPE_FLAG32; };
        _groundObjectPart.remove_if( isFlag );
        _topObjectPart.remove_if( isFlag );
        return;
    }

    ObjectPart * part = getObjectPartWithFlag( uid );
    if ( part != nullptr ) {
        // Replace an existing flag.
        part->icnIndex = objectSpriteIndex;
    }
    else if ( setOnUpperLayer ) {
        _topObjectPart.emplace_back( OBJECT_LAYER, uid, MP2::OBJ_ICN_TYPE_FLAG32, objectSpriteIndex );
    }
    else {
        _groundObjectPart.emplace_back( OBJECT_LAYER, uid, MP2::OBJ_ICN_TYPE_FLAG32, objectSpriteIndex );
    }
}

void Maps::Tile::_updateRoadFlag()
{
    _isTileMarkedAsRoad = isSpriteRoad( _mainObjectPart.icnType, _mainObjectPart.icnIndex );

    if ( _isTileMarkedAsRoad ) {
        return;
    }

    for ( const auto & part : _groundObjectPart ) {
        if ( isSpriteRoad( part.icnType, part.icnIndex ) ) {
            _isTileMarkedAsRoad = true;
            return;
        }
    }
}

void Maps::Tile::fixMP2MapTileObjectType( Tile & tile )
{
    const MP2::MapObjectType originalObjectType = tile.getMainObjectType( false );

    // Left tile of a skeleton on Desert should be marked as non-action tile.
    if ( originalObjectType == MP2::OBJ_SKELETON && tile._mainObjectPart.icnType == MP2::OBJ_ICN_TYPE_OBJNDSRT && tile._mainObjectPart.icnIndex == 83 ) {
        tile.setMainObjectType( MP2::OBJ_NON_ACTION_SKELETON );

        // There is no need to check the rest of things as we fixed this object.
        return;
    }

    // Oasis object has 2 top tiles being marked as part of bottom object layer while in reality they should be at the top level.
    if ( originalObjectType == MP2::OBJ_NON_ACTION_OASIS && tile._mainObjectPart.icnType == MP2::OBJ_ICN_TYPE_OBJNDSRT
         && ( tile._mainObjectPart.icnIndex == 105 || tile._mainObjectPart.icnIndex == 106 ) ) {
        tile._topObjectPart.emplace_back();
        std::swap( tile._topObjectPart.back(), tile._mainObjectPart );

        return;
    }

    // Original Editor marks Reefs as Stones. We're fixing this issue by changing the type of the object without changing the content of a tile.
    // This is also required in order to properly calculate Reefs' passability.
    if ( originalObjectType == MP2::OBJ_ROCK && isValidReefsSprite( tile._mainObjectPart.icnType, tile._mainObjectPart.icnIndex ) ) {
        tile.setMainObjectType( MP2::OBJ_REEFS );

        // There is no need to check the rest of things as we fixed this object.
        return;
    }

    // Some maps have water tiles with OBJ_COAST, it shouldn't be, replace OBJ_COAST with OBJ_NONE
    if ( originalObjectType == MP2::OBJ_COAST && tile.isWater() ) {
        Heroes * hero = tile.getHero();

        if ( hero ) {
            hero->setObjectTypeUnderHero( MP2::OBJ_NONE );
        }
        else {
            tile.setMainObjectType( MP2::OBJ_NONE );
        }

        // There is no need to check the rest of things as we fixed this object.
        return;
    }

    // On some maps (apparently created by some non-standard editors), the object type on tiles with random monsters does not match the index
    // of the monster placeholder sprite. While this engine looks at the object type when placing an actual monster on a tile, the original
    // HoMM2 apparently looks at the placeholder sprite, so we need to keep them in sync.
    if ( tile._mainObjectPart.icnType == MP2::OBJ_ICN_TYPE_MONS32 ) {
        MP2::MapObjectType monsterObjectType = originalObjectType;

        const uint8_t originalObjectSpriteIndex = tile.getMainObjectPart().icnIndex;
        switch ( originalObjectSpriteIndex ) {
        // Random monster placeholder "MON"
        case 66:
            monsterObjectType = MP2::OBJ_RANDOM_MONSTER;
            break;
        // Random monster placeholder "MON 1"
        case 67:
            monsterObjectType = MP2::OBJ_RANDOM_MONSTER_WEAK;
            break;
        // Random monster placeholder "MON 2"
        case 68:
            monsterObjectType = MP2::OBJ_RANDOM_MONSTER_MEDIUM;
            break;
        // Random monster placeholder "MON 3"
        case 69:
            monsterObjectType = MP2::OBJ_RANDOM_MONSTER_STRONG;
            break;
        // Random monster placeholder "MON 4"
        case 70:
            monsterObjectType = MP2::OBJ_RANDOM_MONSTER_VERY_STRONG;
            break;
        default:
            break;
        }

        if ( monsterObjectType != originalObjectType ) {
            tile.setMainObjectType( monsterObjectType );

            DEBUG_LOG( DBG_GAME, DBG_WARN,
                       "Invalid object type index " << tile._index << ": type " << MP2::StringObject( originalObjectType ) << ", object sprite index "
                                                    << static_cast<int>( originalObjectSpriteIndex ) << ", corrected type " << MP2::StringObject( monsterObjectType ) )

            // There is no need to check the rest of things as we fixed this object.
            return;
        }
    }

    // The original maps do not have proper object type being set for The Price of Loyalty' objects.
    // All of them are marked under few common types listed below.
    // The type of an expansion action object or dwelling is stored in object metadata.
    // However, we do not read this information and set the correct object type based on the object part information.
    //
    // We shouldn't even reach this code for Succession Wars maps but it is okay if we execute it since the map is most likely hacked.
    switch ( originalObjectType ) {
    case MP2::OBJ_NON_ACTION_EXPANSION_DWELLING:
    case MP2::OBJ_NON_ACTION_EXPANSION_OBJECT:
    case MP2::OBJ_EXPANSION_DWELLING:
    case MP2::OBJ_EXPANSION_OBJECT: {
        if ( updatePriceOfLoyaltyObjectType( tile._mainObjectPart, tile ) ) {
            return;
        }

        // Object part of ground layer shouldn't even exist if no top object is present.
        // However, let's play safe and verify it as well.
        for ( const auto & part : tile._groundObjectPart ) {
            if ( updatePriceOfLoyaltyObjectType( part, tile ) ) {
                return;
            }
        }

        for ( const auto & part : tile._topObjectPart ) {
            if ( updatePriceOfLoyaltyObjectType( part, tile ) ) {
                return;
            }
        }

        DEBUG_LOG( DBG_GAME, DBG_WARN,
                   "Invalid object type index " << tile._index << ": type " << MP2::StringObject( originalObjectType ) << ", icn ID "
                                                << static_cast<int>( tile._mainObjectPart.icnIndex ) )
        break;
    }

    default:
        break;
    }
}

bool Maps::Tile::removeObjectPartsByUID( const uint32_t objectUID )
{
    bool isObjectPartRemoved = false;
    if ( _mainObjectPart._uid == objectUID ) {
        _mainObjectPart = {};

        isObjectPartRemoved = true;
    }

    size_t partCountBefore = _groundObjectPart.size();
    _groundObjectPart.remove_if( [objectUID]( const auto & v ) { return v._uid == objectUID; } );
    if ( partCountBefore != _groundObjectPart.size() ) {
        isObjectPartRemoved = true;
    }

    partCountBefore = _topObjectPart.size();
    _topObjectPart.remove_if( [objectUID]( const auto & v ) { return v._uid == objectUID; } );
    if ( partCountBefore != _topObjectPart.size() ) {
        isObjectPartRemoved = true;
    }

    if ( isObjectPartRemoved ) {
        // Since an object part was removed we have to update main object type.
        updateObjectType();

        setInitialPassability();

        // The remove of the object part may also affect on the passability on the tiles around.
        // First set the initial passability for tiles.
        const Indexes tilesAround = getAroundIndexes( _index, 1 );
        for ( const int32_t tileIndex : tilesAround ) {
            world.getTile( tileIndex ).setInitialPassability();
        }

        // Update passability based on initial passability.
        updatePassability();
        for ( const int32_t tileIndex : tilesAround ) {
            world.getTile( tileIndex ).updatePassability();
        }

        if ( Heroes::isValidId( _occupantHeroId ) ) {
            Heroes * hero = world.GetHeroes( _occupantHeroId );
            if ( hero != nullptr ) {
                hero->setObjectTypeUnderHero( _mainObjectType );

                setMainObjectType( MP2::OBJ_HERO );
            }
        }

        // TODO: since we remove an object we need to check whether this tile contains any object with additional metadata.
        //       If it doesn't contain we need to reset metadata.
    }

    return isObjectPartRemoved;
}

void Maps::Tile::removeObjects( const MP2::ObjectIcnType objectIcnType )
{
    _groundObjectPart.remove_if( [objectIcnType]( const auto & part ) { return part.icnType == objectIcnType; } );
    _topObjectPart.remove_if( [objectIcnType]( const auto & part ) { return part.icnType == objectIcnType; } );

    if ( _mainObjectPart.icnType == objectIcnType ) {
        _mainObjectPart = {};
    }

    _updateRoadFlag();

    // TODO: update tile's object type after objects' removal.
}

void Maps::Tile::replaceObject( const uint32_t objectUid, const MP2::ObjectIcnType originalObjectIcnType, const MP2::ObjectIcnType newObjectIcnType,
                                const uint8_t originalImageIndex, const uint8_t newImageIndex )
{
    // We can immediately return from the function as only one object per tile can have the same UID.
    for ( auto & part : _groundObjectPart ) {
        if ( part._uid == objectUid && part.icnType == originalObjectIcnType && part.icnIndex == originalImageIndex ) {
            part.icnType = newObjectIcnType;
            part.icnIndex = newImageIndex;
            return;
        }
    }

    for ( auto & part : _topObjectPart ) {
        if ( part._uid == objectUid && part.icnType == originalObjectIcnType && part.icnIndex == originalImageIndex ) {
            part.icnType = newObjectIcnType;
            part.icnIndex = newImageIndex;
            return;
        }
    }

    if ( _mainObjectPart._uid == objectUid && _mainObjectPart.icnType == originalObjectIcnType && _mainObjectPart.icnIndex == originalImageIndex ) {
        _mainObjectPart.icnType = newObjectIcnType;
        _mainObjectPart.icnIndex = newImageIndex;
    }
}

void Maps::Tile::updateObjectImageIndex( const uint32_t objectUid, const MP2::ObjectIcnType objectIcnType, const int imageIndexOffset )
{
    // We can immediately return from the function as only one object per tile can have the same UID.
    for ( auto & part : _groundObjectPart ) {
        if ( part._uid == objectUid && part.icnType == objectIcnType ) {
            assert( part.icnIndex + imageIndexOffset >= 0 && part.icnIndex + imageIndexOffset < 255 );
            part.icnIndex = static_cast<uint8_t>( part.icnIndex + imageIndexOffset );
            return;
        }
    }

    for ( auto & part : _topObjectPart ) {
        if ( part._uid == objectUid && part.icnType == objectIcnType ) {
            assert( part.icnIndex + imageIndexOffset >= 0 && part.icnIndex + imageIndexOffset < 255 );
            part.icnIndex = static_cast<uint8_t>( part.icnIndex + imageIndexOffset );
            return;
        }
    }

    if ( _mainObjectPart._uid == objectUid && _mainObjectPart.icnType == objectIcnType ) {
        assert( _mainObjectPart.icnIndex + imageIndexOffset >= 0 && _mainObjectPart.icnIndex + imageIndexOffset < 255 );
        _mainObjectPart.icnIndex = static_cast<uint8_t>( _mainObjectPart.icnIndex + imageIndexOffset );
    }
}

void Maps::Tile::ClearFog( const PlayerColorsSet colors )
{
    _fogColors &= ~colors;

    // The fog might be cleared even without the hero's movement - for example, the hero can gain a new level of Scouting
    // skill by picking up a Treasure Chest from a nearby tile or buying a map in a Magellan's Maps object using the space
    // bar button. Reset the pathfinder(s) to make the newly discovered tiles immediately available for this hero.
    world.resetPathfinder();
}

void Maps::Tile::updateTileObjectIcnIndex( Maps::Tile & tile, const uint32_t uid, const uint8_t newIndex )
{
    ObjectPart * part = tile.getGroundObjectPart( uid );
    if ( part != nullptr ) {
        part->icnIndex = newIndex;
    }
    else if ( tile._mainObjectPart._uid == uid ) {
        tile._mainObjectPart.icnIndex = newIndex;
    }

    tile._updateRoadFlag();
}

void Maps::Tile::updateObjectType()
{
    if ( _mainObjectType == MP2::OBJ_EVENT ) {
        if ( world.GetMapEvent( Maps::GetPoint( _index ) ) == nullptr ) {
            // No data found for this event type. This may happen in the case of hacked maps.
            DEBUG_LOG( DBG_AI, DBG_INFO, "Adventure Map event at index " << _index << " is missing!" )

            // Remove the event object type because of the missing data.
            setMainObjectType( MP2::OBJ_NONE );
        }

        // Events have no visible parts on the map, so we preserve their type regardless of what part of the object is on the tile.
        return;
    }

    // After removing an object there could be an object part in the main object part.
    MP2::MapObjectType objectType = getObjectTypeByIcn( _mainObjectPart.icnType, _mainObjectPart.icnIndex );
    if ( MP2::isOffGameActionObject( objectType ) ) {
        // Set object type only when this is an interactive object type to make sure that interaction can be done.
        setMainObjectType( objectType );
        return;
    }

    // And sometimes even in the ground layer object parts.
    // Take a note that we iterate object parts from back to front as the latest object part has higher priority.
    for ( auto iter = _groundObjectPart.rbegin(); iter != _groundObjectPart.rend(); ++iter ) {
        const MP2::MapObjectType type = getObjectTypeByIcn( iter->icnType, iter->icnIndex );
        if ( type == MP2::OBJ_NONE ) {
            continue;
        }

        if ( MP2::isOffGameActionObject( type ) ) {
            // Set object type only when this is an interactive object type to make sure that interaction can be done.
            setMainObjectType( type );
            return;
        }

        if ( objectType == MP2::OBJ_NONE ) {
            objectType = type;
        }
    }

    // Or object part can be in the top layer object parts.
    // Take a note that we iterate object parts from back to front as the latest object part has higher priority.
    for ( auto iter = _topObjectPart.rbegin(); iter != _topObjectPart.rend(); ++iter ) {
        const MP2::MapObjectType type = getObjectTypeByIcn( iter->icnType, iter->icnIndex );

        if ( type != MP2::OBJ_NONE ) {
            setMainObjectType( type );
            return;
        }
    }

    // Top objects do not have object type while bottom object do.
    if ( objectType != MP2::OBJ_NONE ) {
        setMainObjectType( objectType );
        return;
    }

    // If an object is removed we should validate if this tile a potential candidate to be a coast.
    // Check if this tile is not water and it has neighbouring water tiles.
    if ( isWater() ) {
        assert( objectType == MP2::OBJ_NONE );
        setMainObjectType( objectType );
        return;
    }

    const Indexes tileIndices = getAroundIndexes( _index, 1 );
    for ( const int tileIndex : tileIndices ) {
        if ( tileIndex < 0 ) {
            // Invalid tile index.
            continue;
        }

        if ( world.getTile( tileIndex ).isWater() ) {
            setMainObjectType( MP2::OBJ_COAST );
            return;
        }
    }

    assert( objectType == MP2::OBJ_NONE );
    setMainObjectType( objectType );
}

uint32_t Maps::Tile::getObjectIdByObjectIcnType( const MP2::ObjectIcnType objectIcnType ) const
{
    if ( _mainObjectPart.icnType == objectIcnType ) {
        return _mainObjectPart._uid;
    }

    for ( const auto & part : _groundObjectPart ) {
        if ( part.icnType == objectIcnType ) {
            return part._uid;
        }
    }

    for ( const auto & part : _topObjectPart ) {
        if ( part.icnType == objectIcnType ) {
            return part._uid;
        }
    }

    return 0;
}

std::vector<MP2::ObjectIcnType> Maps::Tile::getValidObjectIcnTypes() const
{
    std::vector<MP2::ObjectIcnType> objectIcnTypes;

    if ( _mainObjectPart.icnType != MP2::OBJ_ICN_TYPE_UNKNOWN ) {
        objectIcnTypes.emplace_back( _mainObjectPart.icnType );
    }

    for ( const auto & part : _groundObjectPart ) {
        // If this assertion blows up then you put an empty object into an object part which makes no sense!
        assert( part.icnType != MP2::OBJ_ICN_TYPE_UNKNOWN );

        objectIcnTypes.emplace_back( part.icnType );
    }

    for ( const auto & part : _topObjectPart ) {
        // If this assertion blows up then you put an empty object into an object part which makes no sense!
        assert( part.icnType != MP2::OBJ_ICN_TYPE_UNKNOWN );

        objectIcnTypes.emplace_back( part.icnType );
    }

    return objectIcnTypes;
}

bool Maps::Tile::containsAnyObjectIcnType( const std::vector<MP2::ObjectIcnType> & objectIcnTypes ) const
{
    for ( const MP2::ObjectIcnType objectIcnType : objectIcnTypes ) {
        if ( _mainObjectPart.icnType == objectIcnType ) {
            return true;
        }

        for ( const auto & part : _groundObjectPart ) {
            if ( part.icnType == objectIcnType ) {
                return true;
            }
        }

        for ( const auto & part : _topObjectPart ) {
            if ( part.icnType == objectIcnType ) {
                return true;
            }
        }
    }

    return false;
}

bool Maps::Tile::containsSprite( const MP2::ObjectIcnType objectIcnType, const uint32_t imageIdx ) const
{
    if ( _mainObjectPart.icnType == objectIcnType && imageIdx == _mainObjectPart.icnIndex ) {
        return true;
    }

    if ( std::any_of( _groundObjectPart.cbegin(), _groundObjectPart.cend(),
                      [objectIcnType, imageIdx]( const auto & part ) { return part.icnType == objectIcnType && imageIdx == part.icnIndex; } ) ) {
        return true;
    }

    return std::any_of( _topObjectPart.cbegin(), _topObjectPart.cend(),
                        [objectIcnType, imageIdx]( const auto & part ) { return part.icnType == objectIcnType && imageIdx == part.icnIndex; } );
}

bool Maps::Tile::isAnyTallObjectOnTile() const
{
    if ( !isValidDirection( _index, Direction::TOP ) ) {
        // This tile is on a first row. Any object on this tile cannot be tall.
        return false;
    }

    // An object is considered a tall if it has a top part.
    // So, we need to get all object UIDs at this tile and later verify if any of object part of a top layer for a tile above contains the same UID.
    std::vector<uint32_t> objectUids;
    if ( _mainObjectPart.icnType != MP2::OBJ_ICN_TYPE_UNKNOWN && !_mainObjectPart.isPassabilityTransparent() ) {
        objectUids.emplace_back( _mainObjectPart._uid );
    }

    for ( const auto & part : _groundObjectPart ) {
        if ( !part.isPassabilityTransparent() ) {
            objectUids.emplace_back( part._uid );
        }
    }

    const Tile & topTile = world.getTile( GetDirectionIndex( _index, Direction::TOP ) );
    for ( const uint32_t uid : objectUids ) {
        // Check only object parts located at the top layer.
        for ( const auto & part : topTile._topObjectPart ) {
            if ( part._uid == uid ) {
                return true;
            }
        }
    }

    return false;
}

int32_t Maps::Tile::getIndexOfMainTile( const Maps::Tile & tile )
{
    const MP2::MapObjectType objectType = tile.getMainObjectType( false );
    const MP2::MapObjectType correctedObjectType = MP2::getBaseActionObjectType( objectType );

    if ( correctedObjectType == objectType ) {
        // Nothing to do.
        return tile._index;
    }

    assert( correctedObjectType > objectType );

    // It's unknown whether object type belongs to bottom layer or ground. Create a list of UIDs starting from bottom layer.
    std::set<uint32_t> uids;
    uids.insert( tile.getMainObjectPart()._uid );

    for ( const auto & part : tile.getGroundObjectParts() ) {
        uids.insert( part._uid );
    }

    for ( const auto & part : tile.getTopObjectParts() ) {
        uids.insert( part._uid );
    }

    const int32_t tileIndex = tile.GetIndex();
    const int32_t mapWidth = world.w();

    // This is non-main tile of an action object. We have to find the main tile.
    // Since we don't want to care about the size of every object in the game we should find tiles in a certain radius.
    const int32_t radiusOfSearch = 3;

    // Main tile is usually at the bottom of the object so let's start from there. Also there are no objects having tiles below more than 1 row.
    for ( int32_t y = radiusOfSearch; y >= -1; --y ) {
        const int32_t offsetX = tileIndex + y * mapWidth;
        for ( int32_t x = -radiusOfSearch; x <= radiusOfSearch; ++x ) {
            const int32_t index = offsetX + x;
            if ( isValidAbsIndex( index ) ) {
                const Tile & foundTile = world.getTile( index );
                if ( foundTile.getMainObjectType( false ) != correctedObjectType ) {
                    continue;
                }

                if ( foundTile.getMainObjectPart()._uid != 0 && uids.count( foundTile.getMainObjectPart()._uid ) > 0 ) {
                    return foundTile._index;
                }
            }
        }
    }

    // Most likely we have a broken object put by an editor.
    DEBUG_LOG( DBG_GAME, DBG_TRACE, "Tile " << tileIndex << " of type " << MP2::StringObject( objectType ) << " has no parent tile." )
    return -1;
}

bool Maps::Tile::isDetachedObject() const
{
    const MP2::MapObjectType objectType = getMainObjectType( false );
    if ( isDetachedObjectType( objectType ) ) {
        return true;
    }

    const MP2::MapObjectType correctedObjectType = MP2::getBaseActionObjectType( objectType );
    if ( !isDetachedObjectType( correctedObjectType ) ) {
        return false;
    }

    const int32_t mainTileIndex = getIndexOfMainTile( *this );
    if ( mainTileIndex == -1 ) {
        return false;
    }

    const uint32_t objectUID = world.getTile( mainTileIndex ).getMainObjectPart()._uid;
    if ( _mainObjectPart._uid == objectUID ) {
        return !_mainObjectPart.isPassabilityTransparent();
    }

    for ( const auto & part : _groundObjectPart ) {
        if ( part._uid == objectUID ) {
            return !part.isPassabilityTransparent();
        }
    }

    return false;
}

OStreamBase & Maps::operator<<( OStreamBase & stream, const ObjectPart & ta )
{
    return stream << ta.layerType << ta._uid << ta.icnType << ta.icnIndex;
}

IStreamBase & Maps::operator>>( IStreamBase & stream, ObjectPart & ta )
{
    stream >> ta.layerType;

    static_assert( LAST_SUPPORTED_FORMAT_VERSION < FORMAT_VERSION_PRE2_1009_RELEASE, "Remove the logic below." );
    if ( Game::GetVersionOfCurrentSaveFile() < FORMAT_VERSION_PRE2_1009_RELEASE ) {
        ta.layerType = static_cast<ObjectLayerType>( ta.layerType & 0x03 );
    }

    stream >> ta._uid >> ta.icnType;

    static_assert( LAST_SUPPORTED_FORMAT_VERSION < FORMAT_VERSION_PRE2_1009_RELEASE, "Remove the logic below." );
    if ( Game::GetVersionOfCurrentSaveFile() < FORMAT_VERSION_PRE2_1009_RELEASE ) {
        bool temp;
        stream >> temp >> temp;
    }

    return stream >> ta.icnIndex;
}

OStreamBase & Maps::operator<<( OStreamBase & stream, const Tile & tile )
{
    return stream << tile._index << tile._terrainImageIndex << tile._terrainFlags << tile._tilePassabilityDirections << tile._mainObjectPart << tile._mainObjectType
                  << tile._fogColors << tile._metadata << tile._occupantHeroId << tile._isTileMarkedAsRoad << tile._groundObjectPart << tile._topObjectPart
                  << tile._boatOwnerColor;
}

IStreamBase & Maps::operator>>( IStreamBase & stream, Tile & tile )
{
    stream >> tile._index >> tile._terrainImageIndex >> tile._terrainFlags >> tile._tilePassabilityDirections;

    static_assert( LAST_SUPPORTED_FORMAT_VERSION < FORMAT_VERSION_1104_RELEASE, "Remove the logic below." );
    if ( Game::GetVersionOfCurrentSaveFile() < FORMAT_VERSION_1104_RELEASE ) {
        stream >> tile._mainObjectPart._uid >> tile._mainObjectPart.icnType;
    }
    else {
        stream >> tile._mainObjectPart;
    }

    static_assert( LAST_SUPPORTED_FORMAT_VERSION < FORMAT_VERSION_PRE2_1009_RELEASE, "Remove the logic below." );
    if ( Game::GetVersionOfCurrentSaveFile() < FORMAT_VERSION_PRE2_1009_RELEASE ) {
        bool temp;
        stream >> temp >> temp;
    }

    if ( Game::GetVersionOfCurrentSaveFile() < FORMAT_VERSION_1104_RELEASE ) {
        stream >> tile._mainObjectPart.icnIndex;
    }

    static_assert( LAST_SUPPORTED_FORMAT_VERSION < FORMAT_VERSION_PRE3_1100_RELEASE, "Remove the logic below." );
    if ( Game::GetVersionOfCurrentSaveFile() < FORMAT_VERSION_PRE3_1100_RELEASE ) {
        uint8_t mainObjectType = static_cast<uint8_t>( MP2::OBJ_NONE );
        stream >> mainObjectType;

        tile._mainObjectType = static_cast<MP2::MapObjectType>( mainObjectType );
    }
    else {
        stream >> tile._mainObjectType;
    }

    stream >> tile._fogColors >> tile._metadata >> tile._occupantHeroId >> tile._isTileMarkedAsRoad >> tile._groundObjectPart >> tile._topObjectPart;

    if ( Game::GetVersionOfCurrentSaveFile() < FORMAT_VERSION_1104_RELEASE ) {
        stream >> tile._mainObjectPart.layerType;
    }

    static_assert( LAST_SUPPORTED_FORMAT_VERSION < FORMAT_VERSION_1108_RELEASE, "Remove the logic below." );
    if ( Game::GetVersionOfCurrentSaveFile() < FORMAT_VERSION_1108_RELEASE && tile._mainObjectType == MP2::OBJ_MINE
         && tile.getMainObjectPart().icnType != MP2::OBJ_ICN_TYPE_EXTRAOVR ) {
        // Some maps have "hacked" mines with no resources. We need to try to fix these tiles.
        updateObjectInfoTile( tile, true );
    }

    return stream >> tile._boatOwnerColor;
}
