{$MODE OBJFPC}
unit DEngLog;

interface

uses
  dtypes,
  dstrnum;

type
PEngLog = ^TEngLog;
TEngLog = object
public type
  PLine = ^TLine;
  TLine = record
    Time: LongInt;
    Uptime: LongInt;
    Stage: AnsiChar;
    Sub: AnsiString;
    Msg: AnsiString;
    Priority: LongInt;
    Source: AnsiString;
    Color: Cardinal;
    TimeBuffer: AnsiString; // TODO TAnsiView here
    Buffer: AnsiString;
  end;
public type
  PConsumer = ^TConsumer;
  TConsumer = object
  protected
    FEnabled: Boolean;
    procedure ConsumeLine(Line: PLine); virtual;
    procedure SetEnabled(aEnabled: Boolean); virtual;
  public
    constructor Init;
    destructor Done; virtual;
    procedure Consume(Lines: PLine; Count: LongInt); virtual;
    procedure Flush; virtual;
    // calls destructor by default
    procedure OnUnsubscribe; virtual;
    property Enabled: Boolean read FEnabled write SetEnabled;
  end;
  TSystemConsumer = object(TConsumer)
  protected
    procedure ConsumeLine(Line: PLine); virtual;
  end;
  TFileConsumer = object(TConsumer)
        // TODO: dstreamfile.TFileStream?
  private
    FOpened: Boolean;
    FFileName: AnsiString;
    FFile: TextFile;
    procedure SetFileName(const aFileName: AnsiString);
    function GetFileName: AnsiString;
  protected
    procedure SetEnabled(aEnabled: Boolean); virtual;
    procedure ConsumeLine(Line: PLine); virtual;
  public
    constructor Init(const aFileName: AnsiString);
    destructor Done; virtual;
    procedure Flush; virtual;
    property FileName: AnsiString read GetFileName write SetFileName;
  end;
private
  FLine: TLine;
  FConsumers: array of PConsumer;
  FSystemConsumer: TSystemConsumer;
  FFileConsumer: TFileConsumer;
public
  // Use empty FileName for disable printing to file
  constructor Init(StdoutEnable: Boolean;
                   const FileName: AnsiString);
  destructor Done; virtual;
  procedure SetFileName(const FileName: AnsiString);
  procedure SetStage(Stage: AnsiChar);
  procedure AddConsumer(Consumer: PConsumer);
  procedure Unsubscribe(Consumer: PConsumer);
public // Convenience
  procedure LogTagged(const Sub, S: TString; Uptime: PtrInt); overload;
  procedure Log(const Msg: TString; Uptime: PtrInt); overload;
  procedure Flush; inline;
end;

implementation

uses
  {$IF Defined(WINDOWS)}
    windows
  {$ELSEIF Defined(ANDROID)}
    log
  {$ENDIF}
  ;

procedure TEngLog.TConsumer.ConsumeLine(Line: PLine);
begin
end;

procedure TEngLog.TConsumer.SetEnabled(aEnabled: Boolean);
begin
  FEnabled := aEnabled;
end;

constructor TEngLog.TConsumer.Init;
begin
  FEnabled := True;
end;

destructor TEngLog.TConsumer.Done;
begin
end;

procedure TEngLog.TConsumer.Consume(Lines: PLine; Count: LongInt);
var
  I: LongInt;
begin
  for I := 0 to Count - 1 do
    ConsumeLine(@Lines[I]);
end;

procedure TEngLog.TConsumer.Flush;
begin
end;

procedure TEngLog.TConsumer.OnUnsubscribe;
begin
  Done;
end;

procedure TEngLog.TSystemConsumer.ConsumeLine(Line: PLine);
begin
  if FEnabled then begin
{$IFDEF ANDROID}
    LOGW(PAnsiChar(Line^.Buffer));
{$ELSE}
    System.Writeln(Line^.Buffer);
{$ENDIF}
  end;
end;

procedure TEngLog.TFileConsumer.SetFileName(const aFileName: AnsiString);
begin
  if FFileName <> '' then begin
    {$I-}
    Close(FFile);
    {$I+}
    if IOResult <> 0 then
      {Do nothing};
  end;
  FFileName := aFileName;
  if FFileName <> '' then begin
    Assign(FFile, FFileName);
    {$I-}
    Rewrite(FFile);
    {$I+}
    if IOResult <> 0 then
      FFileName := '';
  end;
  FOpened := FEnabled and (FFileName <> '');
end;

function TEngLog.TFileConsumer.GetFileName: AnsiString;
begin
  Result := FFileName;
end;

procedure TEngLog.TFileConsumer.SetEnabled(aEnabled: Boolean);
begin
  inherited SetEnabled(aEnabled);
  FOpened := FEnabled and (FFileName <> '');
end;

procedure TEngLog.TFileConsumer.ConsumeLine(Line: PLine);
begin
  if FOpened then begin
    System.Writeln(FFile, Line^.Buffer);
    System.Flush(FFile);
  end;
end;

procedure TEngLog.TFileConsumer.Flush;
begin
  System.Flush(FFile);
end;

constructor TEngLog.TFileConsumer.Init(const aFileName: AnsiString);
begin
  inherited Init;
  FFileName := '';
  SetFileName(aFileName);
end;

destructor TEngLog.TFileConsumer.Done;
begin
  SetFileName('');
  inherited Done;
end;

constructor TEngLog.Init(StdoutEnable: Boolean; const FileName: AnsiString);
begin
  FConsumers := nil;
  FSystemConsumer.Init;
  FSystemConsumer.Enabled := StdoutEnable;
  AddConsumer(@FSystemConsumer);
  FFileConsumer.Init(FileName);
  AddConsumer(@FFileConsumer);
  FLine.Time := 0;
  FLine.Uptime := 0;
  FLine.Stage := '-';
  FLine.Sub := '';
  FLine.Msg := '';
  FLine.Priority := 0;
  FLine.Source := '';
  FLine.Color := $FFFFFFFF;
  FLine.TimeBuffer := '';
  FLine.Buffer := '';
end;

destructor TEngLog.Done;
var
  I: LongInt;
begin
  Flush;
  // TODO fix unit dependency order to acheive correct finalization order,
  //      then following lines will not stop any output
  for I := 0 to High(FConsumers) do
    FConsumers[I]^.OnUnsubscribe;
  FLine.Buffer := '';
  FLine.TimeBuffer := '';
end;

procedure TEngLog.SetFileName(const FileName: AnsiString);
begin
  FFileConsumer.FileName := FileName;
end;

procedure TEngLog.SetStage(Stage: AnsiChar);
begin
  FLine.Stage := Stage;
  // TODO update TimeBuffer with new Stage
end;

procedure UpdateLineTime(Line: TEngLog.PLine; Time, StartupTime: LongInt);
begin
  if Time <> Line^.Time then begin
    Line^.Time := Time;
    Line^.Uptime := Time - StartupTime;
    // TODO optimize for case when only last digit changes
    Line^.TimeBuffer := UIntToStr(PtrUInt(Line^.Uptime));
    while Length(Line^.TimeBuffer) < 8 do
      Line^.TimeBuffer := '0' + Line^.TimeBuffer;
  end;
end;

procedure UpdateLineMsg(Line: TEngLog.PLine; const Sub, Msg: AnsiString);
begin
  Line^.Sub := Sub;
  Line^.Msg := Msg;
  Line^.Buffer := Line^.TimeBuffer + Line^.Stage + ' ' + Sub + ' ' +  Msg;
end;

procedure TEngLog.LogTagged(const Sub, S: TString; Uptime: PtrInt);
var
  I: LongInt;
begin
  // LastUpdateTime ?
  UpdateLineTime(@FLine, Uptime, 0);
  UpdateLineMsg(@FLine, Sub, S);
  // TODO Optimize here calls to stderr and log.txt
  // Now we can pass filled FLine to all log consumers
  for I := 0 to High(FConsumers) do
    FConsumers[I]^.Consume(@FLine, 1);
end;

var
  GlobalCount: LongInt = 0;
procedure TEngLog.AddConsumer(Consumer: PConsumer);
begin
  SetLength(FConsumers, Length(FConsumers) + 1);
  FConsumers[High(FConsumers)] := Consumer;
end;

procedure TEngLog.Unsubscribe(Consumer: PConsumer);
var
  I, J: LongInt;
begin
  I := 0;
  while I < Length(FConsumers) do begin
    if FConsumers[I] = Consumer then begin
      for J := I to High(FConsumers) - 1 do
        FConsumers[J] := FConsumers[J + 1];
      SetLength(FConsumers, Length(FConsumers) - 1);
    end else
      Inc(I);
  end;
  Consumer^.OnUnsubscribe;
end;

procedure TEngLog.Log(const Msg: TString; Uptime: PtrInt);
begin
  LogTagged('', Msg, Uptime);
end;

procedure TEngLog.Flush;
var
  I: LongInt;
begin
  for I := 0 to High(FConsumers) do
    FConsumers[I]^.Flush;
end;

end.
