//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//=============================================================================//

#if defined ( WIN32 ) && !defined( _X360 )
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#elif defined( OSX )
#include <Carbon/Carbon.h>
#elif defined( LINUX ) || defined(PLATFORM_BSD)
//#error
#elif defined( _X360 )
#else
#error
#endif
#include "FontTextureCache.h"
#include "MatSystemSurface.h"
#include <vgui_surfacelib/BitmapFont.h>
#include <vgui/IVGui.h>
#include <vgui_controls/Controls.h>
#include "bitmap/imageformat.h"
#include "vtf/vtf.h"
#include "materialsystem/imaterialvar.h"
#include "materialsystem/itexture.h"
#include "tier1/KeyValues.h"
#include "tier1/utlbuffer.h"
#include "pixelwriter.h"
#include "tier0/icommandline.h"

// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"

extern CMatSystemSurface g_MatSystemSurface;
static int g_FontRenderBoundingBoxes = -1;

#define TEXTURE_PAGE_WIDTH	256
#define TEXTURE_PAGE_HEIGHT	256

// row size
int CFontTextureCache::s_pFontPageSize[FONT_PAGE_SIZE_COUNT] = 
{
	16,
	32,
	64,
	128,
	256,
};

static bool g_mat_texture_outline_fonts = false;
CON_COMMAND( mat_texture_outline_fonts, "Outline fonts textures." )
{
	g_mat_texture_outline_fonts = !g_mat_texture_outline_fonts;
	Msg( "mat_texture_outline_fonts: %d\n", g_mat_texture_outline_fonts );
	g_MatSystemSurface.ResetFontCaches();
}

//-----------------------------------------------------------------------------
// Purpose: Constructor
//-----------------------------------------------------------------------------
CFontTextureCache::CFontTextureCache() 
	: m_CharCache(0, 256, CacheEntryLessFunc)
{
	Clear();
}

//-----------------------------------------------------------------------------
// Purpose: Destructor
//-----------------------------------------------------------------------------
CFontTextureCache::~CFontTextureCache()
{
}

//-----------------------------------------------------------------------------
// Purpose: Resets the cache
//-----------------------------------------------------------------------------
void CFontTextureCache::Clear()
{
	// remove all existing data
	m_CharCache.RemoveAll();
	m_PageList.RemoveAll();

	// reinitialize
	CacheEntry_t listHead = { 0, 0 };
	m_LRUListHeadIndex = m_CharCache.Insert(listHead);

	m_CharCache[m_LRUListHeadIndex].nextEntry = m_LRUListHeadIndex;
	m_CharCache[m_LRUListHeadIndex].prevEntry = m_LRUListHeadIndex;

	for (int i = 0; i < FONT_PAGE_SIZE_COUNT; ++i)
	{
		m_pCurrPage[i] = -1;
	}
	m_FontPages.SetLessFunc( DefLessFunc( vgui::HFont ) );
	m_FontPages.RemoveAll();
}

//-----------------------------------------------------------------------------
// Purpose: comparison function for cache entries
//-----------------------------------------------------------------------------
bool CFontTextureCache::CacheEntryLessFunc(CacheEntry_t const &lhs, CacheEntry_t const &rhs)
{
	if (lhs.font < rhs.font)
		return true;
	else if (lhs.font > rhs.font)
		return false;

	return (lhs.wch < rhs.wch);
}

//-----------------------------------------------------------------------------
// Purpose: returns the texture info for the given char & font
//-----------------------------------------------------------------------------
bool CFontTextureCache::GetTextureForChar( vgui::HFont font, vgui::FontDrawType_t type, wchar_t wch, int *textureID, float **texCoords )
{
	// Ask for just one character
	return GetTextureForChars( font, type, &wch, textureID, texCoords, 1 );
}

//-----------------------------------------------------------------------------
// Purpose: returns the texture info for the given chars & font
//-----------------------------------------------------------------------------
bool CFontTextureCache::GetTextureForChars( vgui::HFont font, vgui::FontDrawType_t type, const wchar_t *wch, int *textureID, float **texCoords, int numChars )
{
	Assert( wch && textureID && texCoords );
	Assert( numChars >= 1 );

	if ( type == vgui::FONT_DRAW_DEFAULT )
	{
		type = g_MatSystemSurface.IsFontAdditive( font ) ? vgui::FONT_DRAW_ADDITIVE : vgui::FONT_DRAW_NONADDITIVE;
	}

	int typePage = (int)type - 1;
	typePage = clamp( typePage, 0, (int)vgui::FONT_DRAW_TYPE_COUNT - 1 );

	if ( FontManager().IsBitmapFont( font ) )
	{
		const int MAX_BITMAP_CHARS = 256;
		if ( numChars > MAX_BITMAP_CHARS )
		{
			// Increase MAX_BITMAP_CHARS
			Assert( 0 );
			return false;
		}
	
		for ( int i = 0; i < numChars; i++ )
		{
			static float	sTexCoords[ 4*MAX_BITMAP_CHARS ];
			CBitmapFont		*pWinFont;
			float			left, top, right, bottom;
			int				index;
			Page_t			*pPage;
			
			pWinFont = reinterpret_cast< CBitmapFont* >( FontManager().GetFontForChar( font, wch[i] ) );
			if ( !pWinFont )
			{
				// bad font handle
				return false;
			}

			// get the texture coords
			pWinFont->GetCharCoords( wch[i], &left, &top, &right, &bottom );
			sTexCoords[i*4 + 0] = left;
			sTexCoords[i*4 + 1] = top;
			sTexCoords[i*4 + 2] = right;
			sTexCoords[i*4 + 3] = bottom;

			// find font handle in our list of ready pages
			index = m_FontPages.Find( font );
			if ( index == m_FontPages.InvalidIndex() )
			{
				// not found, create the texture id and its materials
				index = m_FontPages.Insert( font );
				pPage = &m_FontPages.Element( index );

				for (int type = 0; type < FONT_DRAW_TYPE_COUNT; ++type )
				{
					pPage->textureID[type] = g_MatSystemSurface.CreateNewTextureID( false );
				}
				CreateFontMaterials( *pPage, pWinFont->GetTexturePage(), true );
			}

			texCoords[i] = &(sTexCoords[ i*4 ]);
			textureID[i] = m_FontPages.Element( index ).textureID[typePage];
		}
	}
	else
	{
		struct newPageEntry_t
		{
			int	page;	// The font page a new character will go in
			int	drawX;	// X location within the font page
			int	drawY;	// Y location within the font page
		};
		
		// Determine how many characters need to have their texture generated
		int numNewChars = 0;
		int maxNewCharTexels = 0;
		int totalNewCharTexels = 0;
		newChar_t		*newChars	= (newChar_t *)_alloca( numChars*sizeof( newChar_t ) );
		newPageEntry_t	*newEntries	= (newPageEntry_t *)_alloca( numChars*sizeof( newPageEntry_t ) );

		font_t *winFont = FontManager().GetFontForChar( font, wch[0] );
		if ( !winFont )
			return false;
		
		for ( int i = 0; i < numChars; i++ )
		{
			CacheEntry_t cacheItem;
			cacheItem.font = font;
			cacheItem.wch = wch[i];
			HCacheEntry cacheHandle = m_CharCache.Find( cacheItem );
			if ( ! m_CharCache.IsValidIndex( cacheHandle ) )
			{
				// All characters must come out of the same font
				if ( winFont != FontManager().GetFontForChar( font, wch[i] ) )
					return false;

				// get the char details
				int a, b, c;
				winFont->GetCharABCWidths( wch[i], a, b, c );
				int fontWide = max( b, 1 );
				int fontTall = max( winFont->GetHeight(), 1 );
				if ( winFont->GetUnderlined() )
				{
					fontWide += ( a + c );
				}

				// Get a texture to render into
				int page, drawX, drawY, twide, ttall;
				if ( !AllocatePageForChar( fontWide, fontTall, page, drawX, drawY, twide, ttall ) )
					return false;

				// accumulate data to pass to GetCharsRGBA below
				newEntries[	numNewChars ].page		= page;
				newEntries[	numNewChars ].drawX		= drawX;
				newEntries[	numNewChars ].drawY		= drawY;
				newChars[	numNewChars ].wch		= wch[i];
				newChars[	numNewChars ].fontWide	= fontWide;
				newChars[	numNewChars ].fontTall	= fontTall;
				newChars[	numNewChars ].offset	= 4*totalNewCharTexels;
				totalNewCharTexels += fontWide*fontTall;
				maxNewCharTexels = max( maxNewCharTexels, fontWide*fontTall );
				numNewChars++;

				// set the cache info
				cacheItem.page = page;

				// the 0.5 texel offset is done in CMatSystemTexture::SetMaterial() / CMatSystemSurface::StartDrawing()
				double adjust =  0.0f;

				cacheItem.texCoords[0] = (float)( (double)drawX / ((double)twide + adjust) );
				cacheItem.texCoords[1] = (float)( (double)drawY / ((double)ttall + adjust) );
				cacheItem.texCoords[2] = (float)( (double)(drawX + fontWide) / (double)twide );
				cacheItem.texCoords[3] = (float)( (double)(drawY + fontTall) / (double)ttall );

				m_CharCache.Insert(cacheItem);
				cacheHandle = m_CharCache.Find( cacheItem );
				Assert( m_CharCache.IsValidIndex( cacheHandle ) );
			}
			
			int page = m_CharCache[cacheHandle].page;
			textureID[i] = m_PageList[page].textureID[typePage];
			texCoords[i] = m_CharCache[cacheHandle].texCoords;
		}

		// Generate texture data for all newly-encountered characters
		if ( numNewChars > 0 )
		{

#ifdef _X360
			if ( numNewChars > 1 )
			{
				MEM_ALLOC_CREDIT();

				// Use the 360 fast path that generates multiple characters at once
				int newCharDataSize = totalNewCharTexels*4;
				CUtlBuffer newCharData( newCharDataSize, newCharDataSize, 0 );
				unsigned char *pRGBA = (unsigned char *)newCharData.Base();
				winFont->GetCharsRGBA( newChars, numNewChars, pRGBA );

				// Copy the data into our font pages
				for ( int i = 0; i < numNewChars; i++ )
				{
					newChar_t		& newChar	= newChars[i];
					newPageEntry_t	& newEntry	= newEntries[i];

					// upload the new sub texture 
					// NOTE: both textureIDs reference the same ITexture, so we're ok
					g_MatSystemSurface.DrawSetTexture( m_PageList[newEntry.page].textureID[typePage] );
					unsigned char *characterRGBA = pRGBA + newChar.offset;
					g_MatSystemSurface.DrawSetSubTextureRGBA( m_PageList[newEntry.page].textureID[typePage], newEntry.drawX, newEntry.drawY, characterRGBA, newChar.fontWide, newChar.fontTall );
				}
			}
			else
#endif
			{
				// create a buffer for new characters to be rendered into
				int nByteCount = maxNewCharTexels * 4;
				unsigned char *pRGBA = (unsigned char *)_alloca( nByteCount );

				// Generate characters individually
				for ( int i = 0; i < numNewChars; i++ )
				{
					newChar_t		& newChar	= newChars[i];
					newPageEntry_t	& newEntry	= newEntries[i];

					// render the character into the buffer
					Q_memset( pRGBA, 0, nByteCount );

					winFont->GetCharRGBA( newChar.wch, newChar.fontWide, newChar.fontTall, pRGBA );

					if ( g_mat_texture_outline_fonts )
					{
						int width = newChar.fontWide;
						int height = newChar.fontTall;

						CPixelWriter pixelWriter;
						pixelWriter.SetPixelMemory( IMAGE_FORMAT_RGBA8888, pRGBA, width * sizeof( BGRA8888_t ) );
						for( int x = 0; x < width; x++ )
						{
							pixelWriter.Seek( x, 0 );
							pixelWriter.WritePixel( 255, 0, 255, 255 );
							pixelWriter.Seek( x, height - 1 );
							pixelWriter.WritePixel( 255, 0, 255, 255 );
						}
						for( int y = 0; y < height; y++ )
						{
							if ( y < 4 || y > height - 4 )
							{
								pixelWriter.Seek( 0, y );
								pixelWriter.WritePixel( 255, 0, 255, 255 );
								pixelWriter.Seek( width - 1, y );
								pixelWriter.WritePixel( 255, 0, 255, 255 );
							}
						}
					}

					// upload the new sub texture 
					// NOTE: both textureIDs reference the same ITexture, so we're ok)
					g_MatSystemSurface.DrawSetTexture( m_PageList[newEntry.page].textureID[typePage] );
					g_MatSystemSurface.DrawSetSubTextureRGBA( m_PageList[newEntry.page].textureID[typePage], newEntry.drawX, newEntry.drawY, pRGBA, newChar.fontWide, newChar.fontTall );
				}
			}
		}
	}

	return true;
}


//-----------------------------------------------------------------------------
// Creates font materials
//-----------------------------------------------------------------------------
void CFontTextureCache::CreateFontMaterials( Page_t &page, ITexture *pFontTexture, bool bitmapFont )
{
	// The normal material
	KeyValues *pVMTKeyValues = new KeyValues( "UnlitGeneric" );
	pVMTKeyValues->SetInt( "$vertexcolor", 1 );
	pVMTKeyValues->SetInt( "$vertexalpha", 1 );
	pVMTKeyValues->SetInt( "$ignorez", 1 );
	pVMTKeyValues->SetInt( "$no_fullbright", 1 );
	pVMTKeyValues->SetInt( "$translucent", 1 );
	pVMTKeyValues->SetString( "$basetexture", pFontTexture->GetName() );
	IMaterial *pMaterial = g_pMaterialSystem->CreateMaterial( "__fontpage", pVMTKeyValues );
	pMaterial->Refresh();

	int typePageNonAdditive = (int)vgui::FONT_DRAW_NONADDITIVE-1;
	g_MatSystemSurface.DrawSetTextureMaterial( page.textureID[typePageNonAdditive], pMaterial );
	pMaterial->DecrementReferenceCount();

	// The additive material
	pVMTKeyValues = new KeyValues( "UnlitGeneric" );
	pVMTKeyValues->SetInt( "$vertexcolor", 1 );
	pVMTKeyValues->SetInt( "$vertexalpha", 1 );
	pVMTKeyValues->SetInt( "$ignorez", 1 );
	pVMTKeyValues->SetInt( "$no_fullbright", 1 );
	pVMTKeyValues->SetInt( "$translucent", 1 );
	pVMTKeyValues->SetInt( "$additive", 1 );
	pVMTKeyValues->SetString( "$basetexture", pFontTexture->GetName() );
	pMaterial = g_pMaterialSystem->CreateMaterial( "__fontpage_additive", pVMTKeyValues );
 	pMaterial->Refresh();

	int typePageAdditive = (int)vgui::FONT_DRAW_ADDITIVE-1;
	if ( bitmapFont )
	{
		g_MatSystemSurface.DrawSetTextureMaterial( page.textureID[typePageAdditive], pMaterial );
	}
	else
	{
		g_MatSystemSurface.ReferenceProceduralMaterial( page.textureID[typePageAdditive], page.textureID[typePageNonAdditive], pMaterial );
	}
	pMaterial->DecrementReferenceCount();
}

//-----------------------------------------------------------------------------
// Computes the page size given a character height
//-----------------------------------------------------------------------------
int CFontTextureCache::ComputePageType( int charTall ) const
{
	for (int i = 0; i < FONT_PAGE_SIZE_COUNT; ++i)
	{
		if ( charTall < s_pFontPageSize[i] )
			return i;
	}

	return -1;
}

//-----------------------------------------------------------------------------
// Purpose: allocates a new page for a given character
//-----------------------------------------------------------------------------
bool CFontTextureCache::AllocatePageForChar(int charWide, int charTall, int &pageIndex, int &drawX, int &drawY, int &twide, int &ttall)
{
	// see if there is room in the last page for this character
	int nPageType = ComputePageType( charTall );
	if ( nPageType < 0 )
	{
		Assert( !"Font is too tall for texture cache of glyphs\n" );
		return false; 
	}
	
	pageIndex = m_pCurrPage[nPageType];

	int nNextX = 0;
	bool bNeedsNewPage = true;
	if ( pageIndex > -1 )
	{
		Page_t &page = m_PageList[ pageIndex ];

		nNextX = page.nextX + charWide;

		// make sure we have room on the current line of the texture page
		if ( nNextX > page.wide )
		{
			// move down a line
			page.nextX = 0;
			nNextX = charWide;
			page.nextY += page.tallestCharOnLine;
			page.tallestCharOnLine = charTall;
		}
		page.tallestCharOnLine = max( page.tallestCharOnLine, (short)charTall );

		bNeedsNewPage = ((page.nextY + page.tallestCharOnLine) > page.tall);
	}
	
	if ( bNeedsNewPage )
	{
		// allocate a new page
		pageIndex = m_PageList.AddToTail();
		Page_t &newPage = m_PageList[pageIndex];
		m_pCurrPage[nPageType] = pageIndex;

		for (int i = 0; i < FONT_DRAW_TYPE_COUNT; ++i )
		{
			newPage.textureID[i] = g_MatSystemSurface.CreateNewTextureID( true );
		}

		newPage.maxFontHeight = s_pFontPageSize[nPageType];
		newPage.wide = TEXTURE_PAGE_WIDTH;
		newPage.tall = TEXTURE_PAGE_HEIGHT;
		newPage.nextX = 0;
		newPage.nextY = 0;
		newPage.tallestCharOnLine = charTall;

		nNextX = charWide;

		static int nFontPageId = 0;
		char pTextureName[64];
		Q_snprintf( pTextureName, 64, "__font_page_%d", nFontPageId );
		++nFontPageId;

		MEM_ALLOC_CREDIT();
		ITexture *pTexture = g_pMaterialSystem->CreateProceduralTexture( 
			pTextureName, 
			TEXTURE_GROUP_VGUI, 
			newPage.wide, 
			newPage.tall, 
			IMAGE_FORMAT_RGBA8888, 
			TEXTUREFLAGS_POINTSAMPLE | TEXTUREFLAGS_CLAMPS | TEXTUREFLAGS_CLAMPT | 
			TEXTUREFLAGS_NOMIP | TEXTUREFLAGS_NOLOD | TEXTUREFLAGS_PROCEDURAL | TEXTUREFLAGS_SINGLECOPY );

		CreateFontMaterials( newPage, pTexture );

		pTexture->DecrementReferenceCount();

		if ( IsPC() || !IsDebug() )
		{
			// clear the texture from the inital checkerboard to black
			// allocate for 32bpp format
			int nByteCount = TEXTURE_PAGE_WIDTH * TEXTURE_PAGE_HEIGHT * 4;
			unsigned char *pRGBA = (unsigned char *)_alloca( nByteCount );
			Q_memset( pRGBA, 0, nByteCount );

			int typePageNonAdditive = (int)(vgui::FONT_DRAW_NONADDITIVE)-1;
			g_MatSystemSurface.DrawSetTextureRGBA( newPage.textureID[typePageNonAdditive], pRGBA, newPage.wide, newPage.tall, false, false );
		}
	}

	// output the position
	Page_t &page = m_PageList[ pageIndex ];
	drawX = page.nextX;
	drawY = page.nextY;
	twide = page.wide;
	ttall = page.tall;

	// Update the next position to draw in
	page.nextX = nNextX + 1;
	return true;
}