/*
 * MODULE: STORAGE.C
 *
 *    Subroutines for data storage.
 *
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

#define MODULE_STORAGE
#include "common.h"


/*
 *             -------======= ROUTINES =======-------
 *
 * fnCheckHeader(): Check that file has requested header.
 * fnDeleteRecord(): Delete record pointed to by listview item.
 * fnFetchRecord(): Fetch a disk record into memory; return ptr to memory
 * fnMakeListRecord(): Make a list record from ADD window gadgets,
 *                     insert it into listview and add to disk.
 * fnReadListFromDisk(): Reads a list of text lines delimeted by '\n' from
 *                       disk; inserts into specified MUI (list) object.
 *                       Returns FAILURE on error.
 * fnReadRecipeData(): Read recipe data from disk...
 * fnSkipNullSpaces(): Read NULLSPACEs until no more or EOF
 * fnWriteListToDisk(): Writes a list of text lines delimeted by '\n' to
 *                      disk from a specified MUI (list) object.
 *                      Returns FAILURE on error.
 *
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
//
// fnCheckHeader(): Check that file has requested header.
//    RETURNS: 0 = FAILURE else address of start of actual data.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
APTR fnCheckHeader( char *pszText, char *pszHeader, ULONG ulSize )
{
   APTR   rc = NULL;
   ULONG  ul;


   for ( ul=0; ul < ulSize; ul++ )
   {
      if ( *( pszText+ul ) == '\n' )
      {
         *( pszText+ul ) = 0;
         if ( strcmpi( pszText, pszHeader ) == 0 )
         {
            rc = pszText+ul+1;
         }
         else
         {
            sprintf( GL_szFault, "This file is not in the proper format.\nLooking for %s.", pszHeader );
            JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
         }
         break;
      }
   }

   return( rc );
}

//
// fnDeleteRecord(): Delete record pointed to by listview item.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
BOOL fnDeleteRecord( LONG lListIndex )
{
   struct RecipeMemNode *pRMNode;
   ULONG                 ul, ulRecordLen;
   UBYTE                 ubNullSpace = NULLSPACE;
   LONG                  lDiskStat;
   BOOL                  fError;


   DoMethod( MAIN.LIS_RecipeList, MUIM_List_GetEntry, lListIndex, &pRMNode );
   Seek( MG.fhRecipes, pRMNode->ulDiskAddr, OFFSET_BEGINNING );

   lDiskStat = Read( MG.fhRecipes, &ulRecordLen, 4 );
   if ( lDiskStat != 4 )
   {
      Fault( IoErr(), "Disk read error!\n", GL_szFault, sizeof( GL_szFault ) );
      strcat( GL_szFault, "\nThe recipe was not deleted from the file." );
      JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
      return( FALSE );
   }

   Seek( MG.fhRecipes, -4, OFFSET_CURRENT );
   for ( fError = FALSE, ul = 0; ul < ulRecordLen+4; ul++ )
   {
      lDiskStat = Write( MG.fhRecipes, &ubNullSpace, 1 );
      if ( lDiskStat != 1 ) fError = TRUE;
   }
   if ( fError == TRUE )
   {
      JMH_Error( "There was an error writing to the data file.\nThe file may be corrupt.", NULL, NULL, NULL, 0 );
   }
   else
   {
      MG.ulNullSpaceSize += ( ulRecordLen+4 );
   }

   DoMethod( MAIN.LIS_RecipeList, MUIM_List_Remove, lListIndex );
}

//
// fnFetchRecord(): Fetch a disk record into memory; return ptr to memory
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
APTR fnFetchRecord( ULONG ulDiskAddr )
{
   APTR   pMemRecord = NULL;
   ULONG  ulRecordLen;
   LONG   lDiskStat;


   Seek( MG.fhRecipes, ulDiskAddr, OFFSET_BEGINNING );
   lDiskStat = Read( MG.fhRecipes, &ulRecordLen, 4 );
   if ( lDiskStat != 4 )
   {
      Fault( IoErr(), "Disk read error!\n", GL_szFault, sizeof( GL_szFault ) );
      JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
      return( NULL );
   }

   pMemRecord = AllocVec( ulRecordLen, MEMF_CLEAR );
   if ( pMemRecord == NULL )
   {
      JMH_Error( "Memory Error!\nCannot get memory for disk record.", NULL, NULL, NULL, 0 );
      return( FALSE );
   }

   lDiskStat = Read( MG.fhRecipes, pMemRecord, ulRecordLen );
   if ( lDiskStat != ulRecordLen )
   {
      Fault( IoErr(), "Disk read error!\n", GL_szFault, sizeof( GL_szFault ) );
      JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
      FreeVec( pMemRecord );
      return( FALSE );
   }

   return( pMemRecord );
}

//
// fnMakeListRecord(): Make a list record from ADD window gadgets,
//    insert it into listview and add to disk.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
BOOL fnMakeListRecord( void )
{
   BOOL   rc = FALSE;         // return pointer
   ULONG  ulRecSize, ulBlockSize, ulDiskBlockSize, ulRemainder;
   ULONG  ul, ulNumEntries;
   char  *psz, *pszData[10], *pszRecord, pszBuffer[260];
   LONG   lDiskStat;
   UBYTE  ubNullSpace = NULLSPACE;
   LONG   lListIndex;
   UBYTE  ubCourse, ubPrep, ubCost, ubTaste;
   UBYTE  ubOven, ubAdjustTimer, ubReserved1, ubReserved2;
   BOOL   fError;
   LONG   lOldRecAddr = 0;

   struct RecipeMemNode *pRMNode;
   struct TimersNode    *pTNode;


   // get active entry
   if ( MG.ubAddOrChangeMode == CHANGE_MODE )
   {
      get( MAIN.LIS_RecipeList, MUIA_List_Active, &lListIndex );
   }
   else
   {
      lListIndex = MUIV_List_Insert_Sorted;
   }

   ulBlockSize = 4 + 8;    // sizeof(recordlen)+sizeof(UBYTE array)

   get( ADD.TXT_CurrentCategory, MUIA_Text_Contents, &psz );
   ulBlockSize += ( strlen( psz ) + 1 );
   get( ADD.STR_RecipeName, MUIA_String_Contents, &psz );
   ulBlockSize += ( strlen( psz ) + 1 );
   get( ADD.STR_Comment, MUIA_String_Contents, &psz );
   ulBlockSize += ( strlen( psz ) + 1 );
   get( ADD.STR_Source, MUIA_String_Contents, &psz );
   ulBlockSize += ( strlen( psz ) + 1 );
   get( ADD.STR_Serves, MUIA_String_Contents, &psz );
   ulBlockSize += ( strlen( psz ) + 1 );
   get( ADD.POP_PicFilespec, MUIA_String_Contents, &psz );
   ulBlockSize += ( strlen( psz ) + 1 );

   get( ADD.LIS_Timers, MUIA_List_Entries, &ulNumEntries );
   ulBlockSize += ulNumEntries * ( sizeof ( struct TimersNode ) + 1 );
   ulBlockSize++;

   // add space for reserved (future) fields
   ulBlockSize += 3;    // reserved1 .. reserved3

   get( ADD.LIS_Ingredients, MUIA_List_Entries, &ulNumEntries );
   for ( ul = 0; ul < ulNumEntries; ul++ )
   {
      DoMethod( ADD.LIS_Ingredients, MUIM_List_GetEntry, ul, &psz );
      ulBlockSize += ( strlen( psz ) + 1 );
   }
   ulBlockSize++;

   get( ADD.LIS_Directions, MUIA_List_Entries, &ulNumEntries );
   for ( ul = 0; ul < ulNumEntries; ul++ )
   {
      DoMethod( ADD.LIS_Directions, MUIM_List_GetEntry, ul, &psz );
      ulBlockSize += ( strlen( psz ) + 1 );
   }
   ulBlockSize++;

   //
   // the size written to disk does NOT include the 4-byte header!!!
   // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   ulDiskBlockSize = ulBlockSize - 4;

   pszRecord = AllocVec( ulBlockSize+100, MEMF_CLEAR );
   if ( pszRecord )
   {
      // fill in disk-addr later!
      get( ADD.CYC_Course, MUIA_Cycle_Active, &ul );        ubCourse      = (UBYTE)ul;
      get( ADD.CYC_Preparation, MUIA_Cycle_Active, &ul );   ubPrep        = (UBYTE)ul;
      get( ADD.CYC_Cost, MUIA_Cycle_Active, &ul );          ubCost        = (UBYTE)ul;
      get( ADD.CYC_Taste, MUIA_Cycle_Active, &ul );         ubTaste       = (UBYTE)ul;
      get( ADD.CYC_Oven, MUIA_Cycle_Active, &ul );          ubOven        = (UBYTE)ul;
      get( ADD.CHE_AdjustTimer, MUIA_Selected, &ul );       ubAdjustTimer = (UBYTE)ul;
      ubReserved1 = 0;
      ubReserved2 = 0;

      get( ADD.TXT_CurrentCategory, MUIA_Text_Contents,   &pszData[0] );
      get( ADD.STR_RecipeName,      MUIA_String_Contents, &pszData[1] );
      get( ADD.STR_Comment,         MUIA_String_Contents, &pszData[2] );
      get( ADD.STR_Source,          MUIA_String_Contents, &pszData[3] );
      get( ADD.STR_Serves,          MUIA_String_Contents, &pszData[4] );
      get( ADD.POP_PicFilespec,     MUIA_String_Contents, &pszData[5] );

      sprintf( pszRecord, "    %c%c%c%c%c%c%c%c%s%c%s%c%s%c%s%c%s%c%s%c",
               ubCourse+1, ubPrep+1, ubCost+1, ubTaste+1,
               ubOven+1, ubAdjustTimer+1, ubReserved1+1, ubReserved2+1,
               pszData[0], LEVEL1BREAK,
               pszData[1], LEVEL1BREAK,
               pszData[2], LEVEL1BREAK,
               pszData[3], LEVEL1BREAK,
               pszData[4], LEVEL1BREAK,
               pszData[5], LEVEL1BREAK
             );

      get( ADD.LIS_Timers, MUIA_List_Entries, &ulNumEntries );
      for ( ul = 0; ul < ulNumEntries; ul++ )
      {
         DoMethod( ADD.LIS_Timers, MUIM_List_GetEntry, ul, &pTNode );
         sprintf( pszBuffer, "%s.%s.%s%c", pTNode->szTimerName,
                                           pTNode->szPrepOrBake,
                                           pTNode->szHMSTime,
                                           LEVEL2BREAK );
         strcat( pszRecord, pszBuffer );
      }
      sprintf( pszBuffer, "%c%c%c%c", LEVEL1BREAK,
                                      LEVEL1BREAK,
                                      LEVEL1BREAK,
                                      LEVEL1BREAK );
      strcat( pszRecord, pszBuffer );

      get( ADD.LIS_Ingredients, MUIA_List_Entries, &ulNumEntries );
      for ( ul = 0; ul < ulNumEntries; ul++ )
      {
         DoMethod( ADD.LIS_Ingredients, MUIM_List_GetEntry, ul, &psz );
         sprintf( pszBuffer, "%s%c", psz, LEVEL2BREAK );
         strcat( pszRecord, pszBuffer );
      }
      sprintf( pszBuffer, "%c", LEVEL1BREAK );
      strcat( pszRecord, pszBuffer );

      get( ADD.LIS_Directions, MUIA_List_Entries, &ulNumEntries );
      for ( ul = 0; ul < ulNumEntries; ul++ )
      {
         DoMethod( ADD.LIS_Directions, MUIM_List_GetEntry, ul, &psz );
         sprintf( pszBuffer, "%s%c", psz, LEVEL2BREAK );
         strcat( pszRecord, pszBuffer );
      }
      sprintf( pszBuffer, "%c", LEVEL1BREAK );
      strcat( pszRecord, pszBuffer );

      switch( MG.ubAddOrChangeMode )
      {
         case CHANGE_MODE:
            //
            // look at disk address; get rec size; if big enough,
            // replace ... else write NULLSPACEs over record and append
            // to end of file.
            // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            DoMethod( MAIN.LIS_RecipeList, MUIM_List_GetEntry, lListIndex, &pRMNode );
            Seek( MG.fhRecipes, pRMNode->ulDiskAddr, OFFSET_BEGINNING );
            lDiskStat = Read( MG.fhRecipes, &ulRecSize, 4 );
            if ( lDiskStat != 4 )
            {
               Fault( IoErr(), "Disk read error!\n", GL_szFault, sizeof( GL_szFault ) );
               strcat( GL_szFault, "\nThe recipe was not added to the file." );
               JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
               return( FALSE );
            }

            lOldRecAddr = Seek( MG.fhRecipes, -4, OFFSET_CURRENT );

            // is old record of sufficient size?
            if ( ulRecSize >= ulDiskBlockSize )
            {
               // add here... blitz remainder with NULLSPACEs...
               ulRemainder = ulRecSize - ulDiskBlockSize;
               *( (ULONG *)pszRecord ) = Seek( MG.fhRecipes, 0, OFFSET_CURRENT );
               if ( JMH_Write( MG.fhRecipes, &ulDiskBlockSize, 4 ) )
               {
                  if ( JMH_Write( MG.fhRecipes, pszRecord+4, ulDiskBlockSize ) )
                  {
                     for ( fError = FALSE, ul = 0; ul < ulRemainder; ul++ )
                     {
                        lDiskStat = Write( MG.fhRecipes, &ubNullSpace, 1 );
                        if ( lDiskStat != 1 ) fError = TRUE;
                     }
                     if ( fError == TRUE )
                     {
                        JMH_Error( "There was an error writing to the data file.\nThe file may be corrupt.", NULL, NULL, NULL, 0 );
                     }
                     else
                     {
                        MG.ulNullSpaceSize += ulRemainder;
                     }
                  }
                  else
                  {
                     JMH_Error( "There was an error writing to the data file.\nThe file may be corrupt.", NULL, NULL, NULL, 0 );
                  }
               }
               else
               {
                  JMH_Error( "There was an error writing to the data file.\nThe file may be corrupt.", NULL, NULL, NULL, 0 );
               }
            }
            else
            {
               // write to end of file
               Seek( MG.fhRecipes, 0, OFFSET_END );
               *( (ULONG *)pszRecord ) = Seek( MG.fhRecipes, 0, OFFSET_CURRENT );
               if ( JMH_Write( MG.fhRecipes, &ulDiskBlockSize, 4 ) )
               {
                  MG.ulTotalFileSize += 4;
                  if ( JMH_Write( MG.fhRecipes, pszRecord+4, ulDiskBlockSize ) )
                  {
                     MG.ulTotalFileSize += ulDiskBlockSize;

                     // blitz old record with NULLSPACEs...
                     Seek( MG.fhRecipes, lOldRecAddr-4, OFFSET_BEGINNING );
                     for ( fError = FALSE, ul = 0; ul < ulRecSize+4; ul++ )
                     {
                        lDiskStat = Write( MG.fhRecipes, &ubNullSpace, 1 );
                        if ( lDiskStat != 1 ) fError = TRUE;
                     }
                     if ( fError == TRUE )
                     {
                        JMH_Error( "There was an error writing to the data file.\nThe file may be corrupt.", NULL, NULL, NULL, 0 );
                     }
                     else
                     {
                        MG.ulNullSpaceSize += ( ulRecSize+4 );
                     }
                  }
                  else
                  {
                     JMH_Error( "There was an error writing to the data file.\nThe file may be corrupt.", NULL, NULL, NULL, 0 );
                  }
               }
               else
               {
                  JMH_Error( "There was an error writing to the data file.\nThe file may be corrupt.", NULL, NULL, NULL, 0 );
               }
            }
            rc = TRUE;
            break;
         case ADD_MODE:
            // write to end of file
            Seek( MG.fhRecipes, 0, OFFSET_END );
            *( (ULONG *)pszRecord ) = Seek( MG.fhRecipes, 0, OFFSET_CURRENT );
            if ( JMH_Write( MG.fhRecipes, &ulDiskBlockSize, 4 ) )
            {
               MG.ulTotalFileSize += 4;
               if ( JMH_Write( MG.fhRecipes, pszRecord+4, ulDiskBlockSize ) )
               {
                  MG.ulTotalFileSize += ulDiskBlockSize;
               }
            }
            rc = TRUE;
            break;
         default:
            rc = FALSE;
            break;
      }

      //
      // finally, add to listview...
      // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      if ( MG.ubAddOrChangeMode == CHANGE_MODE )
      {
         DoMethod( MAIN.LIS_RecipeList, MUIM_List_Remove, lListIndex );
      }
      DoMethod( MAIN.LIS_RecipeList, MUIM_List_Insert, &pszRecord, 1, lListIndex );
      set( MAIN.LIS_RecipeList, MUIA_List_Active, lListIndex );

      FreeVec( pszRecord );
   }
   else
   {
      rc = FALSE;
   }
   return( rc );
}

//
// fnReadListFromDisk(): Reads a list of text lines delimeted by '\n' from
//                       disk; inserts into specified MUI (list) object.
//                       Returns FAILURE on error.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
BOOL fnReadListFromDisk( char *pszFile, APTR pListObject,
                         ULONG ulSortControl, char *pszHeader )
{
   BPTR                  bpFile;
   struct FileInfoBlock *pFIB = NULL;
   BOOL                  rc = FAILURE;
   char                 *pszSave;
   ULONG                 ul;
   char                 *pTextPool;


   if ( pFIB = AllocDosObject( DOS_FIB, NULL ) )
   {
      if ( bpFile = Open( pszFile, MODE_OLDFILE ) )
      {
         if ( ExamineFH( bpFile, pFIB ) )
         {
            if ( pTextPool = (char *)AllocVec( pFIB->fib_Size, 0 ) )
            {
               if ( Read( bpFile, pTextPool, pFIB->fib_Size ) ==
                        pFIB->fib_Size )
               {
                  set( pListObject, MUIA_List_Quiet, TRUE );

                  // check file against pszHeader...
                  if ( pszSave = fnCheckHeader( pTextPool, pszHeader, pFIB->fib_Size ) )
                  {
                     rc = SUCCESS;
                     for ( ul=0; ul < pFIB->fib_Size; ul++ )
                     {
                        if ( *( pTextPool+ul ) == '\n' )
                        {
                           *( pTextPool+ul ) = 0;
                           DoMethod( pListObject, MUIM_List_Insert, &pszSave, 1, ulSortControl );
                           pszSave = pTextPool+ul+1;
                        }
                     }
                     set( pListObject, MUIA_List_Quiet, FALSE );
                  }
               }
               else
               {
                  Fault( IoErr(), "Disk read error!\n", GL_szFault, sizeof( GL_szFault ) );
                  JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
               }
               FreeVec( pTextPool );
            }
            else
            {
               JMH_Error( "Memory Error!\nNo memory for category list.", NULL, NULL, NULL, 0 );
            }
         }
         else
         {
            Fault( IoErr(), "Disk examine error!\n", GL_szFault, sizeof( GL_szFault ) );
            JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
         }
         Close( bpFile );
      }
      else
      {
         Fault( IoErr(), "File error!\n", GL_szFault, sizeof( GL_szFault ) );
         sprintf( GL_szBuf, "\nFile: %s", pszFile );
         strcat( GL_szFault, GL_szBuf );
         JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
      }
      FreeDosObject( DOS_FIB, pFIB );
   }
   else
   {
      JMH_Error( "Memory Error!\nCannot allocate DOS FIB.", NULL, NULL, NULL, 0 );
   }

   return( rc );
}

//
// fnReadRecipeData(): Read recipe data from disk...
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
BOOL fnReadRecipeData( BOOL fEntry )
{
   ULONG  ulBufSize = 2048;
   ULONG *pulBuf, ulRecordSize;
   LONG   lDiskStat;
   char   szBuf[100];
   ULONG  ul, ulStat;
   SHORT  sOK;

   struct Window        *pWindow;
   struct FileRequester *pFReq;


freq:
   if ( !fEntry )          // don't prompt first time thru...
   {
      get( MG.WIN_Main, MUIA_Window , &pWindow );
      if ( pFReq = MUI_AllocAslRequestTags( ASL_FileRequest, TAG_DONE ) )
      {
         if ( MUI_AslRequestTags( pFReq,
               ASLFO_Window         , pWindow,
               ASLFO_PrivateIDCMP   , TRUE,
               ASLFO_TitleText      , "Find Recipe File",
               ASLFR_InitialFile    , MG.szRecipeFilespec,
               TAG_DONE ) )
         {
            // set the new contents for our string gadget
            memset( MG.szRecipeFilespec, 0, sizeof( MG.szRecipeFilespec ) );
            strncpy( MG.szRecipeFilespec, pFReq->fr_Drawer, sizeof( MG.szRecipeFilespec )-1 );
            AddPart( MG.szRecipeFilespec, pFReq->fr_File,   sizeof( MG.szRecipeFilespec )-1 );
         }
         else
         {
            MG.fhRecipes = NULL;
            return( FALSE );
         }
      }
      MUI_FreeAslRequest( pFReq );
   }
   else
   {
      fEntry = FALSE;
      strcpy( MG.szRecipeFilespec, "robocook.data" );
   }

   MG.fhRecipes = Open( MG.szRecipeFilespec, MODE_OLDFILE );
   if ( ( !MG.fhRecipes ) && ( IoErr() == ERROR_OBJECT_NOT_FOUND ) )
   {
      if ( MUI_Request( MG.App, NULL, 0, NULL,
                        "*Respecify|Create New",
                        "I cannot find a recipe data file here.\n\nShould I create a new file\nor do you wish to re-specify the data directory?",
                        NULL ) == 1 )
      {
         goto freq;
      }
      else
      {
         MG.fhRecipes = Open( MG.szRecipeFilespec, MODE_NEWFILE );
         if ( !MG.fhRecipes )
         {
            Fault( IoErr(), "File open error!\n", GL_szFault, sizeof( GL_szFault ) );
            JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
            MG.fhRecipes = NULL;
            return( FALSE );
         }
         lDiskStat = Write( MG.fhRecipes, FILEID_HEADER, strlen( FILEID_HEADER ) );
         if ( lDiskStat != strlen( FILEID_HEADER ) )
         {
            Fault( IoErr(), "Cannot create new file!\n", GL_szFault, sizeof( GL_szFault ) );
            JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
            MG.fhRecipes = NULL;
            return( FALSE );
         }

         lDiskStat = Write( MG.fhRecipes, FILEID_NEW, strlen( FILEID_NEW ) );
         if ( lDiskStat != strlen( FILEID_NEW ) )
         {
            Fault( IoErr(), "Cannot create new file!\n", GL_szFault, sizeof( GL_szFault ) );
            JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
            MG.fhRecipes = NULL;
            return( FALSE );
         }

         //
         // We succeeded in creating a new file!
         // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         MG.ulTotalFileSize = strlen( FILEID_HEADER ) + strlen( FILEID_NEW );
         MG.ulNullSpaceSize = 0;
         return( TRUE );
      }
   }

   //
   // we got the file open... now let's verify that it's one of ours...
   // also, make sure we're not tagged as 'OLD '...
   // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   lDiskStat = Read( MG.fhRecipes, szBuf, strlen( FILEID_HEADER )+strlen( FILEID_NEW ) );
   if ( lDiskStat != ( strlen( FILEID_HEADER )+strlen( FILEID_NEW ) ) )
   {
      Fault( IoErr(), "Disk read error!\n", GL_szFault, sizeof( GL_szFault ) );
      JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
      Close( MG.fhRecipes );
      MG.fhRecipes = NULL;
      return( FALSE );
   }
   if ( strncmp( szBuf, FILEID_HEADER, strlen( FILEID_HEADER ) ) != 0 )
   {
      MUI_Request( MG.App, NULL, 0, NULL, "*Respecify", "This file is not a recipe data file.  Please re-specify.", NULL );
      Close( MG.fhRecipes );
      MG.fhRecipes = NULL;
      goto freq;
   }
   if ( strncmp( &(szBuf[ strlen(FILEID_HEADER) ]), FILEID_NEW, strlen( FILEID_NEW ) ) != 0 )
   {
      ul = MUI_Request( MG.App, NULL, 0, NULL,
                        "*Respecify|Force|Cancel",
                        "This recipe file is not marked as 'NEW'.\nDo you wish to re-specify or read it anyway?", NULL );
      switch( ul )
      {
         case 1:  // Respecify
            Close( MG.fhRecipes );
            goto freq;
            break;
         case 2:  // Force
            ul = MUI_Request( MG.App, NULL, 0, NULL,
                              "Yes|*No|Respecify",
                              "Do you wish to mark this file as 'NEW'?", NULL );
            switch( ul )
            {
               case 1:  // Yes
                  Seek( MG.fhRecipes, -4, OFFSET_CURRENT );
                  lDiskStat = Write( MG.fhRecipes, FILEID_NEW, strlen( FILEID_NEW ) );
                  if ( lDiskStat != strlen( FILEID_NEW ) )
                  {
                     Fault( IoErr(), "Cannot mark file as 'NEW'!\n", GL_szFault, sizeof( GL_szFault ) );
                     JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
                  }
                  break;
               case 2:  // No
                  break;
               case 0:  // Respecify
                  Close( MG.fhRecipes );
                  goto freq;
                  break;
            }
            break;
         case 0:  // Cancel
            MG.fhRecipes = NULL;
            return( FALSE );
      }
   }

   MG.ulTotalFileSize = strlen( FILEID_HEADER ) + strlen( FILEID_NEW );
   MG.ulNullSpaceSize = 0;

   //
   // ok, read the file into memory...
   // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   pulBuf = AllocVec( ulBufSize, MEMF_ANY );
   if ( !pulBuf )    JMH_Quit( "Cannot get memory for recipe buffer." );

   ulStat = 0;
   sOK = fnSkipNullSpaces( &ulStat );
   switch( sOK )
   {
      case 0:     // disk error
         Fault( IoErr(), "Disk read error!\n", GL_szFault, sizeof( GL_szFault ) );
         JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
         FreeVec( pulBuf );
         return( FALSE );
      case 1:     // skipped NULLSPACE; more data exists...
         MG.ulTotalFileSize += ulStat;
         MG.ulNullSpaceSize += ulStat;
         break;
      case 2:     // skipped NULLSPACE; we're at the EOF.
         MG.ulTotalFileSize += ulStat;
         MG.ulNullSpaceSize += ulStat;
         FreeVec( pulBuf );
         return( TRUE );
   }

   lDiskStat = Read( MG.fhRecipes, pulBuf, 4 );
   if ( lDiskStat != 4 )
   {
      Fault( IoErr(), "Disk read error!\n", GL_szFault, sizeof( GL_szFault ) );
      JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
      FreeVec( pulBuf );
      return( FALSE );
   }

   MG.ulTotalFileSize += 4;

   ulRecordSize = *( (ULONG *)pulBuf );
   while ( ulRecordSize != 0 )
   {
      // skip any leading NULLSPACEs...
      if ( ulRecordSize == 0xFCFCFCFC )
      {
         MG.ulNullSpaceSize++;
         ulStat = 0;
         sOK = fnSkipNullSpaces( &ulStat );
         switch( sOK )
         {
            case 0:     // disk error
               Fault( IoErr(), "Disk read error!\n", GL_szFault, sizeof( GL_szFault ) );
               JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
               FreeVec( pulBuf );
               return( FALSE );
            case 1:     // skipped NULLSPACE; more data exists...
               MG.ulTotalFileSize += ulStat;
               MG.ulNullSpaceSize += ulStat;
               lDiskStat = Read( MG.fhRecipes, pulBuf, 4 );
               if ( lDiskStat != 4 )
               {
                  Fault( IoErr(), "Disk read error!\n", GL_szFault, sizeof( GL_szFault ) );
                  JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
                  FreeVec( pulBuf );
                  return( FALSE );
               }
               MG.ulTotalFileSize += 4;
               ulRecordSize = *( (ULONG *)pulBuf );
               break;
            case 2:     // skipped NULLSPACE; we're at the EOF.
               MG.ulTotalFileSize += ulStat;
               MG.ulNullSpaceSize += ulStat;
               FreeVec( pulBuf );
               return( TRUE );
         }
      }

      if ( ulRecordSize > ulBufSize )
      {
         FreeVec( pulBuf );
         ulBufSize = ulRecordSize + 4 + 1;
         pulBuf = AllocVec( ulBufSize, MEMF_ANY );
         if ( pulBuf == NULL )
         {
            JMH_Quit( "Memory Error!\nNot enough memory to read all recipes." );
            return( FALSE );
         }
      }

      *pulBuf = Seek( MG.fhRecipes, 0, OFFSET_CURRENT ) - 4;
      lDiskStat = Read( MG.fhRecipes, pulBuf+1, ulRecordSize );
      if ( lDiskStat != ulRecordSize )
      {
         Fault( IoErr(), "Disk read error!\n", GL_szFault, sizeof( GL_szFault ) );
         JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
         DoMethod( MAIN.LIS_RecipeList, MUIM_List_Clear, TRUE );
         FreeVec( pulBuf );
         return( FALSE );
      }

      MG.ulTotalFileSize += ulRecordSize;

      DoMethod( MAIN.LIS_RecipeList, MUIM_List_Insert,
                (char *)&pulBuf, 1, MUIV_List_Insert_Sorted );

      lDiskStat = Read( MG.fhRecipes, pulBuf, 4 );
      if ( lDiskStat == 0 )
      {
         // END OF FILE -- BREAK THE LOOP
         ulRecordSize = 0;
         break;
      }
      else if ( lDiskStat != 4 )
      {
         Fault( IoErr(), "Disk read error!\n", GL_szFault, sizeof( GL_szFault ) );
         JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
         DoMethod( MAIN.LIS_RecipeList, MUIM_List_Clear, TRUE );
         FreeVec( pulBuf );
         return( FALSE );
      }
      MG.ulTotalFileSize += 4;
      ulRecordSize = *( (ULONG *)pulBuf );
   }

   FreeVec( pulBuf );
   return( TRUE );
}

//
// fnSkipNullSpaces(): Read NULLSPACEs until no more or EOF
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
SHORT fnSkipNullSpaces( ULONG *pulNullBytes )
{
   LONG  lDiskStat;
   UBYTE ubChar;


   lDiskStat = Read( MG.fhRecipes, &ubChar, 1 );
   if ( lDiskStat == 0 )     // if we hit EOF, just return success
   {
      return( 2 );
   }
   else if ( lDiskStat != 1 )
   {
      return( 0 );
   }

   if ( pulNullBytes )  (*pulNullBytes)++;

   while ( ( ubChar == 252 ) && ( lDiskStat == 1 ) )
   {
      lDiskStat = Read( MG.fhRecipes, &ubChar, 1 );
      if ( pulNullBytes )  (*pulNullBytes)++;
   }
   switch( lDiskStat )
   {
      case 1:
         // we've passed the NULLSPACE... reverse direction 1 byte and return
         Seek( MG.fhRecipes, -1, OFFSET_CURRENT );
         return( 1 );
      case 0:
         // we've hit EOF.
         return( 2 );
      default:
         return( 0 );      // disk error
   }
}

//
// fnWriteListToDisk(): Writes a list of text lines delimeted by '\n' to
//                      disk from a specified MUI (list) object.
//                      Returns FAILURE on error.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
BOOL fnWriteListToDisk( char *pszFile, APTR pListObject )
{
   BPTR                  bpFile;
   BOOL                  rc = SUCCESS;
   ULONG                 ulStat;
   LONG   lStat, lDiskStat;
   char  *pszStat;


   lDiskStat = DeleteFile( pszFile );
   if ( lDiskStat )
   {
      if ( bpFile = Open( pszFile, MODE_NEWFILE ) )
      {
         get( pListObject, MUIA_List_Entries, &ulStat );
         for ( lStat = 0; lStat < ulStat; lStat++ )
         {
            DoMethod( pListObject, MUIM_List_GetEntry, lStat, &pszStat );
            lDiskStat = Write( bpFile, pszStat, strlen( pszStat ) );
            if ( lDiskStat != strlen( pszStat ) )
            {
               Fault( IoErr(),
                      "Disk Error!\n",
                      GL_szFault,
                      sizeof( GL_szFault ) );
               strcat( GL_szFault, "\nFile may be corrupt." );
               JMH_Error( GL_szFault, MG.WIN_Main, NULL, NULL, 0 );
               rc = FAILURE;
               break;
            }
            lDiskStat = Write( bpFile, "\n", 1 );
            if ( lDiskStat != 1 )
            {
               Fault( IoErr(),
                      "Disk Error!\n",
                      GL_szFault,
                      sizeof( GL_szFault ) );
               strcat( GL_szFault, "\nFile may be corrupt." );
               JMH_Error( GL_szFault, MG.WIN_Main, NULL, NULL, 0 );
               rc = FAILURE;
               break;
            }
         }
         Close( bpFile );
      }
      else
      {
         Fault( IoErr(), "File error!\n", GL_szFault, sizeof( GL_szFault ) );
         JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
         rc = FAILURE;
      }
   }
   else
   {
      Fault( IoErr(), "Couldn't delete old speaker file because\n", GL_szFault, sizeof( GL_szFault ) );
      JMH_Error( GL_szFault, NULL, NULL, NULL, 0 );
      rc = FAILURE;
   }

   return( rc );
}
