/*
VFileSystem009.h - C++ interface for filesystem_stdio
Copyright (C) 2022 Alibek Omarov

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 3 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.
*/
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <stdarg.h>
#include ALLOCA_H
#include "crtlib.h"
#include "filesystem.h"
#include "filesystem_internal.h"
#include "VFileSystem009.h"

#if __cplusplus < 201103L
#define override
#endif

// GoldSrc Directories and ID
// GAME          gamedir
// GAMECONFIG    gamedir (rodir integration?)
// GAMEDOWNLOAD  gamedir_downloads (gamedir/downloads for us)
// GAME_FALLBACK liblist.gam's fallback_dir
// ROOT and BASE rootdir
// PLATFORM      platform
// CONFIG        platform/config

static inline qboolean IsIdGamedir( const char *id )
{
	return !Q_strcmp( id, "GAME" ) ||
		!Q_strcmp( id, "GAMECONFIG" ) ||
		!Q_strcmp( id, "GAMEDOWNLOAD" );
}

static inline const char *IdToDir( char *dir, size_t size, const char *id )
{
	if( !Q_strcmp( id, "GAME" ))
		return GI->gamefolder;
	else if( !Q_strcmp( id, "GAMEDOWNLOAD" ))
	{
		Q_snprintf( dir, size, "%s/downloaded", GI->gamefolder );
		return dir;
	}
	else if( !Q_strcmp( id, "GAMECONFIG" ))
		return fs_writepath->filename; // full path here so it's totally our write allowed directory
	else if( !Q_strcmp( id, "PLATFORM" ))
		return "platform"; // stub
	else if( !Q_strcmp( id, "CONFIG" ))
		return "platform/config"; // stub
	else // ROOT || BASE
		return fs_rootdir; // give at least root directory
}

static inline void CopyAndFixSlashes( char *p, const char *in )
{
	Q_strcpy( p, in );
	COM_FixSlashes( p );
}

class CXashFS : public IVFileSystem009
{
private:
	class CSearchState
	{
	public:
		CSearchState( CSearchState **head, search_t *search ) :
			next( *head ), search( search ), index( 0 )
		{
			if( *head )
				handle = (*head)->handle + 1;
			else handle = 0;

			*head = this;
		}
		~CSearchState()
		{
			Mem_Free( search );
		}

		CSearchState *next;
		search_t *search;
		int index;
		FileFindHandle_t handle;
	};

	CSearchState *searchHead;

	CSearchState *GetSearchStateByHandle( FileFindHandle_t handle )
	{
		for( CSearchState *state = searchHead; state; state = state->next )
		{
			if( state->handle == handle )
			{
				return state;
			}
		}

		Con_DPrintf( "Can't find search state by handle %d\n", handle );
		return NULL;
	}

public:
	CXashFS() : searchHead( NULL )
	{
	}

	void RemoveAllSearchPaths() override
	{
		FS_ClearSearchPath();
	}

	void AddSearchPath( const char *path, const char *id ) override
	{
		char *p = (char *)alloca( Q_strlen( path ) + 1 );
		CopyAndFixSlashes( p, path );

		FS_AddGameDirectory( p, FS_CUSTOM_PATH );
	}

	void AddSearchPathNoWrite( const char *path, const char *id ) override
	{
		char *p = (char *)alloca( Q_strlen( path ) + 1 );
		CopyAndFixSlashes( p, path );

		FS_AddGameDirectory( p, FS_NOWRITE_PATH | FS_CUSTOM_PATH );
	}

	bool RemoveSearchPath( const char *id ) override
	{
		// TODO:
		return true;
	}

	void RemoveFile( const char *path, const char *id ) override
	{
		FS_Delete( path ); // FS_Delete is aware of slashes
	}

	void CreateDirHierarchy( const char *path, const char *id ) override
	{
		char dir[MAX_VA_STRING], fullpath[MAX_VA_STRING];

		Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", IdToDir( dir, sizeof( dir ), id ), path );
		FS_CreatePath( fullpath ); // FS_CreatePath is aware of slashes
	}

	bool FileExists( const char *path ) override
	{
		char *p = (char *)alloca( Q_strlen( path ) + 1 );
		CopyAndFixSlashes( p, path );

		return FS_FileExists( p, false );
	}

	bool IsDirectory( const char *path ) override
	{
		char *p = (char *)alloca( Q_strlen( path ) + 1 );
		CopyAndFixSlashes( p, path );

		return FS_SysFolderExists( p );
	}

	FileHandle_t Open( const char *path, const char *mode, const char *id ) override
	{
		char *p = (char *)alloca( Q_strlen( path ) + 1 );
		CopyAndFixSlashes( p, path );

		file_t *fd = FS_Open( p, mode, IsIdGamedir( id ) );

		return fd;
	}

	void Close( FileHandle_t handle ) override
	{
		FS_Close( (file_t *)handle );
	}

	void Seek( FileHandle_t handle, int offset, FileSystemSeek_t whence ) override
	{
		int whence_ = SEEK_SET;
		switch( whence )
		{
		case FILESYSTEM_SEEK_HEAD: whence_ = SEEK_SET; break;
		case FILESYSTEM_SEEK_CURRENT: whence_ = SEEK_CUR; break;
		case FILESYSTEM_SEEK_TAIL: whence_ = SEEK_END; break;
		}

		FS_Seek( (file_t *)handle, offset, whence_ );
	}

	unsigned int Tell( FileHandle_t handle ) override
	{
		return FS_Tell( (file_t *)handle );
	}

	unsigned int Size( FileHandle_t handle ) override
	{
		file_t *fd = (file_t *)handle;
		return fd->real_length;
	}

	unsigned int Size( const char *path ) override
	{
		char *p = (char *)alloca( Q_strlen( path ) + 1 );
		CopyAndFixSlashes( p, path );
		return FS_FileSize( p, false );
	}

	long int GetFileTime( const char *path ) override
	{
		char *p = (char *)alloca( Q_strlen( path ) + 1 );
		CopyAndFixSlashes( p, path );
		return FS_FileTime( p, false );
	}

	void FileTimeToString( char *p, int size, long int time ) override
	{
		time_t curtime = time;
		char *buf = ctime( &curtime );
		Q_strncpy( p, buf, size );
	}

	bool IsOk( FileHandle_t handle ) override
	{
		return !FS_Eof( (file_t *)handle );
	}

	void Flush( FileHandle_t handle ) override
	{
		FS_Flush( (file_t *)handle );
	}

	bool EndOfFile( FileHandle_t handle ) override
	{
		return FS_Eof( (file_t *)handle );
	}

	int Read( void *buf, int size, FileHandle_t handle ) override
	{
		return FS_Read( (file_t *)handle, buf, size );
	}

	int Write( const void *buf, int size, FileHandle_t handle ) override
	{
		return FS_Write( (file_t *)handle, buf, size );
	}

	char *ReadLine( char *buf, int size, FileHandle_t handle ) override
	{
		int c = FS_Gets( (file_t *)handle, (byte*)buf, size );

		return c >= 0 ? buf : NULL;
	}

	int FPrintf( FileHandle_t handle, char *fmt, ... ) override
	{
		va_list ap;
		int ret;

		va_start( ap, fmt );
		ret = FS_VPrintf( (file_t *)handle, fmt, ap );
		va_end( ap );

		return ret;
	}

	void * GetReadBuffer(FileHandle_t, int *size, bool) override
	{
		// deprecated by Valve
		*size = 0;
		return NULL;
	}

	void ReleaseReadBuffer(FileHandle_t, void *) override
	{
		// deprecated by Valve
		return;
	}

	const char *FindFirst(const char *pattern, FileFindHandle_t *handle, const char *id) override
	{
		if( !handle || !pattern )
			return NULL;

		char *p = (char *)alloca( Q_strlen( pattern ) + 1 );
		CopyAndFixSlashes( p, pattern );
		search_t *search = FS_Search( p, true, IsIdGamedir( id ));

		if( !search )
			return NULL;

		CSearchState *state = new CSearchState( &searchHead, search );

		*handle = state->handle;

		return state->search->filenames[0];
	}

	const char *FindNext( FileFindHandle_t handle ) override
	{
		CSearchState *state = GetSearchStateByHandle( handle );

		if( !state ) return NULL;

		if( state->index + 1 >= state->search->numfilenames )
			return NULL;

		return state->search->filenames[++state->index];
	}

	bool FindIsDirectory( FileFindHandle_t handle ) override
	{
		CSearchState *state = GetSearchStateByHandle( handle );

		if( !state )
			return false;

		if( state->index >= state->search->numfilenames )
			return false;

		return IsDirectory( state->search->filenames[state->index] );
	}

	void FindClose( FileFindHandle_t handle ) override
	{
		for( CSearchState *state = searchHead, **prev = NULL;
			  state;
			  *prev = state, state = state->next )
		{
			if( state->handle == handle )
			{
				if( prev )
					(*prev)->next = state->next;
				else searchHead = state->next;

				delete state;

				return;
			}
		}

		Con_DPrintf( "FindClose: Can't find search state by handle %d\n", handle );
		return;
	}

	const char * GetLocalPath( const char *name, char *buf, int size ) override
	{
		if( !name ) return NULL;

		char *p = (char *)alloca( Q_strlen( name ) + 1 );
		CopyAndFixSlashes( p, name );

#if !XASH_WIN32
		if( p[0] == '/' )
#else
		if( Q_strchr( p, ':' ))
#endif
		{
			Q_strncpy( buf, p, size );

			return buf;
		}

		const char *fullpath = FS_GetDiskPath( p, false );
		if( !fullpath )
			return NULL;

		Q_strncpy( buf, fullpath, size );
		return buf;
	}

	char *ParseFile( char *buf, char *token, bool *quoted ) override
	{
		qboolean qquoted;

		char *p = COM_ParseFileSafe( buf, token, PFILE_FS_TOKEN_MAX_LENGTH, 0, NULL, &qquoted );
		if( quoted ) *quoted = qquoted;

		return p;
	}

	bool FullPathToRelativePath( const char *path, char *out ) override
	{
		if( !COM_CheckString( path ))
		{
			*out = 0;
			return false;
		}

		char *p = (char *)alloca( Q_strlen( path ) + 1 );
		CopyAndFixSlashes( p, path );
		searchpath_t *sp;

		for( sp = fs_searchpaths; sp; sp = sp->next )
		{
			size_t splen = Q_strlen( sp->filename );

			if( !Q_strnicmp( sp->filename, p, splen ))
			{
				Q_strcpy( out, p + splen + 1 );
				return true;
			}
		}

		Q_strcpy( out, p );
		return false;
	}

	bool GetCurrentDirectory( char *p, int size ) override
	{
		Q_strncpy( p, fs_rootdir, size );

		return true;
	}

	void PrintOpenedFiles() override
	{
		// we don't track this yet
		return;
	}

	void SetWarningFunc(void (*)(const char *, ...)) override
	{
		// TODO:
		return;
	}

	void SetWarningLevel(FileWarningLevel_t) override
	{
		// TODO:
		return;
	}

	int SetVBuf( FileHandle_t handle, char *buf, int mode, long int size ) override
	{
		// TODO:
		return 0;
	}

	void GetInterfaceVersion(char *p, int size) override
	{
		Q_strncpy( p, "Stdio", size );
	}

	bool AddPackFile( const char *path, const char *id ) override
	{
		char dir[MAX_VA_STRING], fullpath[MAX_VA_STRING];

		Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", IdToDir( dir, sizeof( dir ), id ), path );
		CopyAndFixSlashes( fullpath, path );

		return !!FS_AddPak_Fullpath( fullpath, NULL, FS_CUSTOM_PATH );
	}

	FileHandle_t OpenFromCacheForRead( const char *path , const char *mode, const char *id ) override
	{
		char *p = (char *)alloca( Q_strlen( path ) + 1 );
		CopyAndFixSlashes( p, path );

		return FS_OpenReadFile( p, mode, IsIdGamedir( id ));
	}

	// stubs
	void Mount() override {}
	void Unmount() override {}
	void GetLocalCopy(const char *) override {}
	void LogLevelLoadStarted(const char *) override {}
	void LogLevelLoadFinished(const char *) override {}
	void CancelWaitForResources(WaitForResourcesHandle_t) override {}
	int HintResourceNeed(const char *, int) override { return 0; }
	WaitForResourcesHandle_t WaitForResources(const char *) override { return 0; }
	int PauseResourcePreloading() override { return 0; }
	int ResumeResourcePreloading() override { return 0; }
	bool IsAppReadyForOfflinePlay(int) override { return true; }
	bool IsFileImmediatelyAvailable(const char *) override { return true; }
	bool GetWaitForResourcesProgress(WaitForResourcesHandle_t, float *pProgress, bool *pOverride) override
	{
		if( pProgress ) *pProgress = 0;
		if( pOverride ) *pOverride = true;
		return false;
	}
} g_VFileSystem009;

extern "C" void EXPORT *CreateInterface( const char *interface, int *retval )
{
	if( !Q_strcmp( interface, "VFileSystem009" ))
	{
		if( retval ) *retval = 0;
		return &g_VFileSystem009;
	}

	if( !Q_strcmp( interface, FS_API_CREATEINTERFACE_TAG ))
	{
		// return a copy, to disallow overriding
		static fs_api_t copy = { 0 };

		if( !copy.InitStdio )
			memcpy( &copy, &g_api, sizeof( copy ));

		if( retval ) *retval = 0;
		return &copy;
	}

	if( retval ) *retval = 1;
	return NULL;
}
