{ ****************************************************************************** }
{ * Status IO          writen by QQ 600585@qq.com                              * }
{ * https://zpascal.net                                                        * }
{ * https://github.com/PassByYou888/zAI                                        * }
{ * https://github.com/PassByYou888/ZServer4D                                  * }
{ * https://github.com/PassByYou888/PascalString                               * }
{ * https://github.com/PassByYou888/zRasterization                             * }
{ * https://github.com/PassByYou888/CoreCipher                                 * }
{ * https://github.com/PassByYou888/zSound                                     * }
{ * https://github.com/PassByYou888/zChinese                                   * }
{ * https://github.com/PassByYou888/zExpression                                * }
{ * https://github.com/PassByYou888/zGameWare                                  * }
{ * https://github.com/PassByYou888/zAnalysis                                  * }
{ * https://github.com/PassByYou888/FFMPEG-Header                              * }
{ * https://github.com/PassByYou888/zTranslate                                 * }
{ * https://github.com/PassByYou888/InfiniteIoT                                * }
{ * https://github.com/PassByYou888/FastMD5                                    * }
{ ****************************************************************************** }

unit DoStatusIO;

{$INCLUDE zDefine.inc}

interface

uses
{$IFNDEF FPC}
{$IF Defined(WIN32) or Defined(WIN64)}
  Windows,
{$ELSEIF not Defined(Linux)}
  FMX.Types,
{$IFEND}
{$IFEND FPC}
  SysUtils, Classes, SyncObjs,
{$IFDEF FPC}
  FPCGenericStructlist, fgl,
{$ELSE FPC}
  System.Generics.Collections,
{$ENDIF FPC}
  PascalStrings, UPascalStrings, UnicodeMixedLib, CoreClasses;

type
{$IFDEF FPC}
  TDoStatusProc = procedure(Text_: SystemString; const ID: Integer) is nested;
{$ELSE FPC}
  TDoStatusProc = reference to procedure(Text_: SystemString; const ID: Integer);
{$ENDIF FPC}
  TDoStatusMethod = procedure(Text_: SystemString; const ID: Integer) of object;
  TDoStatusCall = procedure(Text_: SystemString; const ID: Integer);

procedure AddDoStatusHook(TokenObj: TCoreClassObject; CallProc: TDoStatusMethod);
procedure AddDoStatusHookM(TokenObj: TCoreClassObject; CallProc: TDoStatusMethod);
procedure AddDoStatusHookC(TokenObj: TCoreClassObject; CallProc: TDoStatusCall);
procedure AddDoStatusHookP(TokenObj: TCoreClassObject; CallProc: TDoStatusProc);
procedure DeleteDoStatusHook(TokenObj: TCoreClassObject);
procedure RemoveDoStatusHook(TokenObj: TCoreClassObject);
procedure DisableStatus;
procedure EnabledStatus;

procedure DoStatus(Text_: SystemString; const ID: Integer); overload;
procedure DoStatus(const v: Pointer; siz, width: NativeInt); overload;
procedure DoStatus(prefix: SystemString; v: Pointer; siz, width: NativeInt); overload;
procedure DoStatus(const v: TCoreClassStrings); overload;
procedure DoStatus(const v: Int64); overload;
procedure DoStatus(const v: Integer); overload;
procedure DoStatus(const v: Single); overload;
procedure DoStatus(const v: Double); overload;
procedure DoStatus(const v: Pointer); overload;
procedure DoStatus(const v: SystemString; const Args: array of const); overload;
procedure DoError(v: SystemString; const Args: array of const); overload;
procedure DoStatus(const v: SystemString); overload;
procedure DoStatus(const v: TPascalString); overload;
procedure DoStatus(const v: TUPascalString); overload;
procedure DoStatus(const v: TMD5); overload;
procedure DoStatus; overload;

procedure DoStatusNoLn(const v: TPascalString); overload;
procedure DoStatusNoLn(const v: SystemString; const Args: array of const); overload;
procedure DoStatusNoLn; overload;

function StrInfo(s: TPascalString): string; overload;
function StrInfo(s: TUPascalString): string; overload;
function BytesInfo(s: TBytes): string; overload;

var
  LastDoStatus: SystemString;
  IDEOutput: Boolean;
  ConsoleOutput: Boolean;
  OnDoStatusHook: TDoStatusCall;
  StatusThreadID: Boolean;

implementation

procedure bufHashToString(hash: Pointer; Size: NativeInt; var output: TPascalString);
const
  HexArr: array [0 .. 15] of SystemChar = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F');
var
  i: Integer;
begin
  output.Len := Size * 2;
  for i := 0 to Size - 1 do
    begin
      output.buff[i * 2] := HexArr[(PByte(nativeUInt(hash) + i)^ shr 4) and $0F];
      output.buff[i * 2 + 1] := HexArr[PByte(nativeUInt(hash) + i)^ and $0F];
    end;
end;

procedure DoStatus(Text_: SystemString; const ID: Integer);
begin
  try
      OnDoStatusHook(Text_, ID);
  except
  end;
end;

procedure DoStatus(const v: Pointer; siz, width: NativeInt);
var
  s: TPascalString;
  i: Integer;
  n: SystemString;
begin
  bufHashToString(v, siz, s);
  n := '';
  for i := 1 to s.Len div 2 do
    begin
      if n <> '' then
          n := n + #32 + s[i * 2 - 1] + s[i * 2]
      else
          n := s[i * 2 - 1] + s[i * 2];

      if i mod (width div 2) = 0 then
        begin
          DoStatus(n);
          n := '';
        end;
    end;
  if n <> '' then
      DoStatus(n);
end;

procedure DoStatus(prefix: SystemString; v: Pointer; siz, width: NativeInt);
var
  s: TPascalString;
  i: Integer;
  n: SystemString;
begin
  bufHashToString(v, siz, s);
  n := '';
  for i := 1 to s.Len div 2 do
    begin
      if n <> '' then
          n := n + #32 + s[i * 2 - 1] + s[i * 2]
      else
          n := s[i * 2 - 1] + s[i * 2];

      if i mod (width div 2) = 0 then
        begin
          DoStatus(prefix + n);
          n := '';
        end;
    end;
  if n <> '' then
      DoStatus(prefix + n);
end;

procedure DoStatus(const v: TCoreClassStrings);
var
  i: Integer;
  o: TCoreClassObject;
begin
  for i := 0 to v.Count - 1 do
    begin
      o := v.Objects[i];
      if o <> nil then
          DoStatus('%s<%s>', [v[i], o.ClassName])
      else
          DoStatus(v[i]);
    end;
end;

procedure DoStatus(const v: Int64);
begin
  DoStatus(IntToStr(v));
end;

procedure DoStatus(const v: Integer);
begin
  DoStatus(IntToStr(v));
end;

procedure DoStatus(const v: Single);
begin
  DoStatus(FloatToStr(v));
end;

procedure DoStatus(const v: Double);
begin
  DoStatus(FloatToStr(v));
end;

procedure DoStatus(const v: Pointer);
begin
  DoStatus(Format('0x%p', [v]));
end;

procedure DoStatus(const v: SystemString; const Args: array of const);
begin
  DoStatus(Format(v, Args));
end;

procedure DoError(v: SystemString; const Args: array of const);
begin
  DoStatus(Format(v, Args), 2);
end;

procedure DoStatus(const v: SystemString);
begin
  DoStatus(v, 0);
end;

procedure DoStatus(const v: TPascalString);
begin
  DoStatus(v.Text, 0);
end;

procedure DoStatus(const v: TUPascalString);
begin
  DoStatus(v.Text, 0);
end;

procedure DoStatus(const v: TMD5);
begin
  DoStatus(umlMD5ToString(v).Text);
end;

type
  TStatusProcStruct = record
    TokenObj: TCoreClassObject;
    OnStatusM: TDoStatusMethod;
    OnStatusC: TDoStatusCall;
    OnStatusP: TDoStatusProc;
  end;

  PStatusProcStruct = ^TStatusProcStruct;

  TStatusStruct = record
    s: SystemString;
    th: TCoreClassThread;
    TriggerTime: TTimeTick;
    ID: Integer;
  end;

  PStatusStruct = ^TStatusStruct;

  TStatusNoLnStruct = record
    s: TPascalString;
    th: TCoreClassThread;
    TriggerTime: TTimeTick;
  end;

  PStatusNoLnStruct = ^TStatusNoLnStruct;

  TStatusProcList = {$IFDEF FPC}specialize {$ENDIF FPC} TGenericsList<PStatusProcStruct>;
  TStatusStructList = {$IFDEF FPC}specialize {$ENDIF FPC} TGenericsList<PStatusStruct>;
  TStatusNoLnStructList = {$IFDEF FPC}specialize {$ENDIF FPC} TGenericsList<PStatusNoLnStruct>;

var
  StatusActive: Boolean;
  HookStatusProcs: TStatusProcList;
  StatusStructList: TStatusStructList;
  StatusCritical: TCriticalSection;
  StatusNoLnStructList: TStatusNoLnStructList;
  Hooked_OnCheckThreadSynchronize: TCheckThreadSynchronize;

function GetOrCreateStatusNoLnData_(th_: TCoreClassThread): PStatusNoLnStruct;
var
  tk: TTimeTick;
  i: Integer;
begin
  tk := GetTimeTick();
  Result := nil;
  i := 0;
  while i < StatusNoLnStructList.Count do
    begin
      if StatusNoLnStructList[i]^.th = th_ then
        begin
          Result := StatusNoLnStructList[i];
          Result^.TriggerTime := tk;

          if i > 0 then
              StatusNoLnStructList.Exchange(i, 0);
          inc(i);
        end
      else if tk - StatusNoLnStructList[i]^.TriggerTime > C_Tick_Minute then
        begin
          Dispose(StatusNoLnStructList[i]);
          StatusNoLnStructList.Delete(i);
        end
      else
          inc(i);
    end;

  if Result = nil then
    begin
      new(Result);
      Result^.s := '';
      Result^.th := th_;
      Result^.TriggerTime := tk;
      StatusNoLnStructList.Add(Result);
    end;
end;

function GetOrCreateStatusNoLnData(): PStatusNoLnStruct;
begin
  Result := GetOrCreateStatusNoLnData_(TCoreClassThread.CurrentThread);
end;

procedure DoStatusNoLn(const v: TPascalString);
var
  L, i: Integer;
  StatusNoLnData: PStatusNoLnStruct;
  pSS: PStatusStruct;
begin
  StatusCritical.Acquire;
  StatusNoLnData := GetOrCreateStatusNoLnData();
  try
    L := v.Len;
    i := 1;
    while i <= L do
      begin
        if CharIn(v[i], [#13, #10]) then
          begin
            if StatusNoLnData^.s.Len > 0 then
              begin
                new(pSS);
                pSS^.s := StatusNoLnData^.s.Text;
                pSS^.th := TCoreClassThread.CurrentThread;
                pSS^.TriggerTime := GetTimeTick;
                pSS^.ID := 0;
                StatusStructList.Add(pSS);
                StatusNoLnData^.s := '';
              end;
            repeat
                inc(i);
            until (i > L) or (not CharIn(v[i], [#13, #10]));
          end
        else
          begin
            StatusNoLnData^.s.Append(v[i]);
            inc(i);
          end;
      end;
  finally
      StatusCritical.Release;
  end;
end;

procedure DoStatusNoLn(const v: SystemString; const Args: array of const);
begin
  DoStatusNoLn(Format(v, Args));
end;

procedure DoStatusNoLn;
var
  StatusNoLnData: PStatusNoLnStruct;
  s: SystemString;
begin
  StatusCritical.Acquire;
  StatusNoLnData := GetOrCreateStatusNoLnData();
  s := StatusNoLnData^.s;
  StatusNoLnData^.s := '';
  StatusCritical.Release;
  if Length(s) > 0 then
      DoStatus(s);
end;

function StrInfo(s: TPascalString): string;
begin
  Result := BytesInfo(s.Bytes);
end;

function StrInfo(s: TUPascalString): string;
begin
  Result := BytesInfo(s.Bytes);
end;

function BytesInfo(s: TBytes): string;
begin
  Result := umlStringOf(s);
end;

procedure _InternalOutput(const Text_: U_String; const ID: Integer);
var
  i: Integer;
  p: PStatusProcStruct;
  n: U_String;
begin
  if Text_.Exists(#10) then
    begin
      n := Text_.DeleteChar(#13);
      _InternalOutput(umlGetFirstStr_Discontinuity(n, #10), ID);
      n := umlDeleteFirstStr_Discontinuity(n, #10);
      _InternalOutput(n, ID);
      exit;
    end;
  if (StatusActive) and (HookStatusProcs.Count > 0) then
    begin
      LastDoStatus := Text_;
      for i := HookStatusProcs.Count - 1 downto 0 do
        begin
          p := HookStatusProcs[i];
          try
            if Assigned(p^.OnStatusM) then
                p^.OnStatusM(Text_, ID);
            if Assigned(p^.OnStatusC) then
                p^.OnStatusC(Text_, ID);
            if Assigned(p^.OnStatusP) then
                p^.OnStatusP(Text_, ID);
          except
          end;
        end;
    end;

{$IFNDEF FPC}
  if (StatusActive) and ((IDEOutput) or (ID = 2)) and (DebugHook <> 0) then
    begin
{$IF Defined(WIN32) or Defined(WIN64)}
      OutputDebugString(PWideChar('"' + Text_ + '"'));
{$ELSEIF not Defined(Linux)}
      FMX.Types.Log.d('"' + Text_ + '"');
{$IFEND}
    end;
{$IFEND FPC}
  if (StatusActive) and ((ConsoleOutput) or (ID = 2)) and (IsConsole) then
      Writeln(Text_.Text);
end;

procedure CheckDoStatus(th: TCoreClassThread);
var
  i: Integer;
  pSS: PStatusStruct;
begin
  if StatusCritical = nil then
      exit;
  if (th = nil) or (th.ThreadID <> MainThreadID) then
      exit;
  StatusCritical.Acquire;
  try
    if StatusStructList.Count > 0 then
      begin
        for i := 0 to StatusStructList.Count - 1 do
          begin
            pSS := StatusStructList[i];
            _InternalOutput(pSS^.s, pSS^.ID);
            pSS^.s := '';
            Dispose(pSS);
          end;
        StatusStructList.Clear;
      end;
  finally
      StatusCritical.Release;
  end;
end;

procedure DoStatus;
begin
  CheckDoStatus(TCoreClassThread.CurrentThread);
end;

procedure InternalDoStatus(Text_: SystemString; const ID: Integer);
var
  th: TCoreClassThread;
  pSS: PStatusStruct;
begin
  th := TCoreClassThread.CurrentThread;
  if (th = nil) or (th.ThreadID <> MainThreadID) then
    begin
      new(pSS);
      if StatusThreadID then
          pSS^.s := '[' + IntToStr(th.ThreadID) + '] ' + Text_
      else
          pSS^.s := Text_;
      pSS^.th := th;
      pSS^.TriggerTime := GetTimeTick();
      pSS^.ID := ID;
      StatusCritical.Acquire;
      StatusStructList.Add(pSS);
      StatusCritical.Release;
      exit;
    end;

  CheckDoStatus(th);
  _InternalOutput(Text_, ID);
end;

procedure AddDoStatusHook(TokenObj: TCoreClassObject; CallProc: TDoStatusMethod);
begin
  AddDoStatusHookM(TokenObj, CallProc);
end;

procedure AddDoStatusHookM(TokenObj: TCoreClassObject; CallProc: TDoStatusMethod);
var
  p: PStatusProcStruct;
begin
  new(p);
  p^.TokenObj := TokenObj;
  p^.OnStatusM := CallProc;
  p^.OnStatusC := nil;
  p^.OnStatusP := nil;
  HookStatusProcs.Add(p);
end;

procedure AddDoStatusHookC(TokenObj: TCoreClassObject; CallProc: TDoStatusCall);
var
  p: PStatusProcStruct;
begin
  new(p);
  p^.TokenObj := TokenObj;
  p^.OnStatusM := nil;
  p^.OnStatusC := CallProc;
  p^.OnStatusP := nil;
  HookStatusProcs.Add(p);
end;

procedure AddDoStatusHookP(TokenObj: TCoreClassObject; CallProc: TDoStatusProc);
var
  p: PStatusProcStruct;
begin
  new(p);
  p^.TokenObj := TokenObj;
  p^.OnStatusM := nil;
  p^.OnStatusC := nil;
  p^.OnStatusP := CallProc;
  HookStatusProcs.Add(p);
end;

procedure DeleteDoStatusHook(TokenObj: TCoreClassObject);
var
  i: Integer;
  p: PStatusProcStruct;
begin
  i := 0;
  while i < HookStatusProcs.Count do
    begin
      p := HookStatusProcs[i];
      if p^.TokenObj = TokenObj then
        begin
          Dispose(p);
          HookStatusProcs.Delete(i);
        end
      else
          inc(i);
    end;
end;

procedure RemoveDoStatusHook(TokenObj: TCoreClassObject);
begin
  DeleteDoStatusHook(TokenObj);
end;

procedure DisableStatus;
begin
  StatusActive := False;
end;

procedure EnabledStatus;
begin
  StatusActive := True;
end;

procedure DoCheckThreadSynchronize;
begin
  DoStatus();
  if Assigned(Hooked_OnCheckThreadSynchronize) then
      Hooked_OnCheckThreadSynchronize();
end;

procedure _DoInit;
begin
  HookStatusProcs := TStatusProcList.Create;
  StatusStructList := TStatusStructList.Create;
  StatusCritical := TCriticalSection.Create;
  StatusNoLnStructList := TStatusNoLnStructList.Create;

  StatusActive := True;
  LastDoStatus := '';
  IDEOutput := False;
  ConsoleOutput := True;
  OnDoStatusHook := {$IFDEF FPC}@{$ENDIF FPC}InternalDoStatus;
  StatusThreadID := True;

  Hooked_OnCheckThreadSynchronize := CoreClasses.OnCheckThreadSynchronize;
  CoreClasses.OnCheckThreadSynchronize := {$IFDEF FPC}@{$ENDIF FPC}DoCheckThreadSynchronize;
end;

procedure _DoFree;
var
  i: Integer;
  pSS: PStatusStruct;
begin
  for i := 0 to HookStatusProcs.Count - 1 do
      Dispose(PStatusProcStruct(HookStatusProcs[i]));
  DisposeObject(HookStatusProcs);

  for i := 0 to StatusStructList.Count - 1 do
    begin
      pSS := StatusStructList[i];
      pSS^.s := '';
      Dispose(pSS);
    end;
  DisposeObject(StatusStructList);

  for i := 0 to StatusNoLnStructList.Count - 1 do
      Dispose(StatusNoLnStructList[i]);
  DisposeObject(StatusNoLnStructList);

  DisposeObject(StatusCritical);

  StatusActive := True;
  StatusCritical := nil;
end;

initialization

_DoInit;

finalization

_DoFree;

end.
