/*===========================================================================
 *
 * File:	Dfbsa.CPP
 * Author:	Dave Humphrey (uesp@m0use.net)
 * Created On:	Wednesday, June 20, 2001
 *
 * Implements the base class CDFBsaFile for handling Daggerfall's BSA type
 * data files.
 *
 *=========================================================================*/

	/* Include Files */
#include "dfbsa.h"


/*===========================================================================
 *
 * Begin Local Variable Definitions
 *
 *=========================================================================*/
  DEFINE_FILE();
/*===========================================================================
 *		End of Local Variable Definitions
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Constructor
 *
 *=========================================================================*/
CDFBsaFile::CDFBsaFile () {
  //DEFINE_FUNCTION("CDFBsaFile::CDFBsaFile()");
  m_NumRecords = 0;
  m_DirType    = DFBSA_DIR_VALUE;
  m_ReadHeader = FALSE;
  m_ReadDir    = FALSE;
  m_pFilename  = NULL;

	/* Initialize arrays */
  m_pNameDir     = NULL;
  m_pValueDir    = NULL;
  m_pRecordOffsets = NULL;
 }
/*===========================================================================
 *		End of Class CDFBsaFile Constructor
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Method - void Destroy (void);
 *
 *=========================================================================*/
void CDFBsaFile::Destroy (void) {
  DEFINE_FUNCTION("CDFBsaFile::Destroy()");

	/* Delete any allocated memory */
  DestroyArrayPointer(m_pNameDir);
  DestroyArrayPointer(m_pValueDir);
  DestroyArrayPointer(m_pRecordOffsets);
  DestroyArrayPointer(m_pFilename);

	/* Clear variables */
  m_NumRecords = 0;
  m_DirType    = DFBSA_DIR_VALUE;
  m_ReadHeader = FALSE;
  m_ReadDir    = FALSE;

	/* Chain to the base class method */
  CGenFile::Destroy();
 }
/*===========================================================================
 *		End of Class Method CDFBsaFile::Destroy()
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Method - void AllocateDirectory (void);
 *
 * Deletes any current directory array and allocates a new one based on
 * the current directory type and directory size.  Also allocates the
 * array of file offsets.
 *
 *=========================================================================*/
void CDFBsaFile::AllocateDirectory (void) {
  DEFINE_FUNCTION("CDFBsaFile::AllocateDirectory()");

	/* Delete any allocated arrays */
  DestroyArrayPointer(m_pNameDir);
  DestroyArrayPointer(m_pValueDir);
  DestroyArrayPointer(m_pRecordOffsets);

	/* Ensure a valid number of directory entries */
  if (m_NumRecords <= 0) return;

	/* Allocate the appropiate structure array */
  if (m_DirType == DFBSA_DIR_NAME) {
    CreateArrayPointer(m_pNameDir, dfbsa_namedir_t, GetNumRecords()); 
   }
  else if (m_DirType == DFBSA_DIR_VALUE) {
    CreateArrayPointer(m_pValueDir, dfbsa_valuedir_t, GetNumRecords()); 
   }
  else {
    ASSERT(FALSE);
   }

	/* Allocate the record offsets array */
  CreateArrayPointer(m_pRecordOffsets, long, GetNumRecords());
 }
/*===========================================================================
 *		End of Class Method CDFBsaFile::AllocateDirectory()
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Method - boolean AssertValid (void);
 *
 * Tests the current contents to ensure validity.  ASSERTs if any object
 * error is encountered.
 *
 *=========================================================================*/
boolean CDFBsaFile::AssertValid (void) {
  DEFINE_FUNCTION("CDFBsaFile::AssertValid()");
  int  Index;
  long LastFileSize;

	/* Ignore if nothing currently read */
  if (!m_ReadDir) return (TRUE);

  ASSERT(m_ReadHeader == TRUE);
  ASSERT(m_pFilename != NULL);	
  ASSERT(m_NumRecords > 0);
  ASSERT(m_pRecordOffsets != NULL);
  ASSERT(m_pRecordOffsets[0] == 4);

	/* Ensure a valid directory type */
  ASSERT(m_DirType == DFBSA_DIR_NAME || m_DirType == DFBSA_DIR_VALUE);

	/* Check the data arrays */
  if (m_DirType == DFBSA_DIR_NAME) {
    ASSERT(m_pNameDir != NULL); 
   }
  else  {
    ASSERT(m_pValueDir != NULL); 
   }

	/* Check the directory array */
  for (Index = 0; Index < m_NumRecords; Index++) {
    if (m_DirType == DFBSA_DIR_NAME) {
      ASSERT(m_pNameDir[Index].Name [0] != NULL_CHAR); 
      ASSERT(m_pNameDir[Index].Size >= 0);
      LastFileSize = m_pNameDir[Index].Size;
     }
    else  {
      ASSERT(m_pValueDir[Index].Size >= 0);
      LastFileSize = m_pValueDir[Index].Size;
     }

   }

	/* Ensure a correct file length */
  long FileSize = ::GetFileSize(m_pFilename);
  ASSERT(FileSize == m_pRecordOffsets[m_NumRecords-1] + LastFileSize + GetDirSize());

  return (TRUE);
 }
/*===========================================================================
 *		End of Class Method CDFBsaFile::AssertValid()
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Method - void Close (void);
 *
 * Closes an opened BSA file.
 *
 *=========================================================================*/
void CDFBsaFile::Close (void) { 

	/* Ignore if not currently open */
  if (IsOpen()) {
    DestroyArrayPointer(m_pFilename);
   }

	/* Chain to base class method */
  CGenFile::Close();
 }
/*===========================================================================
 *		End of Class Method CDFBsaFile::Close()
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Method - void ComputeRecordOffsets (void);
 *
 * Protected class method which computes the offsets of the files relative
 * to the start of the BSA file.
 *
 *=========================================================================*/
void CDFBsaFile::ComputeRecordOffsets (void) {
  DEFINE_FUNCTION("CDFBsaFile::ComputeRecordOffsets()");
  short LoopCounter;
  long  Offset = 4;	/* Start after the BSA header */

	/* Ensure directory has been read */
  ASSERT(m_ReadDir && m_pRecordOffsets != NULL);

  if (m_DirType == DFBSA_DIR_NAME) {
    for (LoopCounter = 0; LoopCounter < m_NumRecords; LoopCounter++) {
      m_pRecordOffsets[LoopCounter] = Offset;
      Offset += m_pNameDir[LoopCounter].Size;
     }
   }
  else if (m_DirType == DFBSA_DIR_VALUE) {
    for (LoopCounter = 0; LoopCounter < m_NumRecords; LoopCounter++) {
      m_pRecordOffsets[LoopCounter] = Offset;
      Offset += m_pValueDir[LoopCounter].Size;
     }
   }
  
 }
/*===========================================================================
 *		End of Class Method CDFBsaFile::ComputeRecordOffsets()
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Method - void Dump (pFileHandle);
 *
 * Outputs object information to the given file stream and the current
 * system log.
 *
 *=========================================================================*/
void CDFBsaFile::Dump (FILE* pFileHandle) {
  int LoopCounter;
  int MaxCount;
  int FirstCount;
  
	/* Output basic object information */
  SystemLog.Printf (pFileHandle, "CDFBbsaFile Object (0x%08p)", this);
  SystemLog.Printf (pFileHandle, "\tFilename   = %s", m_pFilename);
  SystemLog.Printf (pFileHandle, "\tNumRecords = %d", m_NumRecords);
  SystemLog.Printf (pFileHandle, "\tDirType    = 0x%04X", m_DirType);
  SystemLog.Printf (pFileHandle, "\tOpen       = %s", BooleanToString(IsOpen()));

	/* Output first five directory records */
  MaxCount = (m_NumRecords >= 5) ? 5 : (int) m_NumRecords;
  SystemLog.Printf (pFileHandle, "\tDirectory Entries...");

  for (LoopCounter = 0; LoopCounter < MaxCount; LoopCounter++) 
    DumpDirEntry(pFileHandle, LoopCounter);

	/* Output last five directory records */
  FirstCount = (m_NumRecords >= 10) ? m_NumRecords - 5 : MaxCount;
  SystemLog.Printf (pFileHandle, "\t\t....");

  for (LoopCounter = FirstCount; LoopCounter < (int)m_NumRecords; LoopCounter++) 
    DumpDirEntry(pFileHandle, LoopCounter);

 }
/*===========================================================================
 *		End of Class Method CDFBsaFile::Dump()
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Method - void DumpDirEntry (pFileHandle, Index);
 *
 * Dumps the specified directory entry to the given file stream.  
 * Protected class method.
 *
 *=========================================================================*/
void CDFBsaFile::DumpDirEntry (FILE* pFileHandle, const int Index) {
  DEFINE_FUNCTION("CDFBsaFile::DumpDirEntry()");

	/* Ensure a valid index */
  ASSERT(IsValidIndex(Index));

	/* Output based on the directory type */
  switch (m_DirType) {
    case DFBSA_DIR_NAME:
      ASSERT(m_pNameDir != NULL);
      SystemLog.Printf (pFileHandle, "\t\t%d) '%s' = 0x%08lX bytes at 0x%08lX", Index, m_pNameDir[Index].Name,m_pNameDir[Index].Size, m_pRecordOffsets[Index]);
      break;
    case DFBSA_DIR_VALUE:
      ASSERT(m_pValueDir != NULL);
      SystemLog.Printf (pFileHandle, "\t\t%d) 0x%08lX = 0x%08lX bytes at 0x%08lX", Index, m_pValueDir[Index].Value, m_pValueDir[Index].Size, m_pRecordOffsets[Index]);
      break;
    default:
      SystemLog.Printf (pFileHandle, "\t\t%d) Unknown directory type!", Index);
      break;
    }

 }
/*===========================================================================
 *		End of Class Method CDFBsaFile::DumpDirEntry()
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Method - boolean Extract (pFilename, Index);
 *
 * Extracts the given record index to the specified filename.  Returns FALSE
 * on any error.
 *
 *=========================================================================*/
boolean CDFBsaFile::Extract (const char* pFilename, const size_t Index) {
  DEFINE_FUNCTION("CDFBsaFile::Extract()");
  CGenFile OutputFile;
  boolean  Result;
  char     Buffer[DFBSA_EXTRACT_BUFFERSIZE+1];
  size_t   BytesRead;
  size_t   InputSize;
  long     TotalBytesRead;
  long     FileSize;

	/* Ensure valid input */
  ASSERT(IsValidIndex(Index) && pFilename != NULL);

	/* Attempt to jump to start of file offset */
  Result = Seek(m_pRecordOffsets[Index], SEEK_SET);
  if (!Result) return (FALSE);

	/* Attempt to open file for output */
  Result = OutputFile.Open(pFilename, "wb");
  if (!Result) return (FALSE);

  FileSize = GetRecordSize(Index);
  TotalBytesRead = 0;
  
	/* Read/write file in sections */
  while (TotalBytesRead < FileSize) {
    if (TotalBytesRead + DFBSA_EXTRACT_BUFFERSIZE > FileSize) InputSize = (size_t)(FileSize - TotalBytesRead);

		/* Input data */
    Result = Read(Buffer, BytesRead, InputSize);
    if (!Result) return (FALSE);

		/* Output data */
    Result = OutputFile.Write(Buffer, InputSize);
    if (!Result) return (FALSE);

    TotalBytesRead += (long) BytesRead;
   }
  
	/* Ensure an exact output */
  ASSERT (TotalBytesRead == FileSize);
  return (TRUE);
 }
/*===========================================================================
 *		End of Class Method CDFBsaFile::Extract()
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Method - int FindRecord (Value);
 *
 * Attempts to find the directory entry with the given value.  Returns 
 * a negative value if the file was not found or the BSA file does not 
 * contain a value type directory.  Returns the directory index on success.
 * The BSA directory must have been previously read.
 *
 *=========================================================================*/
int CDFBsaFile::FindRecord (const long Value) {
  DEFINE_FUNCTION("CDFBsaFile::FindRecord(long)");
  short Index;

	/* Ensure valid input */
  ASSERT(m_pValueDir != NULL && m_ReadDir && m_DirType == DFBSA_DIR_VALUE);

	/* Ensure correct directory type */
  if (m_DirType != DFBSA_DIR_VALUE) {
    ErrorHandler.AddError(DFERR_BSA_DIRTYPE);
    return (-1);
   }

	/* Search array for value match */
  for (Index = 0; Index < m_NumRecords; Index++) {
    if (m_pValueDir[Index].Value == Value) return (Index);
   }

	/* No match found! */
  ErrorHandler.AddError(DFERR_BSA_NOTFOUND, "Value=%ld", Value);
  return (-1);
 }
/*===========================================================================
 *		End of Class Method CDFBsaFile::CFindFile()
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Method - int FindRecord (pName);
 *
 * Attempts to find the directory entry with the given name.  Returns 
 * a negative value if the file was not found or the BSA file does not 
 * contain a name type directory.  Returns the directory index on success.
 * The BSA directory must have been previously read.  The string comparison
 * is case insensitive.
 *
 *=========================================================================*/
int CDFBsaFile::FindRecord (const char* pName) {
  DEFINE_FUNCTION("CDFBsaFile::FindRecord(char*)");
  short Index;

  	/* Ensure valid input */
  ASSERT(pName != NULL && m_pNameDir != NULL);
  ASSERT(m_ReadDir && m_DirType == DFBSA_DIR_NAME);

	/* Ensure correct directory type */
  if (m_DirType != DFBSA_DIR_NAME) {
    ErrorHandler.AddError(DFERR_BSA_DIRTYPE);
    return (-1);
   }

	/* Search array for name match */
  for (Index = 0; Index < m_NumRecords; Index++) {
    if (stricmp(m_pNameDir[Index].Name, pName) == 0) return (Index);
   }

	/* No match found! */
  ErrorHandler.AddError(DFERR_BSA_NOTFOUND, "Name='%s'", pName);
  return (-1);
 }
/*===========================================================================
 *		End of Class Method CDFBsaFile::FindRecord()
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Method - void* GetDirectory (void);
 *
 * Attempts to return a pointer to the directory array depending on the
 * current directory type.  ASSERTs if the current array is not valid.
 * Protected class method.
 *
 *=========================================================================*/
void* CDFBsaFile::GetDirectory (void) {
  DEFINE_FUNCTION("CDFBsaFile::GetDirectory()");

  switch (m_DirType) {
    case DFBSA_DIR_NAME:
      ASSERT(m_pNameDir != NULL);
      return (void *)(m_pNameDir);

    case DFBSA_DIR_VALUE:
      ASSERT(m_pValueDir != NULL);
      return (void*)(m_pValueDir);
   } 

	/* Unknown directory type? */
  ASSERT(FALSE);
  return (NULL);
 }
/*===========================================================================
 *		End of Class Method CDFBsaFile::GetDirectory()
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Method - boolean Open (pFilename, pMode);
 *
 * Attempt to open the given BSA file for input/output.  Returns FALSE
 * on any error.  Currently only binary input modes are supported.  On
 * success, the BSA header and directory are input.
 *
 *=========================================================================*/
boolean CDFBsaFile::Open (const char* pFilename, const char* pMode) {
  DEFINE_FUNCTION("CDFBsaFile::Open()");
  boolean Result;
  char    NewMode[4] = "rb";

	/* Ensure valid input */
  ASSERT(pMode != NULL);

	/* Check mode types */
  if (pMode[0] == 'r')
    strcpy(NewMode, "rb");
  else {
    ErrorHandler.AddError(DFERR_BSA_READONLY);
    return (FALSE);
   }

	/* Attempt to open file */
  Result = CGenFile::Open(pFilename, NewMode);
  
	/* Attempt to read the BSA file header and directory */
  if (Result) Result = ReadHeader();
  if (Result) Result = ReadDirectory();

	/* Close file on any error */
  if (!Result) 
    Close();
  else
    SetFilename(pFilename);

  return (Result);
 }
/*===========================================================================
 *		End of Class Method CDFBsaFile::Open()
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Method - boolean ReadHeader (void);
 *
 * Protected class method which attempts to input the 4 byte BSA header
 * from the file.  The file must be open.  Returns FALSE on any error.
 *
 *=========================================================================*/
boolean CDFBsaFile::ReadHeader (void) {
  DEFINE_FUNCTION("CDFBsaFile::ReadHeader()");
  boolean Result;

	/* File must be open */
  ASSERT(IsOpen());
  m_ReadHeader = FALSE;
	  
	/* Read in the 4 byte BSA header from start of file */
  Result = Seek(0, SEEK_SET);
  if (Result) Result = ReadShort(m_NumRecords);
  if (Result) Result = ReadShort(m_DirType);
  if (!Result) return (FALSE);

	/* Do a quick check to ensure valid input */
  if (m_DirType != DFBSA_DIR_NAME && m_DirType != DFBSA_DIR_VALUE) {
    ErrorHandler.AddError(DFERR_BSA_DIRTYPE, "File may not be a valid BSA file!");
    return (FALSE);
   }
  else if (m_NumRecords <= 0 || m_NumRecords > 32000) {
    ErrorHandler.AddError(DFERR_BSA_NUMFILES, "NumFiles=%d", m_NumRecords);
    return (FALSE);
   }
  
	/* Return success */
  m_ReadHeader = TRUE;
  return (TRUE);
 }
/*===========================================================================
 *		End of Class Method CDFBsaFile::ReadHeader()
 *=========================================================================*/


/*===========================================================================
 *
 * Class CDFBsaFile Method - boolean ReadDirectory (void);
 *
 * Protected class method which attempts to input the directory from the
 * BSA file.  The file must be open and the BSA header previously read.
 * Returns FALSE on any error.
 *
 *=========================================================================*/
boolean CDFBsaFile::ReadDirectory (void) { 
  DEFINE_FUNCTION("CDFBsaFile::ReadDirectory()");
  boolean Result;

	/* File must be open and header previously read */
  ASSERT(IsOpen() && m_ReadHeader);

	/* Create the directory array to store input */
  AllocateDirectory();

	/* Move to start of the directory and read */
  Result = Seek(-GetDirSize(), SEEK_END);
  if (Result) Result = Read((char *)GetDirectory(), (size_t)GetDirSize());
  if (!Result) return (FALSE);
  m_ReadDir = TRUE;

	/* Compute the file offsets */
  ComputeRecordOffsets();

	/* Return success */
  m_ReadDir = TRUE;
  return (TRUE);
 }
/*===========================================================================
 *		End of Class Method CDFBsaFile::ReadDirectory()
 *=========================================================================*/