unit dgui_console;

{$CODEPAGE UTF8}
{$MODE FPC}
{$MODESWITCH AUTODEREF}
{$MODESWITCH DEFAULTPARAMETERS}
{$MODESWITCH OUT}
{$MODESWITCH RESULT}

interface

uses
  dvector2i,
  drect2i,
  dcolors,
  DEngBase,
  deng,
  DEngLog,
  DDrawFont,
  dgui3;

type
PConsole = ^TConsole;
TConsole = object(TWidget)
public type
  PExecutor = ^TExecutor;
  TExecutor = object
  private
    FConsole: PConsole;
  public
    constructor Init;
    destructor Done; virtual;
    procedure Execute(const S: AnsiString); virtual;
    property Console: PConsole read FConsole;
  end;
private type
  TLogConsumer = object(TEngLog.TConsumer)
  private
    FConsole: PConsole;
  protected
    procedure ConsumeLine(Line: TEngLog.PLine); virtual;
  public
    constructor Init(Console: PConsole);
    procedure OnUnsubscribe; virtual;
  end;
private
  FLogConsumer: TLogConsumer;
  FExecutor: PExecutor;
  FPrompt: AnsiString;
  procedure SetExecutor(Executor: PExecutor);
protected
  FOpened: Boolean;
  procedure Execute;
  procedure Insert(const S: AnsiString);
  procedure RemoveChar(Delta: LongInt);
  procedure DoRender; virtual;
  function DoOnWindowEvent(Ev: PWindowEvent): Boolean; virtual;
public
  FLines: array of AnsiString;
  FActiveLine: AnsiString;
  FHeight: LongInt;
  constructor Init(aParent: PWidget);
  destructor Done; virtual;
  function GetLine(Index: LongInt): AnsiString;
  procedure AddLine(const Line: AnsiString);
  property Opened: Boolean read FOpened write FOpened;
  property Executor: PExecutor write SetExecutor;
end;

implementation

constructor TConsole.TExecutor.Init;
begin
end;

destructor TConsole.TExecutor.Done;
begin
end;

procedure TConsole.TExecutor.Execute(const S: AnsiString);
begin
end;

constructor TConsole.TLogConsumer.Init(Console: PConsole);
begin
  FConsole := Console;
end;

procedure TConsole.TLogConsumer.ConsumeLine(Line: TEngLog.PLine);
begin
  FConsole^.AddLine(Line^.Buffer);
end;

procedure TConsole.TLogConsumer.OnUnsubscribe;
begin
  // Do nothing
end;

function IsLiteral(C: LongInt): Boolean;
begin
  Result := C in [Ord('a')..Ord('z'), Ord('A')..Ord('Z'),
                  //Ord('А')..Ord('Я'), Ord('а')..Ord('я'),
                  Ord('0')..Ord('9'), Ord(' '), Ord('#'),
                  Ord('.'), Ord(','), Ord('?'), Ord('!'),
                  Ord('_'), Ord('-'), Ord('/'), Ord('\'),
                  Ord('['), Ord(']'), Ord('{'), Ord('}'),
                  Ord('('), Ord(')'), Ord('<'), Ord('>'),
                  Ord('$'), Ord('%'), Ord('&'), Ord('@'),
                  Ord(':'), Ord(';'), Ord('"'), Ord(''''),
                  Ord('+'), Ord('*'), Ord('='), Ord('|'),
                  Ord('^')];
end;

procedure TConsole.SetExecutor(Executor: PExecutor);
begin
  FExecutor := Executor;
  if FExecutor <> nil then
    FExecutor^.FConsole := @Self;
end;

procedure TConsole.Execute;
begin
  if FExecutor <> nil then begin
    FExecutor^.Execute(FActiveLine);
  end else begin
    AddLine(FActiveLine);
  end;
  FActiveLine := '';
end;

procedure TConsole.Insert(const S: AnsiString);
begin
  // TODO use Cursor
  FActiveLine := FActiveLine + S;
end;

procedure TConsole.RemoveChar(Delta: LongInt);
begin
  // TODO use Delta and Cursor
  SetLength(FActiveLine, Length(FActiveLine) - 1);
end;

procedure TConsole.DoRender;
var
  S: TVec2i;
  I, Last: LongInt;
  H: LongInt;
  Font: PFont;
begin
  if not Opened then
    Exit;
  FHeight := 200;
  Eng.Draw.Color(Style^.GetColor('consoleBgNormal', RGBA(64, 64, 64, 196)));
  Eng.Draw.RectFilled(drect2i.Rect2i(Vec2i(0, 0), Vec2i(Eng.Width, FHeight)));
  Eng.Draw.Color(Style^.GetColor('consoleMainNormal', RGBA(255, 255, 255, 255)));
  S := Vec2i(20, FHeight);
  Font := Style^.GetFont(Theme, 'console');
  if Font <> nil then begin
    H := Font^.Height(FPrompt + FActiveLine);
  end else
    H := 0;
  S.Y := S.Y - H;
  if Font <> nil then
    Font^.Draw(S, FPrompt + FActiveLine);
  if H < 32 then
    S.Y := S.Y - 32 + H;
  Last := 20;
  if Last > High(FLines) then
    Last := High(FLines);
  if Font <> nil then begin
    for I := 0 to Last do begin
      S.Y := S.Y - Font^.Height(GetLine(I));
      Font^.Draw(S, GetLine(I));
    end;
  end;
end;

function TConsole.DoOnWindowEvent(Ev: PWindowEvent): Boolean;
begin
  if inherited DoOnWindowEvent(Ev) then
    Exit(True);
  Result := False;
  if Ev.IsKeyDown then begin
    if Ev.Key = KEY_TILDE then begin
      FOpened := not FOpened;
    end else if not FOpened then begin
      Exit(False);
    end else if Ev.Key = KEY_BACKSPACE then begin
      if Length(FActiveLine) > 0 then begin
        RemoveChar(-1);
        Exit(True);
      end;
    end else if Ev.Key = KEY_ENTER then begin
      Execute;
    end;
  end else if Ev.IsChar then begin
    if not FOpened then
      Exit(False);
    if IsLiteral(Ev.Key) then begin
      Insert(Char(Ev.Key));
      Exit(True);
    end;
  end;
  Result := False;
end;

constructor TConsole.Init(aParent: PWidget);
begin
  inherited Init(STYLE_DEFAULT, aParent);
  FOpened := False;
  FLogConsumer.Init(@Self);
  Eng.Log.AddConsumer(@FLogConsumer);
  FPrompt := '> ';
end;

destructor TConsole.Done;
begin
  Eng.Log.Unsubscribe(@FLogConsumer);
  FLogConsumer.Done;
end;

function TConsole.GetLine(Index: LongInt): AnsiString;
begin
  if (Index >= 0) and (Index < Length(FLines)) then begin
    Result := FLines[High(FLines) - Index];
  end else
    Result := '';
end;

procedure TConsole.AddLine(const Line: AnsiString);
begin
  SetLength(FLines, Length(FLines) + 1);
  FLines[High(FLines)] := Line;
end;

end.
