/*
 *  The ManaPlus Client
 *  Copyright (C) 2004-2009  The Mana World Development Team
 *  Copyright (C) 2009-2010  The Mana Developers
 *  Copyright (C) 2011-2019  The ManaPlus Developers
 *  Copyright (C) 2019-2021  Andrei Karas
 *
 *  This file is part of The ManaPlus Client.
 *
 *  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
 *  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, see <http://www.gnu.org/licenses/>.
 */

#include "resources/mapreader.h"

#include "configuration.h"
#ifdef USE_OPENGL
#include "graphicsmanager.h"
#endif  // USE_OPENGL
#include "main.h"

#include "const/resources/map/map.h"

#include "enums/resources/map/collisiontype.h"
#include "enums/resources/map/mapitemtype.h"

#include "fs/virtfs/fs.h"

#include "resources/map/map.h"
#include "resources/map/mapheights.h"
#include "resources/map/maplayer.h"
#include "resources/map/tileset.h"

#include "resources/beingcommon.h"
#include "resources/animation/animation.h"

#include "resources/image/image.h"

#ifdef USE_OPENGL
#include "resources/db/mapdb.h"
#include "resources/loaders/atlasloader.h"
#include "resources/loaders/emptyatlasloader.h"
#endif  // USE_OPENGL

#include "resources/map/tileanimation.h"

#include "resources/loaders/imageloader.h"

#include "resources/loaders/walklayerloader.h"

#include "utils/base64.h"
#include "utils/checkutils.h"
#include "utils/delete2.h"
#include "utils/stringmap.h"

#include "utils/translation/podict.h"

#include <zlib.h>

#include "debug.h"

typedef std::map<std::string, XmlNodePtr>::iterator LayerInfoIterator;
typedef std::set<XML::Document*>::iterator DocIterator;

#ifdef USE_OPENGL
Resource *MapReader::mEmptyAtlas = nullptr;
#endif  // USE_OPENGL

namespace
{
    std::map<std::string, XmlNodePtr> mKnownLayers;
    std::set<XML::Document*> mKnownDocs;
}  // namespace

static int inflateMemory(unsigned char *restrict const in,
                         const unsigned int inLength,
                         unsigned char *&restrict out,
                         unsigned int &restrict outLength);

static int inflateMemory(unsigned char *restrict const in,
                         const unsigned int inLength,
                         unsigned char *&restrict out);

static std::string resolveRelativePath(std::string base, std::string relative)
{
    // Remove trailing "/", if present
    size_t i = base.length();
    if (base.at(i - 1) == '/')
        base.erase(i - 1, i);

    while (relative.substr(0, 3) == "../")
    {
        relative.erase(0, 3);  // Remove "../"
        if (!base.empty())  // If base is already empty, we can't trim anymore
        {
            i = base.find_last_of('/');
            if (i == std::string::npos)
                i = 0;
            base.erase(i, base.length());  // Remove deepest folder in base
        }
    }

    // Re-add trailing slash, if needed
    if (!base.empty() && base[base.length() - 1] != '/')
        base.append("/");

    return base + relative;
}

/**
 * Inflates either zlib or gzip deflated memory. The inflated memory is
 * expected to be freed by the caller.
 */
int inflateMemory(unsigned char *restrict const in,
                  const unsigned int inLength,
                  unsigned char *&restrict out,
                  unsigned int &restrict outLength)
{
    int bufferSize = 256 * 1024;
    out = static_cast<unsigned char*>(calloc(bufferSize, 1));

    z_stream strm;
    strm.zalloc = nullptr;
    strm.zfree = nullptr;
    strm.opaque = nullptr;
    strm.next_in = in;
    strm.avail_in = inLength;
    strm.next_out = out;
    strm.avail_out = bufferSize;

PRAGMACLANG6GCC(GCC diagnostic push)
PRAGMACLANG6GCC(GCC diagnostic ignored "-Wold-style-cast")
    int ret = inflateInit2(&strm, 15 + 32);
PRAGMACLANG6GCC(GCC diagnostic pop)

    if (ret != Z_OK)
        return ret;

    do
    {
        if (strm.next_out == nullptr)
        {
            inflateEnd(&strm);
            return Z_MEM_ERROR;
        }

        ret = inflate(&strm, Z_NO_FLUSH);
        if (ret == Z_STREAM_ERROR)
            return ret;

        switch (ret)
        {
            case Z_NEED_DICT:
                ret = Z_DATA_ERROR;
                A_FALLTHROUGH
            case Z_DATA_ERROR:
            case Z_MEM_ERROR:
                (void) inflateEnd(&strm);
                return ret;
            default:
                break;
        }

        if (ret != Z_STREAM_END)
        {
            out = static_cast<unsigned char*>(realloc(out, bufferSize * 2));

            if (out == nullptr)
            {
                inflateEnd(&strm);
                return Z_MEM_ERROR;
            }

            strm.next_out = out + CAST_SIZE(bufferSize);
            strm.avail_out = bufferSize;
            bufferSize *= 2;
        }
    }
    while (ret != Z_STREAM_END);

    outLength = bufferSize - strm.avail_out;
    (void) inflateEnd(&strm);
    return Z_OK;
}

int inflateMemory(unsigned char *restrict const in,
                  const unsigned int inLength,
                  unsigned char *&restrict out)
{
    unsigned int outLength = 0;
    const int ret = inflateMemory(in, inLength, out, outLength);

    if (ret != Z_OK || (out == nullptr))
    {
        if (ret == Z_MEM_ERROR)
        {
            reportAlways("Error: Out of memory while decompressing map data!")
        }
        else if (ret == Z_VERSION_ERROR)
        {
            reportAlways("Error: Incompatible zlib version!")
        }
        else if (ret == Z_DATA_ERROR)
        {
            reportAlways("Error: Incorrect zlib compressed data!")
        }
        else
        {
            reportAlways("Error: Unknown error while decompressing map data!")
        }

        free(out);
        out = nullptr;
        outLength = 0;
    }

    return outLength;
}

void MapReader::addLayerToList(const std::string &fileName,
                               const SkipError skipError)
{
    XML::Document *doc = new XML::Document(fileName,
        UseVirtFs_true,
        skipError);
    XmlNodePtrConst node = doc->rootNode();
    if (node == nullptr)
    {
        delete doc;
        return;
    }

    int cnt = 0;
    for_each_xml_child_node(childNode, node)
    {
        if (!xmlNameEqual(childNode, "layer"))
            continue;
        std::string name = XML::getProperty(childNode, "name", "");
        if (name.empty())
            continue;
        name = toLower(name);
        logger->log("found patch layer: " + name);
        mKnownLayers[name] = childNode;
        mKnownDocs.insert(doc);
        cnt ++;
    }
    if (cnt == 0)
        delete doc;
}

Map *MapReader::readMap(const std::string &restrict filename,
                        const std::string &restrict realFilename)
{
    BLOCK_START("MapReader::readMap str")
    logger->log("Attempting to read map %s", realFilename.c_str());

    XML::Document doc(realFilename, UseVirtFs_true, SkipError_false);
    if (!doc.isLoaded())
    {
        BLOCK_END("MapReader::readMap str")
        return createEmptyMap(filename, realFilename);
    }

    XmlNodePtrConst node = doc.rootNode();

    Map *map = nullptr;
    // Parse the inflated map data
    if (node != nullptr)
    {
        if (!xmlNameEqual(node, "map"))
            logger->log("Error: Not a map file (%s)!", realFilename.c_str());
        else
            map = readMap(node, realFilename);
    }
    else
    {
        reportAlways("Error while parsing map file (%s)!",
            realFilename.c_str())
    }

    if (map != nullptr)
    {
        map->setProperty("_filename", realFilename);
        map->setProperty("_realfilename", filename);

        if (map->getProperty("music", std::string()).empty())
            updateMusic(map);

        map->updateConditionLayers();
        map->preCacheLayers();
    }

    BLOCK_END("MapReader::readMap str")
    return map;
}

void MapReader::loadLayers(const std::string &path)
{
    BLOCK_START("MapReader::loadLayers")
    loadXmlDir2(path, addLayerToList, ".tmx", SkipError_false)
    BLOCK_END("MapReader::loadLayers")
}

void MapReader::unloadTempLayers()
{
    FOR_EACH (DocIterator, it, mKnownDocs)
        delete (*it);
    mKnownLayers.clear();
    mKnownDocs.clear();
}

static void loadReplaceLayer(const LayerInfoIterator &it,
                             Map *const map) A_NONNULL(2);
static void loadReplaceLayer(const LayerInfoIterator &it,
                             Map *const map)
{
    MapReader::readLayer((*it).second, map);
}

Map *MapReader::readMap(XmlNodePtrConst node, const std::string &path)
{
    if (node == nullptr)
        return nullptr;

    BLOCK_START("MapReader::readMap xml")
    // Take the filename off the path
    const std::string pathDir = path.substr(0, path.rfind('/') + 1);

    const int w = XML::getProperty(node, "width", 0);
    const int h = XML::getProperty(node, "height", 0);
    const int tilew = XML::getProperty(node, "tilewidth", -1);
    const int tileh = XML::getProperty(node, "tileheight", -1);

    const bool showWarps = config.getBoolValue("warpParticle");
    const std::string warpPath = pathJoin(paths.getStringValue("particles"),
        paths.getStringValue("portalEffectFile"));

    if (tilew < 0 || tileh < 0)
    {
        reportAlways("MapReader: Warning: "
            "Uninitialized tile width or height value for map: %s",
            path.c_str())
        BLOCK_END("MapReader::readMap xml")
        return nullptr;
    }

    logger->log("loading replace layer list");
    loadLayers(path + "_replace.d");

    Map *const map = new Map(path,
        w, h,
        tilew, tileh);

    map->screenResized();

    const std::string fileName = path.substr(path.rfind(dirSeparator) + 1);
    map->setProperty("shortName", fileName);

#ifdef USE_OPENGL
    BLOCK_START("MapReader::readMap load atlas")
    if (graphicsManager.getUseAtlases())
    {
        const MapInfo *const info = MapDB::getMapAtlas(fileName);
        if (info != nullptr)
        {
            map->setAtlas(Loader::getAtlas(
                info->atlas,
                *info->files));
        }
        else
        {
            reportAlways("Missing atlas for map: %s",
                fileName.c_str())
        }
    }
    BLOCK_END("MapReader::readMap load atlas")
#endif  // USE_OPENGL

    for_each_xml_child_node(childNode, node)
    {
        if (xmlNameEqual(childNode, "tileset"))
        {
            Tileset *const tileset = readTileset(childNode, pathDir, map);
            if (tileset != nullptr)
                map->addTileset(tileset);
        }
        else if (xmlNameEqual(childNode, "layer"))
        {
            std::string name = XML::getProperty(childNode, "name", "");
            name = toLower(name);
            LayerInfoIterator it = mKnownLayers.find(name);
            if (it == mKnownLayers.end())
            {
                readLayer(childNode, map);
            }
            else
            {
                logger->log("load replace layer: " + name);
                loadReplaceLayer(it, map);
            }
        }
        else if (xmlNameEqual(childNode, "properties"))
        {
            readProperties(childNode, map);
            map->setVersion(atoi(map->getProperty(
                "manaplus version", std::string()).c_str()));
        }
        else if (xmlNameEqual(childNode, "objectgroup"))
        {
            // The object group offset is applied to each object individually
            const int tileOffsetX = XML::getProperty(childNode, "x", 0);
            const int tileOffsetY = XML::getProperty(childNode, "y", 0);
            const int offsetX = tileOffsetX * tilew;
            const int offsetY = tileOffsetY * tileh;
            const bool showParticles =
                config.getBoolValue("mapparticleeffects");

            for_each_xml_child_node(objectNode, childNode)
            {
                if (xmlNameEqual(objectNode, "object"))
                {
                    std::string objType = XML::getProperty(
                        objectNode, "type", "");

                    objType = toUpper(objType);

/*
                    if (objType == "NPC" ||
                        objType == "SCRIPT")
                    {
                        logger->log("hidden obj: " + objType);
                        // Silently skip server-side objects.
                        continue;
                    }
*/

                    const std::string objName = XML::getProperty(
                        objectNode, "name", "");
                    const int objX = XML::getProperty(objectNode, "x", 0);
                    const int objY = XML::getProperty(objectNode, "y", 0);
                    const int objW = XML::getProperty(objectNode, "width", 0);
                    const int objH = XML::getProperty(objectNode, "height", 0);

                    logger->log("- Loading object name: %s type: %s at %d:%d"
                        " (%dx%d)", objName.c_str(), objType.c_str(),
                        objX, objY, objW, objH);

                    if (objType == "PARTICLE_EFFECT")
                    {
                        if (objName.empty())
                        {
                            logger->log1("   Warning: No particle file given");
                            continue;
                        }

                        if (showParticles)
                        {
                            map->addParticleEffect(objName,
                                objX + offsetX,
                                objY + offsetY,
                                objW,
                                objH);
                        }
                        else
                        {
                            logger->log("Ignore particle effect: " + objName);
                        }
                    }
                    else if (objType == "WARP")
                    {
                        if (showWarps)
                        {
                            map->addParticleEffect(warpPath,
                                objX, objY, objW, objH);
                        }
                        map->addPortal(objName, MapItemType::PORTAL,
                                       objX, objY, objW, objH);
                    }
                    else if (objType == "SPAWN")
                    {
                        // TRANSLATORS: spawn name
//                      map->addPortal(_("Spawn: ") + objName,
//                            MapItemType::PORTAL,
//                            objX, objY, objW, objH);
                    }
                    else if (objType == "MUSIC")
                    {
                        map->addRange(objName, MapItemType::MUSIC,
                            objX, objY, objW, objH);
                    }
                    else
                    {
                        logger->log1("   Warning: Unknown object type");
                    }
                }
            }
        }
    }

    map->initializeAmbientLayers();
    map->clearIndexedTilesets();
    map->setActorsFix(0,
        atoi(map->getProperty("actorsfix", std::string()).c_str()));
    map->reduce();
    map->setWalkLayer(Loader::getWalkLayer(fileName, map));
    unloadTempLayers();
    map->updateDrawLayersList();
    BLOCK_END("MapReader::readMap xml")
    return map;
}

void MapReader::readProperties(XmlNodeConstPtrConst node,
                               Properties *const props)
{
    BLOCK_START("MapReader::readProperties")
    if (node == nullptr)
    {
        BLOCK_END("MapReader::readProperties")
        return;
    }

    for_each_xml_child_node(childNode, node)
    {
        if (!xmlNameEqual(childNode, "property"))
            continue;

        // Example: <property name="name" value="value"/>
        const std::string name = XML::getProperty(childNode, "name", "");
        const std::string value = XML::getProperty(childNode, "value", "");

        if (!name.empty() && !value.empty())
        {
            if (name == "name")
                props->setProperty(name, translator->getStr(value));
            else
                props->setProperty(name, value);
        }
    }
    BLOCK_END("MapReader::readProperties")
}

inline static void setTile(Map *const map,
                           MapLayer *const layer,
                           const MapLayerTypeT &layerType,
                           MapHeights *const heights,
                           const int x, const int y,
                           const int gid) A_NONNULL(1);

inline static void setTile(Map *const map,
                           MapLayer *const layer,
                           const MapLayerTypeT &layerType,
                           MapHeights *const heights,
                           const int x, const int y,
                           const int gid)
{
    const Tileset * const set = map->getTilesetWithGid(gid);
    switch (layerType)
    {
        case MapLayerType::TILES:
        {
            if (layer == nullptr)
                break;
            if (set != nullptr &&
                !set->isEmpty())
            {
                Image *const img = set->get(gid - set->getFirstGid());
                layer->setTile(x, y, img);
            }
            else
            {
                layer->setTile(x, y, nullptr);
            }
            break;
        }

        case MapLayerType::COLLISION:
        {
            if (set != nullptr)
            {
                if (map->getVersion() >= 1)
                {
                    const int collisionId = gid - set->getFirstGid();
                    CollisionTypeT type;
                    if (collisionId < 0 ||
                        collisionId >= CAST_S32(CollisionType::COLLISION_MAX))
                    {
                        type = CollisionType::COLLISION_EMPTY;
                    }
                    else
                    {
                        type = static_cast<CollisionTypeT>(collisionId);
                    }
                    switch (type)
                    {
                        case CollisionType::COLLISION_EMPTY:
                            map->addBlockMask(x, y, BlockType::GROUND);
                            break;
                        case CollisionType::COLLISION_WALL:
                            map->addBlockMask(x, y, BlockType::WALL);
                            break;
                        case CollisionType::COLLISION_AIR:
                            map->addBlockMask(x, y, BlockType::AIR);
                            break;
                        case CollisionType::COLLISION_WATER:
                            map->addBlockMask(x, y, BlockType::WATER);
                            break;
                        case CollisionType::COLLISION_GROUNDTOP:
                            map->addBlockMask(x, y, BlockType::GROUNDTOP);
                            break;
                        case CollisionType::COLLISION_PLAYER_WALL:
                            map->addBlockMask(x, y, BlockType::PLAYERWALL);
                            break;
                        case CollisionType::COLLISION_MONSTER_WALL:
                            map->addBlockMask(x, y, BlockType::MONSTERWALL);
                            break;
                        case CollisionType::COLLISION_MAX:
                        default:
                            break;
                    }
                }
                else
                {
                    if (gid - set->getFirstGid() != 0)
                        map->addBlockMask(x, y, BlockType::WALL);
                }
            }
            break;
        }

        case MapLayerType::HEIGHTS:
        {
            if (set == nullptr || heights == nullptr)
                break;
            if (map->getVersion() >= 2)
            {
                heights->setHeight(x, y, CAST_U8(
                    gid - set->getFirstGid() + 1));
            }
            else
            {
                Image *const img = set->get(gid - set->getFirstGid());
                if (layer != nullptr)
                    layer->setTile(x, y, img);
            }
            break;
        }

        default:
        case MapLayerType::ACTIONS:
            break;
    }
}

bool MapReader::readBase64Layer(XmlNodeConstPtrConst childNode,
                                Map *const map,
                                MapLayer *const layer,
                                const MapLayerTypeT &layerType,
                                MapHeights *const heights,
                                const std::string &compression,
                                int &restrict x, int &restrict y,
                                const int w, const int h)
{
    if (childNode == nullptr)
        return false;

    if (!compression.empty() && compression != "gzip"
        && compression != "zlib")
    {
        reportAlways("Warning: only gzip and zlib layer"
            " compression supported!")
        return false;
    }

    // Read base64 encoded map file
    if (!XmlHaveChildContent(childNode))
        return true;

    const size_t len = strlen(XmlChildContent(childNode)) + 1;
    unsigned char *charData = new unsigned char[len + 1];
    const char *const xmlChars = XmlChildContent(childNode);
    const char *charStart = reinterpret_cast<const char*>(xmlChars);
    if (charStart == nullptr)
    {
        delete [] charData;
        return false;
    }

    unsigned char *charIndex = charData;

    while (*charStart != 0)
    {
        if (*charStart != ' ' &&
            *charStart != '\t' &&
            *charStart != '\n')
        {
            *charIndex = *charStart;
            charIndex++;
        }
        charStart++;
    }
    *charIndex = '\0';

    int binLen;
    unsigned char *binData = php3_base64_decode(charData,
        CAST_S32(strlen(reinterpret_cast<char*>(
        charData))), &binLen);

    delete [] charData;
//    XmlFree(const_cast<char*>(xmlChars));

    if (binData != nullptr)
    {
        if (compression == "gzip" || compression == "zlib")
        {
            // Inflate the gzipped layer data
            unsigned char *inflated = nullptr;
            const unsigned int inflatedSize =
                inflateMemory(binData, binLen, inflated);

            free(binData);
            binData = inflated;
            binLen = inflatedSize;

            if (inflated == nullptr)
            {
                reportAlways("Error: Could not decompress layer!")
                return false;
            }
        }

        const std::map<int, TileAnimation*> &tileAnimations
            = map->getTileAnimations();

        const bool hasAnimations = !tileAnimations.empty();
        if (hasAnimations)
        {
            for (int i = 0; i < binLen - 3; i += 4)
            {
                const int gid = binData[i] |
                    binData[i + 1] << 8 |
                    binData[i + 2] << 16 |
                    binData[i + 3] << 24;

                setTile(map, layer, layerType, heights, x, y, gid);
                TileAnimationMapCIter it = tileAnimations.find(gid);
                if (it != tileAnimations.end())
                {
                    TileAnimation *const ani = it->second;
                    if (ani != nullptr)
                        ani->addAffectedTile(layer, x + y * w);
                }

                x++;
                if (x == w)
                {
                    x = 0; y++;

                    // When we're done, don't crash on too much data
                    if (y == h)
                        break;
                }
            }
        }
        else
        {
            for (int i = 0; i < binLen - 3; i += 4)
            {
                const int gid = binData[i] |
                    binData[i + 1] << 8 |
                    binData[i + 2] << 16 |
                    binData[i + 3] << 24;

                setTile(map, layer, layerType, heights, x, y, gid);

                x++;
                if (x == w)
                {
                    x = 0; y++;

                    // When we're done, don't crash on too much data
                    if (y == h)
                        break;
                }
            }
        }
        free(binData);
    }
    return true;
}

bool MapReader::readCsvLayer(XmlNodeConstPtrConst childNode,
                             Map *const map,
                             MapLayer *const layer,
                             const MapLayerTypeT &layerType,
                             MapHeights *const heights,
                             int &restrict x, int &restrict y,
                             const int w, const int h)
{
    if (childNode == nullptr)
        return false;

    if (!XmlHaveChildContent(childNode))
        return true;

    const char *const xmlChars = XmlChildContent(childNode);
    const char *const data = reinterpret_cast<const char*>(xmlChars);
    if (data == nullptr)
        return false;

    std::string csv(data);
    size_t oldPos = 0;

    const std::map<int, TileAnimation*> &tileAnimations
        = map->getTileAnimations();
    const bool hasAnimations = !tileAnimations.empty();

    if (hasAnimations)
    {
        while (oldPos != std::string::npos)
        {
            const size_t pos = csv.find_first_of(',', oldPos);
            if (pos == std::string::npos)
                return false;

            const int gid = atoi(csv.substr(oldPos, pos - oldPos).c_str());
            setTile(map, layer, layerType, heights, x, y, gid);
            TileAnimationMapCIter it = tileAnimations.find(gid);
            if (it != tileAnimations.end())
            {
                TileAnimation *const ani = it->second;
                if (ani != nullptr)
                    ani->addAffectedTile(layer, x + y * w);
            }

            x++;
            if (x == w)
            {
                x = 0; y++;

                // When we're done, don't crash on too much data
                if (y == h)
                    return false;
            }
            oldPos = pos + 1;
        }
    }
    else
    {
        while (oldPos != std::string::npos)
        {
            const size_t pos = csv.find_first_of(',', oldPos);
            if (pos == std::string::npos)
                return false;

            const int gid = atoi(csv.substr(oldPos, pos - oldPos).c_str());
            setTile(map, layer, layerType, heights, x, y, gid);

            x++;
            if (x == w)
            {
                x = 0; y++;

                // When we're done, don't crash on too much data
                if (y == h)
                    return false;
            }
            oldPos = pos + 1;
        }
    }
    return true;
}

void MapReader::readLayer(XmlNodeConstPtr node, Map *const map)
{
    if (node == nullptr)
        return;

    // Layers are not necessarily the same size as the map
    const int w = XML::getProperty(node, "width", map->getWidth());
    const int h = XML::getProperty(node, "height", map->getHeight());
    const int offsetX = XML::getProperty(node, "x", 0);
    const int offsetY = XML::getProperty(node, "y", 0);
    std::string name = XML::getProperty(node, "name", "");
    name = toLower(name);

    const bool isFringeLayer = (name.substr(0, 6) == "fringe");
    const bool isCollisionLayer = (name.substr(0, 9) == "collision");
    const bool isHeightLayer = (name.substr(0, 7) == "heights");
    const bool isActionsLayer = (name.substr(0, 7) == "actions");
    int mask = 1;
    int tileCondition = -1;
    int conditionLayer = 0;

    MapLayerTypeT layerType = MapLayerType::TILES;
    if (isCollisionLayer)
        layerType = MapLayerType::COLLISION;
    else if (isHeightLayer)
        layerType = MapLayerType::HEIGHTS;
    else if (isActionsLayer)
        layerType = MapLayerType::ACTIONS;

    map->indexTilesets();

    MapLayer *layer = nullptr;
    MapHeights *heights = nullptr;

    logger->log("- Loading layer \"%s\"", name.c_str());
    int x = 0;
    int y = 0;

    // Load the tile data
    for_each_xml_child_node(childNode, node)
    {
        if (xmlNameEqual(childNode, "properties"))
        {
            for_each_xml_child_node(prop, childNode)
            {
                if (!xmlNameEqual(prop, "property"))
                    continue;
                const std::string pname = XML::getProperty(prop, "name", "");
                const std::string value = XML::getProperty(prop, "value", "");
                // ignoring any layer if property Hidden is 1
                if (pname == "Hidden")
                {
                    if (value == "1")
                        return;
                }
                else if (pname == "Version")
                {
                    if (value > CHECK_VERSION)
                        return;
                }
                else if (pname == "NotVersion")
                {
                    if (value <= CHECK_VERSION)
                        return;
                }
                else if (pname == "Mask")
                {
                    mask = atoi(value.c_str());
                }
                else if (pname == "TileCondition")
                {
                    tileCondition = atoi(value.c_str());
                }
                else if (pname == "ConditionLayer")
                {
                    conditionLayer = atoi(value.c_str());
                }
                else if (pname == "SideView")
                {
                    if (value != "down")
                        return;
                }
            }
        }

        if (!xmlNameEqual(childNode, "data"))
            continue;

        // Disable for future usage "TileCondition" attribute
        // if already set ConditionLayer to non zero
        if (conditionLayer != 0)
            tileCondition = -1;

        switch (layerType)
        {
            case MapLayerType::TILES:
            {
                layer = new MapLayer(name,
                    offsetX, offsetY,
                    w, h,
                    isFringeLayer,
                    mask,
                    tileCondition);
                map->addLayer(layer);
                break;
            }
            case MapLayerType::HEIGHTS:
            {
                heights = new MapHeights(w, h);
                map->addHeights(heights);
                break;
            }
            default:
            case MapLayerType::ACTIONS:
            case MapLayerType::COLLISION:
                break;
        }

        const std::string encoding =
            XML::getProperty(childNode, "encoding", "");
        const std::string compression =
            XML::getProperty(childNode, "compression", "");

        if (encoding == "base64")
        {
            if (readBase64Layer(childNode, map, layer, layerType,
                heights, compression, x, y, w, h))
            {
                continue;
            }
            else
            {
                return;
            }
        }
        else if (encoding == "csv")
        {
            if (readCsvLayer(childNode, map, layer, layerType,
                heights, x, y, w, h))
            {
                continue;
            }
            else
            {
                return;
            }
        }
        else
        {
            const std::map<int, TileAnimation*> &tileAnimations
                = map->getTileAnimations();
            const bool hasAnimations = !tileAnimations.empty();

            // Read plain XML map file
            for_each_xml_child_node(childNode2, childNode)
            {
                if (!xmlNameEqual(childNode2, "tile"))
                    continue;

                const int gid = XML::getProperty(childNode2, "gid", -1);
                setTile(map, layer, layerType, heights, x, y, gid);
                if (hasAnimations)
                {
                    TileAnimationMapCIter it = tileAnimations.find(gid);
                    if (it != tileAnimations.end())
                    {
                        TileAnimation *const ani = it->second;
                        if (ani != nullptr)
                            ani->addAffectedTile(layer, x + y * w);
                    }
                }

                x++;
                if (x == w)
                {
                    x = 0; y++;
                    if (y >= h)
                        break;
                }
            }
        }

        if (y < h)
            std::cerr << "TOO SMALL!\n";
        if (x != 0)
            std::cerr << "TOO SMALL!\n";

        // There can be only one data element
        break;
    }
}

Tileset *MapReader::readTileset(XmlNodePtr node,
                                const std::string &path,
                                Map *const map)
{
    BLOCK_START("MapReader::readTileset")
    if (node == nullptr)
    {
        BLOCK_END("MapReader::readTileset")
        return nullptr;
    }

    const int firstGid = XML::getProperty(node, "firstgid", 0);
    const int margin = XML::getProperty(node, "margin", 0);
    const int spacing = XML::getProperty(node, "spacing", 0);
    XML::Document* doc = nullptr;
    Tileset *set = nullptr;
    std::string pathDir(path);
    std::map<std::string, std::string> props;

    if (XmlHasProp(node, "source"))
    {
        std::string filename = XML::getProperty(node, "source", "");
        filename = resolveRelativePath(path, filename);

        doc = new XML::Document(filename, UseVirtFs_true, SkipError_false);
        node = doc->rootNode();
        if (node == nullptr)
        {
            delete doc;
            BLOCK_END("MapReader::readTileset")
            return nullptr;
        }

        // Reset path to be realtive to the tsx file
        pathDir = filename.substr(0, filename.rfind('/') + 1);
    }

    const int tw = XML::getProperty(node, "tilewidth", map->getTileWidth());
    const int th = XML::getProperty(node, "tileheight", map->getTileHeight());

    for_each_xml_child_node(childNode, node)
    {
        if (xmlNameEqual(childNode, "image"))
        {
            // ignore second other <image> tags in tileset
            if (set != nullptr)
                continue;

            const std::string source = XML::getProperty(
                childNode, "source", "");

            if (!source.empty())
            {
                const std::string sourceResolved = resolveRelativePath(pathDir,
                    source);

                Image *const tilebmp = Loader::getImage(sourceResolved);

                if (tilebmp != nullptr)
                {
                    set = new Tileset(tilebmp,
                        tw, th,
                        firstGid,
                        margin,
                        spacing);
                    tilebmp->decRef();
#ifdef USE_OPENGL
                    if (MapDB::isEmptyTileset(sourceResolved))
                        set->setEmpty(true);
                    if (tilebmp->getType() == ImageType::Image &&
                        map->haveAtlas() == true &&
                        graphicsManager.getUseAtlases())
                    {
                        reportAlways("Error: image '%s' not present in atlas",
                            source.c_str())
                    }
#endif  // USE_OPENGL
                }
                else
                {
                    reportAlways("Error: Failed to load tileset (%s)",
                        source.c_str())
                }
            }
        }
        else if (xmlNameEqual(childNode, "properties"))
        {
            for_each_xml_child_node(propertyNode, childNode)
            {
                if (!xmlNameEqual(propertyNode, "property"))
                    continue;
                const std::string name = XML::getProperty(
                    propertyNode, "name", "");
                if (!name.empty())
                    props[name] = XML::getProperty(propertyNode, "value", "");
            }
        }
        else if (xmlNameEqual(childNode, "tile"))
        {
            bool haveAnimation(false);

            for_each_xml_child_node(tileNode, childNode)
            {
                const bool isProps = xmlNameEqual(tileNode, "properties");
                const bool isAnim = xmlNameEqual(tileNode, "animation");
                if (!isProps && !isAnim)
                    continue;

                const int tileGID = firstGid + XML::getProperty(
                    childNode, "id", 0);

                Animation *ani = new Animation("from map");

                if (isProps)
                {
                    // read tile properties to a map for simpler handling
                    StringIntMap tileProperties;
                    for_each_xml_child_node(propertyNode, tileNode)
                    {
                        if (!xmlNameEqual(propertyNode, "property"))
                            continue;

                        haveAnimation = true;
                        const std::string name = XML::getProperty(
                            propertyNode, "name", "");
                        if (!name.empty())
                        {
                            const int value = XML::getProperty(
                                propertyNode, "value", 0);
                            tileProperties[name] = value;
                            logger->log("Tile Prop of %d \"%s\" = \"%d\"",
                                tileGID, name.c_str(), value);
                        }
                    }

                    // create animation
                    if (set == nullptr ||
                        !config.getBoolValue("playMapAnimations"))
                    {
                        delete ani;
                        continue;
                    }

                    for (int i = 0; ; i++)
                    {
                        const std::string iStr(toString(i));
                        StringIntMapCIter iFrame
                            = tileProperties.find("animation-frame" + iStr);
                        StringIntMapCIter iDelay
                            = tileProperties.find("animation-delay" + iStr);
                        // possible need add random attribute?
                        if (iFrame != tileProperties.end()
                            && iDelay != tileProperties.end())
                        {
                            ani->addFrame(set->get(iFrame->second),
                                iDelay->second, 0, 0, 100);
                        }
                        else
                        {
                            break;
                        }
                    }
                }
                else if (isAnim && !haveAnimation)
                {
                    for_each_xml_child_node(frameNode, tileNode)
                    {
                        if (!xmlNameEqual(frameNode, "frame"))
                            continue;

                        if (set != nullptr)
                        {
                            const int tileId = XML::getProperty(
                                frameNode, "tileid", 0);
                            const int duration = XML::getProperty(
                                frameNode, "duration", 0) /  10;

                            ani->addFrame(set->get(tileId),
                                duration,
                                0, 0, 100);
                        }
                    }
                }

                if (ani->getLength() > 0)
                    map->addAnimation(tileGID, new TileAnimation(ani));
                else
                    delete2(ani)
            }
        }
    }

    delete doc;

    if (set != nullptr)
        set->setProperties(props);
    BLOCK_END("MapReader::readTileset")
    return set;
}

Map *MapReader::createEmptyMap(const std::string &restrict filename,
                               const std::string &restrict realFilename)
{
    logger->log1("Creating empty map");
    Map *const map = new Map("empty map",
        300, 300,
        mapTileSize, mapTileSize);
    map->setProperty("_filename", realFilename);
    map->setProperty("_realfilename", filename);
    updateMusic(map);
    map->setCustom(true);
    MapLayer *layer = new MapLayer("nolayer",
        0, 0,
        300, 300,
        false,
        1,
        -1);
    map->addLayer(layer);
    layer = new MapLayer("nolayer",
        0, 0,
        300, 300,
        true,
        1,
        -1);
    map->addLayer(layer);
    map->updateDrawLayersList();
    map->updateConditionLayers();
    map->preCacheLayers();

    return map;
}

void MapReader::updateMusic(Map *const map)
{
    std::string name = map->getProperty("shortName", std::string());
    const size_t p = name.rfind('.');
    if (p != std::string::npos)
        name = name.substr(0, p);
    name.append(".ogg");
    if (VirtFs::exists(pathJoin(paths.getStringValue("music"), name)))
        map->setProperty("music", name);
}

#ifdef USE_OPENGL
void MapReader::loadEmptyAtlas()
{
    if (!graphicsManager.getUseAtlases())
        return;

    const MapInfo *const info = MapDB::getAtlas(
        paths.getStringValue("emptyAtlasName"));
    if (info != nullptr)
    {
        mEmptyAtlas = Loader::getEmptyAtlas(
            info->atlas,
            *info->files);
        delete info;
    }
}

void MapReader::unloadEmptyAtlas()
{
    if (mEmptyAtlas != nullptr)
        mEmptyAtlas->decRef();
}
#endif  // USE_OPENGL
