/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * The contents of this file are subject to the Mozilla Public
 * License Version 1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 * 
 * The Original Code is nsDiskCacheMap.cpp, released March 23, 2001.
 * 
 * The Initial Developer of the Original Code is Netscape Communications
 * Corporation.  Portions created by Netscape are
 * Copyright (C) 2001 Netscape Communications Corporation.  All
 * Rights Reserved.
 * 
 * Contributor(s): 
 *    Patrick C. Beard <beard@netscape.com>
 *    Gordon Sheridan  <gordon@netscape.com>
 */

#include "nsDiskCacheMap.h"
#include "nsDiskCacheBinding.h"
#include "nsDiskCacheEntry.h"

#include "nsCache.h"

#include "nsCRT.h"

#include <string.h>


/******************************************************************************
 *  nsDiskCacheBucket
 *****************************************************************************/

void
nsDiskCacheBucket::Swap()
{
    nsDiskCacheRecord * record = &mRecords[0];
    for (int i = 0; i < kRecordsPerBucket; ++i) {
        if (record->HashNumber() == 0)
            break;
        record->Swap();
    }
}

void
nsDiskCacheBucket::Unswap()
{
    nsDiskCacheRecord * record = &mRecords[0];
    for (int i = 0; i < kRecordsPerBucket; ++i) {
        if (record->HashNumber() == 0)
            break;
        record->Unswap();
    }
}


PRUint32
nsDiskCacheBucket::CountRecords()
{
    if (mRecords[0].HashNumber() == 0)  return 0;
    
    PRUint32 i      = kRecordsPerBucket >> 1;
    PRUint32 offset = kRecordsPerBucket >> 2;
    
    while (offset > 0) {
        if (mRecords[i].HashNumber())  i += offset;
        else                           i -= offset;
        offset >>= 1;
    }
    
    if (mRecords[i].HashNumber() != 0)
        ++i;
    
    return i;
}


PRUint32
nsDiskCacheBucket::EvictionRank(PRUint32 targetRank)
{
    PRUint32  rank = 0;
    for (int i = CountRecords() - 1; i >= 0; --i) {
        if ((rank < mRecords[i].EvictionRank()) &&
            ((targetRank == 0) || (mRecords[i].EvictionRank() < targetRank)))
                rank = mRecords[i].EvictionRank();
    }
    return rank;
}


PRInt32
nsDiskCacheBucket::VisitEachRecord(nsDiskCacheRecordVisitor *  visitor,
                                   PRUint32                    evictionRank,
                                   PRUint32 *                  result)
{
    PRUint32 recordsDeleted = 0;
    PRInt32  rv = kVisitNextRecord;
    PRInt32  last  = CountRecords() - 1;
    
    // call visitor for each entry (matching any eviction rank)
    for (int i = last; i >= 0; i--) {
        if (evictionRank > mRecords[i].EvictionRank())  continue;
    
        rv = visitor->VisitRecord(&mRecords[i]);
        if (rv == kVisitNextRecord) continue;
        
        if (rv == kDeleteRecordAndContinue) {
            mRecords[i] = mRecords[last];
            mRecords[last].SetHashNumber(0);
            --last;
            ++recordsDeleted;
            continue;
        }

        *result = recordsDeleted;
        return kStopVisitingRecords;  // rv == kStopVisitingRecords
    }
    
    *result = recordsDeleted;
    return rv;
}


/******************************************************************************
 *  nsDiskCacheMap
 *****************************************************************************/

/**
 *  File operations
 */

nsresult
nsDiskCacheMap::Open(nsILocalFile *  cacheDirectory)
{
    NS_ENSURE_ARG_POINTER(cacheDirectory);
    if (mMapFD)  return NS_ERROR_ALREADY_INITIALIZED;

    mCacheDirectory = cacheDirectory;   // save a reference for ourselves
    
    // create nsILocalFile for _CACHE_MAP_
    nsresult rv;
    nsCOMPtr<nsIFile> file;
    rv = cacheDirectory->Clone(getter_AddRefs(file));
    nsCOMPtr<nsILocalFile> localFile(do_QueryInterface(file, &rv));
    if (NS_FAILED(rv))  return rv;
    rv = localFile->Append("_CACHE_MAP_");
    if (NS_FAILED(rv))  return rv;
    
    // open the file
    rv = localFile->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE, 00666, &mMapFD);
    if (NS_FAILED(rv))  return rv;  // unable to open or create file
    
    // check size of map file
    PRUint32  mapSize = PR_Available(mMapFD);
    if (mapSize < 0) {
        rv = NS_ERROR_UNEXPECTED;
        goto error_exit;
    }
    
    if (mapSize == 0) {
        // create the file - initialize in memory
        mHeader.mVersion    = nsDiskCache::kCurrentVersion;
        mHeader.mDataSize   = 0;
        mHeader.mEntryCount = 0;
        mHeader.mIsDirty = PR_TRUE;
        for (int i = 0; i < kBucketsPerTable; ++i) {
            mHeader.mEvictionRank[i] = 0;
        }

        memset(mHeader.reserved, 0, nsDiskCacheHeader::kReservedBytes);
        memset(mBuckets, 0, sizeof(nsDiskCacheBucket) * kBucketsPerTable);
        
    } else if (mapSize == kCacheMapSize) {
        // read it in
        PRUint32 bytesRead = PR_Read(mMapFD, &mHeader, kCacheMapSize);
        if (kCacheMapSize != bytesRead) {
            rv = NS_ERROR_UNEXPECTED;
            goto error_exit;
        }
        mHeader.Unswap();
        if (mHeader.mIsDirty || mHeader.mVersion != nsDiskCache::kCurrentVersion) {
            rv = NS_ERROR_FILE_CORRUPTED;
            goto error_exit;
        }
        
        // Unswap each bucket
        for (PRUint32 i = 0; i < kBucketsPerTable; ++i) {
            mBuckets[i].Unswap();
        }
        
        // XXX verify entry count, check size(?)
        
    } else {
        rv = NS_ERROR_FILE_CORRUPTED;
        goto error_exit;
    }

    rv = OpenBlockFiles();
    if (NS_FAILED(rv))  goto error_exit;

    // set dirty bit and flush header
    mHeader.mIsDirty    = PR_TRUE;
    rv = FlushHeader();
    if (NS_FAILED(rv))  goto error_exit;
    
    return NS_OK;
    
error_exit:
    (void) CloseBlockFiles();
    
    if (mMapFD) {
        (void) PR_Close(mMapFD);
        mMapFD = nsnull;
    }
    
    return rv;
}


nsresult
nsDiskCacheMap::Close()
{
    if (!mMapFD)  return NS_OK;

    // close block files
    nsresult  rv = CloseBlockFiles();
    if (NS_FAILED(rv)) goto exit;  // this is going to be a mess...
    
    // write map record buckets
    rv = FlushBuckets(PR_FALSE);   // don't bother swapping buckets back
    if (NS_FAILED(rv))  goto exit;
    
    // clear dirty bit
    mHeader.mIsDirty    = PR_FALSE;

    rv = FlushHeader();

exit:
    PRStatus err = PR_Close(mMapFD);
    mMapFD = nsnull;
    
    if (NS_FAILED(rv))  return rv;
    return err == PR_SUCCESS ? NS_OK : NS_ERROR_UNEXPECTED;
}


nsresult
nsDiskCacheMap::FlushHeader()
{
    if (!mMapFD)  return NS_ERROR_NOT_AVAILABLE;
    
    // seek to beginning of cache map
    PRInt32 filePos = PR_Seek(mMapFD, 0, PR_SEEK_SET);
    if (filePos != 0)  return NS_ERROR_UNEXPECTED;
    
    // write the header
    mHeader.Swap();
    PRInt32 bytesWritten = PR_Write(mMapFD, &mHeader, sizeof(nsDiskCacheHeader));
    mHeader.Unswap();
    if (sizeof(nsDiskCacheHeader) != bytesWritten) {
        return NS_ERROR_UNEXPECTED;
    }
    
    return NS_OK;
}


nsresult
nsDiskCacheMap::FlushBuckets(PRBool unswap)
{
    if (!mMapFD)  return NS_ERROR_NOT_AVAILABLE;
    
    // seek to beginning of buckets
    PRInt32 filePos = PR_Seek(mMapFD, sizeof(nsDiskCacheHeader), PR_SEEK_SET);
    if (filePos != sizeof(nsDiskCacheHeader))  return NS_ERROR_UNEXPECTED;
    
    // Swap each bucket
    for (PRUint32 i = 0; i < kBucketsPerTable; ++i) {
        mBuckets[i].Swap();
    }
    
    PRInt32 bytesWritten = PR_Write(mMapFD, &mBuckets, sizeof(nsDiskCacheBucket) * kBucketsPerTable);

    if (unswap) {
        // Unswap each bucket
        for (PRUint32 i = 0; i < kBucketsPerTable; ++i) {
            mBuckets[i].Unswap();
        }
    }

    if ( sizeof(nsDiskCacheBucket) * kBucketsPerTable != bytesWritten) {
        return NS_ERROR_UNEXPECTED;
    }
    
    return NS_OK;
}


/**
 *  Record operations
 */

nsresult
nsDiskCacheMap::AddRecord( nsDiskCacheRecord *  mapRecord,
                           nsDiskCacheRecord *  oldRecord)
{
    nsresult            rv;
    PRUint32            hashNumber = mapRecord->HashNumber();
    nsDiskCacheBucket * bucket;
    PRUint32            bucketIndex = GetBucketIndex(hashNumber);
    int                 i;

   oldRecord->SetHashNumber(0);  // signify no record

    rv = GetBucketForHashNumber(hashNumber, &bucket);
    if (NS_FAILED(rv))  return rv;
    
    nsDiskCacheRecord * mostEvictable = &bucket->mRecords[0];
    for (i = 0; i < kRecordsPerBucket; ++i) {
        if (bucket->mRecords[i].HashNumber() == 0) {
            // stick the new record here
            bucket->mRecords[i] = *mapRecord;
            ++mHeader.mEntryCount;
            
            // update eviction rank in header if necessary
            if (mHeader.mEvictionRank[bucketIndex] < mapRecord->EvictionRank())
                mHeader.mEvictionRank[bucketIndex] = mapRecord->EvictionRank();

            NS_ASSERTION(mHeader.mEvictionRank[bucketIndex] == bucket->EvictionRank(0),
                         "eviction rank out of sync");
            return NS_OK;
        }
        
        if (bucket->mRecords[i].EvictionRank() > mostEvictable->EvictionRank())
            mostEvictable = &bucket->mRecords[i];
    }
    
    *oldRecord     = *mostEvictable;    // i == kRecordsPerBucket, so evict the mostEvictable
    *mostEvictable = *mapRecord;        // replace it with the new record
    
    // check if we need to update mostEvictable entry in header
    if ((oldRecord->HashNumber() != 0) ||
         (mapRecord->EvictionRank() > mHeader.mEvictionRank[bucketIndex])) {
         
        mHeader.mEvictionRank[bucketIndex] = bucket->EvictionRank(0);
    }

NS_ASSERTION(mHeader.mEvictionRank[bucketIndex] == bucket->EvictionRank(0),
             "eviction rank out of sync");    
    return NS_OK;
}


nsresult
nsDiskCacheMap::UpdateRecord( nsDiskCacheRecord *  mapRecord)
{
    PRUint32            hashNumber = mapRecord->HashNumber();
    nsDiskCacheBucket * bucket;
    nsresult            rv = GetBucketForHashNumber(hashNumber, &bucket);
    if (NS_FAILED(rv))  return rv;

    for (int i = 0; i < kRecordsPerBucket; ++i) {
        if (bucket->mRecords[i].HashNumber() == mapRecord->HashNumber()) {
            PRUint32 oldRank = bucket->mRecords[i].EvictionRank();

            // stick the new record here            
            bucket->mRecords[i] = *mapRecord;

            // update eviction rank in header if necessary
            PRUint32  bucketIndex = GetBucketIndex(mapRecord->HashNumber());
            if (mHeader.mEvictionRank[bucketIndex] < mapRecord->EvictionRank())
                mHeader.mEvictionRank[bucketIndex] = mapRecord->EvictionRank();
            else if (mHeader.mEvictionRank[bucketIndex] == oldRank)
                mHeader.mEvictionRank[bucketIndex] = bucket->EvictionRank(0);

NS_ASSERTION(mHeader.mEvictionRank[bucketIndex] == bucket->EvictionRank(0),
             "eviction rank out of sync");
            return NS_OK;
        }
    }
    return NS_ERROR_UNEXPECTED;
}


nsresult
nsDiskCacheMap::FindRecord( PRUint32  hashNumber, nsDiskCacheRecord *  result)
{
    nsDiskCacheBucket * bucket;
    nsresult rv = GetBucketForHashNumber(hashNumber, &bucket);
    if (NS_FAILED(rv))  return rv;
    
    for (int i = 0; i < kRecordsPerBucket; ++i) {
        if (bucket->mRecords[i].HashNumber() == 0)  break;
            
        if (bucket->mRecords[i].HashNumber() == hashNumber) {
            *result = bucket->mRecords[i];    // copy the record
            NS_ASSERTION(result->ValidRecord(), "bad cache map record");
            return NS_OK;
        }
    }
    return NS_ERROR_CACHE_KEY_NOT_FOUND;
}


nsresult
nsDiskCacheMap::DeleteRecord( nsDiskCacheRecord *  mapRecord)
{
    nsDiskCacheBucket * bucket;
    nsresult rv = GetBucketForHashNumber(mapRecord->HashNumber(), &bucket);
    if (NS_FAILED(rv))  return rv;
    
    PRUint32 count = bucket->CountRecords();
    for (PRUint32 i = 0; i < count; ++i) {        
        if (bucket->mRecords[i].HashNumber() == mapRecord->HashNumber()) {
            // found it, now delete it.
            PRUint32  evictionRank = bucket->mRecords[i].EvictionRank();
            NS_ASSERTION(evictionRank == mapRecord->EvictionRank(), "evictionRank out of sync");
            if (i != (count - 1)) { // if not the last record, shift last record into opening
                bucket->mRecords[i] = bucket->mRecords[count - 1];
            }
            bucket->mRecords[count - 1].SetHashNumber(0);   // clear last record
            mHeader.mEntryCount--;
            // update eviction rank
            PRUint32  bucketIndex = GetBucketIndex(mapRecord->HashNumber());
            if (mHeader.mEvictionRank[bucketIndex] <= evictionRank) {
                mHeader.mEvictionRank[bucketIndex] = bucket->EvictionRank(0);
            }

            NS_ASSERTION(mHeader.mEvictionRank[bucketIndex] == bucket->EvictionRank(0),
                         "eviction rank out of sync");
            return NS_OK;
        }
    }
    return NS_ERROR_UNEXPECTED;
}


/**
 *  VisitRecords
 *
 *  Visit every record in cache map in the most convenient order
 */
nsresult
nsDiskCacheMap::VisitRecords( nsDiskCacheRecordVisitor *  visitor)
{
    for (PRUint32 i = 0; i < kBucketsPerTable; ++i) {
        // get bucket
        PRUint32 recordsDeleted;
        PRBool continueFlag = mBuckets[i].VisitEachRecord(visitor, 0, &recordsDeleted);
        if (recordsDeleted) {
            // recalc eviction rank
            mHeader.mEvictionRank[i] = mBuckets[i].EvictionRank(0);
            mHeader.mEntryCount -= recordsDeleted;
            // XXX write bucket
        }
        NS_ASSERTION(mHeader.mEvictionRank[i] == mBuckets[i].EvictionRank(0),
                     "eviction rank out of sync");
        if (!continueFlag)  break;
    }
    
    return NS_OK;
}


/**
 *  EvictRecords
 *
 *  Just like VisitRecords, but visits the records in order of their eviction rank
 */
nsresult
nsDiskCacheMap::EvictRecords( nsDiskCacheRecordVisitor * visitor)
{
    PRUint32  tempRank[kBucketsPerTable];
    int       i;
    
    // copy eviction rank array
    for (i = 0; i < kBucketsPerTable; ++i)
        tempRank[i] = mHeader.mEvictionRank[i];
    
    while (1) {
    
        // find bucket with highest eviction rank
        PRUint32    rank  = 0;
        PRUint32    index = 0;
        for (i = 0; i < kBucketsPerTable; ++i) {
            if (rank < tempRank[i]) {
                rank = tempRank[i];
                index = i;
            }
        }
        
        if (rank == 0)  break;  // we've examined all the records
NS_ASSERTION(mHeader.mEvictionRank[index] == mBuckets[index].EvictionRank(0),
             "header eviction rank out of sync");
            
        // visit records in bucket with eviction ranks >= target eviction rank
        PRUint32 recordsDeleted;
        PRInt32  continueResult = mBuckets[index].VisitEachRecord(visitor, rank, &recordsDeleted);
        if (recordsDeleted) {
            // recalc eviction rank
            mHeader.mEvictionRank[index] = mBuckets[index].EvictionRank(0);
            mHeader.mEntryCount -= recordsDeleted;
            // XXX write bucket
        }
        if (continueResult == kStopVisitingRecords)  break;
        
        // find greatest rank less than 'rank'
        tempRank[index] = mBuckets[index].EvictionRank(rank);
    }
    return NS_OK;
}



nsresult
nsDiskCacheMap::OpenBlockFiles()
{
    // create nsILocalFile for block file
    nsCOMPtr<nsILocalFile> blockFile;
    nsresult rv;
    
    for (int i = 0; i < 3; ++i) {
        rv = GetBlockFileForIndex(i, getter_AddRefs(blockFile));
        if (NS_FAILED(rv))  goto error_exit;
    
        PRUint32 blockSize = GetBlockSizeForIndex(i);
        rv = mBlockFile[i].Open(blockFile, blockSize);
        if (NS_FAILED(rv)) goto error_exit;
    }
    return NS_OK;

error_exit:
    (void)CloseBlockFiles();        // we already have an error to report
    return rv;
}


nsresult
nsDiskCacheMap::CloseBlockFiles()
{
    nsresult rv, rv2 = NS_OK;
    for (int i=0; i < 3; ++i) {
        rv = mBlockFile[i].Close();
        if (NS_FAILED(rv))  rv2 = rv;   // if one or more errors, report at least one
    }
    return rv2;
}


nsresult
nsDiskCacheMap::ReadDiskCacheEntry(nsDiskCacheRecord * record, nsDiskCacheEntry ** result)
{
    nsresult            rv         = NS_ERROR_UNEXPECTED;
    nsDiskCacheEntry *  diskEntry  = nsnull;
    PRUint32            metaFile   = record->MetaFile();
    PRFileDesc *        fd         = nsnull;
    *result = nsnull;
    
    if (!record->MetaLocationInitialized())  return NS_ERROR_NOT_AVAILABLE;
    
    if (metaFile == 0) {  // entry/metadata stored in separate file
        // open and read the file
        nsCOMPtr<nsILocalFile> file;
        rv = GetLocalFileForDiskCacheRecord(record, nsDiskCache::kMetaData, getter_AddRefs(file));
        if (NS_FAILED(rv))  return rv;

        PRFileDesc * fd = nsnull;
        rv = file->OpenNSPRFileDesc(PR_RDONLY, 00666, &fd);
        if (NS_FAILED(rv))  return rv;
        
        PRInt32 fileSize = PR_Available(fd);
        if (fileSize < 0) {
            // XXX an error occurred. We could call PR_GetError(), but how would that help?
            rv = NS_ERROR_UNEXPECTED;
            goto exit;
        }

        diskEntry = (nsDiskCacheEntry *) new char[fileSize];
        if (!diskEntry) {
            rv = NS_ERROR_OUT_OF_MEMORY;
            goto exit;
        }
        
        PRInt32 bytesRead = PR_Read(fd, diskEntry, fileSize);
        if (bytesRead < fileSize) {
            rv = NS_ERROR_UNEXPECTED;
            goto exit;
        }

    } else if (metaFile < 4) {  // XXX magic number: use constant
        // entry/metadata stored in cache block file
        
        // allocate buffer
        PRUint32 blockSize  = GetBlockSizeForIndex(metaFile - 1);
        PRUint32 blockCount = record->MetaBlockCount();
        diskEntry = (nsDiskCacheEntry *) new char[blockSize * blockCount];
        
        // read diskEntry
        rv = mBlockFile[metaFile - 1].ReadBlocks((char *)diskEntry,
                                                 record->MetaStartBlock(),
                                                 blockCount);
        if (NS_FAILED(rv))  goto exit;
    }
    
    diskEntry->Unswap();    // disk to memory
    // pass ownership to caller
    *result = diskEntry;
    diskEntry = nsnull;

exit:
    // XXX auto ptr would be nice
    if (fd) (void) PR_Close(fd);
    delete [] (char *)diskEntry;
    return rv;
}


nsresult
nsDiskCacheMap::WriteDiskCacheEntry(nsDiskCacheBinding *  binding)
{
    nsresult            rv        = NS_OK;
    nsDiskCacheEntry *  diskEntry =  CreateDiskCacheEntry(binding);
    if (!diskEntry)  return NS_ERROR_UNEXPECTED;
    
    PRUint32    size = diskEntry->Size();
    PRUint32    fileIndex;
    PRUint32    blocks = 0;
    
    if (size < 1024) {             // block size 256
        fileIndex = 1;
        blocks    = size / 256 + 1;
    } else if (size < 4096) {      // block size 1024
        fileIndex = 2;
        blocks    = size / 1024 + 1;
    } else if (size < 16384) {     // block size 4096
        fileIndex = 3;
        blocks    = size / 4096 + 1;
    } else {                       // separate file
        fileIndex = 0;
    }
    
    PRUint32  metaFile   = binding->mRecord.MetaFile();

    // Deallocate old storage if necessary    
    if (binding->mRecord.MetaLocationInitialized()) {
        // we have existing storage

        if ((metaFile == 0) && (fileIndex == 0)) {  // keeping the separate file
            // just decrement total
            // XXX if bindRecord.MetaFileSize == USHRT_MAX, stat the file to see how big it is
            DecrementTotalSize(binding->mRecord.MetaFileSize() * 1024);
            NS_ASSERTION(binding->mRecord.MetaFileGeneration() == binding->mGeneration,
                         "generations out of sync");
        } else {
            rv = DeleteStorage(&binding->mRecord, nsDiskCache::kMetaData);
            if (NS_FAILED(rv))  goto exit;
        }
    }

    binding->mRecord.SetEvictionRank(ULONG_MAX - SecondsFromPRTime(PR_Now()));
        
    if (fileIndex == 0) {
        // Write entry data to separate file
        PRUint32 metaFileSizeK = ((size + 0x0399) >> 10); // round up to nearest 1k
        nsCOMPtr<nsILocalFile> localFile;
        
        // XXX handle metaFileSizeK > USHRT_MAX
        binding->mRecord.SetMetaFileGeneration(binding->mGeneration);
        binding->mRecord.SetMetaFileSize(metaFileSizeK);
        rv = UpdateRecord(&binding->mRecord);
        if (NS_FAILED(rv))  goto exit;

        rv = GetLocalFileForDiskCacheRecord(&binding->mRecord,
                                            nsDiskCache::kMetaData,
                                            getter_AddRefs(localFile));
        if (NS_FAILED(rv))  goto exit;
        
        // open the file
        PRFileDesc * fd;
        rv = localFile->OpenNSPRFileDesc(PR_RDWR | PR_TRUNCATE | PR_CREATE_FILE, 00666, &fd);
        if (NS_FAILED(rv))  goto exit;  // unable to open or create file

        // write the file
        diskEntry->Swap();
        PRInt32 bytesWritten = PR_Write(fd, diskEntry, size);
        
        PRStatus err = PR_Close(mMapFD);
        if ((bytesWritten != (PRInt32)size) || (err != PR_SUCCESS)) {
            rv = NS_ERROR_UNEXPECTED;
            goto exit;
        }
        // XXX handle metaFileSizeK == USHRT_MAX
        IncrementTotalSize(metaFileSizeK * 1024);
        
    } else {
        // write entry data to disk cache block file
        PRInt32 startBlock = mBlockFile[fileIndex - 1].AllocateBlocks(blocks);
        if (startBlock < 0) {
            rv = NS_ERROR_UNEXPECTED;
            goto exit;
        }
        
        // update binding and cache map record
        binding->mRecord.SetMetaBlocks(fileIndex, startBlock, blocks);
        rv = UpdateRecord(&binding->mRecord);
        if (NS_FAILED(rv))  goto exit;
        // XXX we should probably write out bucket ourselves

        // write data
        diskEntry->Swap();
        rv = mBlockFile[fileIndex - 1].WriteBlocks(diskEntry, startBlock, blocks);
        if (NS_FAILED(rv))  goto exit;
        
        IncrementTotalSize(blocks * GetBlockSizeForIndex(fileIndex - 1));
    }

exit:
    delete [] (char *)diskEntry;
    return rv;
}


nsresult
nsDiskCacheMap::DoomRecord(nsDiskCacheRecord * record)
{
    nsresult  rv = DeleteRecord(record);
    // XXX future: add record to doomed record journal

    return rv;
}


nsresult
nsDiskCacheMap::DeleteStorage(nsDiskCacheRecord * record)
{
    nsresult  rv1 = DeleteStorage(record, nsDiskCache::kData);
    nsresult  rv2 = DeleteStorage(record, nsDiskCache::kMetaData);
    return NS_FAILED(rv1) ? rv1 : rv2;
}


nsresult
nsDiskCacheMap::DeleteStorage(nsDiskCacheRecord * record, PRBool metaData)
{
    nsresult    rv = NS_ERROR_UNEXPECTED;
    PRUint32    fileIndex = metaData ? record->MetaFile() : record->DataFile();
    nsCOMPtr<nsIFile> file;
    
    if (fileIndex == 0) {
        // delete the file
        PRUint32  sizeK = metaData ? record->MetaFileSize() : record->DataFileSize();
        // XXX if sizeK == USHRT_MAX, stat file for actual size

        rv = GetFileForDiskCacheRecord(record, metaData, getter_AddRefs(file));
        if (NS_SUCCEEDED(rv)) {
            rv = file->Remove(PR_FALSE);    // false == non-recursive
        }
        DecrementTotalSize(sizeK * 1024);
        
    } else if (fileIndex < 4) {
        // deallocate blocks
        PRInt32  startBlock = metaData ? record->MetaStartBlock() : record->DataStartBlock();
        PRInt32  blockCount = metaData ? record->MetaBlockCount() : record->DataBlockCount();
        
        rv = mBlockFile[fileIndex - 1].DeallocateBlocks(startBlock, blockCount);
        DecrementTotalSize(blockCount * GetBlockSizeForIndex(fileIndex - 1));
    }
    
    return rv;
}


nsresult
nsDiskCacheMap::DeleteRecordAndStorage(nsDiskCacheRecord * record)
{
    nsresult  rv1 = DeleteStorage(record);
    nsresult  rv2 = DeleteRecord(record);
    return NS_FAILED(rv1) ? rv1 : rv2;
}


nsresult
nsDiskCacheMap::GetFileForDiskCacheRecord(nsDiskCacheRecord * record,
                                             PRBool              meta,
                                             nsIFile **          result)
{
    if (!mCacheDirectory)  return NS_ERROR_NOT_AVAILABLE;
    
    nsCOMPtr<nsIFile> file;
    nsresult rv = mCacheDirectory->Clone(getter_AddRefs(file));
    if (NS_FAILED(rv))  return rv;
    
    PRInt16 generation = record->Generation();
    char name[32];
    ::sprintf(name, "%08X%c%02X", record->HashNumber(),  (meta ? 'm' : 'd'), generation);
    rv = file->Append(name);
    if (NS_FAILED(rv))  return rv;
    
    NS_IF_ADDREF(*result = file);
    return rv;
}

nsresult
nsDiskCacheMap::GetLocalFileForDiskCacheRecord(nsDiskCacheRecord * record,
                                                  PRBool              meta,
                                                  nsILocalFile **     result)
{
    nsCOMPtr<nsIFile> file;
    nsresult rv = GetFileForDiskCacheRecord(record, meta, getter_AddRefs(file));
    if (NS_FAILED(rv))  return rv;
    
    nsCOMPtr<nsILocalFile> localFile = do_QueryInterface(file, &rv);
    if (NS_FAILED(rv))  return rv;
    
    NS_IF_ADDREF(*result = localFile);
    return rv;
}


nsresult
nsDiskCacheMap::GetBlockFileForIndex(PRUint32 index, nsILocalFile ** result)
{
    if (!mCacheDirectory)  return NS_ERROR_NOT_AVAILABLE;
    
    nsCOMPtr<nsIFile> file;
    nsresult rv = mCacheDirectory->Clone(getter_AddRefs(file));
    if (NS_FAILED(rv))  return rv;
    
    char name[32];
    ::sprintf(name, "_CACHE_%03d_", index + 1);
    rv = file->Append(name);
    if (NS_FAILED(rv))  return rv;
    
    nsCOMPtr<nsILocalFile> localFile = do_QueryInterface(file, &rv);
    NS_IF_ADDREF(*result = localFile);

    return rv;
}


PRUint32
nsDiskCacheMap::GetBlockSizeForIndex(PRUint32 index)
{
    return 256 << (2 * (index));  // XXX magic numbers

}
