/******************************************************************************
 *
 * BigBlockFile
 *
 * This is the implementation of a file that consists of blocks of 
 * a predetermined size.
 * This class is used in the Compound File implementation of the 
 * IStorage and IStream interfaces. It provides the functionality 
 * to read and write any blocks in the file as well as setting and 
 * obtaining the size of the file.
 * The blocks are indexed sequentially from the start of the file
 * starting with -1.
 * 
 * TODO:
 * - Support for a transacted mode
 *
 * Copyright 1999 Thuy Nguyen
 *
 */

#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "winbase.h"
#include "winerror.h"
#include "wine/obj_storage.h"
#include "ole2.h"

#include "storage32.h"

#include "debugtools.h"

DEFAULT_DEBUG_CHANNEL(storage)

/***********************************************************
 * Data structures used internally by the BigBlockFile
 * class.
 */

/***
 * Itdentifies a single big block and the related 
 * information
 */
struct BigBlock
{
  BigBlock * next;
  DWORD index;
  DWORD access_mode;
  LPVOID lpBlock;
};

/***
 * This structure identifies the paged that are mapped
 * from the file and their position in memory. It is 
 * also used to hold a reference count to those pages.
 */
struct MappedPage
{
  MappedPage * next;
  DWORD number;
  int ref;
  LPVOID lpBytes;
};

#define PAGE_SIZE       131072
#define BLOCKS_PER_PAGE 256

#define NUMBER_OF_MAPPED_PAGES 100

/***********************************************************
 * Prototypes for private methods
 */
static void*     BIGBLOCKFILE_GetMappedView(LPBIGBLOCKFILE This,
                                            DWORD          pagenum,
                                            DWORD          desired_access);
static void      BIGBLOCKFILE_ReleaseMappedPage(LPBIGBLOCKFILE This,
                                                DWORD          pagenum,
                                                DWORD          access);
static void      BIGBLOCKFILE_FreeAllMappedPages(LPBIGBLOCKFILE This);
static void*     BIGBLOCKFILE_GetBigBlockPointer(LPBIGBLOCKFILE This,
                                                 ULONG          index,
                                                 DWORD          desired_access);
static BigBlock* BIGBLOCKFILE_GetBigBlockFromPointer(LPBIGBLOCKFILE This, 
                                                     void*          pBlock);
static void      BIGBLOCKFILE_RemoveBlock(LPBIGBLOCKFILE This,
                                          ULONG          index);
static BigBlock* BIGBLOCKFILE_AddBigBlock(LPBIGBLOCKFILE This, 
                                          ULONG          index);
static BigBlock* BIGBLOCKFILE_CreateBlock(ULONG index);
static DWORD     BIGBLOCKFILE_GetProtectMode(DWORD openFlags);
static BOOL      BIGBLOCKFILE_FileInit(LPBIGBLOCKFILE This, HANDLE hFile);
static BOOL      BIGBLOCKFILE_MemInit(LPBIGBLOCKFILE This, ILockBytes* plkbyt);
static void      BIGBLOCKFILE_RemoveAllBlocks(LPBIGBLOCKFILE This);

/******************************************************************************
 *      BIGBLOCKFILE_Construct
 *
 * Construct a big block file. Create the file mapping object. 
 * Create the read only mapped pages list, the writeable mapped page list
 * and the blocks in use list.
 */
BigBlockFile * BIGBLOCKFILE_Construct(
  HANDLE   hFile,
  ILockBytes* pLkByt,
  DWORD    openFlags,
  ULONG    blocksize,
  BOOL     fileBased)
{
  LPBIGBLOCKFILE This;

  This = (LPBIGBLOCKFILE)HeapAlloc(GetProcessHeap(), 0, sizeof(BigBlockFile));

  if (This == NULL)
    return NULL;

  This->fileBased = fileBased;

  This->flProtect = BIGBLOCKFILE_GetProtectMode(openFlags);

  This->blocksize = blocksize;

  /* initialize the block list
   */
  This->headblock = NULL;

  if (This->fileBased)
  {
    if (!BIGBLOCKFILE_FileInit(This, hFile))
    {
      HeapFree(GetProcessHeap(), 0, This);
      return NULL;
    }
  }
  else
  {
    if (!BIGBLOCKFILE_MemInit(This, pLkByt))
    {
      HeapFree(GetProcessHeap(), 0, This);
      return NULL;
    }
  }

  return This;
}

/******************************************************************************
 *      BIGBLOCKFILE_FileInit
 *
 * Initialize a big block object supported by a file.
 */
static BOOL BIGBLOCKFILE_FileInit(LPBIGBLOCKFILE This, HANDLE hFile)
{
  This->pLkbyt     = NULL;
  This->hbytearray = 0;
  This->pbytearray = NULL;

  This->hfile = hFile;

  if (This->hfile == INVALID_HANDLE_VALUE)
    return FALSE;

  /* create the file mapping object
   */
  This->hfilemap = CreateFileMappingA(This->hfile,
                                      NULL,
                                      This->flProtect,
                                      0, 0,
                                      NULL);

  if (This->hfilemap == NULL)
  {
    CloseHandle(This->hfile);
    return FALSE;
  }

  This->filesize.LowPart = GetFileSize(This->hfile, NULL);

  /* create the mapped pages list
   */
  This->maplisthead = HeapAlloc(GetProcessHeap(), 0, sizeof(MappedPage));

  if (This->maplisthead == NULL)
  {
    CloseHandle(This->hfilemap);
    CloseHandle(This->hfile);
    return FALSE;
  }

  This->maplisthead->next = NULL;

  return TRUE;
}

/******************************************************************************
 *      BIGBLOCKFILE_MemInit
 *
 * Initialize a big block object supported by an ILockBytes on HGLOABL.
 */
static BOOL BIGBLOCKFILE_MemInit(LPBIGBLOCKFILE This, ILockBytes* plkbyt)
{
  This->hfile       = 0;
  This->hfilemap    = NULL;
  This->maplisthead = NULL;

  /*
   * Retrieve the handle to the byte array from the LockByte object.
   */
  if (GetHGlobalFromILockBytes(plkbyt, &(This->hbytearray)) != S_OK)
  {
    FIXME("May not be an ILockBytes on HGLOBAL\n");
    return FALSE;
  }

  This->pLkbyt = plkbyt;

  /*
   * Increment the reference count of the ILockByte object since
   * we're keeping a reference to it.
   */
  ILockBytes_AddRef(This->pLkbyt);

  This->filesize.LowPart = GlobalSize(This->hbytearray);
  This->filesize.HighPart = 0;

  This->pbytearray = GlobalLock(This->hbytearray);

  return TRUE;
}

/******************************************************************************
 *      BIGBLOCKFILE_Destructor
 *
 * Destructor. Clean up, free memory.
 */
void BIGBLOCKFILE_Destructor(
  LPBIGBLOCKFILE This)
{
  if (This->fileBased)
  {
    /* unmap all views and destroy the mapped page list
     */
    BIGBLOCKFILE_FreeAllMappedPages(This);
    HeapFree(GetProcessHeap(), 0, This->maplisthead);
  
    /* close all open handles
     */
    CloseHandle(This->hfilemap);
    CloseHandle(This->hfile);
  }
  else
  {
    GlobalUnlock(This->hbytearray);
    ILockBytes_Release(This->pLkbyt);
  }

  /*
   * Destroy the blocks list.
   */
  BIGBLOCKFILE_RemoveAllBlocks(This);

  /* destroy this
   */
  HeapFree(GetProcessHeap(), 0, This);
}

/******************************************************************************
 *      BIGBLOCKFILE_GetROBigBlock
 *
 * Returns the specified block in read only mode.
 * Will return NULL if the block doesn't exists.
 */
void* BIGBLOCKFILE_GetROBigBlock(
  LPBIGBLOCKFILE This,
  ULONG          index)
{
  /*
   * block index starts at -1
   * translate to zero based index
   */
  if (index == 0xffffffff)
    index = 0;
  else
    index++;

  /*
   * validate the block index
   * 
   */
  if ((This->blocksize * (index + 1)) >
      (This->filesize.LowPart +
       (This->blocksize - (This->filesize.LowPart % This->blocksize))))
    return 0;

  return BIGBLOCKFILE_GetBigBlockPointer(This, index, FILE_MAP_READ);
}

/******************************************************************************
 *      BIGBLOCKFILE_GetBigBlock
 *
 * Returns the specified block.
 * Will grow the file if necessary.
 */
void* BIGBLOCKFILE_GetBigBlock(LPBIGBLOCKFILE This, ULONG index)
{
  /*
   * block index starts at -1
   * translate to zero based index
   */
  if (index == 0xffffffff)
    index = 0;
  else
    index++;

  /*
   * make sure that the block physically exists
   */
  if ((This->blocksize * (index + 1)) > This->filesize.LowPart)
  {
    ULARGE_INTEGER newSize;

    newSize.HighPart = 0;
    newSize.LowPart = This->blocksize * (index + 1);

    BIGBLOCKFILE_SetSize(This, newSize);
  }

  return BIGBLOCKFILE_GetBigBlockPointer(This, index, FILE_MAP_WRITE);
}

/******************************************************************************
 *      BIGBLOCKFILE_ReleaseBigBlock
 *
 * Releases the specified block.
 */
void BIGBLOCKFILE_ReleaseBigBlock(LPBIGBLOCKFILE This, void *pBlock)
{
  DWORD page_num;
  BigBlock* theBigBlock;

  if (pBlock == NULL)
    return;

  /*
   * get the block from the block list
   */
  theBigBlock = BIGBLOCKFILE_GetBigBlockFromPointer(This, pBlock);

  if (theBigBlock == NULL)
    return;

  if (This->fileBased)
  {
    /*
     * find out which page this block is in
     */
    page_num = theBigBlock->index / BLOCKS_PER_PAGE;
  
    /*
     * release this page
     */
    BIGBLOCKFILE_ReleaseMappedPage(This, page_num, theBigBlock->access_mode);
  }

  /*
   * remove block from list
   */
  BIGBLOCKFILE_RemoveBlock(This, theBigBlock->index);
}

/******************************************************************************
 *      BIGBLOCKFILE_SetSize
 *
 * Sets the size of the file.
 * 
 */
void BIGBLOCKFILE_SetSize(LPBIGBLOCKFILE This, ULARGE_INTEGER newSize)
{
  if (This->filesize.LowPart == newSize.LowPart)
    return;

  if (This->fileBased)
  {
    /*
     * unmap all views, must be done before call to SetEndFile
     */
    BIGBLOCKFILE_FreeAllMappedPages(This);
  
    /*
     * close file-mapping object, must be done before call to SetEndFile
     */
    CloseHandle(This->hfilemap);
    This->hfilemap = NULL;
  
    /*
     * set the new end of file
     */
    SetFilePointer(This->hfile, newSize.LowPart, NULL, FILE_BEGIN);
    SetEndOfFile(This->hfile);
  
    /*
     * re-create the file mapping object
     */
    This->hfilemap = CreateFileMappingA(This->hfile,
                                        NULL,
                                        This->flProtect,
                                        0, 0, 
                                        NULL);
  }
  else
  {
    GlobalUnlock(This->hbytearray);

    /*
     * Resize the byte array object.
     */
    ILockBytes_SetSize(This->pLkbyt, newSize);

    /*
     * Re-acquire the handle, it may have changed.
     */
    GetHGlobalFromILockBytes(This->pLkbyt, &This->hbytearray);
    This->pbytearray = GlobalLock(This->hbytearray);
  }

  /*
   * empty the blocks list.
   */
  BIGBLOCKFILE_RemoveAllBlocks(This);

  This->filesize.LowPart = newSize.LowPart;
  This->filesize.HighPart = newSize.HighPart;
}

/******************************************************************************
 *      BIGBLOCKFILE_GetSize
 *
 * Returns the size of the file.
 * 
 */
ULARGE_INTEGER BIGBLOCKFILE_GetSize(LPBIGBLOCKFILE This)
{
  return This->filesize;
}

/******************************************************************************
 *      BIGBLOCKFILE_GetBigBlockPointer     [PRIVATE]
 *
 * Returns a pointer to the specified block.
 */
static void* BIGBLOCKFILE_GetBigBlockPointer(
  LPBIGBLOCKFILE This, 
  ULONG          index, 
  DWORD          desired_access)
{
  DWORD block_num;
  void * pBytes;
  BigBlock *aBigBlock;

  /* get the big block from the list or add it to the list
   */
  aBigBlock = BIGBLOCKFILE_AddBigBlock(This, index);

  if (aBigBlock == NULL)
    return NULL;

  /* we already have an address for this block
   */
  if (aBigBlock->lpBlock != NULL)
  {
    /* make sure the desired access matches what we already have
     */
    if (aBigBlock->access_mode == desired_access)
      return aBigBlock->lpBlock;
    else
      return NULL;
  }

  /*
   * else aBigBlock->lpBigBlock == NULL, it's a new block
   */

  if (This->fileBased)
  {
    DWORD page_num;

    /* find out which page this block is in
     */
    page_num = index / BLOCKS_PER_PAGE;
  
    /* offset of the block in the page
     */
    block_num = index % BLOCKS_PER_PAGE;
  
    /* get a pointer to the first byte in the page
     */
    pBytes = BIGBLOCKFILE_GetMappedView(This, page_num, desired_access);
  }
  else
  {
    pBytes = This->pbytearray; 
    block_num = index;
  }

  if (pBytes == NULL)
    return NULL;

  /* initialize block
   */
  aBigBlock->lpBlock = ((BYTE*)pBytes + (block_num*This->blocksize));
  aBigBlock->access_mode = desired_access;

  return aBigBlock->lpBlock;
}

/******************************************************************************
 *      BIGBLOCKFILE_CreateBlock      [PRIVATE]
 *
 * Creates a node of the blocks list.
 */
static BigBlock* BIGBLOCKFILE_CreateBlock(
  ULONG index)
{
  BigBlock *newBigBlock;

  /* create new list node
   */
  newBigBlock = HeapAlloc(GetProcessHeap(), 0, sizeof(BigBlock));

  if (newBigBlock == NULL)
    return NULL;

  /* initialize node
   */
  newBigBlock->index = index;
  newBigBlock->lpBlock = NULL;

  return newBigBlock;
}

/******************************************************************************
 *      BIGBLOCKFILE_AddBigBlock      [PRIVATE]
 *
 * Returns the specified block from the blocks list.
 * If the block is not found in the list, we will create it and add it to the
 * list.
 */
static BigBlock* BIGBLOCKFILE_AddBigBlock(
  LPBIGBLOCKFILE This, 
  ULONG          index)
{
  BigBlock *current = This->headblock;
  BigBlock *newBigBlock;

  if (current == NULL) /* empty list */
  {
    newBigBlock = BIGBLOCKFILE_CreateBlock(index);
    
    if (newBigBlock != NULL)
    {
      newBigBlock->next = NULL;
      This->headblock = newBigBlock;
    }

    return newBigBlock;
  }
  else
  {
    /* 
     * special handling for head of the list
     */

    if (current->index == index) /* it's already here */
      return current;
    else if (current->index > index) /* insertion at head of the list */
    {
      newBigBlock = BIGBLOCKFILE_CreateBlock(index);

      if (newBigBlock != NULL)
      {
        newBigBlock->next = current;
        This->headblock = newBigBlock;
      }

      return newBigBlock;
    }
  }

  /* iterate through rest the list
   */
  while (current->next != NULL)
  {
    if (current->next->index == index) /* found it */
    {
      return current->next;
    }
    else if (current->next->index > index) /* it's not in the list */
    {
      newBigBlock = BIGBLOCKFILE_CreateBlock(index);

      if (newBigBlock != NULL)
      {
        newBigBlock->next = current->next;
        current->next = newBigBlock;
      }

      return newBigBlock;
    }
    else
      current = current->next;
  }

  /* 
   * insertion at end of the list
   */
  if (current->next == NULL)
  {
    newBigBlock = BIGBLOCKFILE_CreateBlock(index);

    if (newBigBlock != NULL)
    {
      newBigBlock->next = NULL;
      current->next = newBigBlock;
    }

    return newBigBlock;
  }

  return NULL;
}

/******************************************************************************
 *      BIGBLOCKFILE_RemoveAllBlocks      [PRIVATE]
 *
 * Removes all blocks from the blocks list.
 */
static void BIGBLOCKFILE_RemoveAllBlocks(
  LPBIGBLOCKFILE This)
{
  BigBlock *current;

  while (This->headblock != NULL)
  {
    current = This->headblock;
    This->headblock = current->next;
    HeapFree(GetProcessHeap(), 0, current);
  }
}

/******************************************************************************
 *      BIGBLOCKFILE_RemoveBlock      [PRIVATE]
 *
 * Removes the specified block from the blocks list.
 */
static void BIGBLOCKFILE_RemoveBlock(
  LPBIGBLOCKFILE This, 
  ULONG          index)
{
  BigBlock *current = This->headblock;

  /* 
   * empty list
   */
  if (current == NULL)
    return;

  /* 
   *special case: removing head of list
   */
  if (current->index == index)
  {
    /*
     * set new head free the old one
     */
    This->headblock = current->next;
    HeapFree(GetProcessHeap(), 0, current);
    
    return;
  }

  /* 
   * iterate through rest of the list
   */
  while (current->next != NULL)
  {
    if (current->next->index == index) /* found it */
    {
      /* 
       * unlink the block and free the block
       */
      current->next = current->next->next;
      HeapFree(GetProcessHeap(), 0, current->next);

      return;
    }
    else
    {
      /* next node
       */
      current = current->next;
    }
  }
}

/******************************************************************************
 *      BIGBLOCKFILE_GetBigBlockFromPointer     [PRIVATE]
 *
 * Given a block pointer, this will return the corresponding block
 * from the blocks list.
 */
static BigBlock* BIGBLOCKFILE_GetBigBlockFromPointer(
  LPBIGBLOCKFILE This, 
  void*          pBlock)
{
  BigBlock *current = This->headblock;

  while (current != NULL)
  {
    if (current->lpBlock == pBlock)
    {
      break;
    }
    else
      current = current->next;
  }

  return current;
}

/******************************************************************************
 *      BIGBLOCKFILE_GetMappedView      [PRIVATE]
 *
 * Gets the page requested if it is already mapped.
 * If it's not already mapped, this method will map it
 */
static void * BIGBLOCKFILE_GetMappedView(
  LPBIGBLOCKFILE This,
  DWORD          pagenum,
  DWORD          desired_access)
{
  MappedPage* current = This->maplisthead;
  ULONG       count   = 1;
  BOOL        found   = FALSE;

  assert(This->maplisthead != NULL);

  /*
   * Search for the page in the list.
   */
  while ((found == FALSE) && (current->next != NULL))
  {
    if (current->next->number == pagenum)
    {
      found = TRUE;

      /*
       * If it's not already at the head of the list
       * move it there.
       */
      if (current != This->maplisthead)
      {
        MappedPage* temp = current->next;

        current->next = current->next->next;

        temp->next = This->maplisthead->next;
        This->maplisthead->next = temp;
      }
    }

    /*
     * The list is full and we haven't found it.
     * Free the last element of the list because we'll add a new
     * one at the head.
     */
    if ((found == FALSE) && 
        (count >= NUMBER_OF_MAPPED_PAGES) &&
        (current->next != NULL))
    {
      UnmapViewOfFile(current->next->lpBytes);

      HeapFree(GetProcessHeap(), 0, current->next);
      current->next = NULL;
    }

    if (current->next != NULL)
      current = current->next;

    count++;
  }

  /*
   * Add the page at the head of the list.
   */
  if (found == FALSE)
  {
    MappedPage* newMappedPage;
    DWORD       numBytesToMap;
    DWORD       hioffset  = 0;
    DWORD       lowoffset = PAGE_SIZE * pagenum;

    newMappedPage = HeapAlloc(GetProcessHeap(), 0, sizeof(MappedPage));

    if (newMappedPage == NULL)
      return NULL;

    newMappedPage->number = pagenum;
    newMappedPage->ref = 0;

    newMappedPage->next = This->maplisthead->next;
    This->maplisthead->next = newMappedPage;

    if (((pagenum + 1) * PAGE_SIZE) > This->filesize.LowPart)
      numBytesToMap = This->filesize.LowPart - (pagenum * PAGE_SIZE);
    else
      numBytesToMap = PAGE_SIZE;

    if (This->flProtect == PAGE_READONLY)
      desired_access = FILE_MAP_READ;
    else
      desired_access = FILE_MAP_WRITE;

    newMappedPage->lpBytes = MapViewOfFile(This->hfilemap,
                                           desired_access,
                                           hioffset,
                                           lowoffset,
                                           numBytesToMap);
  }

  /*
   * The page we want should now be at the head of the list.
   */
  assert(This->maplisthead->next != NULL);

  current = This->maplisthead->next;
  current->ref++;

  return current->lpBytes;
}

/******************************************************************************
 *      BIGBLOCKFILE_ReleaseMappedPage      [PRIVATE]
 *
 * Decrements the reference count of the mapped page.
 */
static void BIGBLOCKFILE_ReleaseMappedPage(
  LPBIGBLOCKFILE This,
  DWORD          pagenum,
  DWORD          access)
{
  MappedPage* previous = This->maplisthead;
  MappedPage* current;

  assert(This->maplisthead->next != NULL);

  current = previous->next;

  /* search for the page in the list
   */
  while (current != NULL)
  {
    if (current->number == pagenum)
    {
      /* decrement the reference count
       */
      current->ref--;
      return;
    }
    else
    {
      previous = current;
      current = current->next;
    }
  }
}

/******************************************************************************
 *      BIGBLOCKFILE_FreeAllMappedPages     [PRIVATE]
 *
 * Unmap all currently mapped pages.
 * Empty mapped pages list.
 */
static void BIGBLOCKFILE_FreeAllMappedPages(
  LPBIGBLOCKFILE This)
{
  MappedPage * current = This->maplisthead->next;

  while (current != NULL)
  {
    /* Unmap views.
     */
    UnmapViewOfFile(current->lpBytes);

    /* Free the nodes.
     */
    This->maplisthead->next = current->next;
    HeapFree(GetProcessHeap(), 0, current);

    current = This->maplisthead->next;
  }
}

/****************************************************************************
 *      BIGBLOCKFILE_GetProtectMode
 *
 * This function will return a protection mode flag for a file-mapping object
 * from the open flags of a file.
 */
static DWORD BIGBLOCKFILE_GetProtectMode(DWORD openFlags)
{
  DWORD flProtect        = PAGE_READONLY;
  BOOL bSTGM_WRITE     = ((openFlags & STGM_WRITE) == STGM_WRITE);
  BOOL bSTGM_READWRITE = ((openFlags & STGM_READWRITE) == STGM_READWRITE);
  BOOL bSTGM_READ      = ! (bSTGM_WRITE || bSTGM_READWRITE);

  if (bSTGM_READ)
    flProtect = PAGE_READONLY;

  if ((bSTGM_WRITE) || (bSTGM_READWRITE))
    flProtect = PAGE_READWRITE;

  return flProtect;
}

 
