/***************************************************************************
*                                                                          *
* Project64-video - A Nintendo 64 gfx plugin.                              *
* http://www.pj64-emu.com/                                                 *
* Copyright (C) 2017 Project64. All rights reserved.                       *
* Copyright (C) 2007 Hiroshi Morii                                         *
* Copyright (C) 2003 Rice1964                                              *
*                                                                          *
* License:                                                                 *
* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html                       *
* version 2 of the License, or (at your option) any later version.         *
*                                                                          *
****************************************************************************/

#ifdef WIN32
#pragma warning(disable: 4786)
#endif

#include <string.h> /* memcpy, memset */
#include <stdlib.h> /* malloc, free */

#include "TxCache.h"
#include "TxDbg.h"
#include <zlib/zlib.h>
#include <Common/path.h>
#include <Common/StdString.h>
#include <Project64-video/Renderer/types.h>

TxCache::~TxCache()
{
    /* free memory, clean up, etc */
    clear();

    delete _txUtil;
}

TxCache::TxCache(int options, int cachesize, const char *path, const char *ident,
    dispInfoFuncExt callback)
{
    _txUtil = new TxUtil();

    _options = options;
    _cacheSize = cachesize;
    _callback = callback;
    _totalSize = 0;

    /* save path name */
    if (path)
    {
        _path.assign(path);
    }

    /* save ROM name */
    if (ident)
    {
        _ident.assign(ident);
    }

    /* zlib memory buffers to (de)compress hires textures */
    if (_options & (GZ_TEXCACHE | GZ_HIRESTEXCACHE))
    {
        _gzdest0 = TxMemBuf::getInstance()->get(0);
        _gzdest1 = TxMemBuf::getInstance()->get(1);
        _gzdestLen = (TxMemBuf::getInstance()->size_of(0) < TxMemBuf::getInstance()->size_of(1)) ?
            TxMemBuf::getInstance()->size_of(0) : TxMemBuf::getInstance()->size_of(1);

        if (!_gzdest0 || !_gzdest1 || !_gzdestLen)
        {
            _options &= ~(GZ_TEXCACHE | GZ_HIRESTEXCACHE);
            _gzdest0 = NULL;
            _gzdest1 = NULL;
            _gzdestLen = 0;
        }
    }
}

bool
TxCache::add(uint64_t checksum, GHQTexInfo *info, int dataSize)
{
    /* NOTE: dataSize must be provided if info->data is zlib compressed. */

    if (!checksum || !info->data) return 0;

    uint8 *dest = info->data;
    uint16 format = info->format;

    if (!dataSize)
    {
        dataSize = _txUtil->sizeofTx(info->width, info->height, info->format);

        if (!dataSize) return 0;

        if (_options & (GZ_TEXCACHE | GZ_HIRESTEXCACHE))
        {
            /* zlib compress it. compression level:1 (best speed) */
            uLongf destLen = _gzdestLen;
            dest = (dest == _gzdest0) ? _gzdest1 : _gzdest0;
            if (compress2(dest, &destLen, info->data, dataSize, 1) != Z_OK)
            {
                dest = info->data;
                DBG_INFO(80, "Error: zlib compression failed!\n");
            }
            else
            {
                DBG_INFO(80, "zlib compressed: %.02fkb->%.02fkb\n", (float)dataSize / 1000, (float)destLen / 1000);
                dataSize = destLen;
                format |= GFX_TEXFMT_GZ;
            }
        }
    }

    /* if cache size exceeds limit, remove old cache */
    if (_cacheSize > 0)
    {
        _totalSize += dataSize;
        if ((_totalSize > _cacheSize) && !_cachelist.empty())
        {
            /* _cachelist is arranged so that frequently used textures are in the back */
            std::list<uint64_t>::iterator itList = _cachelist.begin();
            while (itList != _cachelist.end())
            {
                /* find it in _cache */
                std::map<uint64_t, TXCACHE*>::iterator itMap = _cache.find(*itList);
                if (itMap != _cache.end())
                {
                    /* yep we have it. remove it. */
                    _totalSize -= (*itMap).second->size;
                    free((*itMap).second->info.data);
                    delete (*itMap).second;
                    _cache.erase(itMap);
                }
                itList++;

                /* check if memory cache has enough space */
                if (_totalSize <= _cacheSize)
                    break;
            }
            /* remove from _cachelist */
            _cachelist.erase(_cachelist.begin(), itList);

            DBG_INFO(80, "+++++++++\n");
        }
        _totalSize -= dataSize;
    }

    /* cache it */
    uint8 *tmpdata = (uint8*)malloc(dataSize);
    if (tmpdata)
    {
        TXCACHE *txCache = new TXCACHE;
        if (txCache)
        {
            /* we can directly write as we filter, but for now we get away
             * with doing memcpy after all the filtering is done.
             */
            memcpy(tmpdata, dest, dataSize);

            /* copy it */
            memcpy(&txCache->info, info, sizeof(GHQTexInfo));
            txCache->info.data = tmpdata;
            txCache->info.format = format;
            txCache->size = dataSize;

            /* add to cache */
            if (_cacheSize > 0)
            {
                _cachelist.push_back(checksum);
                txCache->it = --(_cachelist.end());
            }
            /* _cache[checksum] = txCache; */
            _cache.insert(std::map<uint64_t, TXCACHE*>::value_type(checksum, txCache));

#ifdef DEBUG
            DBG_INFO(80, "[%5d] added!! crc:%08X %08X %d x %d gfmt:%x total:%.02fmb\n",
                _cache.size(), (uint32)(checksum >> 32), (uint32)(checksum & 0xffffffff),
                info->width, info->height, info->format, (float)_totalSize / 1000000);

            DBG_INFO(80, "smalllodlog2:%d largelodlog2:%d aspectratiolog2:%d\n",
                txCache->info.smallLodLog2, txCache->info.largeLodLog2, txCache->info.aspectRatioLog2);

            if (info->tiles)
            {
                DBG_INFO(80, "tiles:%d un-tiled size:%d x %d\n", info->tiles, info->untiled_width, info->untiled_height);
            }

            if (_cacheSize > 0)
            {
                DBG_INFO(80, "cache max config:%.02fmb\n", (float)_cacheSize / 1000000);

                if (_cache.size() != _cachelist.size())
                {
                    DBG_INFO(80, "Error: cache/cachelist mismatch! (%d/%d)\n", _cache.size(), _cachelist.size());
                }
            }
#endif

            /* total cache size */
            _totalSize += dataSize;

            return 1;
        }
        free(tmpdata);
    }

    return 0;
}

bool
TxCache::get(uint64_t checksum, GHQTexInfo *info)
{
    if (!checksum || _cache.empty()) return 0;

    /* find a match in cache */
    std::map<uint64_t, TXCACHE*>::iterator itMap = _cache.find(checksum);
    if (itMap != _cache.end())
    {
        /* yep, we've got it. */
        memcpy(info, &(((*itMap).second)->info), sizeof(GHQTexInfo));

        /* push it to the back of the list */
        if (_cacheSize > 0)
        {
            _cachelist.erase(((*itMap).second)->it);
            _cachelist.push_back(checksum);
            ((*itMap).second)->it = --(_cachelist.end());
        }

        /* zlib decompress it */
        if (info->format & GFX_TEXFMT_GZ)
        {
            uLongf destLen = _gzdestLen;
            uint8 *dest = (_gzdest0 == info->data) ? _gzdest1 : _gzdest0;
            if (uncompress(dest, &destLen, info->data, ((*itMap).second)->size) != Z_OK)
            {
                DBG_INFO(80, "Error: zlib decompression failed!\n");
                return 0;
            }
            info->data = dest;
            info->format &= ~GFX_TEXFMT_GZ;
            DBG_INFO(80, "zlib decompressed: %.02fkb->%.02fkb\n", (float)(((*itMap).second)->size) / 1000, (float)destLen / 1000);
        }
        return 1;
    }
    return 0;
}

bool TxCache::save(const char *path, const char *filename, int config)
{
    if (!_cache.empty())
    {
        CPath(path, "").DirectoryCreate();

        gzFile gzfp = gzopen(CPath(path, filename), "wb1");
        DBG_INFO(80, "gzfp:%x file:%ls\n", gzfp, filename);
        if (gzfp)
        {
            /* write header to determine config match */
            gzwrite(gzfp, &config, 4);

            std::map<uint64_t, TXCACHE*>::iterator itMap = _cache.begin();
            while (itMap != _cache.end())
            {
                uint8 *dest = (*itMap).second->info.data;
                uint32 destLen = (*itMap).second->size;
                uint16 format = (*itMap).second->info.format;

                /* to keep things simple, we save the texture data in a zlib uncompressed state. */
                /* sigh... for those who cannot wait the extra few seconds. changed to keep
                 * texture data in a zlib compressed state. if the GZ_TEXCACHE or GZ_HIRESTEXCACHE
                 * option is toggled, the cache will need to be rebuilt.
                 */
                 /*if (format & GFX_TEXFMT_GZ) {
                   dest = _gzdest0;
                   destLen = _gzdestLen;
                   if (dest && destLen) {
                   if (uncompress(dest, &destLen, (*itMap).second->info.data, (*itMap).second->size) != Z_OK) {
                   dest = NULL;
                   destLen = 0;
                   }
                   format &= ~GFX_TEXFMT_GZ;
                   }
                   }*/

                if (dest && destLen)
                {
                    /* texture checksum */
                    gzwrite(gzfp, &((*itMap).first), 8);

                    /* other texture info */
                    gzwrite(gzfp, &((*itMap).second->info.width), 4);
                    gzwrite(gzfp, &((*itMap).second->info.height), 4);
                    gzwrite(gzfp, &format, 2);

                    gzwrite(gzfp, &((*itMap).second->info.smallLodLog2), 4);
                    gzwrite(gzfp, &((*itMap).second->info.largeLodLog2), 4);
                    gzwrite(gzfp, &((*itMap).second->info.aspectRatioLog2), 4);

                    gzwrite(gzfp, &((*itMap).second->info.tiles), 4);
                    gzwrite(gzfp, &((*itMap).second->info.untiled_width), 4);
                    gzwrite(gzfp, &((*itMap).second->info.untiled_height), 4);

                    gzwrite(gzfp, &((*itMap).second->info.is_hires_tex), 1);

                    gzwrite(gzfp, &destLen, 4);
                    gzwrite(gzfp, dest, destLen);
                }

                itMap++;

                /* not ready yet */
                /*if (_callback)
                  (*_callback)("Total textures saved to HDD: %d\n", std::distance(itMap, _cache.begin()));*/
            }
            gzclose(gzfp);
        }
    }
    return _cache.empty();
}

bool TxCache::load(const char *path, const char *filename, int config)
{
    /* find it on disk */
    CPath cbuf(path, filename);

    gzFile gzfp = gzopen(cbuf, "rb");
    DBG_INFO(80, "gzfp:%x file:%ls\n", gzfp, filename);
    if (gzfp)
    {
        /* yep, we have it. load it into memory cache. */
        int dataSize;
        uint64_t checksum;
        GHQTexInfo tmpInfo;
        int tmpconfig;
        /* read header to determine config match */
        gzread(gzfp, &tmpconfig, 4);

        if (tmpconfig == config)
        {
            do
            {
                memset(&tmpInfo, 0, sizeof(GHQTexInfo));

                gzread(gzfp, &checksum, 8);

                gzread(gzfp, &tmpInfo.width, 4);
                gzread(gzfp, &tmpInfo.height, 4);
                gzread(gzfp, &tmpInfo.format, 2);

                gzread(gzfp, &tmpInfo.smallLodLog2, 4);
                gzread(gzfp, &tmpInfo.largeLodLog2, 4);
                gzread(gzfp, &tmpInfo.aspectRatioLog2, 4);

                gzread(gzfp, &tmpInfo.tiles, 4);
                gzread(gzfp, &tmpInfo.untiled_width, 4);
                gzread(gzfp, &tmpInfo.untiled_height, 4);

                gzread(gzfp, &tmpInfo.is_hires_tex, 1);

                gzread(gzfp, &dataSize, 4);

                tmpInfo.data = (uint8*)malloc(dataSize);
                if (tmpInfo.data)
                {
                    gzread(gzfp, tmpInfo.data, dataSize);

                    /* add to memory cache */
                    add(checksum, &tmpInfo, (tmpInfo.format & GFX_TEXFMT_GZ) ? dataSize : 0);

                    free(tmpInfo.data);
                }
                else
                {
                    gzseek(gzfp, dataSize, SEEK_CUR);
                }

                /* skip in between to prevent the loop from being tied down to vsync */
                if (_callback && (!(_cache.size() % 100) || gzeof(gzfp)))
                    (*_callback)("[%d] total mem:%.02fmb - %ls\n", _cache.size(), (float)_totalSize / 1000000, filename);
            } while (!gzeof(gzfp));
            gzclose(gzfp);
        }
    }
    return !_cache.empty();
}

bool TxCache::del(uint64_t checksum)
{
    if (!checksum || _cache.empty()) return 0;

    std::map<uint64_t, TXCACHE*>::iterator itMap = _cache.find(checksum);
    if (itMap != _cache.end())
    {
        /* for texture cache (not hi-res cache) */
        if (!_cachelist.empty()) _cachelist.erase(((*itMap).second)->it);

        /* remove from cache */
        free((*itMap).second->info.data);
        _totalSize -= (*itMap).second->size;
        delete (*itMap).second;
        _cache.erase(itMap);

        DBG_INFO(80, "removed from cache: checksum = %08X %08X\n", (uint32)(checksum & 0xffffffff), (uint32)(checksum >> 32));

        return 1;
    }

    return 0;
}

bool TxCache::is_cached(uint64_t checksum)
{
    std::map<uint64_t, TXCACHE*>::iterator itMap = _cache.find(checksum);
    if (itMap != _cache.end()) return 1;

    return 0;
}

void TxCache::clear()
{
    if (!_cache.empty())
    {
        std::map<uint64_t, TXCACHE*>::iterator itMap = _cache.begin();
        while (itMap != _cache.end())
        {
            free((*itMap).second->info.data);
            delete (*itMap).second;
            itMap++;
        }
        _cache.clear();
    }

    if (!_cachelist.empty()) _cachelist.clear();

    _totalSize = 0;
}