//-----------------------------------------------------------------------------
// Torque Game Engine
// Copyright (C) GarageGames.com, Inc.
//-----------------------------------------------------------------------------

#include "sim/decalManager.h"
#include "dgl/dgl.h"
#include "sceneGraph/sceneGraph.h"
#include "sceneGraph/sceneState.h"
#include "ts/tsShapeInstance.h"
#include "core/bitStream.h"
#include "console/consoleTypes.h"

bool DecalManager::smDecalsOn = true;

const U32 DecalManager::csmFreePoolBlockSize = 256;
U32       DecalManager::smMaxNumDecals = 256;
U32       DecalManager::smDecalTimeout = 5000;

DecalManager* gDecalManager = NULL;
IMPLEMENT_CONOBJECT(DecalManager);
IMPLEMENT_CO_DATABLOCK_V1(DecalData);

namespace {

int QSORT_CALLBACK cmpDecalInstance(const void* p1, const void* p2)
{
   const DecalInstance** pd1 = (const DecalInstance**)p1;
   const DecalInstance** pd2 = (const DecalInstance**)p2;

   return int(((char *)(*pd1)->decalData) - ((char *)(*pd2)->decalData));
}

} // namespace {}


//--------------------------------------------------------------------------
DecalData::DecalData()
{
   sizeX = 1;
   sizeY = 1;
   textureName = "";
}

DecalData::~DecalData()
{
   if(gDecalManager)
      gDecalManager->dataDeleted(this);
}


void DecalData::packData(BitStream* stream)
{
   Parent::packData(stream);

   stream->write(sizeX);
   stream->write(sizeY);
   stream->writeString(textureName);
}

void DecalData::unpackData(BitStream* stream)
{
   Parent::unpackData(stream);

   stream->read(&sizeX);
   stream->read(&sizeY);
   textureName = stream->readSTString();
}

bool DecalData::preload(bool server, char errorBuffer[256])
{
   if (Parent::preload(server, errorBuffer) == false)
      return false;

   if (sizeX < 0.0) {
      Con::warnf("DecalData::preload: sizeX < 0");
      sizeX = 0;
   }
   if (sizeY < 0.0) {
      Con::warnf("DecalData::preload: sizeX < 0");
      sizeY = 0;
   }
   if (textureName == NULL || textureName[0] == '\0') {
      Con::errorf("No texture name for decal!");
      return false;
   }

   if (!server) {
      textureHandle = TextureHandle(textureName, MeshTexture);
      if (textureHandle.getGLName() == 0) {
         Con::errorf("Unable to load texture: %s for decal!", textureName);
         return false;
      }
   }

   return true;
}

IMPLEMENT_CONSOLETYPE(DecalData)
IMPLEMENT_SETDATATYPE(DecalData)
IMPLEMENT_GETDATATYPE(DecalData)

void DecalData::initPersistFields()
{
   addField("sizeX",       TypeF32,       Offset(sizeX,       DecalData));
   addField("sizeY",       TypeF32,       Offset(sizeY,       DecalData));
   addField("textureName", TypeFilename,  Offset(textureName, DecalData));
}

DecalManager::DecalManager()
{
   mTypeMask |= DecalManagerObjectType;

   mObjBox.min.set(-1e7, -1e7, -1e7);
   mObjBox.max.set( 1e7,  1e7,  1e7);
   mWorldBox.min.set(-1e7, -1e7, -1e7);
   mWorldBox.max.set( 1e7,  1e7,  1e7);

   mFreePool = NULL;
   VECTOR_SET_ASSOCIATION(mDecalQueue);
   VECTOR_SET_ASSOCIATION(mFreePoolBlocks);
}


DecalManager::~DecalManager()
{
   mFreePool = NULL;
   for (S32 i = 0; i < mFreePoolBlocks.size(); i++)
   {
      delete [] mFreePoolBlocks[i];
   }
   mDecalQueue.clear();
}


DecalInstance* DecalManager::allocateDecalInstance()
{
   if (mFreePool == NULL)
   {
      // Allocate a new block of decals
      mFreePoolBlocks.push_back(new DecalInstance[csmFreePoolBlockSize]);

      // Init them onto the free pool chain
      DecalInstance* pNewInstances = mFreePoolBlocks.last();
      for (U32 i = 0; i < csmFreePoolBlockSize - 1; i++)
         pNewInstances[i].next = &pNewInstances[i + 1];
      pNewInstances[csmFreePoolBlockSize - 1].next = NULL;
      mFreePool = pNewInstances;
   }
   AssertFatal(mFreePool != NULL, "Error, should always have a free pool available here!");

   DecalInstance* pRet = mFreePool;
   mFreePool = pRet->next;
   pRet->next = NULL;
   return pRet;
}


void DecalManager::freeDecalInstance(DecalInstance* trash)
{
   AssertFatal(trash != NULL, "Error, no trash pointer to free!");

   trash->next = mFreePool;
   mFreePool = trash;
}


void DecalManager::dataDeleted(DecalData *data)
{
   for(S32 i = mDecalQueue.size() - 1; i >= 0; i--)
   {
      DecalInstance *inst = mDecalQueue[i];
      if(inst->decalData == data)
      {
         freeDecalInstance(inst);
         mDecalQueue.erase(U32(i));
      }
   }
}

void DecalManager::consoleInit()
{
   Con::addVariable("$pref::decalsOn",     TypeBool, &smDecalsOn);
   Con::addVariable("$pref::Decal::maxNumDecals", TypeS32, &smMaxNumDecals);
   Con::addVariable("$pref::Decal::decalTimeout", TypeS32, &smDecalTimeout);
}

void DecalManager::addDecal(const Point3F& pos,
                            Point3F normal,
                            DecalData* decalData)
{
   if (smMaxNumDecals == 0)
      return;

   // DMM: Rework this, should be based on time
   if(mDecalQueue.size() >= smMaxNumDecals)
   {
      DecalInstance* holder = mDecalQueue.front();
      mDecalQueue.pop_front();
      freeDecalInstance(holder);
   }

   Point3F vecX, vecY;
   DecalInstance* newDecal = allocateDecalInstance();
   newDecal->decalData = decalData;
   newDecal->allocTime = Platform::getVirtualMilliseconds();

   if(mFabs(normal.z) > 0.9f)
      mCross(normal, Point3F(0.0f, 1.0f, 0.0f), &vecX);
   else
      mCross(normal, Point3F(0.0f, 0.0f, 1.0f), &vecX);

   mCross(vecX, normal, &vecY);

   normal.normalizeSafe();
   Point3F position = Point3F(pos.x + (normal.x * 0.008), pos.y + (normal.y * 0.008), pos.z + (normal.z * 0.008));

   vecX.normalizeSafe();
   vecY.normalizeSafe();

   vecX *= decalData->sizeX;
   vecY *= decalData->sizeY;

   newDecal->point[0] = position + vecX + vecY;
   newDecal->point[1] = position + vecX - vecY;
   newDecal->point[2] = position - vecX - vecY;
   newDecal->point[3] = position - vecX + vecY;

   mDecalQueue.push_back(newDecal);
   mQueueDirty = true;
}

void DecalManager::addDecal(const Point3F& pos,
                            const Point3F& rot,
                            Point3F normal,
                            DecalData* decalData)
{
   if (smMaxNumDecals == 0)
      return;

    addDecal( pos, rot, normal, Point3F( 1, 1, 1 ), decalData );
}

void DecalManager::addDecal(const Point3F& pos,
                            const Point3F& rot,
                            Point3F normal,
                            const Point3F& scale,
                            DecalData* decalData)
{
   if (smMaxNumDecals == 0)
      return;

   if(mDot(rot, normal) < 0.98)
   {
      // DMM: Rework this, should be based on time
      if(mDecalQueue.size() >= smMaxNumDecals)
      {
         DecalInstance* holder = mDecalQueue.front();
         mDecalQueue.pop_front();
         freeDecalInstance(holder);
      }

      Point3F vecX, vecY;
      DecalInstance* newDecal = allocateDecalInstance();
      newDecal->decalData = decalData;
      newDecal->allocTime = Platform::getVirtualMilliseconds();

      mCross(rot, normal, &vecX);
      mCross(normal, vecX, &vecY);

      normal.normalize();
      Point3F position = Point3F(pos.x + (normal.x * 0.008), pos.y + (normal.y * 0.008), pos.z + (normal.z * 0.008));

      vecX.normalize();
      vecX.convolve( scale );
      vecY.normalize();
      vecY.convolve( scale );

      vecX *= decalData->sizeX;
      vecY *= decalData->sizeY;

      newDecal->point[0] = position + vecX + vecY;
      newDecal->point[1] = position + vecX - vecY;
      newDecal->point[2] = position - vecX - vecY;
      newDecal->point[3] = position - vecX + vecY;

      mDecalQueue.push_back(newDecal);
      mQueueDirty = true;
   }
}

bool DecalManager::prepRenderImage(SceneState* state, const U32 stateKey,
                                   const U32 /*startZone*/, const bool /*modifyBaseState*/)
{
   if (!smDecalsOn) return false;

   if (isLastState(state, stateKey))
      return false;
   setLastState(state, stateKey);

   if (mDecalQueue.size() == 0)
      return false;

   // This should be sufficient for most objects that don't manage zones, and
   //  don't need to return a specialized RenderImage...
   SceneRenderImage* image = new SceneRenderImage;
   image->obj = this;
   image->isTranslucent = true;
   image->sortType      = SceneRenderImage::BeginSort;
   state->insertRenderImage(image);

   U32 currMs = Platform::getVirtualMilliseconds();
   for (S32 i = mDecalQueue.size() - 1; i >= 0; i--)
   {
      U32 age = currMs - mDecalQueue[i]->allocTime;
      if (age > smDecalTimeout)
      {
         freeDecalInstance(mDecalQueue[i]);
         mDecalQueue.erase(i);
      }
      else if (age > ((3 * smDecalTimeout) / 4))
      {
         mDecalQueue[i]->fade = 1.0f - (F32(age - ((3 * smDecalTimeout) / 4)) / F32(smDecalTimeout / 4));
      }
      else
      {
         mDecalQueue[i]->fade = 1.0f;
      }
   }

   if (mQueueDirty == true)
   {
      // Sort the decals based on the data pointers...
      dQsort(mDecalQueue.address(),
             mDecalQueue.size(),
             sizeof(DecalInstance*),
             cmpDecalInstance);
      mQueueDirty = false;
   }

   return false;
}

void DecalManager::renderObject(SceneState* state, SceneRenderImage*)
{
   if (!smDecalsOn) return;

   AssertFatal(dglIsInCanonicalState(), "Error, GL not in canonical state on entry");

   RectI viewport;
   glMatrixMode(GL_PROJECTION);
   glPushMatrix();
   dglGetViewport(&viewport);

   state->setupBaseProjection();

   glMatrixMode(GL_MODELVIEW);
   glPushMatrix();

   renderDecal();

   glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);

   glMatrixMode(GL_MODELVIEW);
   glPopMatrix();

   glMatrixMode(GL_PROJECTION);
   glPopMatrix();
   glMatrixMode(GL_MODELVIEW);
   dglSetViewport(viewport);

   AssertFatal(dglIsInCanonicalState(), "Error, GL not in canonical state on exit");
}

void DecalManager::renderDecal()
{
   glEnable(GL_TEXTURE_2D);
   glEnable(GL_BLEND);
   glEnable(GL_ALPHA_TEST);
   glDepthMask(GL_FALSE);
   glAlphaFunc(GL_GREATER, 0.1f);
   glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
   glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

   DecalData* pLastData = NULL;
   for (S32 x = 0; x < mDecalQueue.size(); x++)
   {
      if (mDecalQueue[x]->decalData != pLastData)
      {
         glBindTexture(GL_TEXTURE_2D, mDecalQueue[x]->decalData->textureHandle.getGLName());
         pLastData = mDecalQueue[x]->decalData;
      }

      glColor4f(1, 1, 1, mDecalQueue[x]->fade);
      glBegin(GL_TRIANGLE_FAN); {
         glTexCoord2f(0, 0); glVertex3fv(mDecalQueue[x]->point[0]);
         glTexCoord2f(0, 1); glVertex3fv(mDecalQueue[x]->point[1]);
         glTexCoord2f(1, 1); glVertex3fv(mDecalQueue[x]->point[2]);
         glTexCoord2f(1, 0); glVertex3fv(mDecalQueue[x]->point[3]);
      } glEnd();
   }

   glDepthMask(GL_TRUE);
   glDisable(GL_BLEND);
   glDisable(GL_TEXTURE_2D);
   glDisable(GL_ALPHA_TEST);
}

