/*
 *  The ManaPlus Client
 *  Copyright (C) 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 "being/compoundsprite.h"

#include "configuration.h"

#include "sdlshared.h"

#include "being/compounditem.h"

#include "render/surfacegraphics.h"

#if defined(USE_OPENGL) || !defined(USE_SDL2)
#include "resources/imagehelper.h"
#endif  // USE_OPENGL
#include "resources/image/image.h"

#include "utils/delete2.h"
#include "utils/dtor.h"
#include "utils/foreach.h"
#include "utils/likely.h"
#include "utils/sdlcheckutils.h"

#ifndef USE_SDL2
#include "game.h"

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

#include "resources/map/map.h"

#include "utils/timer.h"

PRAGMA48(GCC diagnostic push)
PRAGMA48(GCC diagnostic ignored "-Wshadow")
#ifndef SDL_BIG_ENDIAN
#include <SDL_endian.h>
#endif  // SDL_BYTEORDER
PRAGMA48(GCC diagnostic pop)
#endif  // USE_SDL2

#include "debug.h"

#ifndef USE_SDL2
static const int BUFFER_WIDTH = 100;
static const int BUFFER_HEIGHT = 100;
static const unsigned cache_max_size = 10;
static const unsigned cache_clean_part = 3;
#endif  // USE_SDL2

bool CompoundSprite::mEnableDelay = true;

CompoundSprite::CompoundSprite() :
    Sprite(),
    mSprites(),
    imagesCache(),
    mCacheItem(nullptr),
    mImage(nullptr),
    mAlphaImage(nullptr),
    mOffsetX(0),
    mOffsetY(0),
    mStartTime(0),
    mLastTime(0),
#ifndef USE_SDL2
    mNextRedrawTime(0),
#endif  // USE_SDL2
    mNeedsRedraw(false),
    mEnableAlphaFix(config.getBoolValue("enableAlphaFix")),
    mDisableAdvBeingCaching(config.getBoolValue("disableAdvBeingCaching")),
    mDisableBeingCaching(config.getBoolValue("disableBeingCaching"))
{
    mAlpha = 1.0F;
}

CompoundSprite::~CompoundSprite()
{
    clear();
    mImage = nullptr;
    mAlphaImage = nullptr;
}

bool CompoundSprite::reset()
{
    bool ret = false;
    FOR_EACH (SpriteIterator, it, mSprites)
    {
        if (*it != nullptr)
            ret |= (*it)->reset();
    }
    if (ret)
        mLastTime = 0;
    mNeedsRedraw |= ret;
    return ret;
}

bool CompoundSprite::play(const std::string &action)
{
    bool ret = false;
    bool ret2 = true;
    FOR_EACH (SpriteIterator, it, mSprites)
    {
        if (*it != nullptr)
        {
            const bool tmpVal = (*it)->play(action);
            ret |= tmpVal;
            ret2 &= tmpVal;
        }
    }
    mNeedsRedraw |= ret;
    if (ret2)
        mLastTime = 0;
    return ret;
}

bool CompoundSprite::update(const int time)
{
    bool ret = false;
    if (A_UNLIKELY(mLastTime == 0))
        mStartTime = time;
    mLastTime = time;
    FOR_EACH (SpriteIterator, it, mSprites)
    {
        if (*it != nullptr)
            ret |= (*it)->update(time);
    }
    mNeedsRedraw |= ret;
    return ret;
}

void CompoundSprite::drawSimple(Graphics *const graphics,
                                const int posX,
                                const int posY) const
{
    FUNC_BLOCK("CompoundSprite::draw", 1)
    if (mNeedsRedraw)
        updateImages();

    if (mSprites.empty())  // Nothing to draw
        return;

    if (mAlpha == 1.0F && (mImage != nullptr))
    {
        graphics->drawImage(mImage, posX + mOffsetX, posY + mOffsetY);
    }
    else if ((mAlpha != 0.0F) && (mAlphaImage != nullptr))
    {
        mAlphaImage->setAlpha(mAlpha);
        graphics->drawImage(mAlphaImage,
            posX + mOffsetX, posY + mOffsetY);
    }
    else
    {
        CompoundSprite::drawSprites(graphics, posX, posY);
    }
}

void CompoundSprite::drawSprites(Graphics *const graphics,
                                 const int posX,
                                 const int posY) const
{
    FOR_EACH (SpriteConstIterator, it, mSprites)
    {
        if (*it != nullptr)
        {
            (*it)->setAlpha(mAlpha);
            (*it)->draw(graphics, posX, posY);
        }
    }
}

void CompoundSprite::drawSpritesSDL(Graphics *const graphics,
                                    const int posX,
                                    const int posY) const
{
    FOR_EACH (SpriteConstIterator, it, mSprites)
    {
        if (*it != nullptr)
            (*it)->draw(graphics, posX, posY);
    }
}

int CompoundSprite::getWidth() const
{
    FOR_EACH (SpriteConstIterator, it, mSprites)
    {
        const Sprite *const base = *it;
        if (base != nullptr)
            return base->getWidth();
    }

    return 0;
}

int CompoundSprite::getHeight() const
{
    FOR_EACH (SpriteConstIterator, it, mSprites)
    {
        const Sprite *const base = *it;
        if (base != nullptr)
            return base->getHeight();
    }

    return 0;
}

const Image *CompoundSprite::getImage() const
{
    return mImage;
}

bool CompoundSprite::setSpriteDirection(const SpriteDirection::Type direction)
{
    bool ret = false;
    FOR_EACH (SpriteIterator, it, mSprites)
    {
        if (*it != nullptr)
            ret |= (*it)->setSpriteDirection(direction);
    }
    if (ret)
        mLastTime = 0;
    mNeedsRedraw |= ret;
    return ret;
}

int CompoundSprite::getNumberOfLayers() const
{
    if ((mImage != nullptr) || (mAlphaImage != nullptr))
        return 1;
    return CAST_S32(mSprites.size());
}

unsigned int CompoundSprite::getCurrentFrame() const
{
    FOR_EACH (SpriteConstIterator, it, mSprites)
    {
        if (*it != nullptr)
            return (*it)->getCurrentFrame();
    }
    return 0;
}

unsigned int CompoundSprite::getFrameCount() const
{
    FOR_EACH (SpriteConstIterator, it, mSprites)
    {
        if (*it != nullptr)
            return (*it)->getFrameCount();
    }
    return 0;
}

void CompoundSprite::addSprite(Sprite *const sprite)
{
    mSprites.push_back(sprite);
    mNeedsRedraw = true;
}

void CompoundSprite::setSprite(const size_t layer, Sprite *const sprite)
{
    // Skip if it won't change anything
    if (mSprites[layer] == sprite)
        return;

    delete mSprites[layer];
    mSprites[layer] = sprite;
    mNeedsRedraw = true;
}

void CompoundSprite::removeSprite(const int layer)
{
    // Skip if it won't change anything
    if (mSprites[layer] == nullptr)
        return;

    delete2(mSprites[layer])
    mNeedsRedraw = true;
}

void CompoundSprite::clear()
{
    // Skip if it won't change anything
    if (!mSprites.empty())
    {
        delete_all(mSprites);
        mSprites.clear();
    }
    mNeedsRedraw = true;
    delete_all(imagesCache);
    imagesCache.clear();
    delete2(mCacheItem)
    mLastTime = 0;
}

void CompoundSprite::ensureSize(const size_t layerCount)
{
    // Skip if it won't change anything
    if (mSprites.size() >= layerCount)
        return;

//    resize(layerCount, nullptr);
    mSprites.resize(layerCount);
}

void CompoundSprite::redraw() const
{
#ifndef USE_SDL2

#if SDL_BYTEORDER == SDL_BIG_ENDIAN
    const uint32_t rmask = 0xff000000U;
    const uint32_t gmask = 0x00ff0000U;
    const uint32_t bmask = 0x0000ff00U;
    const uint32_t amask = 0x000000ffU;
#else  // SDL_BYTEORDER == SDL_BIG_ENDIAN
    const uint32_t rmask = 0x000000ffU;
    const uint32_t gmask = 0x0000ff00U;
    const uint32_t bmask = 0x00ff0000U;
    const uint32_t amask = 0xff000000U;
#endif  // SDL_BYTEORDER == SDL_BIG_ENDIAN

    SDL_Surface *const surface = MSDL_CreateRGBSurface(SDL_HWSURFACE,
        BUFFER_WIDTH, BUFFER_HEIGHT, 32, rmask, gmask, bmask, amask);

    if (surface == nullptr)
        return;

    SurfaceGraphics *graphics = new SurfaceGraphics;
    graphics->setBlitMode(BlitMode::BLIT_GFX);
    graphics->setTarget(surface);
    graphics->beginDraw();

    int tileX = mapTileSize / 2;
    int tileY = mapTileSize;

    const Game *const game = Game::instance();
    if (game != nullptr)
    {
        const Map *const map = game->getCurrentMap();
        if (map != nullptr)
        {
            tileX = map->getTileWidth() / 2;
            tileY = map->getTileWidth();
        }
    }

    const int posX = BUFFER_WIDTH / 2 - tileX;
    const int posY = BUFFER_HEIGHT - tileY;

    mOffsetX = tileX - BUFFER_WIDTH / 2;
    mOffsetY = tileY - BUFFER_HEIGHT;

    drawSpritesSDL(graphics, posX, posY);

    delete2(graphics)

    SDL_SetAlpha(surface, 0, SDL_ALPHA_OPAQUE);

    delete mAlphaImage;

    if (ImageHelper::mEnableAlpha)
    {
        SDL_Surface *const surfaceA = MSDL_CreateRGBSurface(SDL_HWSURFACE,
            BUFFER_WIDTH, BUFFER_HEIGHT, 32, rmask, gmask, bmask, amask);
        SDL_BlitSurface(surface, nullptr, surfaceA, nullptr);
        mAlphaImage = imageHelper->loadSurface(surfaceA);
        MSDL_FreeSurface(surfaceA);
    }
    else
    {
        mAlphaImage = nullptr;
    }

    delete mImage;
    mImage = imageHelper->loadSurface(surface);
    MSDL_FreeSurface(surface);
#endif  // USE_SDL2
}

void CompoundSprite::setAlpha(float alpha)
{
    if (alpha != mAlpha)
    {
        if (mEnableAlphaFix &&
#ifdef USE_OPENGL
            imageHelper->useOpenGL() == RENDER_SOFTWARE &&
#endif  // USE_OPENGL
            mSprites.size() > 3U)
        {
            FOR_EACH (SpriteConstIterator, it, mSprites)
            {
                if (*it != nullptr)
                    (*it)->setAlpha(alpha);
            }
        }
        mAlpha = alpha;
    }
}

void CompoundSprite::updateImages() const
{
#ifndef USE_SDL2
#ifdef USE_OPENGL
    if (imageHelper->useOpenGL() != RENDER_SOFTWARE)
        return;
#endif  // USE_OPENGL

    if (mEnableDelay)
    {
        if (get_elapsed_time1(mNextRedrawTime) < 10)
            return;
        mNextRedrawTime = tick_time;
    }
    mNeedsRedraw = false;

    if (!mDisableBeingCaching)
    {
        if (mSprites.size() <= 3U)
            return;

        if (!mDisableAdvBeingCaching)
        {
            if (updateFromCache())
                return;

            redraw();

            if (mImage != nullptr)
                initCurrentCacheItem();
        }
        else
        {
            redraw();
        }
    }
#endif  // USE_SDL2
}

bool CompoundSprite::updateFromCache() const
{
#ifndef USE_SDL2
//    static int hits = 0;
//    static int miss = 0;

    if ((mCacheItem != nullptr) && (mCacheItem->image != nullptr))
    {
        imagesCache.push_front(mCacheItem);
        mCacheItem = nullptr;
        if (imagesCache.size() > cache_max_size)
        {
            for (unsigned f = 0; f < cache_clean_part; f ++)
            {
                CompoundItem *item = imagesCache.back();
                imagesCache.pop_back();
                delete item;
            }
        }
    }

//    logger->log("cache size: %d, hit %d, miss %d",
//        (int)imagesCache.size(), hits, miss);

    const size_t sz = mSprites.size();
    FOR_EACH (ImagesCache::iterator, it, imagesCache)
    {
        CompoundItem *const ic = *it;
        if ((ic != nullptr) && ic->data.size() == sz)
        {
            bool fail(false);
            VectorPointers::const_iterator it2 = ic->data.begin();
            const VectorPointers::const_iterator it2_end = ic->data.end();

            for (SpriteConstIterator it1 = mSprites.begin(),
                 it1_end = mSprites.end();
                 it1 != it1_end && it2 != it2_end;
                 ++ it1, ++ it2)
            {
                const void *ptr1 = nullptr;
                const void *ptr2 = nullptr;
                if (*it1 != nullptr)
                    ptr1 = (*it1)->getHash();
                if (*it2 != nullptr)
                    ptr2 = *it2;
                if (ptr1 != ptr2)
                {
                    fail = true;
                    break;
                }
            }
            if (!fail)
            {
//                hits ++;
                mImage = (*it)->image;
                mAlphaImage = (*it)->alphaImage;
                imagesCache.erase(it);
                mCacheItem = ic;
                return true;
            }
        }
    }
    mImage = nullptr;
    mAlphaImage = nullptr;
//    miss++;
#endif  // USE_SDL2
    return false;
}

void CompoundSprite::initCurrentCacheItem() const
{
    delete mCacheItem;
    mCacheItem = new CompoundItem;
    mCacheItem->image = mImage;
    mCacheItem->alphaImage = mAlphaImage;
//    mCacheItem->alpha = mAlpha;

    FOR_EACH (SpriteConstIterator, it, mSprites)
    {
        if (*it != nullptr)
            mCacheItem->data.push_back((*it)->getHash());
        else
            mCacheItem->data.push_back(nullptr);
    }
}

bool CompoundSprite::updateNumber(const unsigned num)
{
    bool res(false);
    FOR_EACH (SpriteConstIterator, it, mSprites)
    {
        if (*it != nullptr)
        {
            if ((*it)->updateNumber(num))
                res = true;
        }
    }
    return res;
}

CompoundItem::CompoundItem() :
    data(),
    image(nullptr),
    alphaImage(nullptr)
{
}

CompoundItem::~CompoundItem()
{
    delete image;
    delete alphaImage;
}
