//========= Copyright Valve Corporation, All rights reserved. ============//
#include "cbase.h"
#include "custom_texture_cache.h"
#include "materialsystem/imaterialproxy.h"
#include "materialsystem/imaterialvar.h"
#include "materialsystem/itexture.h"
#include "pixelwriter.h"
#include "checksum_md5.h"
#include "imageutils.h"
#include "toolframework_client.h"
#include "econ_gcmessages.h"
#include "econ_item_inventory.h"

#include "VGuiMatSurface/IMatSystemSurface.h"
#include "bitmap/bitmap.h"

using namespace CustomTextureSystem;

ITexture *CustomTextureSystem::g_pPreviewCustomTexture = NULL;

CEconItemView *CustomTextureSystem::g_pPreviewEconItem = NULL;

bool CustomTextureSystem::g_pPreviewCustomTextureDirty = true;

const char CustomTextureSystem::k_rchCustomTextureFilterPreviewImageName[] = "__CustomTextureFilterPreview";
const char CustomTextureSystem::k_rchCustomTextureFilterPreviewTextureName[] = "vgui/__CustomTextureFilterPreview";

//-----------------------------------------------------------------------------

static ISteamRemoteStorage *GetISteamRemoteStorage()
{
	return steamapicontext?steamapicontext->SteamRemoteStorage():NULL;
//	return Steam3Client().SteamRemoteStorage();
}

static void CalcMD5Ascii( char *szDigestAscii, const void *data, int dataSz )
{
	MD5Context_t context;
	unsigned char digest[ MD5_DIGEST_LENGTH ];
	MD5Init( &context );
	MD5Update( &context, (const unsigned char *)data, dataSz );
	MD5Final( digest, &context );
	Q_binarytohex( digest, MD5_DIGEST_LENGTH, szDigestAscii, MD5_DIGEST_LENGTH*2+1 );
}

static bool BReadSteamRemoteFileToBuffer( CUtlBuffer &outBuffer, const char *pchRemoteFilename )
{
	outBuffer.Purge();

	ISteamRemoteStorage *pRemoteStorage = GetISteamRemoteStorage();
	if ( !pRemoteStorage )
		return false;
	if ( !pRemoteStorage->FileExists( pchRemoteFilename ))
		return false;
	int nFileSize = pRemoteStorage->GetFileSize( pchRemoteFilename );
	if ( nFileSize <= 0 )
		return false;

	// Allocate space
	outBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, nFileSize );
	int nSizeRead = pRemoteStorage->FileRead( pchRemoteFilename, outBuffer.Base(), nFileSize );
	return ( nSizeRead == nFileSize );
}

//-----------------------------------------------------------------------------
// Local cache of custom images.  This cache contains files from the cloud, and
// and also a few virtual textures that are used during autioning / tweaking

/// Name of cloud-backed config file remembering the custom images that the user
/// has uploaded to the cloud.
static const char k_szCustomTextureRecentListFilename[] = "stamped_items_mru.txt";

/// Track a single entry
struct SCustomImageCacheEntry : private ITextureRegenerator
{

	/// If this has been assigned a cloud ID, what is it?
	UGCHandle_t m_hCloudID;

	/// If this is one of our files, then we know the MD5.
	/// This is empty for other people's files
	char m_szDigestAscii[ MD5_DIGEST_LENGTH*2 + 4];

	//
	// Bookkeeping for steam downloads of UGC.
	//

	/// -1 = failure, 0 = not started, 1 = in progress, 2 = finished OK and image should be in memory
	int	m_nStatus;

	/// Handle to the active download, or k_uAPICallInvalid if not active
	SteamAPICall_t m_hDownloadApiCall;

	/// Procedural texture object.  We hold a reference.
	ITexture *m_pTexture;

	/// Procedurally-created material.  We hold a reference.
	IMaterial *m_pMaterial;

	/// GUI texture handle.  (It's bound to the material.)
	int m_iVguiHandle;

	/// The raw image
	Bitmap_t m_image;

	/// Doubly-linked list.  We keep it in MRU order so we know what to eject from the cache
	SCustomImageCacheEntry *m_pPrev;
	SCustomImageCacheEntry *m_pNext;

	SCustomImageCacheEntry()
		: m_hCloudID(0)
		, m_pTexture(NULL)
		, m_nStatus(0)
		, m_hDownloadApiCall(k_uAPICallInvalid)
		, m_pPrev(NULL)
		, m_pNext(NULL)
		, m_pMaterial(NULL)
		, m_iVguiHandle(0)
	{
		m_szDigestAscii[0] = '\0';
	}

	virtual ~SCustomImageCacheEntry()
	{
		Clear();
	}

	// Release texture / VGUI resources.  This doesn't free the image we have
	// loaded or stop any async actions that were in progress.  (Use Clear())
	void ReleaseResources()
	{
		if ( m_pTexture )
		{
			ITexture *tex = m_pTexture;
			m_pTexture = NULL; // clear pointer first, to prevent infinite recursion
			tex->SetTextureRegenerator( NULL );
			tex->Release();
		}
		if ( m_pMaterial )
		{
			m_pMaterial->Release();
			m_pMaterial = NULL;
		}
		if ( m_iVguiHandle != 0 )
		{
			g_pMatSystemSurface->DestroyTextureID( m_iVguiHandle );
			m_iVguiHandle = 0;
		}
	}

	/// Inherited from ITextureRegenerator
	///
	/// Gets called when our ITextureRegenerator interface gets detached from the texture.
	/// We should be the only ones doing this --- so that means we had better have already
	/// cleared our texture at this point!
	virtual void Release()
	{
		Assert( m_pTexture == NULL );
	}

	/// Inherited from ITextureRegenerator
	///
	/// The main interface function that actually supplies the texture bits
	virtual void RegenerateTextureBits( ITexture *pTexture, IVTFTexture *pVTFTexture, Rect_t *pRect )
	{

		Assert( pVTFTexture->FrameCount() == 1 );
		Assert( pVTFTexture->FaceCount() == 1 );
		Assert( pTexture == m_pTexture );
		Assert( !pTexture->IsMipmapped() );

		int nWidth, nHeight, nDepth;
		pVTFTexture->ComputeMipLevelDimensions( 0, &nWidth, &nHeight, &nDepth );
		Assert( nDepth == 1 );
		Assert( nWidth == m_image.Width() && nHeight == m_image.Height() );

		CPixelWriter pixelWriter;
		pixelWriter.SetPixelMemory( pVTFTexture->Format(), 
			pVTFTexture->ImageData( 0, 0, 0 ), pVTFTexture->RowSizeInBytes( 0 ) );

		// !SPEED! 'Tis probably DEATHLY slow...
		for ( int y = 0; y < nHeight; ++y )
		{
			pixelWriter.Seek( 0, y );
			for ( int x = 0; x < nWidth; ++x )
			{
				Color c = m_image.GetColor( x, y );
				pixelWriter.WritePixel( c.r(), c.g(), c.b(), c.a() );
			}
		}
	}

	void Clear()
	{
		ReleaseResources();
		m_image.Clear();
		m_szDigestAscii[0] = '\0';
		m_nStatus = 0;
		m_hCloudID = 0;

		// !KLUDGE! How can I clean this up properly if something is
		// in progress?
		m_hDownloadApiCall = k_uAPICallInvalid;
	}

	/// Poll the entry and update bookeeping if we're busy.
	void Poll()
	{

		// We must know our cloud ID
		if ( m_hCloudID == 0 )
		{
			Assert( m_hCloudID != 0 );
			return;
		}

		// If texture already exists, then we are definitely done!
		if ( m_pTexture )
		{
			Assert( m_nStatus == 2 );
			return;
		}

		// Check if we have not yet initiated anything
		if ( m_nStatus == 0 )
		{

			// We'll need to download it.
			// Start by assuming failure.
			m_nStatus = -1;

			// Start download
			ISteamRemoteStorage *pRemoteStorage = GetISteamRemoteStorage();
			if ( pRemoteStorage )
			{
				m_hDownloadApiCall = pRemoteStorage->UGCDownload( m_hCloudID, 0 );
				if ( m_hDownloadApiCall != k_uAPICallInvalid )
				{
					// Mark download as in progress
					Msg( "Started download of cloud file %08X%08X\n", (uint32)(m_hCloudID>>32), (uint32)m_hCloudID );
					m_nStatus = 1;
				}
			}
		}

		// If we're in progress, poll the result
		if ( m_nStatus == 1 )
		{
			PollDownload();
		}

		// If result has completed, then fetch the texture
		Assert( m_pTexture == NULL );
		if ( m_nStatus == 2 && m_image.IsValid() )
		{
			// Generate the logical texture name
			char rchTextureName[MAX_PATH];
			GenerateLocalTextureName( rchTextureName );

			ITexture *pTexture = NULL;
			if ( g_pMaterialSystem->IsTextureLoaded( rchTextureName ) )
			{
				pTexture = g_pMaterialSystem->FindTexture( rchTextureName, TEXTURE_GROUP_VGUI );
				pTexture->AddRef();
				Assert( pTexture );
			}
			else
			{
				pTexture = g_pMaterialSystem->CreateProceduralTexture(
					rchTextureName,
					TEXTURE_GROUP_VGUI,
					k_nCustomImageSize, k_nCustomImageSize, 
					IMAGE_FORMAT_RGBA8888,
					TEXTUREFLAGS_CLAMPS | TEXTUREFLAGS_CLAMPT | TEXTUREFLAGS_NOMIP | TEXTUREFLAGS_NOLOD
				);
				Assert( pTexture );
			}
			pTexture->SetTextureRegenerator( this ); // note carefully order of operations here.  See Release()
			m_pTexture = pTexture;

			// Upload the data now
			m_pTexture->Download();
		}
		else
		{
			Assert( m_nStatus < 2 );
			Assert( !m_image.IsValid() );
		}
	}

	void PollDownload()
	{

		Assert( m_nStatus == 1 );

		// Sanity check we have everything we need
		ISteamRemoteStorage *pRemoteStorage = GetISteamRemoteStorage();
		if ( m_hDownloadApiCall == k_uAPICallInvalid || !steamapicontext || !steamapicontext->SteamUtils() || !pRemoteStorage )
		{
			// ???
			Assert( m_hDownloadApiCall != k_uAPICallInvalid );
			Assert( steamapicontext && steamapicontext->SteamUtils() );
			Assert( pRemoteStorage );
			m_nStatus = -1;
			return;
		}

		// Poll progress
		bool bFailed;
		RemoteStorageDownloadUGCResult_t result;
		if ( !steamapicontext->SteamUtils()->GetAPICallResult(m_hDownloadApiCall,
			&result, sizeof(result), RemoteStorageDownloadUGCResult_t::k_iCallback, &bFailed) )
		{
			// Still busy.
			return;
		}

		// Make sure we got back the file we were expecting
		Assert( result.m_hFile == m_hCloudID );

		// Clear status, mark success
		m_hDownloadApiCall = k_uAPICallInvalid;

		// Completed.  Did we succeed?
		if ( bFailed )
		{
			Warning( "Download of custom image file from UFS (UGC=%08X%08X) failed.\n", (uint32)(m_hCloudID >> 32), (uint32)(m_hCloudID) );
			m_nStatus = -1;
			return;
		}

		// Fetch file details
		AppId_t nAppID;
		char *pchName;
		int32 nFileSizeInBytes = -1;
		CSteamID steamIDOwner;
		if ( !pRemoteStorage->GetUGCDetails( m_hCloudID, &nAppID, &pchName, &nFileSizeInBytes, &steamIDOwner )
			|| nFileSizeInBytes <= 0 || nFileSizeInBytes >= k_nMaxCustomImageFileSize )
		{
			Warning( "GetUGCDetails failed? (UGC=%08X%08X nFileSizeInBytes=%d).\n", (uint32)(m_hCloudID >> 32), (uint32)(m_hCloudID), nFileSizeInBytes );
			m_nStatus = -1;
			return;
		}

		// Load the file data
		CUtlBuffer fileData;
		fileData.SeekPut( CUtlBuffer::SEEK_HEAD, nFileSizeInBytes );

		// Read in the data.  Phil says this is supposed to be basically a memcpy
		// or some other fast, local operation.
		if ( pRemoteStorage->UGCRead( m_hCloudID, fileData.Base( ), nFileSizeInBytes, 0, k_EUGCRead_ContinueReadingUntilFinished ) != nFileSizeInBytes )
		{
			Warning( "UGCRead failed? (UGC=%08X%08X).\n", (uint32)(m_hCloudID >> 32), (uint32)(m_hCloudID) );
			m_nStatus = -1;
			return;
		}

		// Parse the PNG file data
		if ( ImgUtl_LoadPNGBitmapFromBuffer( fileData, m_image ) != CE_SUCCESS )
		{
			Warning( "Corrupt PNG file, UGC=%08X%08X.\n", (uint32)(m_hCloudID >> 32), (uint32)(m_hCloudID) );
			m_nStatus = -1;
			return;
		}

		// We have the raw data
		m_nStatus = 2;
	}

	int GetGuiHandle()
	{
		// Should never be called on entries without a cloud ID
		if ( m_hCloudID == 0 )
		{
			Assert( m_hCloudID != 0 );
			return 0;
		}

		// Already have one?
		if ( m_iVguiHandle != 0 )
		{
			return m_iVguiHandle;
		}

		// Process texture downloading, etc
		Poll();

		// If we don't have a texture yet, or don't know our logical name, then we cannot draw
		if ( m_pTexture == NULL )
		{
			return 0;
		}

		// Make a material, if we don't already have one
		if ( m_pMaterial == NULL )
		{

			// Generate the material name
			char rchImageName[MAX_PATH], rchMaterialName[MAX_PATH];
			GenerateLocalImageNameBase( rchImageName );
			Q_snprintf( rchMaterialName, MAX_PATH, "vgui/%s.mtl", rchImageName );

			// Does it already exist?
			if ( g_pMaterialSystem->IsMaterialLoaded( rchMaterialName ) )
			{
				m_pMaterial = g_pMaterialSystem->FindMaterial( rchMaterialName, TEXTURE_GROUP_VGUI );
				Assert( m_pMaterial );
			}
			else
			{

				// Fetch the texture name
				char rchTextureName[MAX_PATH];
				GenerateLocalTextureName( rchTextureName );

				// Create dummy material KV data
				KeyValues *pVMTKeyValues = new KeyValues( "UnlitGeneric" );
				pVMTKeyValues->SetString( "$basetexture", rchTextureName );
				pVMTKeyValues->SetInt( "$vertexcolor", 1 );
				pVMTKeyValues->SetInt( "$vertexalpha", 1 );
				pVMTKeyValues->SetInt( "$translucent", 1 );

				// Create the material
				m_pMaterial = g_pMaterialSystem->CreateMaterial(
					rchMaterialName,
					pVMTKeyValues
				);
			}

			// Bind the material to a new VGUI texture object
			m_iVguiHandle = g_pMatSystemSurface->CreateNewTextureID();
			g_pMatSystemSurface->DrawSetTextureMaterial( m_iVguiHandle, m_pMaterial );
		}

		return m_iVguiHandle;
	}

	// Generate logical image name, with no leading materials or vgui directories
	// nor a file extension.
	void GenerateLocalImageNameBase( char *result ) const
	{
		Assert( m_hCloudID != 0 );

		// Generate the local filenames.  !KLUDGE! I'm not sure the platform-safe way
		// to print a 64-bit int, so I'll just print both halves myself
		Q_snprintf( result, 64, "cloud_custom_images/%08X%08X", (uint32)(m_hCloudID >> 32), (uint32)(m_hCloudID) );
	}

	/// Logical texture name, including "vgui" but not "materials"
	void GenerateLocalTextureName( char *result ) const
	{
		char rchImageName[MAX_PATH];
		GenerateLocalImageNameBase( rchImageName );
		Q_snprintf( result, MAX_PATH, "vgui/%s.vtf", rchImageName );
	}

	/// Full local filename, including leading "materials" directory
	void GenerateLocalFilename( char *result ) const
	{
		char szLocalTextureName[MAX_PATH];
		GenerateLocalTextureName( szLocalTextureName );

		Q_snprintf( result, MAX_PATH, "materials/%s", szLocalTextureName );
	}
};

/// Head of linked list of entries, in MRU order
static SCustomImageCacheEntry *mruCustomImageEntry = NULL;

/// Map of entries, indexed by cloud ID
typedef CUtlMap<UGCHandle_t, SCustomImageCacheEntry *, int> tCustomTextureInfoMap;
static tCustomTextureInfoMap g_mapCustomTextureInfoByCloudId( DefLessFunc(UGCHandle_t) );

// Remove from linked list, without deleting.  The item must already be in the list
static void CustomTextureCache_Remove(SCustomImageCacheEntry *pEntry)
{
	Assert( pEntry );

	// List had better not be empty.  Commence paranoia.
	Assert( mruCustomImageEntry );
	Assert( !mruCustomImageEntry->m_pPrev );

	SCustomImageCacheEntry *p = pEntry->m_pPrev;
	SCustomImageCacheEntry *n = pEntry->m_pNext;

	// Detach from next, if we're not last
	if ( n != NULL )
	{
		Assert( n->m_pPrev == pEntry);
		n->m_pPrev = p;
	}

	// At the head?
	if ( !p )
	{
		Assert( mruCustomImageEntry == pEntry );
		mruCustomImageEntry = n;
	}
	else
	{

		// Detach from previous
		Assert( p->m_pNext == pEntry );
		p->m_pNext = n;
	}

	// Clear pointers
	pEntry->m_pPrev = pEntry->m_pNext = NULL;
}

// Insert the item at the head (MRU) slot.  The item shouldn't
// already be in the list
static void CustomTextureCache_InsertAtHead(SCustomImageCacheEntry *pEntry)
{
	Assert( pEntry );
	Assert( !pEntry->m_pNext );
	Assert( !pEntry->m_pPrev );

	// Edge case of inserting into empty list
	if ( mruCustomImageEntry )
	{
		Assert( !mruCustomImageEntry->m_pPrev );
		mruCustomImageEntry->m_pPrev = pEntry;
	}
	else
	{
		// Inserting into an empty list
	}

	// Do the head insertion.
	pEntry->m_pNext = mruCustomImageEntry;
	mruCustomImageEntry = pEntry;
}

// Reorder list, setting item at the head (MRU) slot.  The item must already
// be in the list somewhere.
static void CustomTextureCache_SetMRU(SCustomImageCacheEntry *pEntry)
{
	// Note: even if we are already at the head, go through the motions, anyway,
	// to exercise all of the sanity checking code.
	CustomTextureCache_Remove(pEntry);
	CustomTextureCache_InsertAtHead(pEntry);
}

static SCustomImageCacheEntry *CustomTextureCache_NewEntry()
{

	SCustomImageCacheEntry *pEntry = new SCustomImageCacheEntry;

	// Go ahead and put us at the head
	CustomTextureCache_InsertAtHead(pEntry);

	// Return the new entry
	return pEntry;
}

static SCustomImageCacheEntry *CustomTextureCache_FindOrAddByCloudId( UGCHandle_t ugcHandle )
{

	// Locate the bookeeping entry, if one exists
	int idx = g_mapCustomTextureInfoByCloudId.Find( ugcHandle );
	SCustomImageCacheEntry *pEntry;
	if ( g_mapCustomTextureInfoByCloudId.IsValidIndex( idx ) )
	{
		pEntry = g_mapCustomTextureInfoByCloudId[idx];

		// We're accessing it, so move it to the head, the MRU slot
		CustomTextureCache_SetMRU(pEntry);
	}
	else
	{
		// Grab a new entry
		pEntry = CustomTextureCache_NewEntry();

		// Assign the cloud ID
		pEntry->m_hCloudID = ugcHandle;

		// Add it to the map by cloud ID
		idx = g_mapCustomTextureInfoByCloudId.Insert( ugcHandle );
		g_mapCustomTextureInfoByCloudId[idx] = pEntry;
	}

	// Return the entry
	return pEntry;
}

// Locate an entry by hash create a new entry if one doesn't already exist
static SCustomImageCacheEntry *CustomTextureCache_FindOrAddByDigest( const char *szDigestAscii )
{
	Assert( strlen(szDigestAscii) == MD5_DIGEST_LENGTH*2 );

	// Brute-force linear search.  This should never be called in time-critical
	// situations
	SCustomImageCacheEntry *pEntry = mruCustomImageEntry;
	while ( pEntry )
	{

		// Match?
		if ( !Q_stricmp(pEntry->m_szDigestAscii, szDigestAscii) )
		{

			// Found.  Se at MRU and return it.
			CustomTextureCache_SetMRU(pEntry);
			return pEntry;
		}

		// Keep looking
		pEntry = pEntry->m_pNext;
	}

	// Not found.  Make a new entry
	pEntry = CustomTextureCache_NewEntry();
	V_strcpy_safe(pEntry->m_szDigestAscii, szDigestAscii);
	return pEntry;
}

//-----------------------------------------------------------------------------
int GetCustomTextureGuiHandle( uint64 hCloudId )
{

	// Find or create the entry
	SCustomImageCacheEntry *pEntry = CustomTextureCache_FindOrAddByCloudId( hCloudId );

	// Poll entry and return GUI handle if it's finally ready
	return pEntry->GetGuiHandle();
}

//-----------------------------------------------------------------------------

class CCustomTextureOnItemProxy : public IMaterialProxy
{
public:
	CCustomTextureOnItemProxy();
	virtual ~CCustomTextureOnItemProxy();

	virtual bool Init( IMaterial* pMaterial, KeyValues *pKeyValues );
	virtual void OnBind( void *pC_BaseEntity );
	virtual void Release();
	virtual IMaterial *GetMaterial();

protected:
	virtual void	OnBindInternal( CEconItemView *pScriptItem );

private:
	IMaterialVar	*m_pBaseTextureVar;
	ITexture		*m_pOriginalTexture;
};

EXPOSE_INTERFACE( CCustomTextureOnItemProxy, IMaterialProxy, "CustomSteamImageOnModel" IMATERIAL_PROXY_INTERFACE_VERSION );

CCustomTextureOnItemProxy::CCustomTextureOnItemProxy()
: m_pBaseTextureVar( NULL )
, m_pOriginalTexture( NULL )
{
	
}

CCustomTextureOnItemProxy::~CCustomTextureOnItemProxy()
{
}

bool CCustomTextureOnItemProxy::Init( IMaterial *pMaterial, KeyValues *pKeyValues )
{
	Release();

	bool found = false;
	m_pBaseTextureVar = pMaterial->FindVar( "$basetexture", &found );
	if ( !found )
	{
		return false;
	}

	// No! Don't do this until, because the material/texture might
	// not have been cached.  If we call this, it causes the material
	// to try to get cached, but instead of loading the texture
	// synchronously, it just goes into a queue, and we get the error
	// texture instead.  We'll just defer it until later when we know
	// for sure that everything is ready to go.
	//m_pOriginalTexture = m_pBaseTextureVar->GetTextureValue();
	//if ( m_pOriginalTexture )
	//{
	//	m_pOriginalTexture->AddRef();
	//}
	return true;
}

void CCustomTextureOnItemProxy::OnBind( void *pC_BaseEntity )
{
	if ( pC_BaseEntity )
	{
		CEconItemView *pScriptItem = NULL;
		IClientRenderable *pRend = (IClientRenderable *)pC_BaseEntity;
		C_BaseEntity *pEntity = pRend->GetIClientUnknown()->GetBaseEntity();
		if ( pEntity )
		{
			CEconEntity *pItem = dynamic_cast< CEconEntity* >( pEntity );
			if ( pItem )
			{
				pScriptItem = pItem->GetAttributeContainer()->GetItem();
			}
		}
		else
		{
			// Proxy data can be a script created item itself, if we're in a vgui CModelPanel
			pScriptItem = dynamic_cast< CEconItemView* >( pRend );
		}
		if ( pScriptItem )
		{
			OnBindInternal( pScriptItem );
		}
	}
}

void CCustomTextureOnItemProxy::Release()
{
	if ( m_pOriginalTexture )
	{
		m_pOriginalTexture->Release();
		m_pOriginalTexture = NULL;
	}
}

IMaterial *CCustomTextureOnItemProxy::GetMaterial()
{
	return m_pBaseTextureVar->GetOwningMaterial();
}

void CCustomTextureOnItemProxy::OnBindInternal( CEconItemView *pScriptItem )
{
	if ( !m_pBaseTextureVar || !m_pBaseTextureVar->IsTexture() )
	{
		return;
	}

	// Snag the original texture object the first time.
	// And make sure we're 100% ready to go.
	if ( m_pOriginalTexture == NULL )
	{
		m_pOriginalTexture = m_pBaseTextureVar->GetTextureValue();
		if ( m_pOriginalTexture == NULL )
		{
			return;
		}
		if ( m_pOriginalTexture->IsError() )
		{
			m_pOriginalTexture = NULL;
			return;
		}

		// Success! Let's hang on to this guy
		m_pOriginalTexture->AddRef();
	}
	ITexture *texture = m_pOriginalTexture;

	// Fetch the UGC handle from the item
	UGCHandle_t ugcHandle = pScriptItem->GetCustomUserTextureID();

	// Are we in a preview window?
	if ( pScriptItem == g_pPreviewEconItem ) // !KLUDGE!
	{
		Assert( g_pPreviewCustomTexture );
		if ( g_pPreviewCustomTexture )
		{
			texture = g_pPreviewCustomTexture;

			// Re-fetch the bits if necessary
			if ( g_pPreviewCustomTextureDirty )
			{
				g_pPreviewCustomTexture->Download();
				Assert( !g_pPreviewCustomTextureDirty );
			}
		}
	}
	else if (ugcHandle != 0)
	{

		SCustomImageCacheEntry *pEntry = CustomTextureCache_FindOrAddByCloudId(ugcHandle);
		pEntry->Poll();
		texture = pEntry->m_pTexture; // might be NULL if texture isn't ready yet
	}

	if ( texture )
	{
		m_pBaseTextureVar->SetTextureValue( texture );
	}

	if ( ToolsEnabled() )
	{
		ToolFramework_RecordMaterialParams( GetMaterial() );
	}
}

//-----------------------------------------------------------------------------
// The custom texture cache needs to init/shutdown and get some frame ticking
//-----------------------------------------------------------------------------
class CCustomTextureToolCache : public CBaseGameSystemPerFrame
{
public:
	CCustomTextureToolCache() {}
	virtual ~CCustomTextureToolCache() {}

	//
	// CAutoGameSystemPerFrame overrides
	//
	virtual char const *Name()
	{
		return "CCustomTextureToolCache";
	}
	virtual bool Init()
	{
		return true;
	}

	virtual void Shutdown()
	{

		// Destroy all the cache entries
		SCustomImageCacheEntry *pEntry = mruCustomImageEntry;
		mruCustomImageEntry = NULL;
		while ( pEntry != NULL )
		{
			SCustomImageCacheEntry *pNext = pEntry->m_pNext;
			delete pEntry;
			pEntry = pNext;
		}
	}

	// At level shutdown, release all of our GPU resources.
	// We'll still hang on to the bitmap data, since it isn't
	// that large and is in regular virtual memory which is easily
	// swapped out if stale.  But the video RAM we want to be more
	// agressive at cleaning out.
	virtual void LevelShutdownPreEntity()
	{
		// Destroy all the cache entries
		for ( SCustomImageCacheEntry *pEntry = mruCustomImageEntry ; pEntry ; pEntry = pEntry->m_pNext )
		{	
			pEntry->ReleaseResources();
		}
	}

// CAutoGameSystemPerFrame defines different stuff depending on which DLL we're building
#ifdef CLIENT_DLL

	// Do our frame-time processing after rendering
	virtual void PostRender()
	{
		// !FIXME! Here's where we should scan the list and eject
		// entries that haven't been used recently to limit the
		// hardware resources we're using.
	}
#else
	// This file shouldn't be compiled outside of client.dll.  Right?
	#error "Say what?"
#endif
};

static CCustomTextureToolCache s_CustomTextureToolCache;
IGameSystem *CustomTextureToolCacheGameSystem()
{
	return &s_CustomTextureToolCache;
}


CApplyCustomTextureJob::CApplyCustomTextureJob( itemid_t nToolItemID, itemid_t nSubjectItemID, const void *pPNGData, int nPNGDataBytes )
: GCSDK::CGCClientJob( GCClientSystem()->GetGCClient() )
, m_nToolItemID( nToolItemID )
, m_nSubjectItemID( nSubjectItemID )
, m_hCloudID( 0 )
{
	m_chRemoteStorageName[0] = '\0';
	m_bufPNGData.Put( pPNGData, nPNGDataBytes );
}

bool CApplyCustomTextureJob::BYieldingRunGCJob()
{
	YieldingRunJob();
	CleanUp();
	return true;
}

void CApplyCustomTextureJob::CleanUp()
{
	// If we had a cloud file, delete it from the logical
	// cloud filespace.  We are using the cloud system really just
	// to get the file into the UGC system and get a handle to it.
	// But once it's up there, it really isn't this user.  They will
	// fetch it by UGC handle just like any other user.  They paid
	// for this action, and we don't want it taking up any of their
	// quota.
	ISteamRemoteStorage *pRemoteStorage = GetISteamRemoteStorage();
	if ( pRemoteStorage && m_chRemoteStorageName[0] != '\0' )
	{
		pRemoteStorage->FileDelete( m_chRemoteStorageName );
	}
}

EResult CApplyCustomTextureJob::YieldingRunJob()
{
	EResult result = YieldingFindFileIncacheOrUploadFileToCDN();
	if ( result != k_EResultOK )
	{
		return result;
	}
	Assert( m_hCloudID != 0 );

	result = YieldingApplyTool();
	if ( result != k_EResultOK )
	{
		return result;
	}

	// OK!
	return k_EResultOK;
}

EResult CApplyCustomTextureJob::YieldingFindFileIncacheOrUploadFileToCDN()
{

	int nFileSize = m_bufPNGData.TellPut();
	Assert( nFileSize <= k_nMaxCustomImageFileSize ); // what the heck is out image converter doing?!

	// Generate the hash
	char szDigestAscii[ MD5_DIGEST_LENGTH*2 + 4];
	CalcMD5Ascii( szDigestAscii, m_bufPNGData.Base(), nFileSize );

	// Find or create an existing cache entry
	SCustomImageCacheEntry *pSelectedCacheEntry = CustomTextureCache_FindOrAddByDigest( szDigestAscii );

	KeyValuesAD pkvMruFile( "StampedItems" );

	{
		// Load up list of images recently used and uploaded
		CUtlBuffer listFileData;
		listFileData.SetBufferType( true, true );
		if ( BReadSteamRemoteFileToBuffer( listFileData, k_szCustomTextureRecentListFilename ) )
		{
			if ( !pkvMruFile->LoadFromBuffer( k_szCustomTextureRecentListFilename, listFileData ) )
			{
				pkvMruFile->Clear();
			}
		}
	}
	KeyValues *pkvMruUploadedImages = pkvMruFile->FindKey( "Uploaded", true );

	// !FIXME! Check for duplicates!

	// Make sure we are ready
	Assert( pSelectedCacheEntry != NULL );
	Assert( pSelectedCacheEntry->m_hCloudID == 0 );
	Assert( strlen(pSelectedCacheEntry->m_szDigestAscii) == MD5_DIGEST_LENGTH*2 );
	Assert( m_bufPNGData.TellPut() > 0 );

	// Generate filename in the cloud file space.  Each user has their own
	// namespace, and Phil requested that we keep the filenames simple
	// and easily optimizeable by string table.  (I.e. don't use the
	// hash or something else)
	//
	// We *could* just always use the same filename, and each file would
	// be its own "version."  But that doesn't seem to be the proper
	// spirit of the cloud system.  So I'll just use a simple integer name,
	// based on how many images they have uploaded.  It isn't critical what
	// this logical filename is, because once the GC gets a message to tag the file,
	// that UGC ID should always refer to that version of the file and can never
	// be changed or deleted, even if we reuse the filename.
	int iFileIndex = 1;
	KeyValues *pKey;
	for ( pKey = pkvMruUploadedImages->GetFirstTrueSubKey() ; pKey ; pKey = pKey->GetNextTrueSubKey() )
	{
		int index = atoi(pKey->GetName());
		iFileIndex = MAX( iFileIndex, index+1 );
	}
	Q_snprintf( m_chRemoteStorageName, sizeof( m_chRemoteStorageName ), "my_custom_images/%d.png", iFileIndex );

	// Write the local copy of the file
	Msg( "Saving %s to cloud....\n", m_chRemoteStorageName );
	ISteamRemoteStorage *pRemoteStorage = GetISteamRemoteStorage();
	if ( !pRemoteStorage || !pRemoteStorage->FileWrite( m_chRemoteStorageName, m_bufPNGData.Base(), m_bufPNGData.TellPut() ) )
	{
		Warning( "Failed to save local copy of custom image %s\n", m_chRemoteStorageName);
		return k_EResultFail;
	}

	// Share it.  This initiates the upload to cloud
	Msg( "Starting upload of %s to UFS....\n", m_chRemoteStorageName );
	SteamAPICall_t hFileShareApiCall = pRemoteStorage->FileShare( m_chRemoteStorageName );
	if ( hFileShareApiCall == k_uAPICallInvalid )
	{
		return k_EResultFail;
	}

	bool bFailed;
	RemoteStorageFileShareResult_t shareResult;
	while ( !steamapicontext->SteamUtils()->GetAPICallResult(hFileShareApiCall,
		&shareResult, sizeof(shareResult), RemoteStorageFileShareResult_t::k_iCallback, &bFailed) )
	{
		BYield();
	}

	if ( bFailed || shareResult.m_eResult != k_EResultOK )
	{
		Warning( "Custom texture uploaded to cloud FAILED\n" );
		return k_EResultFail;
	}

	Msg( "Custom texture uploaded to cloud completed OK, assigned UGC ID %08X%08X\n", (uint32)(shareResult.m_hFile >> 32), (uint32)(shareResult.m_hFile) );

	// Remember the handle to the cloud file
	m_hCloudID = pSelectedCacheEntry->m_hCloudID = shareResult.m_hFile;

	// Update the MRU list
	pKey = pkvMruUploadedImages->GetFirstTrueSubKey();
	while ( pKey )
	{
		int index = atoi(pKey->GetName());
		int mruValue = pKey->GetInt( "mru", 0 );
		const char *entryDigsetAscii = pKey->GetString("md5", "");
		UGCHandle_t ugcID = pKey->GetUint64( "ugcid", 0);
		if ( index <= 0 || mruValue <= 0 || strlen(entryDigsetAscii) != MD5_DIGEST_LENGTH*2 || ugcID == 0 )
		{
			// Bah!  Bogus data!
			Assert(false);
			continue;
		}

		// Is this the one they selected?
		if ( ugcID == pSelectedCacheEntry->m_hCloudID )
		{

			// This *can* happen if the list file gets lost and they reuse an image.  It means we are wasting
			// some of their cloud quota, but should be rare, and it's harmless.
			Assert( !Q_stricmp(entryDigsetAscii, pSelectedCacheEntry->m_szDigestAscii) );
			break;
		}

		pKey = pKey->GetNextTrueSubKey();
	}

	// Found it?
	int oldIndex = 0x7fffffff;
	if ( pKey )
	{

		// Renumber them in MRU order
		oldIndex = pKey->GetInt( "mru", 1 );
	}
	else
	{

		// Create a new key
		pKey = pkvMruUploadedImages->CreateNewKey();

		// Remember hash and cloud file location in subkeys
		pKey->SetString( "md5", pSelectedCacheEntry->m_szDigestAscii );
		pKey->SetUint64( "ugcid", pSelectedCacheEntry->m_hCloudID );
		//pKey->SetString( "remoteStorageName", m_chSelectedRemoteStorageNameBase );
	}
	for ( KeyValues *p = pkvMruUploadedImages->GetFirstTrueSubKey() ; p ; p = p->GetNextTrueSubKey() )
	{
		if ( p != pKey )
		{
			int mruValue = p->GetInt( "mru", 0 );
			Assert( mruValue > 0 );
			if (mruValue < oldIndex)
			{
				p->SetInt( "mru", mruValue+1 );
			}
		}
	}

	pKey->SetInt( "mru", 1);

	// Re-save the cloud-backed MRU list file
	Msg( "Saving MRU list file %s\n", k_szCustomTextureRecentListFilename );
	if ( pRemoteStorage )
	{
		CUtlBuffer listFileData;
		listFileData.SetBufferType( true, true );
		pkvMruFile->RecursiveSaveToFile( listFileData, 0 );
		pRemoteStorage->FileWrite( k_szCustomTextureRecentListFilename, listFileData.Base(), listFileData.TellPut() );
	}

	return k_EResultOK;
}

EResult CApplyCustomTextureJob::YieldingApplyTool()
{

	Msg( "Sending tool request to GC.\n" );

	// At this point, we need to know the cloud ID and hash of the image we are applying
	Assert( m_hCloudID != 0 );

	// Send the message to the GC
	GCSDK::CGCMsg< MsgGCCustomizeItemTexture_t > msg( k_EMsgGCCustomizeItemTexture );
	msg.Body().m_unToolItemID = m_nToolItemID;
	msg.Body().m_unSubjectItemID = m_nSubjectItemID;
	msg.Body().m_unImageUGCHandle = m_hCloudID;

	GCSDK::CGCMsg<MsgGCStandardResponse_t> msgReply;
	if ( !BYldSendMessageAndGetReply( msg, 10, &msgReply, k_EMsgGCCustomizeItemTextureResponse ) )
	{
		Warning( "Customize texture tool failed: Did not get reply from GC\n" );
		return k_EResultTimeout;
	}

	// OK!
	InventoryManager()->ShowItemsPickedUp( true );
	return k_EResultOK;
};

