{*******************************************************}
{                                                       }
{       Tocsg.Serializer                                }
{                                                       }
{       Copyright (C) 2022 sunk                         }
{                                                       }
{*******************************************************}

unit Tocsg.Serializer;

interface

uses
  Tocsg.Obj, System.Classes, System.SysUtils, Winapi.Windows,
  Tocsg.Exception;

const
  SER_SIG: AnsiString = 'KzSer.';
  SER_VER = 1;

type
  TTgSerHeader = packed record
    sSig: array [0..11] of AnsiChar;
    nVer: Integer;
  end;

  ETgSerializer = class(ETgException);
  TTgSerializerBase = class(TTgObject)
  private
    bStreamFree_: Boolean;
  protected
    Header_: TTgSerHeader;
    Stream_: TStream;
    function GetPosition: LONGLONG;
    function GetSize: LONGLONG;
  public
    Constructor Create(aStream: TStream; bAutoStreamFree: Boolean = false); overload;
    Constructor Create; overload;
    Destructor Destroy; override;

    procedure SeekFront;
    procedure SeekEnd;

    property Stream: TStream read Stream_;
    property Header: TTgSerHeader read Header_;
  end;

  TTgSerializerSave = class(TTgSerializerBase)
  public
    procedure SaveHeader(nVer: Integer = SER_VER);
    procedure S_WideString(sVal: WideString);
    procedure S_AnsiString(sVal: AnsiString);
    procedure S_UTF8String(sVal: UTF8String);

    procedure S_Integer(nVal: Integer);
    procedure S_DWORD(dwVal: DWORD);
    procedure S_WORD(wVal: WORD);
    procedure S_LONGLONG(llVal: LONGLONG);
    procedure S_ULONGLONG(ullVal: ULONGLONG);
    procedure S_DateTime(dtVal: TDateTime);
    procedure S_Strings(aList: TStrings);
    procedure S_Boolean(bVal: Boolean);
    procedure S_Stream(aStream: TStream);

    procedure SaveToFile(sPath: String);
  end;

  TTgSerializerLoad = class(TTgSerializerSave)
  public
    function LoadHeader: Boolean;
    function L_WideString: WideString;
    function L_AnsiString: AnsiString;
    function L_UTF8String: UTF8String;

    function L_Integer: Integer;
    function L_DWORD: DWORD;
    function L_WORD: WORD;
    function L_LONGLONG: LONGLONG;
    function L_ULONGLONG: ULONGLONG;
    function L_DateTime: TDateTime;
    function L_Strings(aList: TStrings): Integer;
    function L_Boolean: Boolean;
    function L_Stream(aStream: TStream): LONGLONG;

    function IsEndOfFile: Boolean;
    function GetRemainSize: LONGLONG;
    function GetReadPercent: WORD;

    procedure LoadFromFile(sPath: String);
  end;

implementation

{ TTgSerializerBase }

Constructor TTgSerializerBase.Create(aStream: TStream; bAutoStreamFree: Boolean = false);
begin
  Inherited Create;
  ZeroMemory(@Header_, SizeOf(Header_));
  bStreamFree_ := bAutoStreamFree;
  Stream_ := aStream;
  if Stream_ = nil then
    raise ETgSerializer.Create('스트림이 지정되지 않았습니다.');
end;

Constructor TTgSerializerBase.Create;
begin
  Inherited Create;
  ZeroMemory(@Header_, SizeOf(Header_));
  Stream_ := TMemoryStream.Create;
  bStreamFree_ := true;
end;

Destructor TTgSerializerBase.Destroy;
begin
  if (Stream_ <> nil) and bStreamFree_ then
    FreeAndNil(Stream_);
  Inherited;
end;

function TTgSerializerBase.GetPosition: LONGLONG;
begin
  Result := Stream_.Position;
end;

function TTgSerializerBase.GetSize: LONGLONG;
begin
  Result := Stream_.Size;
end;

procedure TTgSerializerBase.SeekFront;
begin
  Stream_.Seek(0, soBeginning);
end;

procedure TTgSerializerBase.SeekEnd;
begin
  Stream_.Seek(0, soEnd)
end;

{ TTgSerializerSave }

procedure TTgSerializerSave.SaveHeader(nVer: Integer = SER_VER);
begin
  if Stream_.Position > 0 then
    raise Exception.Create('offset 값이 0보다 큽니다.');

  if Length(SER_SIG) > SizeOf(Header_.sSig) then
    raise Exception.Create('시그너처 길이가 너무 큽니다.');

  ZeroMemory(@Header_.sSig, SizeOf(Header_.sSig));
  CopyMemory(@Header_.sSig[0], @SER_SIG[1], Length(SER_SIG));
  Header_.nVer := nVer;

  Stream_.Write(Header_, SizeOf(Header_));
end;

procedure TTgSerializerSave.S_WideString(sVal: WideString);
var
  nLen: Integer;
begin
  nLen := Length(sVal) * 2;
  S_Integer(nLen);
  Stream_.Write(PWideChar(sVal)^, nLen);
end;

procedure TTgSerializerSave.S_AnsiString(sVal: AnsiString);
var
  nLen: Integer;
begin
  nLen := Length(sVal);
  S_Integer(nLen);
  Stream_.Write(PAnsiChar(sVal)^, nLen);
end;

procedure TTgSerializerSave.S_UTF8String(sVal: UTF8String);
var
  nLen: Integer;
begin
  nLen := Length(sVal);
  S_Integer(nLen);
  Stream_.Write(PUTF8String(sVal)^, nLen);
end;

procedure TTgSerializerSave.S_Integer(nVal: Integer);
begin
  try
    Stream_.Write(nVal, SizeOf(nVal));
  except
    _Trace('Fail .. S_Integer()');
  end;
end;

procedure TTgSerializerSave.S_DWORD(dwVal: DWORD);
begin
  try
    Stream_.Write(dwVal, SizeOf(dwVal));
  except
    _Trace('Fail .. S_DWORD()');
  end;
end;

procedure TTgSerializerSave.S_WORD(wVal: WORD);
begin
  try
    Stream_.Write(wVal, SizeOf(wVal));
  except
    _Trace('Fail .. S_WORD()');
  end;
end;

procedure TTgSerializerSave.S_LONGLONG(llVal: LONGLONG);
begin
  try
    Stream_.Write(llVal, SizeOf(llVal));
  except
    _Trace('Fail .. S_LONGLONG()');
  end;
end;

procedure TTgSerializerSave.S_ULONGLONG(ullVal: ULONGLONG);
begin
  try
    Stream_.Write(ullVal, SizeOf(ullVal));
  except
    _Trace('Fail .. S_ULONGLONG()');
  end;
end;

procedure TTgSerializerSave.S_DateTime(dtVal: TDateTime);
begin
  try
    Stream_.Write(dtVal, SizeOf(dtVal));
  except
    _Trace('Fail .. S_DateTime()');
  end;
end;

procedure TTgSerializerSave.S_Strings(aList: TStrings);
var
  i: Integer;
begin
  try
    S_Integer(aList.Count);
    for i := 0 to aList.Count - 1 do
      S_UTF8String(aList[i]);
  except
    _Trace('Fail .. S_Strings()');
  end;
end;

procedure TTgSerializerSave.S_Boolean(bVal: Boolean);
begin
  try
    Stream_.Write(bVal, SizeOf(bVal));
  except
    _Trace('Fail .. S_Boolean()');
  end;
end;

procedure TTgSerializerSave.S_Stream(aStream: TStream);
begin
  try
    aStream.Position := 0;
    S_LONGLONG(aStream.Size);
    Stream_.CopyFrom(aStream, aStream.Size);
  except
    _Trace('Fail .. S_Stream()');
  end;
end;

procedure TTgSerializerSave.SaveToFile(sPath: String);
begin
  if not (Stream_ is TMemoryStream) then
    raise ETgSerializer.Create('Stream이 TMemoryStream가 아닙니다.');

  try
    TMemoryStream(Stream_).SaveToFile(sPath);
  except
    on E: Exception do
      ETgException.TraceException(Self, E, 'Fail .. SaveToFile()');
  end;
end;

{ TTgSerializerLoad }

function TTgSerializerLoad.LoadHeader: Boolean;
begin
  Result := false;

  ZeroMemory(@Header_, SizeOf(Header_));

  if Stream_.Position > 0 then
    raise Exception.Create('offset 값이 0보다 큽니다.');

  try
    if Stream_.Read(Header_, SizeOf(Header_)) <> SizeOf(Header_) then
      exit;

  // 크기 체크 추가 19_0710 17:01:52 kku
    Result := (Stream_.Size > Stream_.Position) and
              CompareMem(@Header_.sSig[0], @SER_SIG[1], Length(SER_SIG));
  finally
    if not Result then
      Stream_.Position := 0;
  end;
end;

function TTgSerializerLoad.L_WideString: WideString;
var
  nLen: Integer;
begin
  Result := '';
  try
    nLen := L_Integer;
    if nLen = 0 then
      exit;

    if nLen > GetRemainSize then
      exit;
//      raise ETgSerializer.Create('남은 데이터가 부족합니다.');

    SetLength(Result, nLen div 2);
    Stream_.Read(Result[1], nLen);
  except
    _Trace('Fail .. L_WideString()');
  end;
end;

function TTgSerializerLoad.L_AnsiString: AnsiString;
var
  nLen: Integer;
begin
  Result := '';
  try
    nLen := L_Integer;
    if nLen = 0 then
      exit;

    if nLen > GetRemainSize then
      exit;
//      raise ETgSerializer.Create('남은 데이터가 부족합니다.');

    SetLength(Result, nLen);
    Stream_.Read(Result[1], nLen);
  except
    _Trace('Fail .. L_AnsiString()');
  end;
end;

function TTgSerializerLoad.L_UTF8String: UTF8String;
var
  nLen: Integer;
begin
  Result := '';
  try
    nLen := L_Integer;
    if nLen = 0 then
      exit;

    if nLen > GetRemainSize then
      exit;
//      raise ETgSerializer.Create('남은 데이터가 부족합니다.');

    SetLength(Result, nLen);
    Stream_.Read(Result[1], nLen);
  except
    _Trace('Fail .. L_UTF8String()');
  end;
end;

function TTgSerializerLoad.L_Integer: Integer;
begin
  try
    Stream_.Read(Result, SizeOf(Result));
  except
    _Trace('Fail .. L_Integer()');
  end;
end;

function TTgSerializerLoad.L_DWORD: DWORD;
begin
  try
    Stream_.Read(Result, SizeOf(Result));
  except
    _Trace('Fail .. L_DWORD()');
  end;
end;

function TTgSerializerLoad.L_WORD: WORD;
begin
  try
    Stream_.Read(Result, SizeOf(Result));
  except
    _Trace('Fail .. L_WORD()');
  end;
end;

function TTgSerializerLoad.L_LONGLONG: LONGLONG;
begin
  try
    Stream_.Read(Result, SizeOf(Result));
  except
    _Trace('Fail .. L_LONGLONG()');
  end;
end;

function TTgSerializerLoad.L_ULONGLONG: ULONGLONG;
begin
  try
    Stream_.Read(Result, SizeOf(Result));
  except
    _Trace('Fail .. L_ULONGLONG()');
  end;
end;

function TTgSerializerLoad.L_DateTime: TDateTime;
begin
  try
    Stream_.Read(Result, SizeOf(Result));
  except
    _Trace('Fail .. L_DateTime()');
  end;
end;

function TTgSerializerLoad.L_Strings(aList: TStrings): Integer;
var
  i, nCnt: Integer;
begin
  try
    nCnt := L_Integer;
    for i := 0 to nCnt - 1 do
      aList.Add(L_UTF8String);
  except
    _Trace('Fail .. L_Strings()');
  end;
end;

function TTgSerializerLoad.L_Boolean: Boolean;
begin
  try
    Stream_.Read(Result, SizeOf(Result));
  except
    _Trace('Fail .. L_Boolean()');
  end;
end;

function TTgSerializerLoad.L_Stream(aStream: TStream): LONGLONG;
var
  llSize: LONGLONG;
begin
  try
    llSize := L_LONGLONG;
    if llSize > 0 then
    begin
      Result := aStream.CopyFrom(Stream_, llSize);
      if Result <> llSize then
        raise ETgSerializer.Create('남은 데이터가 부족합니다.');
    end else
      Result := 0;
  except
    _Trace('Fail .. L_Stream()');
  end;
end;

function TTgSerializerLoad.IsEndOfFile: Boolean;
begin
  Result := Stream_.Position >= Stream_.Size;
end;

function TTgSerializerLoad.GetRemainSize: LONGLONG;
begin
  Result := Stream_.Size - Stream_.Position;
end;

function TTgSerializerLoad.GetReadPercent: WORD;
begin
  if Stream_.Size > 0 then
    Result := (Stream_.Position * 100) div Stream_.Size
  else
    Result := 0;
end;

procedure TTgSerializerLoad.LoadFromFile(sPath: String);
begin
  if not (Stream_ is TMemoryStream) then
    raise ETgSerializer.Create('Stream이 TMemoryStream가 아닙니다.');

  try
    TMemoryStream(Stream_).LoadFromFile(sPath);
    Stream_.Position := 0;
  except
    on E: Exception do
      ETgException.TraceException(Self, E, 'Fail .. LoadFromFile()');
  end;
end;

end.
