/*
 * dbwrite.c
 * File revision 0
 * Write some data to a database.
 * (c) 2000 Jacob Lundberg, jacob@chaos2.org
 */


/*
 * 2000.10.26	Initial implementation
 */


#include <stdio.h>
#include <errno.h>
#include "db.h"


int db_write(DB_FILE *db_fp, char *key, void *data, size_t length) {
/*
 * db_write()
 * Write some data to an open database described by db_fp.
 */

   char *cur_key;
   db_header header;
   db_data_header data_header;
   int key_size = db_key_size(db_fp);
   int hash_size = db_hash_size(db_fp);
   off_t last_pos = 0;
   off_t data_pos = 0;
   off_t next_pos = 0;
   off_t table_pos = 0;

   /* Take a read lock on the database header. */
   if(db_lock_read(db_fp->file, 0)) return(-1);

   /* Read the header. */
   if(fseek(db_fp->file, 0, SEEK_SET) || !fread(&header, DB_HEADER_SIZE, 1, db_fp->file))
      return(-1);

   /* Drop the read lock from the header. */
   db_unlock_read(db_fp->file, 0);

   /* Determine the expected offset of the correct hash item. */
   table_pos = header.trans_table + DB_LOCK_SIZE + sizeof(off_t) * hash(key, hash_size);

   /* Take a write lock on the lookup table. */
   if(db_lock_write(db_fp->file, header.trans_table)) return(-1);

   /* Now data_pos will store the expected data offset. */
   if(fseek(db_fp->file, table_pos, SEEK_SET) || !fread(&data_pos, sizeof(off_t), 1, db_fp->file))
      return(-1);

   /* Take the case where there's no data at this hash yet (no data at all). */
   if(data_pos < DB_HEADER_SIZE) {
      /* Find a free zone large enough for this data chunk. */
      data_pos = get_locked_free_zone(db_fp, length);
      if(data_pos < DB_HEADER_SIZE) {
         errno = ENOSPC;
         return(-1);
      }

      /* Upgrade the advisory lock taken by get_locked_free_zone to a write lock. */
      if(db_upgrade_advisory(db_fp->file, data_pos)) {
         put_locked_free_zone(db_fp, data_pos, length);
         return(-1);
      }

      /* Read in the data header. */
      if(fseek(db_fp->file, data_pos, SEEK_SET) ||
            !fread(&data_header, DB_DATA_HEADER_SIZE, 1, db_fp->file)) {
         put_locked_free_zone(db_fp, data_pos, length);
         return(-1);
      }

      data_header.size = length;
      data_header.next = 0;

      /* Write the new data header. */
      if(fseek(db_fp->file, data_pos, SEEK_SET) ||
            !fwrite(&data_header, DB_DATA_HEADER_SIZE, 1, db_fp->file)) {
         put_locked_free_zone(db_fp, data_pos, length);
         return(-1);
      }

      /* Write the new key. */
      if(fseek(db_fp->file, data_pos + DB_DATA_HEADER_SIZE, SEEK_SET) ||
            !fwrite(key, key_size, 1, db_fp->file)) {
         put_locked_free_zone(db_fp, data_pos, length);
         return(-1);
      }

      /* Write the new data. */
      if(fseek(db_fp->file, data_pos + DB_DATA_HEADER_SIZE + key_size, SEEK_SET) ||
            !fwrite(data, length, 1, db_fp->file)) {
         put_locked_free_zone(db_fp, data_pos, length);
         return(-1);
      }

      /* Drop the lock on the new data. */
      db_unlock_write(db_fp->file, data_pos);

      /* Write out the new translation pointer. */
      if(fseek(db_fp->file, table_pos, SEEK_SET) ||
            !fwrite(&data_pos, sizeof(off_t), 1, db_fp->file)) {
         put_locked_free_zone(db_fp, data_pos, length);
         return(-1);
      }

      /* Unlock the translation table. */
      db_unlock_write(db_fp->file, header.trans_table);

      /* Done!  =) */
      return(0);
   }

   /*
    * Now the case where we're writing to an extant chain.  Look for
    * an entry with the same key (replace) and if we don't find one
    * then append the new data to the end of the list.
    */

   while(data_pos >= DB_HEADER_SIZE) {
      /* Lock the new data. */
      if(db_lock_write(db_fp->file, data_pos))
         return(-1);

      /* Read in the current data header. */
      if(fseek(db_fp->file, data_pos, SEEK_SET) ||
            !fread(&data_header, DB_DATA_HEADER_SIZE, 1, db_fp->file))
         return(-1);

      /* Allocate for testing the key. */
      cur_key = (char *)malloc(key_size);
      if(cur_key == NULL) {
         errno = ENOMEM;
         return(-1);
      }

      /* Read in the current key for comparison. */
      if(fseek(db_fp->file, data_pos + DB_DATA_HEADER_SIZE, SEEK_SET) ||
            !fread(cur_key, key_size, 1, db_fp->file)) {
         free(cur_key);
         return(-1);
      }

      /* Check whether this is the same data and needs replaced. */
      if(!strncmp(cur_key, key, key_size)) {
         /* Remember where the next data will be. */
         next_pos = data_header.next;

         /* Toss the old data. */
         put_locked_free_zone(db_fp, data_pos, length);

         /* Find a free zone large enough for this data chunk. */
         data_pos = get_locked_free_zone(db_fp, length);
         if(data_pos < DB_HEADER_SIZE) {
            errno = ENOSPC;
            return(-1);
         }

         /* Upgrade the advisory lock taken by get_locked_free_zone to a write lock. */
         if(db_upgrade_advisory(db_fp->file, data_pos)) {
            put_locked_free_zone(db_fp, data_pos, length);
            return(-1);
         }

         /* Read in the data header. */
         if(fseek(db_fp->file, data_pos, SEEK_SET) ||
               !fread(&data_header, DB_DATA_HEADER_SIZE, 1, db_fp->file)) {
            put_locked_free_zone(db_fp, data_pos, length);
            return(-1);
         }

         data_header.size = length;
         data_header.next = next_pos;

         /* Write the new data header. */
         if(fseek(db_fp->file, data_pos, SEEK_SET) ||
               !fwrite(&data_header, DB_DATA_HEADER_SIZE, 1, db_fp->file)) {
            put_locked_free_zone(db_fp, data_pos, length);
            return(-1);
         }

         /* Write the new key. */
         if(fseek(db_fp->file, data_pos + DB_DATA_HEADER_SIZE, SEEK_SET) ||
               !fwrite(key, key_size, 1, db_fp->file)) {
            put_locked_free_zone(db_fp, data_pos, length);
            return(-1);
         }

         /* Write the new data. */
         if(fseek(db_fp->file, data_pos + DB_DATA_HEADER_SIZE + key_size, SEEK_SET) ||
               !fwrite(data, length, 1, db_fp->file)) {
            put_locked_free_zone(db_fp, data_pos, length);
            return(-1);
         }

         /* Drop the lock on the new data. */
         db_unlock_write(db_fp->file, data_pos);

         if(last_pos) {
            /* Read in the old last data header. */
            if(fseek(db_fp->file, last_pos, SEEK_SET) ||
                  !fread(&data_header, DB_DATA_HEADER_SIZE, 1, db_fp->file)) {
               put_locked_free_zone(db_fp, data_pos, length);
               return(-1);
            }

            /* Set the new next value on the old last header. */
            data_header.next = data_pos;

            /* Write out the old last data header. */
            if(fseek(db_fp->file, last_pos, SEEK_SET) ||
                  !fwrite(&data_header, DB_DATA_HEADER_SIZE, 1, db_fp->file)) {
               put_locked_free_zone(db_fp, data_pos, length);
               return(-1);
            }

            /* Drop the lock on the prev data. */
            db_unlock_write(db_fp->file, last_pos);
         } else {
            /* Write out the new translation pointer. */
            if(fseek(db_fp->file, table_pos, SEEK_SET) ||
                  !fwrite(&data_pos, sizeof(off_t), 1, db_fp->file)) {
               put_locked_free_zone(db_fp, data_pos, length);
               return(-1);
            }
         }

         /* Drop the lock from the lookup table. */
         db_unlock_write(db_fp->file, header.trans_table);

         /* Success and completion. */
         free(cur_key);
         return(0);
      }

      /* Free the temp key space. */
      free(cur_key);

      /* If it has been set, unlock the old data. */
      if(last_pos >= DB_HEADER_SIZE)
         db_unlock_write(db_fp->file, last_pos);

      /* Remember our parent for the next iteration. */
      last_pos = data_pos;
      data_pos = data_header.next;
   }

   /* Drop the lock from the lookup table. */
   db_unlock_write(db_fp->file, header.trans_table);

   /* Add to the end of the chain.  Wheee! */

   /* Find a free zone large enough for this data chunk. */
   data_pos = get_locked_free_zone(db_fp, length);
   if(data_pos < DB_HEADER_SIZE) {
      errno = ENOSPC;
      return(-1);
   }

   /* Upgrade the advisory lock taken by get_locked_free_zone to a write lock. */
   if(db_upgrade_advisory(db_fp->file, data_pos)) {
      put_locked_free_zone(db_fp, data_pos, length);
      return(-1);
   }

   /* Read in the data header. */
   if(fseek(db_fp->file, data_pos, SEEK_SET) ||
         !fread(&data_header, DB_DATA_HEADER_SIZE, 1, db_fp->file)) {
      put_locked_free_zone(db_fp, data_pos, length);
      return(-1);
   }

   data_header.size = length;
   data_header.next = 0;

   /* Write the new data header. */
   if(fseek(db_fp->file, data_pos, SEEK_SET) ||
         !fwrite(&data_header, DB_DATA_HEADER_SIZE, 1, db_fp->file)) {
      put_locked_free_zone(db_fp, data_pos, length);
      return(-1);
   }

   /* Write the new key. */
   if(fseek(db_fp->file, data_pos + DB_DATA_HEADER_SIZE, SEEK_SET) ||
         !fwrite(key, key_size, 1, db_fp->file)) {
      put_locked_free_zone(db_fp, data_pos, length);
      return(-1);
   }

   /* Write the new data. */
   if(fseek(db_fp->file, data_pos + DB_DATA_HEADER_SIZE + key_size, SEEK_SET) ||
         !fwrite(data, length, 1, db_fp->file)) {
      put_locked_free_zone(db_fp, data_pos, length);
      return(-1);
   }

   /* Drop the lock on the new data. */
   db_unlock_write(db_fp->file, data_pos);

   /* Read in the old last data header. */
   if(fseek(db_fp->file, last_pos, SEEK_SET) ||
         !fread(&data_header, DB_DATA_HEADER_SIZE, 1, db_fp->file)) {
      put_locked_free_zone(db_fp, data_pos, length);
      return(-1);
   }

   /* Set the new next value on the old last header. */
   data_header.next = data_pos;

   /* Write out the old last data header. */
   if(fseek(db_fp->file, last_pos, SEEK_SET) ||
         !fwrite(&data_header, DB_DATA_HEADER_SIZE, 1, db_fp->file)) {
      put_locked_free_zone(db_fp, data_pos, length);
      return(-1);
   }

   /* Unlock the former last record. */
   db_unlock_write(db_fp->file, last_pos);

   /* Done!  =) */
   return(0);

}

