//////////////////////////////////////////////////////////////////////////////
//
// Asura_PS2_TOCCache.cpp
//
// Copyright (c) 2003-2005 Rebellion. All rights reserved.
//
//////////////////////////////////////////////////////////////////////////////

// I N C L U D E S ///////////////////////////////////////////////////////////

#include "Core/Asura.h"

// This file's header.
#include "FileAccess/Asura_PS2_TOCCache.h"

// Stdlib independent.
#include <ctype.h>
#include <string.h>

// Engine independent.
#include "Core/Asura_String.h"
#include "Debugging/Asura_Assert.h"
#include "Debugging/Asura_Logfile.h"

#if ASURA_PS2_USE_TOC_CACHE

// C O N S T A N T S /////////////////////////////////////////////////////////

static const u_int MAX_DIRS_TO_PROCESS = 256;

// G L O B A L S /////////////////////////////////////////////////////////////

static u_char* g_pucSectorBuf = NULL;

// P R O T O T Y P E S   A N D   I N L I N E   F U N C T I O N S /////////////

static void ReadSector( sceCdRMode* pxReadMode, u_int uSectorNum );

static inline u_int ReadUnalignedInt( const u_char* pucIn )
{
    u_int uValue;

    uValue  = pucIn[0];
    uValue += pucIn[1] << 8;
    uValue += pucIn[2] << 16;
    uValue += pucIn[3] << 24;

    return uValue;
}

// C L A S S   I M P L E M E N T A T I O N S /////////////////////////////////

struct Asura_PS2_TOCCache::Dir
{
    char  szName[256];
    u_int uFirstSector;
    u_int uNumSectors;
    bool  bIsRootDir;
};

Asura_PS2_TOCCache::Entry Asura_PS2_TOCCache::s_axTOCCache[ASURA_PS2_MAX_TOC_ENTRIES];
u_int Asura_PS2_TOCCache::s_uNumEntries = 0;
char Asura_PS2_TOCCache::s_szExecutableName[EXE_NAME_LEN + 1] = "";
char Asura_PS2_TOCCache::s_szISOTimestamp[ISO_TIMESTAMP_LEN + 1] = "";

// (PSJE-14.01.2003) Cache the CD/DVDs TOC at startup to avoid sceCdSearchFile
// (MP - 07.05.2003) Adapted from lowcdaccess sample on ps2-pro.com.
void Asura_PS2_TOCCache::ReadTOC( void )
{
	s_uNumEntries = 0;

	g_pucSectorBuf = reinterpret_cast<u_char*>( Asura_AllocateAlign(2048, ASURA_ALIGN_64_BYTE) );

    Dir*  pxDirs   = new Dir[MAX_DIRS_TO_PROCESS + 1]; // Keep a spare entry at the end so we always have workspace.
    u_int uNumDirs = 0;

	sceCdRMode xReadMode;
	xReadMode.trycount    = 0;
	xReadMode.spindlctrl  = SCECdSpinNom;
	xReadMode.datapattern = SCECdSecS2048;

	sceCdSync( 0 );

    // Retrieve required information from primary volume descriptor.
    ProcessPVD( &xReadMode, &pxDirs[0] );

    // Finish setting up root dir info.
    {
        pxDirs[0].szName[0] = '\\';
        pxDirs[0].szName[1] = '\0';

        pxDirs[0].bIsRootDir = true;

        uNumDirs = 1;
    }

    bool bHitLimit = false;
    for( u_int uNextDir = 0; !bHitLimit && uNextDir < uNumDirs; ++uNextDir )
	{
        for( u_int uSector = 0; !bHitLimit && uSector < pxDirs[uNextDir].uNumSectors; ++uSector )
        {
            ReadSector( &xReadMode, pxDirs[uNextDir].uFirstSector + uSector );

		    for( u_int uByte = 0; g_pucSectorBuf[uByte]; uByte += g_pucSectorBuf[uByte] )
		    {
			    // Ignore "." and "..".
			    if( 1 == g_pucSectorBuf[uByte + 32] && (0 == g_pucSectorBuf[uByte + 33] || 1 == g_pucSectorBuf[uByte + 33]) )
                {
                    continue;
                }

                // Dir?
                bool bIsDir = ( 0 != (g_pucSectorBuf[uByte + 25] & 2) );

                if( (bIsDir && uNumDirs >= MAX_DIRS_TO_PROCESS) || (!bIsDir && s_uNumEntries >= ASURA_PS2_MAX_TOC_ENTRIES) )
                {
                    bHitLimit = true;
                    break;
                }

                // Keep ptr to beginning of actual name.
                const char* szEntryName;

                // Copy the name into the next dir's name. If it's actually
                // a dir then that's where it belongs anyway; otherwise we
                // just need to stick the name somewhere temporary so we can
                // hash it.
                {
                    char* szOut = pxDirs[uNumDirs].szName;

                    const char* szParentDir = pxDirs[uNextDir].szName;
                    while( *szParentDir )
                    {
                        *szOut++ = *szParentDir++;
                    }

                    szEntryName = szOut;

                    const char* szNext = reinterpret_cast<const char*>( &g_pucSectorBuf[uByte + 33] );
                    const char* szLast = reinterpret_cast<const char*>( &g_pucSectorBuf[uByte + 32 + g_pucSectorBuf[uByte + 32]] );

                    while( szNext <= szLast )
                    {
					    *szOut++ = *szNext++;
                    }

                    if( bIsDir )
                    {
                        *szOut++ = '\\';
                    }

				    *szOut = '\0';
                }

#ifdef ASURA_PS2_DEMODISK
				// ignore if not our root dir
				if (!strstr(pxDirs[uNumDirs].szName, ASURA_PS2_DEMODISK_ROOT))
				{
					continue;
				}
#endif

                u_int uEntryFirstSector = ReadUnalignedInt( &g_pucSectorBuf[uByte +  2] );
                u_int uEntryLength      = ReadUnalignedInt( &g_pucSectorBuf[uByte + 10] );

				if( bIsDir )
				{
					pxDirs[uNumDirs].uFirstSector = uEntryFirstSector;
                    pxDirs[uNumDirs].uNumSectors  = (uEntryLength + 2047) / 2048;
                    pxDirs[uNumDirs].bIsRootDir   = false;
                    ++uNumDirs;
				}
				else // not bIsDir
				{
					s_axTOCCache[s_uNumEntries].m_uFilenameHash  = Asura_GetHashID( pxDirs[uNumDirs].szName );
					s_axTOCCache[s_uNumEntries].m_uFirstSector   = uEntryFirstSector;
					s_axTOCCache[s_uNumEntries].m_uLengthInBytes = uEntryLength;

                    // Store extension.
                    {
                        s_axTOCCache[s_uNumEntries].m_uExtAsUint = 0;

                        const char* szExtStart = strrchr( pxDirs[uNumDirs].szName, '.' );
                        if( szExtStart )
                        {
                            ++szExtStart;

                            const char* szSemiColon = strchr( szExtStart, ';' );
                            if( szSemiColon && szSemiColon - szExtStart <= 3 )
                            {
                                char* szExtOut = s_axTOCCache[s_uNumEntries].m_szExtension;
                                while( szExtStart < szSemiColon )
                                {
                                    *szExtOut++ = *szExtStart++;
                                }
                            }
                        }
                    }

                    // Is this the executable?
                    // (MP - 27.06.2003) For now, assume that it will reside
                    // in the root directory. This isn't necessarily true -
                    // eg. demo discs - but I think it probably will be for
                    // the cases we're interested in.
                    if( pxDirs[uNextDir].bIsRootDir )
                    {
                        if( EXE_NAME_LEN + 2 == strlen(szEntryName) ) // +2 for the ";1" at the end.
                        {
                            if( 'S' == szEntryName[0] && '_' == szEntryName[4] && '.' == szEntryName[8] )
                            {
                                bool bAllDigits = true;
                                
                                for( u_int u = 5; u <  8; ++u ) { if( !isdigit(szEntryName[u]) ) { bAllDigits = false; } }
                                for( u_int u = 9; u < 11; ++u ) { if( !isdigit(szEntryName[u]) ) { bAllDigits = false; } }

                                if( bAllDigits )
                                {
                                    Asura_Assert( !s_szExecutableName[0], "Multiple executables?" );

                                    strncpy( s_szExecutableName, szEntryName, EXE_NAME_LEN );
                                    s_szExecutableName[EXE_NAME_LEN] = '\0';
                                }
                            }
                        }
                    }

					++s_uNumEntries;
				}
                // End else not bIsDir
		    }
            // End loop through this sector's bytes.
        }
        // End loop through this dir's sectors.
	}
    // End loop through dirs.

    if( bHitLimit )
    {
        Asura_OutputToDebugger_Retail(( "ERROR: Exceeded max %s limit while caching CD TOC.\n", (uNumDirs >= MAX_DIRS_TO_PROCESS ? "dirs" : "files") ));

        // (MP - 21.07.2003) Make sure this doesn't get missed because it'll
        // be quite subtle in some cases, eg. music not playing at one
        // particular point in a level.
        while( true ) { Asura_Assert( false, "" ); }
    }
	
    delete[] pxDirs;

	Asura_DeallocateAlign( g_pucSectorBuf );
    g_pucSectorBuf = NULL;

	Asura_OutputToDebugger(( "Cached %u files from TOC.\n", s_uNumEntries ));
}

// (PSJE-14.01.2003) replacement for sceCdSearchFile which uses the cached TOC
int	Asura_PS2_TOCCache::CdSearchFile( sceCdlFILE* pxFileInfo, const char* szName )
{
    // Skip leading "cdrom0:" if necessary.
    if( !strncmp(szName, "cdrom0:", 7) ) { szName += 7; }
    
	const Asura_Hash_ID uHashedName = Asura_GetHashID( szName );
	for( u_int u = 0; u < s_uNumEntries; ++u )
	{
		if( uHashedName == s_axTOCCache[u].m_uFilenameHash )
		{
			// These are the only values that should be used...
			pxFileInfo->lsn  = s_axTOCCache[u].m_uFirstSector;
			pxFileInfo->size = s_axTOCCache[u].m_uLengthInBytes;

			return 1;
		}
	}

	return 0;
}

void Asura_PS2_TOCCache::ProcessPVD( sceCdRMode* pxReadMode, Dir* pxRootDir )
{
    // PVD is at sector 16.
    ReadSector( pxReadMode, 16 );

    // Sector offset is at bytes 158-161 within this sector.
    pxRootDir->uFirstSector = ReadUnalignedInt( &g_pucSectorBuf[158] );   

    // ISO size is at bytes 166-169. On DVDs this is not aligned to the
    // sector size so we need to account for that here.
    pxRootDir->uNumSectors = (ReadUnalignedInt(&g_pucSectorBuf[166]) + 2047) / 2048;

    // The volume creation string starts at byte 813. We'll fiddle with it
    // a little bit to make it more readable. The input data is in the form:
    // YYYYMMDDhhmmssff (where ff is obviously(!) hundredths of a second.)
    {
        const char* szSrcData = reinterpret_cast<const char*>(g_pucSectorBuf) + 813;
        Asura_PrintfToBfr( s_szISOTimestamp,
                           "%.4s-%.2s-%.2s @ %.2s:%.2s",
                           ISO_TIMESTAMP_LEN + 1, true,
                           szSrcData + 0, szSrcData + 4, szSrcData + 6,
                           szSrcData + 8, szSrcData + 10 );
    }
}

// F U N C T I O N   I M P L E M E N T A T I O N S ///////////////////////////

static void ReadSector( sceCdRMode* pxReadMode, u_int uSectorNum )
{
    // Check previous commands have finished.
    sceCdSync( 0 );

    while( true )
    {
        // Wait until we can issue another command.
        sceCdDiskReady( 0 );

        while( !sceCdRead(uSectorNum, 1, g_pucSectorBuf, pxReadMode) ) { }

        // Wait for that to complete (sceCdRead() is non-blocking.)
        sceCdSync( 0 );

        // Check that command completed successfully. If not, we'll try
        // again.
        int iErr = sceCdGetError();
        if( SCECdErNO == iErr )
        {
            break;
        }
        else
        {
		    Asura_OutputToDebugger_Retail(( "Asura_PS2_TOCCache: sceCdGetError() returned %i.\n", iErr ));
        }
    }
}

#else // #if ASURA_PS2_USE_TOC_CACHE

int Asura_PS2_TOCCache::CdSearchFile( sceCdlFILE* pxFileInfo, const char* szName )
{
	Asura_Assert(false, "shouldn't be called - not a CD build");
	return 0;
}

#endif // #else not #if ASURA_PS2_USE_TOC_CACHE
