/*
 *  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/map/maplayer.h"

#include "configuration.h"

#include "being/localplayer.h"

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

#include "gui/userpalette.h"

#ifdef USE_OPENGL
#include "utils/foreach.h"
#endif  // USE_OPENGL

#include "render/graphics.h"

#include "resources/image/image.h"

#include "resources/map/mapitem.h"
#include "resources/map/maprowvertexes.h"
#include "resources/map/metatile.h"
#include "resources/map/speciallayer.h"

#include "debug.h"

MapLayer::MapLayer(const std::string &name,
                   const int x,
                   const int y,
                   const int width,
                   const int height,
                   const bool fringeLayer,
                   const int mask,
                   const int tileCondition) :
    mX(x),
    mY(y),
    mPixelX(mX * mapTileSize),
    mPixelY(mY * mapTileSize + mapTileSize),
    mWidth(width),
    mHeight(height),
    mTiles(new TileInfo[mWidth * mHeight]),
    mDrawLayerFlags(MapType::NORMAL),
    mSpecialLayer(nullptr),
    mTempLayer(nullptr),
    mName(name),
    mTempRows(),
    mMask(mask),
    mTileCondition(tileCondition),
    mActorsFix(0),
    mIsFringeLayer(fringeLayer),
    mHighlightAttackRange(config.getBoolValue("highlightAttackRange")),
    mSpecialFlag(true)
{
//    std::fill_n(mTiles, mWidth * mHeight, static_cast<Image*>(nullptr));

    config.addListener("highlightAttackRange", this);
}

MapLayer::~MapLayer()
{
    config.removeListener("highlightAttackRange", this);
    CHECKLISTENERS
    delete []mTiles;
    delete_all(mTempRows);
    mTempRows.clear();
}

void MapLayer::optionChanged(const std::string &value) restrict
{
    if (value == "highlightAttackRange")
    {
        mHighlightAttackRange =
            config.getBoolValue("highlightAttackRange");
    }
}

void MapLayer::draw(Graphics *const graphics,
                    int startX,
                    int startY,
                    int endX,
                    int endY,
                    const int scrollX,
                    const int scrollY) const restrict
{
    BLOCK_START("MapLayer::draw")
    startX -= mX;
    startY -= mY;
    endX -= mX;
    endY -= mY;

    if (startX < 0)
        startX = 0;
    if (startY < 0)
        startY = 0;
    if (endX > mWidth)
        endX = mWidth;
    if (endY > mHeight)
        endY = mHeight;

    const int dx = mPixelX - scrollX;
    const int dy = mPixelY - scrollY;
    for (int y = startY; y < endY; y++)
    {
        const int y32 = y * mapTileSize;
        const int yWidth = y * mWidth;

        const int py0 = y32 + dy;

        int x0 = startX;
        TileInfo *tilePtr = &mTiles[CAST_SIZE(x0 + yWidth)];
        if (tilePtr->isEnabled == false)
        {
            if (x0 + tilePtr->nextTile + 1 >= endX)
            {
                continue;
            }
            x0 += tilePtr->nextTile + 1;
            tilePtr = &mTiles[CAST_SIZE(x0 + yWidth)];
            if (mTiles[x0 + yWidth].isEnabled == false)
            {
                continue;
            }
        }
        for (int x = x0; x < endX; x++, tilePtr++)
        {
            const int x32 = x * mapTileSize;

            const Image *const img = tilePtr->image;
            const int px = x32 + dx;
            const int py = py0 - img->mBounds.h;
            if (mSpecialFlag ||
                img->mBounds.h <= mapTileSize)
            {
                if (tilePtr->count == 0)
                {
                    graphics->drawImage(img, px, py);
                }
                else
                {
                    graphics->drawPattern(img,
                        px,
                        py,
                        tilePtr->width,
                        img->mBounds.h);
                }
            }

            const int nextTile = tilePtr->nextTile;
            x += nextTile;
            tilePtr += nextTile;
        }
    }
    BLOCK_END("MapLayer::draw")
}

void MapLayer::drawSDL(Graphics *const graphics) const restrict2
{
    BLOCK_START("MapLayer::drawSDL")
    MapRows::const_iterator rit = mTempRows.begin();
    const MapRows::const_iterator rit_end = mTempRows.end();
    while (rit != rit_end)
    {
        MapRowImages *const images = &(*rit)->images;
        MapRowImages::const_iterator iit = images->begin();
        const MapRowImages::const_iterator iit_end = images->end();
        while (iit != iit_end)
        {
            graphics->drawTileVertexes(*iit);
            ++ iit;
        }
        ++ rit;
    }
    BLOCK_END("MapLayer::drawSDL")
}

#ifdef USE_OPENGL
void MapLayer::updateSDL(const Graphics *const graphics,
                         int startX,
                         int startY,
                         int endX,
                         int endY,
                         const int scrollX,
                         const int scrollY) restrict2
{
    BLOCK_START("MapLayer::updateSDL")
    delete_all(mTempRows);
    mTempRows.clear();

    startX -= mX;
    startY -= mY;
    endX -= mX;
    endY -= mY;

    if (startX < 0)
        startX = 0;
    if (startY < 0)
        startY = 0;
    if (endX > mWidth)
        endX = mWidth;
    if (endY > mHeight)
        endY = mHeight;

    const int dx = mPixelX - scrollX;
    const int dy = mPixelY - scrollY;

    for (int y = startY; y < endY; y++)
    {
        MapRowVertexes *const row = new MapRowVertexes;
        mTempRows.push_back(row);

        const Image *lastImage = nullptr;
        ImageVertexes *imgVert = nullptr;

        const int yWidth = y * mWidth;
        const int py0 = y * mapTileSize + dy;
        TileInfo *tilePtr = &mTiles[CAST_SIZE(startX + yWidth)];

        for (int x = startX; x < endX; x++, tilePtr++)
        {
            if (!tilePtr->isEnabled)
                continue;
            Image *const img = (*tilePtr).image;
            const int px = x * mapTileSize + dx;
            const int py = py0 - img->mBounds.h;
            if (mSpecialFlag ||
                img->mBounds.h <= mapTileSize)
            {
                if (lastImage != img)
                {
                    imgVert = new ImageVertexes;
                    imgVert->image = img;
                    row->images.push_back(imgVert);
                    lastImage = img;
                }
                graphics->calcTileSDL(imgVert, px, py);
            }
        }
    }
    BLOCK_END("MapLayer::updateSDL")
}

void MapLayer::updateOGL(Graphics *const graphics,
                         int startX,
                         int startY,
                         int endX,
                         int endY,
                         const int scrollX,
                         const int scrollY) restrict2
{
    BLOCK_START("MapLayer::updateOGL")
    delete_all(mTempRows);
    mTempRows.clear();

    startX -= mX;
    startY -= mY;
    endX -= mX;
    endY -= mY;

    if (startX < 0)
        startX = 0;
    if (startY < 0)
        startY = 0;
    if (endX > mWidth)
        endX = mWidth;
    if (endY > mHeight)
        endY = mHeight;

    const int dx = mPixelX - scrollX;
    const int dy = mPixelY - scrollY;

    MapRowVertexes *const row = new MapRowVertexes;
    mTempRows.push_back(row);
    Image *lastImage = nullptr;
    ImageVertexes *imgVert = nullptr;
    typedef std::map<int, ImageVertexes*> ImageVertexesMap;
    ImageVertexesMap imgSet;

    for (int y = startY; y < endY; y++)
    {
        const int yWidth = y * mWidth;
        const int py0 = y * mapTileSize + dy;
        TileInfo *tilePtr = &mTiles[CAST_SIZE(startX + yWidth)];
        for (int x = startX; x < endX; x++, tilePtr++)
        {
            if (!tilePtr->isEnabled)
                continue;
            Image *const img = (*tilePtr).image;
            const int px = x * mapTileSize + dx;
            const int py = py0 - img->mBounds.h;
            const GLuint imgGlImage = img->mGLImage;
            if (mSpecialFlag ||
                img->mBounds.h <= mapTileSize)
            {
                if ((lastImage == nullptr) ||
                    lastImage->mGLImage != imgGlImage)
                {
                    if (img->mBounds.w > mapTileSize)
                        imgSet.clear();

                    if (imgSet.find(imgGlImage) != imgSet.end())
                    {
                        imgVert = imgSet[imgGlImage];
                    }
                    else
                    {
                        if (lastImage != nullptr)
                            imgSet[lastImage->mGLImage] = imgVert;
                        imgVert = new ImageVertexes;
                        imgVert->ogl.init();
                        imgVert->image = img;
                        row->images.push_back(imgVert);
                    }
                }
                lastImage = img;
                graphics->calcTileVertexes(imgVert, lastImage, px, py);
            }
        }
    }
    FOR_EACH (MapRowImages::iterator, it, row->images)
    {
        graphics->finalize(*it);
    }
    BLOCK_END("MapLayer::updateOGL")
}

void MapLayer::drawOGL(Graphics *const graphics) const restrict2
{
    BLOCK_START("MapLayer::drawOGL")
    MapRows::const_iterator rit = mTempRows.begin();
    const MapRows::const_iterator rit_end = mTempRows.end();
    while (rit != rit_end)
    {
        const MapRowImages *const images = &(*rit)->images;
        MapRowImages::const_iterator iit = images->begin();
        const MapRowImages::const_iterator iit_end = images->end();
        while (iit != iit_end)
        {
            graphics->drawTileVertexes(*iit);
            ++ iit;
        }
        ++ rit;
    }
    BLOCK_END("MapLayer::drawOGL")
}
#endif  // USE_OPENGL

void MapLayer::drawSpecialLayer(Graphics *const graphics,
                                const int y,
                                const int startX,
                                const int endX,
                                const int scrollX,
                                const int scrollY) const restrict
{
    const int y32 = y * mapTileSize;
    const int py1 = y32 - scrollY;
    int endX1 = endX;
    int specialWidth = mSpecialLayer->mWidth;
    int ptr = y * specialWidth;
    if (endX1 > specialWidth)
        endX1 = specialWidth;
    if (endX1 < 0)
        endX1 = 0;
    int x0 = startX;
    const MapItem *item0 = mSpecialLayer->mTiles[ptr + startX];
    if ((item0 == nullptr) || item0->mType == MapItemType::EMPTY)
    {
        x0 += mSpecialLayer->mCache[ptr + startX] + 1;
    }
    for (int x = x0; x < endX1; x++)
    {
        const int px1 = x * mapTileSize - scrollX;
        const MapItem *const item = mSpecialLayer->mTiles[ptr + x];
        if (item != nullptr)
        {
            item->draw(graphics, px1, py1,
                mapTileSize, mapTileSize);
        }
        x += mSpecialLayer->mCache[ptr + x];
    }

    x0 = startX;
    specialWidth = mTempLayer->mWidth;
    ptr = y * specialWidth;
    endX1 = endX;
    if (endX1 > specialWidth)
        endX1 = specialWidth;
    item0 = mTempLayer->mTiles[ptr + startX];
    if ((item0 == nullptr) || item0->mType == MapItemType::EMPTY)
    {
        x0 += mTempLayer->mCache[ptr + startX] + 1;
    }
    for (int x = x0; x < endX1; x++)
    {
        const int px1 = x * mapTileSize - scrollX;
        const MapItem *const item = mTempLayer->mTiles[ptr + x];
        item->draw(graphics, px1, py1,
            mapTileSize, mapTileSize);
        x += mTempLayer->mCache[ptr + x];
    }
}

void MapLayer::drawFringe(Graphics *const graphics,
                          int startX,
                          int startY,
                          int endX,
                          int endY,
                          const int scrollX,
                          const int scrollY,
                          const Actors &actors) const restrict
{
    BLOCK_START("MapLayer::drawFringe")
    if ((localPlayer == nullptr) ||
        (mSpecialLayer == nullptr) ||
        (mTempLayer == nullptr))
    {
        BLOCK_END("MapLayer::drawFringe")
        return;
    }

    startX -= mX;
    startY -= mY;
    endX -= mX;
    endY -= mY;

    if (startX < 0)
        startX = 0;
    if (startY < 0)
        startY = 0;
    if (endX > mWidth)
        endX = mWidth;
    if (endY > mHeight)
        endY = mHeight;

    ActorsCIter ai = actors.begin();
    const ActorsCIter ai_end = actors.end();

    const int dx = mPixelX - scrollX;
    const int dy = mPixelY - scrollY;

    const int specialHeight = mSpecialLayer->mHeight;

    const bool flag = mDrawLayerFlags == MapType::SPECIAL3 ||
        mDrawLayerFlags == MapType::SPECIAL4 ||
        mDrawLayerFlags == MapType::BLACKWHITE;

    const int minEndY = std::min(specialHeight, endY);

    if (flag)
    {   // flag
        for (int y = startY; y < minEndY; y ++)
        {
            const int y32s = (y + mActorsFix) * mapTileSize;

            BLOCK_START("MapLayer::drawFringe drawmobs")
            // If drawing the fringe layer, make sure all actors above this
            // row of tiles have been drawn
            while (ai != ai_end && (*ai)->getSortPixelY() <= y32s)
            {
                (*ai)->draw(graphics, -scrollX, -scrollY);
                ++ ai;
            }
            BLOCK_END("MapLayer::drawFringe drawmobs")

            // remove this condition, because it always true
            if (y < specialHeight)
            {
                drawSpecialLayer(graphics,
                    y,
                    startX,
                    endX,
                    scrollX,
                    scrollY);
            }
        }

        for (int y = minEndY; y < endY; y++)
        {
            const int y32s = (y + mActorsFix) * mapTileSize;

            BLOCK_START("MapLayer::drawFringe drawmobs")
            // If drawing the fringe layer, make sure all actors above this
            // row of tiles have been drawn
            while (ai != ai_end && (*ai)->getSortPixelY() <= y32s)
            {
                (*ai)->draw(graphics, -scrollX, -scrollY);
                ++ ai;
            }
            BLOCK_END("MapLayer::drawFringe drawmobs")
        }
    }
    else
    {   // !flag
        for (int y = startY; y < minEndY; y ++)
        {
            const int y32 = y * mapTileSize;
            const int y32s = (y + mActorsFix) * mapTileSize;
            const int yWidth = y * mWidth;

            BLOCK_START("MapLayer::drawFringe drawmobs")
            // If drawing the fringe layer, make sure all actors above this
            // row of tiles have been drawn
            while (ai != ai_end &&
                   (*ai)->getSortPixelY() <= y32s)
            {
                (*ai)->draw(graphics, -scrollX, -scrollY);
                ++ ai;
            }
            BLOCK_END("MapLayer::drawFringe drawmobs")

            const int py0 = y32 + dy;

            int x0 = startX;
            TileInfo *tilePtr = &mTiles[CAST_SIZE(x0 + yWidth)];
            if (tilePtr->isEnabled == false)
            {
                drawSpecialLayer(graphics,
                    y,
                    0,
                    std::min(x0 + tilePtr->nextTile + 1, endX),
                    scrollX,
                    scrollY);
                if (x0 + tilePtr->nextTile + 1 >= endX)
                {
                    continue;
                }
                x0 += tilePtr->nextTile + 1;
                tilePtr = &mTiles[CAST_SIZE(x0 + yWidth)];
                if (mTiles[x0 + yWidth].isEnabled == false)
                    continue;
            }
            for (int x = x0; x < endX; x++, tilePtr++)
            {
                const int x32 = x * mapTileSize;
                const Image *const img = tilePtr->image;
                if (mSpecialFlag ||
                    img->mBounds.h <= mapTileSize)
                {
                    const int px = x32 + dx;
                    const int py = py0 - img->mBounds.h;

                    if (tilePtr->count == 0)
                    {
                        graphics->drawImage(img, px, py);
                    }
                    else
                    {
                        graphics->drawPattern(img,
                            px,
                            py,
                            tilePtr->width,
                            img->mBounds.h);
                    }
                }

                const int nextTile = tilePtr->nextTile;
                // remove this condition, because it always true
                if (y < specialHeight)
                {
                    drawSpecialLayer(graphics,
                        y,
                        x,
                        std::min(x + nextTile + 1, endX),
                        scrollX,
                        scrollY);
                }
                x += nextTile;
                tilePtr += nextTile;
            }
        }

        for (int y = minEndY; y < endY; y++)
        {
            const int y32 = y * mapTileSize;
            const int y32s = (y + mActorsFix) * mapTileSize;
            const int yWidth = y * mWidth;

            BLOCK_START("MapLayer::drawFringe drawmobs")
            // If drawing the fringe layer, make sure all actors above this
            // row of tiles have been drawn
            while (ai != ai_end && (*ai)->getSortPixelY() <= y32s)
            {
                (*ai)->draw(graphics, -scrollX, -scrollY);
                ++ ai;
            }
            BLOCK_END("MapLayer::drawFringe drawmobs")

            const int py0 = y32 + dy;

            int x0 = startX;
            TileInfo *tilePtr = &mTiles[CAST_SIZE(x0 + yWidth)];
            if (tilePtr->isEnabled == false)
            {
                if (x0 + tilePtr->nextTile + 1 >= endX)
                    continue;
                x0 += tilePtr->nextTile + 1;
                tilePtr = &mTiles[CAST_SIZE(x0 + yWidth)];
                if (mTiles[x0 + yWidth].isEnabled == false)
                    continue;
            }
            for (int x = x0; x < endX; x++, tilePtr++)
            {
                const int x32 = x * mapTileSize;
                const Image *const img = tilePtr->image;
                const int px = x32 + dx;
                const int py = py0 - img->mBounds.h;
                if (mSpecialFlag ||
                    img->mBounds.h <= mapTileSize)
                {
                    const int c = tilePtr->count;

                    if (c == 0)
                    {
                        graphics->drawImage(img, px, py);
                    }
                    else
                    {
                        graphics->drawPattern(img,
                            px,
                            py,
                            tilePtr->width,
                            img->mBounds.h);
                    }
                }
                const int nextTile = tilePtr->nextTile;
                x += nextTile;
                tilePtr += nextTile;
            }
        }
    }  // !flag

    // Draw any remaining actors
    if (mDrawLayerFlags != MapType::SPECIAL3 &&
        mDrawLayerFlags != MapType::SPECIAL4)
    {
        BLOCK_START("MapLayer::drawFringe drawmobs")
        while (ai != ai_end)
        {
            (*ai)->draw(graphics, -scrollX, -scrollY);
            ++ai;
        }
        BLOCK_END("MapLayer::drawFringe drawmobs")
        if (mHighlightAttackRange)
        {
            const int px = localPlayer->getPixelX()
                - scrollX - mapTileSize / 2;
            const int py = localPlayer->getPixelY() - scrollY - mapTileSize;
            const int attackRange = localPlayer->getAttackRange()
                * mapTileSize;

            int x = px - attackRange;
            int y = py - attackRange;
            int w = 2 * attackRange + mapTileSize;
            int h = w;
            if (attackRange <= mapTileSize)
            {
                x -= mapTileSize / 2;
                y -= mapTileSize / 2;
                w += mapTileSize;
                h += mapTileSize;
            }

            if (userPalette != nullptr)
            {
                graphics->setColor(userPalette->getColorWithAlpha(
                    UserColorId::ATTACK_RANGE));
                graphics->fillRectangle(Rect(x, y, w, h));
                graphics->setColor(userPalette->getColorWithAlpha(
                    UserColorId::ATTACK_RANGE_BORDER));
                graphics->drawRectangle(Rect(x, y, w, h));
            }
        }
    }
    BLOCK_END("MapLayer::drawFringe")
}

int MapLayer::getTileDrawWidth(const TileInfo *restrict tilePtr,
                               const int endX,
                               int &restrict width,
                               int &restrict nextTile)
{
    BLOCK_START("MapLayer::getTileDrawWidth")
    const Image *const img1 = tilePtr->image;
    int c = 0;
    width = img1->mBounds.w;
    for (int x = 1; x < endX; x++)
    {
        tilePtr ++;
        const Image *const img = tilePtr->image;
        if (img == nullptr ||
            tilePtr->isEnabled == false)
        {
            break;
        }
        if (img != img1)
        {
            nextTile = c;
            BLOCK_END("MapLayer::getTileDrawWidth")
            return c;
        }
        c ++;
        width += img->mBounds.w;
    }
    int c2 = c;
    for (int x2 = c2 + 1; x2 < endX; x2++)
    {
        if (tilePtr->image != nullptr &&
            tilePtr->isEnabled == true)
        {
            break;
        }
        c2 ++;
        tilePtr ++;
    }
    nextTile = c2;
    BLOCK_END("MapLayer::getTileDrawWidth")
    return c;
}

int MapLayer::getEmptyTileDrawWidth(const TileInfo *restrict tilePtr,
                                    const int endX,
                                    int &restrict nextTile)
{
    BLOCK_START("MapLayer::getEmptyTileDrawWidth")
    int c = 0;
    for (int x = 1; x < endX; x++)
    {
        tilePtr ++;
        const Image *const img = tilePtr->image;
        if (img != nullptr && tilePtr->isEnabled == true)
            break;
        c ++;
    }
    BLOCK_END("MapLayer::getEmptyTileDrawWidth")

    nextTile = c;
    return c;
}

void MapLayer::updateConditionTiles(const MetaTile *const metaTiles,
                                    const int width,
                                    const int height) restrict
{
    const int width1 = width < mWidth ? width : mWidth;
    const int height1 = height < mHeight ? height : mHeight;

    for (int y = mY; y < height1; y ++)
    {
        const MetaTile *metaPtr = metaTiles + (y - mY) * width;
        TileInfo *tilePtr = mTiles + y * mWidth;
        for (int x = mX; x < width1; x ++, metaPtr ++, tilePtr ++)
        {
            if (tilePtr->image != nullptr &&
                (((metaPtr->blockmask & mTileCondition) != 0) ||
                (metaPtr->blockmask == 0 &&
                mTileCondition == BlockMask::GROUND)))
            {
                tilePtr->isEnabled = true;
            }
            else
            {
                tilePtr->isEnabled = false;
            }
        }
    }
}

void MapLayer::updateCache(const int width,
                           const int height) restrict
{
    const int width1 = width < mWidth ? width : mWidth;
    const int height1 = height < mHeight ? height : mHeight;

    for (int y = mY; y < height1; y ++)
    {
        for (int x = mX; x < width1; x ++)
        {
            TileInfo *tilePtr = mTiles + y * mWidth + x;
            int nextTile = 0;
            if (tilePtr->image == nullptr || tilePtr->isEnabled == false)
            {
                tilePtr->isEnabled = false;
                tilePtr->count = getEmptyTileDrawWidth(tilePtr,
                    width1 - x,
                    nextTile);
                tilePtr->width = 0;
            }
            else
            {
                int tileWidth = 0;
                tilePtr->count = getTileDrawWidth(tilePtr,
                    width1 - x,
                    tileWidth,
                    nextTile);
                tilePtr->width = tileWidth;
            }
            tilePtr->nextTile = nextTile;
        }
    }
}

int MapLayer::calcMemoryLocal() const
{
    return static_cast<int>(sizeof(MapLayer) +
        sizeof(TileInfo) * mWidth * mHeight +
        sizeof(MapRowVertexes) * mTempRows.capacity());
}

int MapLayer::calcMemoryChilds(const int level) const
{
    int sz = 0;
    if (mSpecialLayer != nullptr)
        sz += mSpecialLayer->calcMemory(level + 1);
    if (mTempLayer != nullptr)
        sz += mTempLayer->calcMemory(level + 1);
    return sz;
}
