unit dddToolsAdminLog;

interface

uses
  Windows,
  Messages,
  SysUtils,
  Variants,
  Classes,
  Graphics,
  Controls,
  Forms,
  Dialogs,
  ExtCtrls,
  StdCtrls,
  CheckLst,
  Menus,
  Grids,
  SynCommons,
  SynLog,
  mORMot,
  mORMotDDD;

type
  TLogFrame = class(TFrame)
    pnlLeft: TPanel;
    pnlRight: TPanel;
    spl1: TSplitter;
    edtSearch: TEdit;
    chklstEvents: TCheckListBox;
    pmFilter: TPopupMenu;
    mmoBottom: TMemo;
    drwgrdEvents: TDrawGrid;
    btnStartLog: TButton;
    tmrRefresh: TTimer;
    edtExistingLogKB: TEdit;
    lblExistingLogKB: TLabel;
    btnStopLog: TButton;
    spl2: TSplitter;
    BtnSearchNext: TButton;
    BtnSearchPrevious: TButton;
    procedure chklstEventsDrawItem(Control: TWinControl; Index: Integer; Rect:
      TRect; State: TOwnerDrawState);
    procedure btnStartLogClick(Sender: TObject);
    procedure tmrRefreshTimer(Sender: TObject);
    procedure drwgrdEventsDrawCell(Sender: TObject; ACol, ARow: Integer; Rect:
      TRect; State: TGridDrawState);
    procedure drwgrdEventsClick(Sender: TObject); virtual;
    procedure btnSearchNextClick(Sender: TObject);
    procedure chklstEventsDblClick(Sender: TObject);
    procedure btnStopLogClick(Sender: TObject);
    procedure chklstEventsClickCheck(Sender: TObject);
    procedure drwgrdEventsDblClick(Sender: TObject);
  protected
    FLog: TSynLogFileView;
    FMenuFilterAll, FMenuFilterNone: TMenuItem;
    FCallbackPattern: RawUTF8;
    FLogSafe: TSynLocker;
    procedure EventsCheckToLogEvents;
    procedure pmFilterClick(Sender: Tobject);
    procedure ReceivedOne(const Text: RawUTF8);
    procedure SetListItem(Index: integer; const search: RawUTF8 = '');
  public
    Admin: IAdministratedDaemon;
    Callback: ISynLogCallback;
    OnLogReceived: function(Sender: TLogFrame; Level: TSynLogInfo;
      const Text: RawUTF8): boolean of object;
    constructor Create(Owner: TComponent; const aAdmin: IAdministratedDaemon); reintroduce;
    constructor CreateCustom(Owner: TComponent; const aAdmin: IAdministratedDaemon;
      const aEvents, aPattern: RawUTF8); virtual;
    destructor Destroy; override;
    procedure LogFilter(F: TSynLogInfos);
    procedure Closing;
  end;

  TLogFrameClass = class of TLogFrame;

  TLogFrameDynArray = array of TLogFrame;

  TLogFrameChat = class(TLogFrame)
  protected
    procedure mmoChatKeyPress(Sender: TObject; var Key: Char);
  public
    mmoChat: TMemo;
    constructor CreateCustom(Owner: TComponent; const aAdmin:
      IAdministratedDaemon; const aEvents, aPattern: RawUTF8); override;
  end;

implementation

uses
  dddToolsAdminMain;

{$R *.dfm}

{ TLogFrameCallback }

type
  TLogFrameCallback = class(TInterfacedObject, ISynLogCallback)
  public
    Owner: TLogFrame;
    Pattern: RawUTF8;
    procedure Log(Level: TSynLogInfo; const Text: RawUTF8);
  end;

procedure TLogFrameCallback.Log(Level: TSynLogInfo; const Text: RawUTF8);
begin
  if (Pattern <> '') and (Level <> sllNone) then
    if PosI(pointer(Pattern), Text) = 0 then
      exit;
  Owner.ReceivedOne(Text);
  if Assigned(Owner.OnLogReceived) then
    Owner.OnLogReceived(Owner, Level, Text);
end;

procedure TLogFrame.chklstEventsDrawItem(Control: TWinControl; Index: Integer;
  Rect: TRect; State: TOwnerDrawState);
var
  E: TSynLogInfo;
begin
  if Index < 0 then
    exit;
  E := TSynLogInfo(chklstEvents.Items.Objects[Index]);
  with chklstEvents.Canvas do begin
    Brush.Color := LOG_LEVEL_COLORS[false, E];
    Font.Color := LOG_LEVEL_COLORS[true, E];
    TextRect(Rect, Rect.Left + 4, Rect.Top, ToCaption(E));
  end;
end;

var
  LogFrameCount: integer;

constructor TLogFrame.Create(Owner: TComponent; const aAdmin: IAdministratedDaemon);
var
  F: TSynLogFilter;
  M: TMenuItem;
begin
  inherited Create(Owner);
  FLogSafe.Init;
  Admin := aAdmin;
  Name := 'LogFrame' + IntToStr(LogFrameCount);
  inc(LogFrameCount);
  for F := low(F) to high(F) do begin
    M := TMenuItem.Create(self);
    M.Caption := ToCaption(F);
    M.Tag := ord(F);
    M.OnClick := pmFilterClick;
    if F = lfAll then
      FMenuFilterAll := M
    else if F = lfNone then
      FMenuFilterNone := M;
    pmFilter.Items.Add(M);
  end;
  btnStopLogClick(nil);
end;

constructor TLogFrame.CreateCustom(Owner: TComponent;
  const aAdmin: IAdministratedDaemon; const aEvents, aPattern: RawUTF8);
var
  P: PUTF8Char;
  e: integer;
begin
  Create(Owner, aAdmin);
  pmFilterClick(FMenuFilterNone);
  P := pointer(aEvents);
  while P <> nil do begin
    e := PTypeInfo(TypeInfo(TSynLogInfo))^.EnumBaseType.GetEnumNameValue(
      pointer(GetNextItem(P)));
    if e > 0 then // ignore e=0=sllNone
      chklstEvents.Checked[e - 1] := True;
  end;
  FCallbackPattern := UpperCase(aPattern);
  btnStartLogClick(self);
  btnStopLog.Hide; { TODO: allow event log closing }
end;

destructor TLogFrame.Destroy;
begin
  FLogSafe.Done;
  inherited;
end;

procedure TLogFrame.btnStopLogClick(Sender: TObject);
var
  E: TSynLogInfo;
begin
  chklstEvents.Top := 56;
  chklstEvents.Items.Clear;
  for E := succ(sllNone) to high(E) do begin
    if (Sender = Self) and not (E in FLog.Events) then
      continue; // from TLogFrame.CreateCustom()
    chklstEvents.Items.AddObject(ToCaption(E), pointer(ord(E)));
  end;
  chklstEvents.Height := 8 + chklstEvents.Count * chklstEvents.ItemHeight;
  pmFilterClick(FMenuFilterAll);
  if Sender = nil then
    exit;
  btnStartLog.Show;
  btnStopLog.Hide;
  edtExistingLogKB.Show;
  lblExistingLogKB.Show;
  edtSearch.Hide;
  btnSearchNext.Hide;
  BtnSearchPrevious.Hide;
  mmoBottom.Text := '';
  drwgrdEvents.Row := 0;
  drwgrdEvents.RowCount := 0;
  drwgrdEvents.Tag := 0;
  tmrRefresh.Enabled := false;
  (Owner as TAdminControl).EndLog(self);
end;

procedure TLogFrame.LogFilter(F: TSynLogInfos);
var
  i: integer;
begin
  for i := 0 to chklstEvents.Count - 1 do
    chklstEvents.Checked[i] := TSynLogInfo(chklstEvents.Items.Objects[i]) in F;
  chklstEventsClickCheck(nil);
end;

procedure TLogFrame.pmFilterClick(Sender: Tobject);
begin
  if Sender.InheritsFrom(TMenuItem) then
    LogFilter(LOG_FILTER[TSynLogFilter(TMenuItem(Sender).Tag)]);
end;

procedure TLogFrame.EventsCheckToLogEvents;
var
  i: integer;
  events: TSynLogInfos;
begin
 integer(events) := 0;
  for i := 0 to chklstEvents.Count - 1 do
    if chklstEvents.Checked[i] then
      Include(events, TSynLogInfo(chklstEvents.Items.Objects[i]));
  FLog.Events := events;
end;

procedure TLogFrame.btnStartLogClick(Sender: TObject);
var
  cb: TLogFrameCallback;
  kb, i: integer;
begin
  cb := TLogFrameCallback.Create;
  cb.Owner := Self;
  cb.Pattern := FCallbackPattern;
  Callback := cb;
  FLogSafe.Lock;
  try
    try
      FLog := TSynLogFileView.Create;
      drwgrdEvents.DoubleBuffered := true;
      drwgrdEvents.ColCount := 4;
      drwgrdEvents.ColWidths[0] := 70;
      drwgrdEvents.ColWidths[1] := 60;
      drwgrdEvents.ColWidths[2] := 24;
      drwgrdEvents.ColWidths[3] := 2000;
      if Sender = self then
        kb := 64 // from TLogFrame.CreateCustom
      else
        kb := StrToIntDef(edtExistingLogKB.Text, 0);
      EventsCheckToLogEvents; // fill FLog.Events
      Admin.SubscribeLog(FLog.Events, Callback, kb);
      chklstEvents.Top := lblExistingLogKB.Top;
      for i := chklstEvents.Count - 1 downto 0 do
        if not chklstEvents.Checked[i] then
          chklstEvents.Items.Delete(i);
      chklstEvents.Height := 8 + chklstEvents.Count * chklstEvents.ItemHeight;
      btnStopLog.Top := chklstEvents.Top + chklstEvents.Height + 8;
      btnStartLog.Hide;
      btnStopLog.Show;
      edtExistingLogKB.Hide;
      lblExistingLogKB.Hide;
      edtSearch.Show;
      btnSearchNext.Show;
      BtnSearchPrevious.Show;
      drwgrdEvents.Show;
      tmrRefresh.Enabled := true;
    except
      Callback := nil;
      FreeAndNil(FLog);
    end;
  finally
    fLogSafe.UnLock;
  end;
end;

const
  TAG_NONE = 0;
  TAG_REFRESH = 1;

procedure TLogFrame.ReceivedOne(const Text: RawUTF8);
var
  P: PUTF8Char;
  line: RawUTF8;
begin
  // warning: this method is called from WebSockets thread, not UI thread
  if Callback = nil then
    exit;
  FLogSafe.Lock;
  try
    if (FLog = nil) or (Text = '') then
      exit;
    P := pointer(Text);
    repeat // handle multiple log rows in the incoming text
      line := GetNextLine(P, P);
      if length(line) < 24 then
        continue;
      FLog.AddInMemoryLine(line);
      tmrRefresh.Tag := TAG_REFRESH; // notify tmrRefreshTimer()
    until P = nil;
  finally
    FLogSafe.UnLock;
  end;
end;

procedure TLogFrame.tmrRefreshTimer(Sender: TObject);
var
  moveToLast: boolean;
begin
  FLogSafe.Lock; // to protect tmrRefresh.Tag access from ReceivedOne()
  try
    if (tmrRefresh.Tag = TAG_NONE) or (fLog = nil) then
      exit;
    moveToLast := drwgrdEvents.Row = drwgrdEvents.RowCount - 1;
    drwgrdEvents.RowCount := FLog.SelectedCount;
    if FLog.SelectedCount > 0 then
      if (drwgrdEvents.Tag = 0) or moveToLast then begin
        drwgrdEvents.Row := FLog.SelectedCount - 1;
        drwgrdEvents.Tag := 1;
      end;
    drwgrdEvents.Invalidate;
    tmrRefresh.Tag := TAG_NONE;
  finally
    FLogSafe.UnLock;
  end;
end;

procedure TLogFrame.drwgrdEventsDrawCell(Sender: TObject; ACol, ARow: Integer;
  Rect: TRect; State: TGridDrawState);
var
  txt: string;
  inverted: boolean;
  level: TSynLogInfo;
begin
  with drwgrdEvents.Canvas do begin
    Brush.Style := bsClear;
    FLogSafe.Lock;
    try
      txt := FLog.GetCell(ACol,ARow,level);
    finally
      FLogSafe.UnLock;
    end;
    if level=sllNone then
      Brush.Color := clLtGray else begin
      inverted := (gdFocused in State) or (gdSelected in State);
      if inverted then
        Brush.Color := clBlack else
        Brush.Color := LOG_LEVEL_COLORS[inverted,level];
      Font.Color  := LOG_LEVEL_COLORS[not inverted,level];
    end;
    FillRect(Rect);
    TextRect(Rect,Rect.Left+4,Rect.Top,txt);
  end;
end;

procedure TLogFrame.drwgrdEventsClick(Sender: TObject);
var
  row: integer;
  s: string;
  sel: TGridRect;
begin
  row := drwgrdEvents.Row;
  sel := drwgrdEvents.Selection;
  FLogSafe.Lock;
  try
    s := FLog.GetLineForMemo(row,sel.Top,sel.Bottom);
  finally
    FLogSafe.UnLock;
  end;
  mmoBottom.Text := s;
end;

procedure TLogFrame.btnSearchNextClick(Sender: TObject);
var
  s: RawUTF8;
  ndx: integer;
begin
  s := UpperCase(StringToUTF8(edtSearch.Text));
  FLogSafe.Lock;
  Screen.Cursor := crHourGlass;
  try
    if Sender=BtnSearchPrevious then
      ndx := FLog.SearchPreviousText(s,drwgrdEvents.Row) else
    if Sender=edtSearch then
      ndx := FLog.SearchNextText(s,drwgrdEvents.Row,0) else
      ndx := FLog.SearchNextText(s,drwgrdEvents.Row,1); // e.g. BtnSearchNext
    if ndx>=0 then
      SetListItem(ndx,s);
  finally
    FLogSafe.UnLock;
    Screen.Cursor := crDefault;
  end;
end;

procedure TLogFrame.SetListItem(Index: integer; const search: RawUTF8);
var
  i: integer;
  s, ss: string;
begin
  if (FLog = nil) or (cardinal(Index) >= cardinal(FLog.SelectedCount)) then
    mmoBottom.Text := ''
  else begin
    drwgrdEvents.Row := Index;
    if (search = '') and drwgrdEvents.Visible then
      drwgrdEvents.SetFocus;
    s := FLog.EventString(FLog.Selected[Index], '', 0, true);
    mmoBottom.Text := s;
    if search <> '' then begin
      ss := UTF8ToString(search);
      i := Pos(ss, SysUtils.UpperCase(s));
      if i > 0 then begin
        mmoBottom.SelStart := i - 1;
        mmoBottom.SelLength := length(ss);
        mmoBottom.SetFocus;
      end;
    end;
  end;
end;

procedure TLogFrame.Closing;
begin
  Callback := nil;
  FLogSafe.Lock;
  try
    FreeAndNil(fLog);
  finally
    FLogSafe.UnLock;
  end;
end;

procedure TLogFrame.chklstEventsDblClick(Sender: TObject);
var
  i: integer;
  E: TSynLogInfo;
begin
  if FLog.EventLevel = nil then // plain text file does not handle this
    exit;
  i := chklstEvents.ItemIndex;
  if i < 0 then
    exit;
  E := TSynLogInfo(chklstEvents.Items.Objects[i]);
  FLogSafe.Lock;
  try
    i := FLog.SearchNextEvent(E,drwgrdEvents.Row);
    if i>=0 then
      SetListItem(i);
  finally
    FLogSafe.UnLock;
  end;
end;

procedure TLogFrame.chklstEventsClickCheck(Sender: TObject);
var
  selected: integer;
begin
  if FLog = nil then
    exit;
  EventsCheckToLogEvents; // fill FLog.Events
  FLogSafe.Lock;
  try
    selected := FLog.Select(drwgrdEvents.Row);
    if cardinal(selected) < cardinal(FLog.SelectedCount) then
      drwgrdEvents.Row := 0; // avoid "Grid Out Of Range" when setting RowCount
    drwgrdEvents.RowCount := FLog.SelectedCount;
    if selected>=0 then
      SetListItem(selected);
  finally
    FLogSafe.UnLock;
  end;
  if drwgrdEvents.Visible then begin
    drwgrdEvents.Repaint;
    drwgrdEventsClick(nil);
  end;
end;

procedure TLogFrame.drwgrdEventsDblClick(Sender: TObject);
var ndx: integer;
begin
  ndx := fLog.SearchEnterLeave(drwgrdEvents.Row);
  if ndx>=0 then
    SetListItem(ndx);
end;


{ TLogFrameChat }

constructor TLogFrameChat.CreateCustom(Owner: TComponent; const aAdmin:
  IAdministratedDaemon; const aEvents, aPattern: RawUTF8);
begin
  inherited;
  chklstEvents.Enabled := false;
  mmoChat := TMemo.Create(self);
  mmoChat.Parent := self;
  mmoChat.Height := 40;
  mmoChat.Align := alTop;
  mmoChat.OnKeyPress := mmoChatKeyPress;
end;

procedure TLogFrameChat.mmoChatKeyPress(Sender: TObject; var Key: Char);
begin
  if Key = #13 then begin
    if Assigned(Admin) then
      Admin.DatabaseExecute('', FormatUTF8('#chat % %',
        [ExeVersion.User, StringToUTF8(mmoChat.Text)]));
    mmoChat.Clear;
    Key := #0;
  end;
end;

end.

