/*
wad.c - WAD support for filesystem
Copyright (C) 2003-2006 Mathieu Olivier
Copyright (C) 2000-2007 DarkPlaces contributors
Copyright (C) 2007 Uncle Mike
Copyright (C) 2015-2023 Xash3D FWGS contributors

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 <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#if XASH_POSIX
#include <unistd.h>
#endif
#include <errno.h>
#include <stddef.h>
#include "port.h"
#include "filesystem_internal.h"
#include "crtlib.h"
#include "common/com_strings.h"
#include "wadfile.h"

/*
========================================================================
.WAD archive format	(WhereAllData - WAD)

List of compressed files, that can be identify only by TYPE_*

<format>
header:	dwadinfo_t[dwadinfo_t]
file_1:	byte[dwadinfo_t[num]->disksize]
file_2:	byte[dwadinfo_t[num]->disksize]
file_3:	byte[dwadinfo_t[num]->disksize]
...
file_n:	byte[dwadinfo_t[num]->disksize]
infotable	dlumpinfo_t[dwadinfo_t->numlumps]
========================================================================
*/
#define WAD3_NAMELEN	16
#define HINT_NAMELEN	5	// e.g. _mask, _norm
#define MAX_FILES_IN_WAD	65535	// real limit as above <2Gb size not a lumpcount

#include "const.h"

typedef struct
{
	int		ident;		// should be WAD3
	int		numlumps;		// num files
	int		infotableofs;	// LUT offset
} dwadinfo_t;

typedef struct
{
	int		filepos;		// file offset in WAD
	int		disksize;		// compressed or uncompressed
	int		size;		// uncompressed
	signed char	type;		// TYP_*
	signed char	attribs;		// file attribs
	signed char	pad0;
	signed char	pad1;
	char		name[WAD3_NAMELEN];	// must be null terminated
} dlumpinfo_t;

struct wfile_s
{
	int		infotableofs;
	int		numlumps;
	poolhandle_t mempool;			// W_ReadLump temp buffers
	file_t		*handle;
	dlumpinfo_t	*lumps;
	time_t		filetime;
};

// WAD errors
#define WAD_LOAD_OK			0
#define WAD_LOAD_COULDNT_OPEN		1
#define WAD_LOAD_BAD_HEADER		2
#define WAD_LOAD_BAD_FOLDERS		3
#define WAD_LOAD_TOO_MANY_FILES	4
#define WAD_LOAD_NO_FILES		5
#define WAD_LOAD_CORRUPTED		6

typedef struct wadtype_s
{
	const char		*ext;
	signed char		type;
} wadtype_t;

// associate extension with wad type
static const wadtype_t wad_types[7] =
{
{ "pal", TYP_PALETTE	}, // palette
{ "dds", TYP_DDSTEX 	}, // DDS image
{ "lmp", TYP_GFXPIC		}, // quake1, hl pic
{ "fnt", TYP_QFONT		}, // hl qfonts
{ "mip", TYP_MIPTEX		}, // hl/q1 mip
{ "txt", TYP_SCRIPT		}, // scripts
{ NULL,  TYP_NONE		}
};

/*
===========
W_TypeFromExt

Extracts file type from extension
===========
*/
static signed char W_TypeFromExt( const char *lumpname )
{
	const char	*ext = COM_FileExtension( lumpname );
	const wadtype_t	*type;

	// we not known about filetype, so match only by filename
	if( !Q_strcmp( ext, "*" ) || !COM_CheckStringEmpty( ext ))
		return TYP_ANY;

	for( type = wad_types; type->ext; type++ )
	{
		if( !Q_stricmp( ext, type->ext ))
			return type->type;
	}
	return TYP_NONE;
}

/*
===========
W_ExtFromType

Convert type to extension
===========
*/
static const char *W_ExtFromType( signed char lumptype )
{
	const wadtype_t	*type;

	// we not known aboyt filetype, so match only by filename
	if( lumptype == TYP_NONE || lumptype == TYP_ANY )
		return "";

	for( type = wad_types; type->ext; type++ )
	{
		if( lumptype == type->type )
			return type->ext;
	}
	return "";
}

/*
===========
W_FindLump

Serach for already existed lump
===========
*/
static dlumpinfo_t *W_FindLump( wfile_t *wad, const char *name, const signed char matchtype )
{
	int	left, right;

	if( !wad || !wad->lumps || matchtype == TYP_NONE )
		return NULL;

	// look for the file (binary search)
	left = 0;
	right = wad->numlumps - 1;

	while( left <= right )
	{
		int	middle = (left + right) / 2;
		int	diff = Q_stricmp( wad->lumps[middle].name, name );

		if( !diff )
		{
			if(( matchtype == TYP_ANY ) || ( matchtype == wad->lumps[middle].type ))
				return &wad->lumps[middle]; // found
			else if( wad->lumps[middle].type < matchtype )
				diff = 1;
			else if( wad->lumps[middle].type > matchtype )
				diff = -1;
			else break; // not found
		}

		// if we're too far in the list
		if( diff > 0 ) right = middle - 1;
		else left = middle + 1;
	}

	return NULL;
}

/*
====================
W_AddFileToWad

Add a file to the list of files contained into a package
and sort LAT in alpha-bethical order
====================
*/
static dlumpinfo_t *W_AddFileToWad( const char *wadfile, const char *name, wfile_t *wad, dlumpinfo_t *newlump )
{
	int		left, right;
	dlumpinfo_t	*plump;

	// look for the slot we should put that file into (binary search)
	left = 0;
	right = wad->numlumps - 1;

	while( left <= right )
	{
		int	middle = ( left + right ) / 2;
		int	diff = Q_stricmp( wad->lumps[middle].name, name );

		if( !diff )
		{
			if( wad->lumps[middle].type < newlump->type )
				diff = 1;
			else if( wad->lumps[middle].type > newlump->type )
				diff = -1;
			else Con_Reportf( S_WARN "Wad %s contains the file %s several times\n", wadfile, name );
		}

		// If we're too far in the list
		if( diff > 0 ) right = middle - 1;
		else left = middle + 1;
	}

	// we have to move the right of the list by one slot to free the one we need
	plump = &wad->lumps[left];
	memmove( plump + 1, plump, ( wad->numlumps - left ) * sizeof( *plump ));
	wad->numlumps++;

	*plump = *newlump;
	memcpy( plump->name, name, sizeof( plump->name ));

	return plump;
}

/*
===========
FS_CloseWAD

finalize wad or just close
===========
*/
static void FS_CloseWAD( wfile_t *wad )
{
	Mem_FreePool( &wad->mempool );
	if( wad->handle != NULL )
		FS_Close( wad->handle );
	Mem_Free( wad ); // free himself
}

/*
===========
FS_Close_WAD
===========
*/
static void FS_Close_WAD( searchpath_t *search )
{
	FS_CloseWAD( search->wad );
}

/*
===========
FS_OpenFile_WAD
===========
*/
static file_t *FS_OpenFile_WAD( searchpath_t *search, const char *filename, const char *mode, int pack_ind )
{
	return NULL;
}

/*
===========
W_Open

open the wad for reading & writing
===========
*/
static wfile_t *W_Open( const char *filename, int *error )
{
	wfile_t		*wad = (wfile_t *)Mem_Calloc( fs_mempool, sizeof( wfile_t ));
	int		i, lumpcount;
	dlumpinfo_t	*srclumps;
	size_t		lat_size;
	dwadinfo_t	header;

	// NOTE: FS_Open is load wad file from the first pak in the list (while fs_ext_path is false)
	if( fs_ext_path )
	{
		int ind;
		searchpath_t *search = FS_FindFile( filename, &ind, NULL, 0, false );

		// allow direct absolute paths
		// TODO: catch them in FS_FindFile_DIR!
		if( !search || ind < 0 )
		{
			wad->handle = FS_SysOpen( filename, "rb" );
		}
		else
		{
			wad->handle = search->pfnOpenFile( search, filename, "rb", ind );
		}
	}
	else
	{
		const char *basename = COM_FileWithoutPath( filename );
		wad->handle = FS_Open( basename, "rb", false );
	}

	if( wad->handle == NULL )
	{
		Con_Reportf( S_ERROR "W_Open: couldn't open %s: %s\n", filename, strerror( errno ));
		if( error ) *error = WAD_LOAD_COULDNT_OPEN;
		FS_CloseWAD( wad );
		return NULL;
	}

	// copy wad name
	wad->filetime = FS_SysFileTime( filename );
	wad->mempool = Mem_AllocPool( filename );

	if( FS_Read( wad->handle, &header, sizeof( dwadinfo_t )) != sizeof( dwadinfo_t ))
	{
		Con_Reportf( S_ERROR "W_Open: %s can't read header\n", filename );
		if( error ) *error = WAD_LOAD_BAD_HEADER;
		FS_CloseWAD( wad );
		return NULL;
	}

	if( header.ident != IDWAD2HEADER && header.ident != IDWAD3HEADER )
	{
		Con_Reportf( S_ERROR "W_Open: %s is not a WAD2 or WAD3 file\n", filename );
		if( error ) *error = WAD_LOAD_BAD_HEADER;
		FS_CloseWAD( wad );
		return NULL;
	}

	lumpcount = header.numlumps;

	if( lumpcount >= MAX_FILES_IN_WAD )
	{
		Con_Reportf( S_WARN "W_Open: %s is full (%i lumps)\n", filename, lumpcount );
		if( error ) *error = WAD_LOAD_TOO_MANY_FILES;
	}
	else if( lumpcount <= 0 )
	{
		Con_Reportf( S_ERROR "W_Open: %s has no lumps\n", filename );
		if( error ) *error = WAD_LOAD_NO_FILES;
		FS_CloseWAD( wad );
		return NULL;
	}
	else if( error ) *error = WAD_LOAD_OK;

	wad->infotableofs = header.infotableofs; // save infotableofs position

	if( FS_Seek( wad->handle, wad->infotableofs, SEEK_SET ) == -1 )
	{
		Con_Reportf( S_ERROR "W_Open: %s can't find lump allocation table\n", filename );
		if( error ) *error = WAD_LOAD_BAD_FOLDERS;
		FS_CloseWAD( wad );
		return NULL;
	}

	lat_size = lumpcount * sizeof( dlumpinfo_t );

	// NOTE: lumps table can be reallocated for O_APPEND mode
	srclumps = (dlumpinfo_t *)Mem_Malloc( wad->mempool, lat_size );

	if( FS_Read( wad->handle, srclumps, lat_size ) != lat_size )
	{
		Con_Reportf( S_ERROR "W_ReadLumpTable: %s has corrupted lump allocation table\n", filename );
		if( error ) *error = WAD_LOAD_CORRUPTED;
		Mem_Free( srclumps );
		FS_CloseWAD( wad );
		return NULL;
	}

	// starting to add lumps
	wad->lumps = (dlumpinfo_t *)Mem_Calloc( wad->mempool, lat_size );
	wad->numlumps = 0;

	// sort lumps for binary search
	for( i = 0; i < lumpcount; i++ )
	{
		char	name[16];
		int	k;

		// cleanup lumpname
		Q_strnlwr( srclumps[i].name, name, sizeof( srclumps[i].name ));

		// check for '*' symbol issues (quake1)
		k = Q_strlen( Q_strrchr( name, '*' ));
		if( k ) name[Q_strlen( name ) - k] = '!';

		// check for Quake 'conchars' issues (only lmp loader really allows to read this lame pic)
		if( srclumps[i].type == 68 && !Q_stricmp( srclumps[i].name, "conchars" ))
			srclumps[i].type = TYP_GFXPIC;

		W_AddFileToWad( filename, name, wad, &srclumps[i] );
	}

	// release source lumps
	Mem_Free( srclumps );

	// and leave the file open
	return wad;
}

/*
===========
FS_FileTime_WAD

===========
*/
static int FS_FileTime_WAD( searchpath_t *search, const char *filename )
{
	return search->wad->filetime;
}

/*
===========
FS_PrintInfo_WAD

===========
*/
static void FS_PrintInfo_WAD( searchpath_t *search, char *dst, size_t size )
{
	Q_snprintf( dst, size, "%s (%i files)", search->filename, search->wad->numlumps );
}

/*
===========
FS_FindFile_WAD

===========
*/
static int FS_FindFile_WAD( searchpath_t *search, const char *path, char *fixedname, size_t len )
{
	dlumpinfo_t	*lump;
	signed char		type = W_TypeFromExt( path );
	qboolean		anywadname = true;
	string		wadname, wadfolder;
	string		shortname;

	// quick reject by filetype
	if( type == TYP_NONE )
		return -1;

	COM_ExtractFilePath( path, wadname );
	wadfolder[0] = '\0';

	if( COM_CheckStringEmpty( wadname ))
	{
		string wadbasename;

		COM_FileBase( wadname, wadbasename, sizeof( wadbasename ));

		Q_strncpy( wadfolder, wadbasename, sizeof( wadfolder ));
		Q_snprintf( wadname, sizeof( wadname ), "%s.wad", wadbasename );
		anywadname = false;
	}

	// make wadname from wad fullpath
	COM_FileBase( search->filename, shortname, sizeof( shortname ));
	COM_DefaultExtension( shortname, ".wad", sizeof( shortname ));

	// quick reject by wadname
	if( !anywadname && Q_stricmp( wadname, shortname ))
		return -1;

	// NOTE: we can't using long names for wad,
	// because we using original wad names[16];
	COM_FileBase( path, shortname, sizeof( shortname ));

	lump = W_FindLump( search->wad, shortname, type );

	if( lump )
	{
		if( fixedname )
			Q_strncpy( fixedname, lump->name, len );
		return lump - search->wad->lumps;
	}

	return -1;
}

/*
===========
FS_Search_WAD

===========
*/
static void FS_Search_WAD( searchpath_t *search, stringlist_t *list, const char *pattern, int caseinsensitive )
{
	string	wadpattern, wadname, temp2;
	signed char	type = W_TypeFromExt( pattern );
	qboolean	anywadname = true;
	string	wadfolder, temp;
	int j, i;
	const char *slash, *backslash, *colon, *separator;
	char buf[MAX_VA_STRING];

	// quick reject by filetype
	if( type == TYP_NONE )
		return;

	COM_ExtractFilePath( pattern, wadname );
	COM_FileBase( pattern, wadpattern, sizeof( wadpattern ));
	wadfolder[0] = '\0';

	if( COM_CheckStringEmpty( wadname ))
	{
		string wadbasename;

		COM_FileBase( wadname, wadbasename, sizeof( wadbasename ));

		Q_strncpy( wadfolder, wadbasename, sizeof( wadfolder ));
		Q_snprintf( wadname, sizeof( wadname ), "%s.wad", wadbasename );
		anywadname = false;
	}

	// make wadname from wad fullpath
	COM_FileBase( search->filename, temp2, sizeof( temp2 ));
	COM_DefaultExtension( temp2, ".wad", sizeof( temp2 ));

	// quick reject by wadname
	if( !anywadname && Q_stricmp( wadname, temp2 ))
		return;

	for( i = 0; i < search->wad->numlumps; i++ )
	{
		// if type not matching, we already have no chance ...
		if( type != TYP_ANY && search->wad->lumps[i].type != type )
			continue;

		// build the lumpname with image suffix (if present)
		Q_strncpy( temp, search->wad->lumps[i].name, sizeof( temp ));

		while( temp[0] )
		{
			if( matchpattern( temp, wadpattern, true ))
			{
				for( j = 0; j < list->numstrings; j++ )
				{
					if( !Q_strcmp( list->strings[j], temp ))
						break;
				}

				if( j == list->numstrings )
				{
					// build path: wadname/lumpname.ext
					Q_snprintf( temp2, sizeof( temp2 ), "%s/%s", wadfolder, temp );
					Q_snprintf( buf, sizeof( buf ), ".%s", W_ExtFromType( search->wad->lumps[i].type ));
					COM_DefaultExtension( temp2, buf, sizeof( temp2 ));
					stringlistappend( list, temp2 );
				}
			}

			// strip off one path element at a time until empty
			// this way directories are added to the listing if they match the pattern
			slash = Q_strrchr( temp, '/' );
			backslash = Q_strrchr( temp, '\\' );
			colon = Q_strrchr( temp, ':' );
			separator = temp;
			if( separator < slash )
				separator = slash;
			if( separator < backslash )
				separator = backslash;
			if( separator < colon )
				separator = colon;
			*((char *)separator) = 0;
		}
	}
}


/*
===========
W_ReadLump

reading lump into temp buffer
===========
*/
static byte *W_ReadLump( searchpath_t *search, const char *path, int pack_ind, fs_offset_t *lumpsizeptr, void *( *pfnAlloc )( size_t ), void ( *pfnFree )( void * ))
{
	const wfile_t *wad = search->wad;
	const dlumpinfo_t *lump = &wad->lumps[pack_ind];
	size_t	oldpos, size = 0;
	byte	*buf;

	// assume error
	if( lumpsizeptr ) *lumpsizeptr = 0;

	// no wads loaded
	if( !wad || !lump ) return NULL;

	oldpos = FS_Tell( wad->handle ); // don't forget restore original position

	if( FS_Seek( wad->handle, lump->filepos, SEEK_SET ) == -1 )
	{
		Con_Reportf( S_ERROR "W_ReadLump: %s is corrupted\n", lump->name );
		FS_Seek( wad->handle, oldpos, SEEK_SET );
		return NULL;
	}

	buf = (byte *)pfnAlloc( lump->disksize );
	if( unlikely( !buf ))
	{
		Con_Reportf( S_ERROR "%s: can't alloc %d bytes, no free memory\n", __func__, lump->disksize );
		FS_Seek( wad->handle, oldpos, SEEK_SET );
		return NULL;
	}

	size = FS_Read( wad->handle, buf, lump->disksize );
	FS_Seek( wad->handle, oldpos, SEEK_SET );

	if( size < lump->disksize )
	{
		Con_Reportf( S_WARN "W_ReadLump: %s is probably corrupted\n", lump->name );
		pfnFree( buf );
		return NULL;
	}

	if( lumpsizeptr ) *lumpsizeptr = lump->disksize;

	return buf;
}

/*
====================
FS_AddWad_Fullpath
====================
*/
searchpath_t *FS_AddWad_Fullpath( const char *wadfile, int flags )
{
	searchpath_t *search;
	wfile_t *wad;
	int errorcode = WAD_LOAD_COULDNT_OPEN;

	wad = W_Open( wadfile, &errorcode );

	if( !wad )
	{
		if( errorcode != WAD_LOAD_NO_FILES )
			Con_Reportf( S_ERROR "FS_AddWad_Fullpath: unable to load wad \"%s\"\n", wadfile );
		return NULL;
	}

	search = (searchpath_t *)Mem_Calloc( fs_mempool, sizeof( searchpath_t ));
	Q_strncpy( search->filename, wadfile, sizeof( search->filename ));
	search->wad = wad;
	search->type = SEARCHPATH_WAD;
	search->flags = flags;

	search->pfnPrintInfo = FS_PrintInfo_WAD;
	search->pfnClose = FS_Close_WAD;
	search->pfnOpenFile = FS_OpenFile_WAD;
	search->pfnFileTime = FS_FileTime_WAD;
	search->pfnFindFile = FS_FindFile_WAD;
	search->pfnSearch = FS_Search_WAD;
	search->pfnLoadFile = W_ReadLump;

	Con_Reportf( "Adding WAD: %s (%i files)\n", wadfile, wad->numlumps );
	return search;
}
