//
// This unit is part of the GLScene Engine, http://glscene.org
//

unit GLFilePAK;

(* Methods for PAK Archiving *)

interface

{$I GLScene.inc}

uses
  System.Classes,
  System.SysUtils,
  GLS.ArchiveManager;

const
   SIGN = 'PACK';

type

   TPakHeader = record
      Signature: array[0..3] of AnsiChar;
      DirOffset: integer;
      DirLength: integer;
   end;

   TFileSection = record
      FileName: array[0..119] of AnsiChar;
      FilePos: integer;
      FileLength: integer;
   end;

  TPAKArchive=class(TGLBaseArchive)
    private
      FHeader: TPakHeader;
      FStream: TFileStream;
      function GetContentCount: integer;
      procedure MakeContentList;
    protected
      Procedure SetCompressionLevel(aValue: TCompressionLevel); override;
    public
     property ContentCount: integer Read GetContentCount;

      procedure LoadFromFile(const FileName: string); override;
      procedure Clear; override;

      function ContentExists(ContentName: string): boolean; override;

      function GetContent(Stream: TStream; index: integer): TStream;  override;
      function GetContent(index: integer): TStream;  override;
      function GetContent(ContentName: string): TStream;  override;

      function GetContentSize(index: integer): integer; override;
      function GetContentSize(ContentName: string): integer; override;

      procedure AddFromStream(ContentName, Path: string; FS: TStream); override;
      procedure AddFromFile(FileName, Path: string); override;

      procedure RemoveContent(index: integer); override;
      procedure RemoveContent(ContentName: string); override;

      procedure Extract(index: integer; NewName: string); override;
      procedure Extract(ContentName, NewName: string); override;
  end;

//-----------------------------------------------------------
implementation
//-----------------------------------------------------------

var
   Dir: TFileSection;

//-------------------------------
// TPAKArchive 
//-------------------------------

function TPAKArchive.GetContentCount: integer;
begin
   Result := FHeader.DirLength div SizeOf(TFileSection);
end;

procedure TPAKArchive.MakeContentList;
var
   I: integer;
begin
   FStream.Seek(FHeader.DirOffset, soFromBeginning);
   FContentList.Clear;
   for i := 0 to ContentCount - 1 do
   begin
      FStream.ReadBuffer(Dir, SizeOf(TFileSection));
      FContentList.Add(string(Dir.FileName));
   end;
end;

procedure TPAKArchive.SetCompressionLevel(aValue: TCompressionLevel);
begin
  aValue := clNone;
  inherited SetCompressionLevel(aValue);
end;

procedure TPAKArchive.LoadFromFile(const FileName: string);
begin
   FFileName := FileName;
    FStream := TFileStream.Create(FileName, fmOpenReadWrite or fmShareDenyWrite);
    //?????????? ?????
    If (FStream = nil) then exit;


    if FStream.Size = 0 then
   begin
     FHeader.Signature := SIGN;
     FHeader.DirOffset := SizeOf(TPakHeader);
     FHeader.DirLength := 0;
     FStream.WriteBuffer(FHeader, SizeOf(TPakHeader));
     FStream.Position := 0;
   end;

   FStream.ReadBuffer(FHeader, SizeOf(TPakHeader));
   if (FHeader.Signature <> SIGN)  then
   begin
      FreeAndNil(FStream); // nil it too to avoid own Clear() giving AV
      raise Exception.Create(FileName+' - This is not PAK file');
      Exit;
   end;
   if ContentCount <> 0 then
      MakeContentList;
end;

procedure TPAKArchive.Clear;
begin
   If FStream <> nil then FStream.Free;
   FContentList.Clear;
end;

function TPAKArchive.ContentExists(ContentName: string): boolean;
begin
   Result := (FContentList.IndexOf(ContentName) > -1);
end;

function TPAKArchive.GetContent(Stream: TStream; index: integer): TStream;
begin
      FStream.Seek(FHeader.DirOffset + SizeOf(TFileSection) * index, soFromBeginning);
      FStream.Read(Dir, SizeOf(TFileSection));
      FStream.Seek(Dir.FilePos, soFromBeginning);
      Result := Stream;
      Result.CopyFrom(FStream, Dir.FileLength);
      Result.Position := 0;
end;

function TPAKArchive.GetContent(index: integer): TStream;
begin
   Result:=GetContent(TMemoryStream.Create, index);
end;

function TPAKArchive.GetContent(ContentName: string): TStream;
begin
   Result := nil;
   if ContentExists(ContentName) then
      Result := GetContent(FContentList.IndexOf(ContentName));
end;

function TPAKArchive.GetContentSize(index: integer): integer;
begin
   FStream.Seek(FHeader.DirOffset + SizeOf(TFileSection) * index, soFromBeginning);
   FStream.Read(Dir, SizeOf(Dir));
   Result := Dir.FileLength;
end;

function TPAKArchive.GetContentSize(ContentName: string): integer;
begin
   Result := -1;
   if ContentExists(ContentName) then
      Result := GetContentSize(FContentList.IndexOf(ContentName));
end;

procedure TPAKArchive.AddFromStream(ContentName, Path: string; FS: TStream);
var
   Temp: TMemoryStream;
begin
      //?????????? ?????
   If (FStream = nil) or ContentExists(ContentName) then exit;
   Temp := nil;
   FStream.Position := FHeader.DirOffset;
   if FHeader.DirLength > 0 then
   begin
      Temp := TMemoryStream.Create;
      Temp.CopyFrom(FStream, FHeader.DirLength);
      Temp.Position    := 0;
      FStream.Position := FHeader.DirOffset;
   end;
   Dir.FilePos    := FHeader.DirOffset;
   Dir.FileLength := FS.Size;
   FStream.CopyFrom(FS, 0);

   FHeader.DirOffset := FStream.Position;
   if FHeader.DirLength > 0 then
   begin
      FStream.CopyFrom(Temp, 0);
      Temp.Free;
   end;
   StrPCopy(Dir.FileName, AnsiString(Path + ExtractFileName(ContentName)));
   FStream.WriteBuffer(Dir, SizeOf(TFileSection));
   FHeader.DirLength := FHeader.DirLength + SizeOf(TFileSection);
   FStream.Position  := 0;
   FStream.WriteBuffer(FHeader, SizeOf(TPakHeader));
   FContentList.Add(string(Dir.FileName));
end;

procedure TPAKArchive.AddFromFile(FileName, Path: string);
var
   FS: TFileStream;
begin
   if not FileExists(FileName) then
      exit;
   FS := TFileStream.Create(FileName, fmOpenRead);
   try
      AddFromStream(FileName, Path, FS);
   finally
      FS.Free;
   end;
end;

procedure TPAKArchive.RemoveContent(index: integer);
var
   Temp: TMemoryStream;
   i:    integer;
   f:    TFileSection;
begin
   Temp := TMemoryStream.Create;
   FStream.Seek(FHeader.DirOffset + SizeOf(TFileSection) * index, soFromBeginning);
   FStream.ReadBuffer(Dir, SizeOf(TFileSection));
   FStream.Seek(Dir.FilePos + Dir.FileLength, soFromBeginning);
   Temp.CopyFrom(FStream, FStream.Size - FStream.Position);
   FStream.Position := Dir.FilePos;
   FStream.CopyFrom(Temp, 0);
   FHeader.DirOffset := FHeader.DirOffset - dir.FileLength;
   Temp.Clear;
   for i := 0 to ContentCount - 1 do
      if i > index then
      begin
         FStream.Seek(FHeader.DirOffset + SizeOf(TFileSection) * i, soFromBeginning);
         FStream.ReadBuffer(f, SizeOf(TFileSection));
         FStream.Position := FStream.Position - SizeOf(TFileSection);
         f.FilePos := f.FilePos - dir.FileLength;
         FStream.WriteBuffer(f, SizeOf(TFileSection));
      end;

   i := FHeader.DirOffset + SizeOf(TFileSection) * index;
   FStream.Position := Cardinal(i + SizeOf(TFileSection));
   if FStream.Position < FStream.Size then
   begin
      Temp.CopyFrom(FStream, FStream.Size - FStream.Position);
      FStream.Position := i;
      FStream.CopyFrom(Temp, 0);
   end;
   Temp.Free;
   FHeader.DirLength := FHeader.DirLength - SizeOf(TFileSection);
   FStream.Position  := 0;
   FStream.WriteBuffer(FHeader, SizeOf(TPakHeader));
   FStream.Size := FStream.Size - dir.FileLength - SizeOf(TFileSection);
   MakeContentList;
end;

procedure TPAKArchive.RemoveContent(ContentName: string);
begin
   if ContentExists(ContentName) then
      RemoveContent(FContentList.IndexOf(ContentName));
end;

procedure TPAKArchive.Extract(index: integer; NewName: string);
var
   vExtractFileStream: TFileStream;
   vTmpStream: Tstream;
begin
   if NewName = '' then
      Exit;
   if (index < 0) or (index >= ContentCount) then
      exit;
   vExtractFileStream := TFileStream.Create(NewName, fmCreate);
   vTmpStream := GetContent(index);
   vExtractFileStream.CopyFrom(vTmpStream, 0);
   vTmpStream.Free;
   vExtractFileStream.Free;
end;

procedure TPAKArchive.Extract(ContentName, NewName: string);
begin
   if ContentExists(ContentName) then
      Extract(FContentList.IndexOf(ContentName), NewName);
end;

//----------------------------------------
initialization
//----------------------------------------

RegisterArchiveFormat('pak','PAK File',TPAKArchive);

end.
