{ 10-05-1999 10:36:02 PM > [martin on MARTIN] checked out /Reformatting
   according to Delphi guidelines. }
{ 06-04-1999 2:42:04 AM > [martin on MARTIN] update: Removed checking
   pragmas Initial Version (0.1) /  }
{ 06-04-1999 1:46:37 AM > [martin on MARTIN] check in: (0.0) Initial Version
   / None }
unit MCHMemoryStream;

{Martin Harvey 18/7/1998

For donkeys years I've had performance problems when reading to
or writing from memory streams in small increments. This unit
intends to fix this problem.

Completely rewritten: Martin Harvey 5/4/1999.

I was unhappy with some performance issues in the original,
and the scheme for calculating size and position was not very logical,
and had special cases. This is now a more consistent and effecient rewrite}

{$RANGECHECKS OFF}
{$STACKCHECKS OFF}
{$IOCHECKS OFF}
{$OVERFLOWCHECKS OFF}

interface

{This stream acts as a memory stream by storing the data in 4k blocks,
 all of which are attached to a TList}

uses Classes;

const
  DataBlockSize = 4096;

type
  TDataBlockOffset = 0..DataBlockSize - 1;

  TDataBlock = array[0..DataBlockSize - 1] of byte;

  PDataBlock = ^TDataBlock;

{New rules for offset values are as follows:

 FPosBlock contains the number of the block which is about to be read or written to,
 given the current position.

 FPosOfs contains the offset of the byte that is about to be read or written to,
 given the current position. Always between 0 and DataBlockSize-1

 The number of blocks in the stream is given by the list count. If we are at the
 end of the stream, and the size of the stream is an exact multiple of the
 block size, then the last block will be empty.

 ie: The last block is never full.

}
  TMCHMemoryStream = class(TStream)
  private
    FBlockList:TList;
    FPosBlock:Longint;
    FPosOfs:TDataBlockOffset;
    FLastOfs:TDataBlockOffset;
    {FLastOfs is the offset of the byte to be read or written just off the end of the stream}
  protected
    function GetSize:longint;
    function GetPosition:longint;
    function ConvertOffsetsToLongint(Blocks:longint;BlockOfs:TDataBlockOffset):longint;
    procedure ConvertLongintToOffsets(Input:longint;var Blocks:longint;var BlockOfs:TDataBlockOffset);
    procedure ResizeBlockList(NewNumBlocks:longint);
  public
    constructor Create;
    destructor Destroy;override;
    {Necessary overrides}
    function Read(var Buffer;Count:longint):longint;override;
    function Write(const Buffer;Count:longint):longint;override;
    function Seek(Offset:Longint;Origin:word):Longint;override;
    {Procedures duplicating TCustomMemoryStream functionality}
    procedure SaveToStream(Stream:TStream);
    procedure SaveToFile(const FileName:string);
    {Procedures Duplicating TMemoryStream functionality}
    procedure Clear;
    procedure LoadFromStream(Stream:TStream);
    procedure LoadFromFile(const FileName:string);
    procedure SetSize(NewSize:longint);override;
  end;


implementation

uses SysUtils,Windows;

procedure TMCHMemoryStream.ResizeBlockList(NewNumBlocks:longint);

var
  iter,CurCount:longint;
  NewBlock:PDataBlock;

begin
  CurCount := FBlockList.Count;
  if NewNumBlocks > CurCount then
  begin
    for iter := CurCount to NewNumBlocks - 1 do
    begin
      New(NewBlock);
      FBlockList.Add(NewBlock);
    end;
  end
  else if NewNumBlocks < CurCount then
  begin
    for iter := NewNumBlocks to CurCount - 1 do
    begin
      Dispose(PDataBlock(FBlockList.Items[FBlockList.Count - 1]));
      FBlockList.Delete(FBlockList.Count - 1);
    end;
  end;
end;

function TMCHMemoryStream.GetSize;
begin
  result := ConvertOffsetsToLongint(FBlockList.Count - 1,FLastOfs);
end;

function TMCHMemoryStream.GetPosition;
begin
  result := ConvertOffsetsToLongint(FPosBlock,FposOfs);
end;

function TMCHMemoryStream.ConvertOffsetsTolongint(Blocks:longint;BlockOfs:TDataBlockOffset):longint;
begin
  Result := Blocks * DataBlockSize;
  Result := Result + BlockOfs;
end;

procedure TMCHMemoryStream.ConvertLongintToOffsets(Input:longint;var Blocks:longint;var BlockOfs:TDataBlockOffset);
begin
  Blocks := Input div DataBlockSize;
  BlockOfs := Input mod DataBlockSize;
end;

procedure TMCHMemoryStream.SetSize(NewSize:longint);

var
  NewNumBlocks:longint;
  CurPosition:longint;

begin
  if NewSize >= 0 then
  begin
    {Calculate current position}
    CurPosition := GetPosition;
    {Calculate end offsets for new size}
    ConvertLongintToOffsets(NewSize,NewNumBlocks,FLastOfs);
    {Now have the number of blocks needed, and the offset in the last block}
    ResizeBlockList(NewNumBlocks + 1);
    {List resized}
    {Now adjust position vars if needed}
    if NewSize < CurPosition then
    begin
      {Set current position to the end of the stream}
      FPosBlock := NewNumBlocks - 1;
      FPosOfs := FLastOfs;
    end;
  end;
end;

procedure TMCHMemoryStream.LoadFromStream(Stream:TStream);

var
  TempBlock:TDataBlock;
  BytesThisIteration:longint;

begin
  Stream.Seek(0,soFromBeginning);
  repeat
    BytesThisIteration := DataBlockSize;
    if BytesThisIteration > (Stream.Size - Stream.Position) then
      BytesThisIteration := Stream.Size - Stream.Position;
    Stream.ReadBuffer(TempBlock,BytesThisIteration);
    WriteBuffer(TempBlock,BytesThisIteration);
  until Stream.Position = Stream.Size;
end;

procedure TMCHMemoryStream.LoadFromFile(const FileName:string);
var
  Stream:TStream;
begin
  Stream := TFileStream.Create(FileName,fmOpenRead);
  try
    LoadFromStream(Stream);
  finally
    Stream.Free;
  end;
end;


procedure TMCHMemoryStream.SaveToStream(Stream:TStream);

var
  TempBlock:TDataBlock;
  BytesThisIteration:longint;

begin
  Seek(0,soFromBeginning);
  repeat
    BytesThisIteration := DataBlockSize;
    if BytesThisIteration > (Size - Position) then
      BytesThisIteration := Size - Position;
    ReadBuffer(TempBlock,BytesThisIteration);
    Stream.WriteBuffer(TempBlock,BytesThisIteration);
  until Position = Size;
end;

procedure TMCHMemoryStream.SaveToFile(const FileName:string);
var
  Stream:TStream;
begin
  Stream := TFileStream.Create(FileName,fmCreate);
  try
    SaveToStream(Stream);
  finally
    Stream.Free;
  end;
end;

function TMCHMemoryStream.Write(const Buffer;Count:longint):longint;

var
  CurPos,CurSize,BytesWritten,BytesThisBlock:longint;
  Src:Pointer;
  DestBlock:PDataBlock;

begin
  {Returns bytes written}
  if Count < 0 then
  begin
    result := 0;
    exit;
  end;
  result := count;
  CurPos := GetPosition;
  CurSize := GetSize;
  if CurPos + Result > CurSize then
    SetSize(CurPos + Result);
  {Enough blocks allocated, may result in zero sized block at end}
  {Now do the write}
  Src := @Buffer;
  BytesWritten := 0;
  repeat
    DestBlock := PDataBlock(FBlockList.Items[FPosBlock]);
    BytesThisBlock := DataBlockSize - FPosOfs;
    if BytesThisBlock > (Result - BytesWritten) then
      BytesThisBlock := Result - BytesWritten;
    CopyMemory(@DestBlock^[FPosOfs],Src,BytesThisBlock);
    {Now update position vars}
    if BytesThisBlock + FPosOfs = DataBlockSize then
    begin
      FPosOfs := 0;
      Inc(FPosBlock);
    end
    else
      FPosOfs := FPosOfs + BytesThisBlock;
    BytesWritten := BytesWritten + BytesThisBlock;
    Src := Pointer(Integer(Src) + BytesThisBlock);
  until BytesWritten = result;
end;

function TMCHMemoryStream.Read(var Buffer;Count:longint):longint;

var
  CurPos,CurSize,BytesRead,BytesThisBlock:longint;
  SrcBlock:PDataBlock;
  Dest:pointer;


begin
  {Returns bytes read}
  CurPos := GetPosition;
  CurSize := GetSize;
  result := Count;
  if result < 0 then result := 0;
  if result > (CurSize - CurPos) then result := CurSize - CurPos;
  if result > 0 then
  begin
    Dest := @Buffer;
    BytesRead := 0;
    repeat
      SrcBlock := PDataBlock(FBlockList.items[FPosBlock]);
      BytesThisBlock := DataBlockSize;
      if FPosBlock = FBlockList.Count - 1 then {We're on the last block}
        BytesThisBlock := FLastOfs;
      BytesThisBlock := BytesThisBlock - FPosOfs;
      if BytesThisBlock > (result - BytesRead) then
        BytesThisBlock := result - BytesRead;
      {Now copy the required number of bytes}
      CopyMemory(Dest,@SrcBlock^[FPosOfs],BytesThisBlock);
      {Now update position state}
      if BytesThisBlock + FPosOfs = DataBlockSize then
      begin
        FPosOfs := 0;
        Inc(FPosBlock);
      end
      else
        FPosOfs := FPosOfs + BytesThisBlock;
      BytesRead := BytesRead + BytesThisBlock;
      Dest := Pointer(Integer(Dest) + BytesThisBlock);
    until BytesRead = result;
  end;
end;


function TMCHMemoryStream.Seek(Offset:Longint;Origin:word):longint;

var
  CurPos,CurSize:longint;

begin
  {Remember that it returns new position}
  CurPos := GetPosition;
  CurSize := GetSize;
  case Origin of
    soFromBeginning:result := Offset;
    soFromCurrent:result := CurPos + Offset;
    soFromEnd:result := CurSize - Offset;
  else
    result := CurPos;
  end;
  ConvertLongintToOffsets(result,FPosBlock,FPosOfs);
end;

procedure TMCHMemoryStream.Clear;

begin
  SetSize(0);
end;

destructor TMCHMemoryStream.Destroy;

begin
  Clear;
  Dispose(PDataBlock(FBlockList.Items[0]));
  FBlockList.Free;
  inherited Destroy;
end;

constructor TMCHMemoryStream.Create;
begin
  inherited Create;
  FBlockList := TList.Create;
  Clear; {Allocates first block}
end;

end.

