/******************************************************************************
 * Arachnoid Graphics Plugin for Mupen64Plus
 * https://github.com/mupen64plus/mupen64plus-video-arachnoid/
 *
 * Copyright (C) 2007 Kristofer Karlsson, Rickard Niklasson
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *****************************************************************************/

#include <algorithm>

#include "CachedTexture.h"
#include "GBIDefs.h"
#include "MathLib.h"
#include "OpenGL.h"
#include "RDP.h"
#include "RSP.h"
#include "TextureCache.h"
#include "TextureLoader.h"

    using std::min;
#include "Memory.h"
#include "MultiTexturingExt.h"    
#include "OpenGLRenderer.h"
#include "m64p.h"

    //gSPBgRect1Cyc
//gSPBgRectCopy
#define GL_CLAMP_TO_EDGE                  0x812F
#define GL_GENERATE_MIPMAP                0x8191

#include <iostream>

#include "Logger.h"

//-----------------------------------------------------------------------------
//! Constructor
//-----------------------------------------------------------------------------
TextureCache::TextureCache()
{
    m_currentTextures[0] = 0;
    m_currentTextures[1] = 0;
}

//-----------------------------------------------------------------------------
//! Destructor
//-----------------------------------------------------------------------------
TextureCache::~TextureCache()
{   
    dispose();
}

//-----------------------------------------------------------------------------
//* Initialize
//-----------------------------------------------------------------------------
bool TextureCache::initialize(RSP* rsp, RDP* rdp, Memory* memory, unsigned int textureBitDepth, unsigned int cacheSize)
{
    m_rsp = rsp;
    m_rdp = rdp;
    m_memory   = memory;
    m_bitDepth = textureBitDepth;
    m_maxBytes = cacheSize;
    
    return true;
}

//-----------------------------------------------------------------------------
//* Update
//-----------------------------------------------------------------------------
void TextureCache::update(unsigned int tile)
{
    //if (cache.bitDepth != OGL.textureBitDepth)
    //{
    //    TextureCache_Destroy();
    //    TextureCache_Init();
    //}

    //Special textures?
    if ( m_rdp->getTextureMode() == TM_BGIMAGE )
    {
        return;
    }
    else if ( m_rdp->getTextureMode() == TM_FRAMEBUFFER )
    {
        return;
    }


    CachedTexture temp;    
    unsigned int maskWidth = 0, maskHeight = 0;
    _calculateTextureSize(tile, &temp, maskWidth, maskHeight);

    static int hits = 0;
    static int misses = 0;

    //For each texture in texture cache
    for (TextureList::iterator it=m_cachedTextures.begin(); it!=m_cachedTextures.end(); ++it)
      {
        CachedTexture* temp2 = (*it);

        if ( *temp2 == temp )
        {
            _activateTexture( tile, (*it) );
            hits++;
            return;
        }
    }
    misses++;

    // If multitexturing, set the appropriate texture
    //if (OGL.ARB_multitexture)
    glActiveTextureARB( GL_TEXTURE0_ARB + tile );

    //Add new texture to cache
    m_currentTextures[tile] = addTop();
    m_currentTextures[tile]->activate();

    m_currentTextures[tile]->address     = m_rdp->getTextureImage()->address;
    m_currentTextures[tile]->crc         = temp.crc;
    m_currentTextures[tile]->width       = temp.width;
    m_currentTextures[tile]->height      = temp.height;
    m_currentTextures[tile]->clampWidth  = temp.clampWidth;
    m_currentTextures[tile]->clampHeight = temp.clampHeight;

    m_currentTextures[tile]->format = m_rsp->getTile(tile)->format;
    m_currentTextures[tile]->size =  m_rsp->getTile(tile)->size;

    m_currentTextures[tile]->palette = m_rsp->getTile(tile)->palette;
/*    m_currentTextures[tile]->fulS = rsp.getTile(tile)->fulS;
    m_currentTextures[tile]->fulT = rsp.getTile(tile)->fulT;
    m_currentTextures[tile]->ulS = rsp.getTile(tile)->ulS;
    m_currentTextures[tile]->ulT = rsp.getTile(tile)->ulT;
    m_currentTextures[tile]->lrS = rsp.getTile(tile)->lrS;
    m_currentTextures[tile]->lrT = rsp.getTile(tile)->lrT;*/
    m_currentTextures[tile]->maskS   = m_rsp->getTile(tile)->masks;
    m_currentTextures[tile]->maskT   = m_rsp->getTile(tile)->maskt;
    m_currentTextures[tile]->mirrorS = m_rsp->getTile(tile)->mirrors;
    m_currentTextures[tile]->mirrorT = m_rsp->getTile(tile)->mirrort;
    m_currentTextures[tile]->clampS  = m_rsp->getTile(tile)->clamps;
    m_currentTextures[tile]->clampT  = m_rsp->getTile(tile)->clampt;
    m_currentTextures[tile]->line    = m_rsp->getTile(tile)->line;
    m_currentTextures[tile]->tMem    = m_rsp->getTile(tile)->tmem;


    
//    cache.current[tile]->lastDList = RSP.DList;
//    cache.current[tile]->frameBufferTexture = FALSE;

    //Calculate Real Width
    if (m_currentTextures[tile]->clampS) 
    {
        m_currentTextures[tile]->realWidth = pow2( temp.clampWidth );
    }
    else if (m_currentTextures[tile]->mirrorS)
    {
        m_currentTextures[tile]->realWidth = maskWidth << 1;
    }
    else
    {
        m_currentTextures[tile]->realWidth = pow2( temp.width );
    }

    //Calculate Real Height
    if (m_currentTextures[tile]->clampT)
    {
        m_currentTextures[tile]->realHeight = pow2( temp.clampHeight );
    }
    else if (m_currentTextures[tile]->mirrorT)
    {
        m_currentTextures[tile]->realHeight = maskHeight << 1;
    }
    else
    {
        m_currentTextures[tile]->realHeight = pow2( temp.height );
    }

    //Calculate Scale
    m_currentTextures[tile]->scaleS = 1.0f / (float)(m_currentTextures[tile]->realWidth);
    m_currentTextures[tile]->scaleT = 1.0f / (float)(m_currentTextures[tile]->realHeight);
    m_currentTextures[tile]->shiftScaleS = 1.0f;
    m_currentTextures[tile]->shiftScaleT= 1.0f;
    #if 0
        //m_currentTextures[tile]->offsetS = OGL.enable2xSaI ? 0.25f : 0.5f;
        //m_currentTextures[tile]->offsetT = OGL.enable2xSaI ? 0.25f : 0.5f;
    #else
        m_currentTextures[tile]->offsetS = 0.5f;
        m_currentTextures[tile]->offsetT = 0.5f;    
    #endif

    if (m_rsp->getTile(tile)->shifts > 10)
        m_currentTextures[tile]->shiftScaleS = (float)(1 << (16 - m_rsp->getTile(tile)->shifts));
    else if (m_rsp->getTile(tile)->shifts > 0)
        m_currentTextures[tile]->shiftScaleS /= (float)(1 << m_rsp->getTile(tile)->shifts);

    if (m_rsp->getTile(tile)->shiftt > 10)
        m_currentTextures[tile]->shiftScaleT = (float)(1 << (16 - m_rsp->getTile(tile)->shiftt));
    else if (m_rsp->getTile(tile)->shiftt > 0)
        m_currentTextures[tile]->shiftScaleT /= (float)(1 << m_rsp->getTile(tile)->shiftt);





    _loadTexture( m_currentTextures[tile] );
    _activateTexture( tile, m_currentTextures[tile] );

    m_cachedBytes += m_currentTextures[tile]->getTextureSize();

}

//-----------------------------------------------------------------------------
//* Add Top
//! Adds a texture to cache
//-----------------------------------------------------------------------------
CachedTexture* TextureCache::addTop()
{
    //If no memory left, remove old textures from cache
    while ( m_cachedBytes > m_maxBytes )
    {
        this->removeBottom();
    }

    //Allocate memory
    CachedTexture* newTexture = new CachedTexture();

    //Generate a texture
    glGenTextures(1, &newTexture->m_id);

    //Add Texture to cache
    m_cachedTextures.push_front(newTexture);

    return newTexture;
}

//-----------------------------------------------------------------------------
// Remove Bottom
//-----------------------------------------------------------------------------
void TextureCache::removeBottom() 
{
    //Get Last Texture in list
    CachedTexture* lastTexture = *(--m_cachedTextures.end());

    //Remove Texture
    m_cachedTextures.pop_back();
    m_cachedBytes -= lastTexture->getTextureSize();

    //if (cache.bottom->frameBufferTexture)
    //    FrameBuffer_RemoveBuffer( cache.bottom->address );

    //Delete texture
    glDeleteTextures(1, &lastTexture->m_id);

    delete lastTexture;
}

//-----------------------------------------------------------------------------
// Remove
//-----------------------------------------------------------------------------
void TextureCache::remove( CachedTexture *texture ) 
{

}

//-----------------------------------------------------------------------------
//Move Texture to top
//-----------------------------------------------------------------------------
void TextureCache::moveToTop( CachedTexture *newtop ) 
{
    //Get Texture
    TextureList::iterator it = std::find( m_cachedTextures.begin(), m_cachedTextures.end(), newtop);

    //Erase Texture
    if ( it != m_cachedTextures.end() )
    {
        m_cachedTextures.erase(it);
    }

    //Add texture to the front of the list
    m_cachedTextures.push_front(newtop);
}

//-----------------------------------------------------------------------------
// Dispose
//-----------------------------------------------------------------------------
void TextureCache::dispose()
{
    //For each texture
    for (TextureList::iterator it=m_cachedTextures.begin(); it!=m_cachedTextures.end(); ++it )
    {
        delete (*it);
    }
    m_cachedTextures.clear();
}

//-----------------------------------------------------------------------------
// Load Texture
//-----------------------------------------------------------------------------
void TextureCache::_loadTexture(CachedTexture* texture)
{
    //Select Image Type
    GetTexelFunc getTexelFunc;
    unsigned int internalFormat;
    int             imageType;
    m_formatSelector.detectImageFormat(texture, m_bitDepth, getTexelFunc, internalFormat, imageType, m_rdp->getTextureLUT());

    //Allocate memory
    unsigned int* dest = new unsigned int[ texture->getTextureSize() ];

    //Get Line Size
    unsigned short line = (unsigned short)texture->line;
    if (texture->size == G_IM_SIZ_32b)
        line <<= 1;

    //
    //Work Your magic
    //

    unsigned short mirrorSBit, maskSMask, clampSClamp;
    unsigned short mirrorTBit, maskTMask, clampTClamp;

    if (texture->maskS)
    {
        clampSClamp = (unsigned short)(texture->clampS ? texture->clampWidth - 1 : (texture->mirrorS ? (texture->width << 1) - 1 : texture->width - 1));
        maskSMask = (1 << texture->maskS) - 1;
        mirrorSBit = texture->mirrorS ? 1 << texture->maskS : 0;
    }
    else
    {
        clampSClamp = (unsigned short)min( texture->clampWidth, texture->width ) - 1;
        maskSMask = 0xFFFF;
        mirrorSBit = 0x0000;
    }

    if (texture->maskT)
    {
        clampTClamp = (unsigned short)(texture->clampT ? texture->clampHeight - 1 : (texture->mirrorT ? (texture->height << 1) - 1: texture->height - 1));
        maskTMask = (1 << texture->maskT) - 1;
        mirrorTBit = texture->mirrorT ?    1 << texture->maskT : 0;
    }
    else
    {
        clampTClamp = (unsigned short)min( texture->clampHeight, texture->height ) - 1;
        maskTMask = 0xFFFF;
        mirrorTBit = 0x0000;
    }

    // Hack for Zelda warp texture
    if (((texture->tMem << 3) + (texture->width * texture->height << texture->size >> 1)) > 4096)
        texture->tMem = 0;

    // limit clamp values to min-0 (Perfect Dark has height=0 textures, making negative clamps)
    if (clampTClamp & 0x8000)
        clampTClamp = 0;
    if (clampSClamp & 0x8000)
        clampSClamp = 0;

    //
    //Retrive texture from source (TMEM) and copy it to dest
    //

    unsigned short x, y, i, j, tx, ty;

    unsigned long long* src;

    j = 0;
    for (y = 0; y < texture->realHeight; y++)
    {
        ty = min(y, clampTClamp) & maskTMask;

        if (y & mirrorTBit) {
            ty ^= maskTMask;
        }

        //TODO: remove old if new works
        //src = m_memory->getTextureMemory(texture->tMem) + line * ty;
        src = m_memory->getTextureMemory((texture->tMem + line * ty) & 511);
        

        i = (ty & 1) << 1;
        for (x = 0; x < texture->realWidth; x++)
        {
            tx = min(x, clampSClamp) & maskSMask;

            if (x & mirrorSBit)
                tx ^= maskSMask;

            if (internalFormat == GL_RGBA8)
                ((unsigned int*)dest)[j++] = getTexelFunc( src, tx, i, texture->palette );
            else
                ((unsigned short*)dest)[j++] = getTexelFunc( src, tx, i, texture->palette );
        }
    }

    //Send Texture to OpenGL
    glTexImage2D( GL_TEXTURE_2D, 0, internalFormat, texture->realWidth, texture->realHeight, 0, GL_RGBA, imageType, dest );
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    delete[] dest;
}


void TextureCache::_calculateTextureSize(unsigned int tile, CachedTexture* out, unsigned int& maskWidth, unsigned int& maskHeight )
{
    RDPTile* rspTile = m_rsp->getTile(tile);

    //Calculate Tile Size
    unsigned int tileWidth =  rspTile->getWidth();
    unsigned int tileHeight = rspTile->getHeight();

    //Get Mask Size
    maskWidth = 1 << rspTile->masks;
    maskHeight = 1 << rspTile->maskt;

    //Get Current Tile Size
    unsigned int loadWidth = m_rdp->getCurrentTile()->getWidth();
    unsigned int loadHeight = m_rdp->getCurrentTile()->getHeight();

    unsigned int maxTexels = ImageFormatSelector::imageFormats[rspTile->size][rspTile->format].maxTexels;

    //Get Line Width (depending on imageformat)
    unsigned int lineWidth = rspTile->line << ImageFormatSelector::imageFormats[rspTile->size][rspTile->format].lineShift;
    unsigned int lineHeight; 
    if ( lineWidth ) // Don't allow division by zero
        lineHeight = min( maxTexels / lineWidth, tileHeight );
    else
        lineHeight = 0;

    unsigned int width;
    unsigned int height;

    if ( m_rdp->getTextureMode() == TM_TEXRECT )
    {
        unsigned short texRectWidth = (unsigned short)(m_rdp->getTexRectWidth() - rspTile->uls);
        unsigned short texRectHeight = (unsigned short)(m_rdp->getTexRectHeight() - rspTile->ult);

        if (rspTile->masks && ((maskWidth * maskHeight) <= maxTexels))
            width = maskWidth;
        else if ((tileWidth * tileHeight) <= maxTexels)
            width = tileWidth;
        else if ((tileWidth * texRectHeight) <= maxTexels)
            width = tileWidth;
        else if ((texRectWidth * tileHeight) <= maxTexels)
            width = m_rdp->getTexRectWidth();
        else if ((texRectWidth * texRectHeight) <= maxTexels)
            width = m_rdp->getTexRectWidth();
        else if (m_rdp->getLoadType() == LOADTYPE_TILE)
            width = loadWidth;
        else
            width = lineWidth;

        if (rspTile->maskt && ((maskWidth * maskHeight) <= maxTexels))
            height = maskHeight;
        else if ((tileWidth * tileHeight) <= maxTexels)
            height = tileHeight;
        else if ((tileWidth * texRectHeight) <= maxTexels)
            height = m_rdp->getTexRectHeight();
        else if ((texRectWidth * tileHeight) <= maxTexels)
            height = tileHeight;
        else if ((texRectWidth * texRectHeight) <= maxTexels)
            height = m_rdp->getTexRectHeight();
        else if (m_rdp->getLoadType() == LOADTYPE_TILE)
            height = loadHeight;
        else
            height = lineHeight;
    }
    else
    {
        if (rspTile->masks && ((maskWidth * maskHeight) <= maxTexels))
            width = maskWidth; // Use mask width if set and valid
        else if ((tileWidth * tileHeight) <= maxTexels)
            width = tileWidth; // else use tile width if valid
        else if (m_rdp->getLoadType() == LOADTYPE_TILE)
            width = loadWidth; // else use load width if load done with LoadTile
        else
            width = lineWidth; // else use line-based width

        if (rspTile->maskt && ((maskWidth * maskHeight) <= maxTexels))
            height = maskHeight;
        else if ((tileWidth * tileHeight) <= maxTexels)
            height = tileHeight;
        else if (m_rdp->getLoadType() == LOADTYPE_TILE)
            height = loadHeight;
        else
            height = lineHeight;
    }

     unsigned int clampWidth = rspTile->clamps ? tileWidth : width;
    unsigned int clampHeight = rspTile->clampt ? tileHeight : height;

    if (clampWidth > 256)
        rspTile->clamps = 0;
    if (clampHeight > 256)
        rspTile->clampt = 0;

    // Make sure masking is valid
    if (maskWidth > width) 
    {
        rspTile->masks = powof( width );
        maskWidth = 1 << rspTile->masks;
    }

    if (maskHeight > height)
    {
        rspTile->maskt = powof( height );
        maskHeight = 1 << rspTile->maskt;
    }

    //Set output data
    out->width       = width;
    out->height      = height;
    out->clampWidth  = clampWidth;
    out->clampHeight = clampHeight;
    out->maskS       = m_rsp->getTile(tile)->masks;
    out->maskT       = m_rsp->getTile(tile)->maskt;
    out->mirrorS     = m_rsp->getTile(tile)->mirrors;
    out->mirrorT     = m_rsp->getTile(tile)->mirrort;
    out->clampS      = m_rsp->getTile(tile)->clamps;
    out->clampT      = m_rsp->getTile(tile)->clampt;
    out->format      = m_rsp->getTile(tile)->format;
    out->size        = m_rsp->getTile(tile)->size; 
    out->crc         = _calculateCRC(tile, width, height );
}

unsigned int TextureCache::_calculateCRC(unsigned int t, unsigned int width, unsigned int height)
{
    RDPTile* tile = m_rsp->getTile(t);

    unsigned int crc;
    unsigned int y, bpl, line;
    unsigned long long *src;

    //TODO: remove if new works
    //src = m_memory->getTextureMemory(tile->tmem);
    bpl = width << tile->size >> 1;

    line = tile->line;
     if (tile->size == G_IM_SIZ_32b)
        line <<= 1;

    crc = 0xFFFFFFFF;
     for (y=0; y<height; ++y)
    {
        src = m_memory->getTextureMemory((tile->tmem + (y * line)) & 511);
        crc = m_crcCalculator.calcCRC( crc, src, bpl );
        //TODO: remove if new works
        //src += line;
    }

       if ( tile->format == G_IM_FMT_CI )
    {
        if ( tile->size == G_IM_SIZ_4b )
            crc = m_crcCalculator.calcCRC( crc, &m_rdp->m_paletteCRC16[tile->palette], 4 );
        else if (tile->size == G_IM_SIZ_8b)
            crc = m_crcCalculator.calcCRC( crc, &m_rdp->m_paletteCRC256, 4 );
    }
    return crc;
}

void TextureCache::_activateTexture( unsigned int t, CachedTexture *texture )
{
    // If multitexturing, set the appropriate texture
    //if (OGL.ARB_multitexture)
        glActiveTextureARB( GL_TEXTURE0_ARB + t );

    // Bind the cached texture
    texture->activate();

    // Set filter mode. Almost always bilinear, but check anyways
    unsigned int textureFiltering = m_rdp->getTextureFiltering();
    if ( textureFiltering == G_TF_BILERP || textureFiltering == G_TF_AVERAGE )
    {
        if( m_mipmap > 0 )
        {
            // Set Mipmap
            if(m_mipmap == 1)    // nearest
            {
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
            }
            else if(m_mipmap == 2)    // bilinear
            {
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
            }
            else if(m_mipmap == 3)    // trilinear
            {
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
            }
            
            // Tell to hardware to generate mipmap (himself) when glTexImage2D is called
            glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
        }
        else    // no mipmapping
        {
            glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
            glTexParameteri( GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_FALSE );
        }
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
        
    }
    else
    {
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
    }

    

    // Set clamping modes
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, texture->clampS ? GL_CLAMP_TO_EDGE : GL_REPEAT );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, texture->clampT ? GL_CLAMP_TO_EDGE : GL_REPEAT );

    //texture->lastDList = RSP.DList;

    moveToTop( texture );

    m_currentTextures[t] = texture;
}
