{$MODE OBJFPC}
{$MODESWITCH DUPLICATELOCALS}
{$MODESWITCH AUTODEREF}
{$MODESWITCH POINTERTOPROCVAR}
unit DEng;

interface

uses
  {$IF Defined(DENG_DEMO_MODE)}
  DEvLog,
  {$ENDIF}
  {$IF Defined(WINDOWS)}
    windows,
  {$ELSEIF Defined(ANDROID)}
    ctypes, looper, android_native_app_glue, log, native_window, input,
    keycodes,
  {$ENDIF}
  dtypes,
  dstrnum,
  dvector2i,
  dresourcemanager,
  DEngBase,
  DDraw,
  DMusic,
  DAudio,
  DWinApi,
  DControl,
  DMetrics,
  DEngLog;

{$I DEngConsts.inc}

type
TEngine         = ^OEngine;
TCallback       = procedure (Eng: TEngine);

TBlendMode      = DEngBase.TBlendMode;

PWindowEvent    = TWinApi.PWindowEvent;
TWindowEvent    = TWinApi.TWindowEvent;

PEngSubsystem = ^TEngSubsystem;
TEngSubsystem = object
private
  FEng: TEngine;
  FName: AnsiString;
public
  constructor Init(const Name: AnsiString; Eng: TEngine);
  destructor Done; virtual;
  procedure Update; virtual;
  procedure OnWindowEvent(Event: PWindowEvent); virtual;
  procedure OnInit; virtual;
  procedure OnDone; virtual;
  procedure OnUnsubscribe; virtual;
  property Name: AnsiString read FName;
  property Eng: TEngine read FEng;
end;

OEngine = object
public type
  ImplTWinApi = TWinApi;
  TWinApi = ImplTWinApi;
  ImplTAudio = TAudio;
  TAudio = ImplTAudio;
  ImplTMusic = TMusic;
  TMusic = ImplTMusic;
  ImplTDraw = TDraw;
  TDraw = ImplTDraw;
  ImplTControl = TControl;
  TControl = ImplTControl;
private
  FWinApi: TWinApi;
  FAudio: TAudio;
  FMusic: TMusic;
  FDraw: TDraw;
  FControl: TControl;
  FLog: TEngLog;
  FMetrics: TMetrics;
  FDurationStart: LongInt;
  FDurationStartMicroSeconds: Double;
  FLoopCycleHistogram: THistogram;
  FRenderHistogram: THistogram;
  FUpdateHistogram: THistogram;
  FEventHistogram: THistogram;
  function GetWinApi: PWinApi; inline;
  function GetAudio: PAudio; inline;
  function GetMusic: PMusic; inline;
  function GetDraw: PDraw; inline;
  function GetControl: PControl; inline;
  function GetLog: PEngLog; inline;
  function GetMetrics: PMetrics; inline;
public
  property WinApi: PWinApi read GetWinApi;
  property Audio: PAudio read GetAudio;
  property Music: PMusic read GetMusic;
  property Draw: PDraw read GetDraw;
  property Control: PControl read GetControl;
  property Log: PEngLog read GetLog;
  public property Metrics: PMetrics read GetMetrics;
private
  FInit: TCallback;
  FDone: TCallback;
  FRender: TCallback;
  FUpdate: TCallback;
  FUpdateInterval: LongInt;
  FLastUpdateTime: LongInt;
  FStartupTime: LongInt;
  FWidth: LongInt;
  FHeight: LongInt;
  FBits: LongInt;
  FFlags: LongInt;
  FTitle: TString;

  FFPS: LongInt;
  FLastRenderMicroSeconds: Double;
  FFrameTimeMicroSeconds: Double;
  FExiting: Boolean;
  FSubsystems: array of PEngSubsystem;

  FUser: Pointer;
  {$IFDEF ANDROID}
  Loaded: Boolean;
  {$ENDIF}

  procedure Update;
public
  // TODO make it private later. The Render method is used in the DWinApi on
  // windows to force rendering after window resize.
  procedure Render;
public
  procedure Init;
  procedure Done;
  procedure Free;
  // call these Init* functions before StartLoop
  // TODO Init* -> Set* , because Init* are using only for constructors
  procedure InitLogPath(const LogPath: AnsiString);
  procedure InitDisplayMode(Width, Height, Bits, Flags: LongInt);
  procedure InitTitle(const Title: TString);
  procedure InitInitFunc(InitCallback: TCallback);
  procedure InitDoneFunc(DoneCallback: TCallback);
  procedure InitRenderFunc(RenderCallback: TCallback);
  procedure InitUpdateFunc(UpdateCallback: TCallback; Interval: LongInt);
  procedure InitEventFunc(OnEvent: TOnWindowEvent); overload; inline;
  procedure InitEventFunc(OnEvent: TOnWindowEventFunc); overload; inline;
  procedure RegistSubsystem(Subsystem: PEngSubsystem);
  // TODO rename Init/Done callbacks to Startup/Shutdown
  procedure OnInit;
  procedure OnDone;
  procedure StartLoop;
  procedure StopLoop;
  procedure Run; overload;
  procedure Run(Render, Update: TCallback;
                Interval: LongInt = 30;
                Init: TCallback = nil;
                Done: TCallback = nil); overload;
  function Time: LongInt;
  function TimeMicroSeconds: Double; inline;
  function Delta: LongInt;
  function LastUpdateTime: LongInt;
  function FrameTime: PtrInt;
  // delegating
  procedure PrintLogTagged(const Sub, S: TString); overload;
  procedure PrintLogTagged(const Sub, S: TString; Groups: LongInt); overload;
  procedure FlushLog;
  function GetMouse: TVec2i; inline;
  function Width: LongInt; inline; // screen width
  function Height: LongInt; inline; // screen height
  property Mouse: TVec2i read GetMouse;
  property User: Pointer read FUser write FUser;
  property Graph: PDraw read GetDraw;
  property StartupTime: LongInt read FStartupTime;
  property FPS: LongInt read FFPS;
  property FrameTimeMicroSeconds: Double read FFrameTimeMicroSeconds;
{$IFDEF DENG_DEMO_MODE}
public
  procedure PlayDemo(const LogFileName: TString);
{$ENDIF}
end;

var
  Eng: OEngine;

procedure CallInitialization;
procedure NopCallback(Eng: TEngine);
procedure LogTagged(const G, S: AnsiString); overload; inline;
procedure Log(const S: AnsiString); overload; inline;

{$IFDEF ANDROID}
procedure _SetAndroidState(State: Pointer); inline;
{$ENDIF}

// :P
const
  Writeln = 0 deprecated 'Use the Log function instead of the Writeln in DEng applications, cause it leads GUI Windows applications to crash. Or use it as System.Writeln if you want to Writeln anyway.';
  Write = 0 deprecated 'Use the Log function instead of the Write in DEng applications, cause it leads GUI Windows applications to crash. Or use it as System.Write if you want to Write anyway.';

implementation

procedure NopCallback(Eng: TEngine);
begin
end;

constructor TEngSubsystem.Init(const Name: AnsiString; Eng: TEngine);
begin
  FName := Name;
  FEng := Eng;
end;

destructor TEngSubsystem.Done;
begin
end;

procedure TEngSubsystem.Update;
begin
end;

procedure TEngSubsystem.OnWindowEvent(Event: PWindowEvent);
begin
end;

procedure TEngSubsystem.OnInit;
begin
end;

procedure TEngSubsystem.OnDone;
begin
end;

procedure TEngSubsystem.OnUnsubscribe;
begin
  Done;
end;

procedure OEngine.Init;
var
  I: LongInt;
begin
  FStartupTime := Time;
  FLog.Init(IsConsole, 'log.txt');
  FWinApi.Init;
  FAudio.Init;
  FMusic.Init;
  FDraw.Init(@FWinApi);
  FControl.Init(@FWinApi);
  {$IFDEF ANDROID}
  Loaded := False;
  {$ENDIF}
  FMetrics.Init;
  FInit := nil;
  FDone := nil;
  FUpdate := nil;
  FRender := nil;
  for I := 0 to High(FSubsystems) do
    FSubsystems[I]^.OnInit;
end;

procedure OEngine.Done;
var
  I: LongInt;
begin
  for I := 0 to High(FSubsystems) do
    FSubsystems[I]^.OnUnsubscribe;
  SetLength(FSubsystems, 0);
  FMetrics.Done;
  FControl.Done;
  FDraw.Done;
  FWinApi.Done;
  FMusic.Done;
  FAudio.Done;
  FLog.Done; // see DEngLog finalization
end;

procedure OEngine.Free;
begin
  if @Self <> nil then
    Done;
end;

function OEngine.GetWinApi: PWinApi;
begin
  Result := @FWinApi;
end;

function OEngine.GetAudio: PAudio;
begin
  Result := @FAudio;
end;

function OEngine.GetMusic: PMusic;
begin
  Result := @FMusic;
end;

function OEngine.GetDraw: PDraw;
begin
  Result := @FDraw;
end;

function OEngine.GetControl: PControl;
begin
  Result := @FControl;
end;

function OEngine.GetLog: PEngLog;
begin
  Result := @FLog;
end;

function OEngine.GetMetrics: PMetrics;
begin
  Result := @FMetrics;
end;

procedure OEngine.Update;
const
  QUOTA = 1;
var
  Dur: TMetricDuration;
  I: LongInt;
  ResourceLoadingEnd: LongInt;
  Gap: LongInt;
begin
  {$IFDEF ANDROID}
  if not FWinApi.ReadyRender then
    Exit;
  if not Loaded then
    Exit;
  {$ENDIF}
  if {$IFNDEF ANDROID}@{$ENDIF}FUpdate <> nil then begin
    Dur.Init('update_dt');
    FUpdate(@Self);
    Dur.Done;
  end;
  FMusic.Update;
  FControl.Update;
  for I := 0 to High(FSubsystems) do
    FSubsystems[I].Update;
  ResourceLoadingEnd := GetTimer + QUOTA;
  while ResourceManager.UpdateStep and (GetTimer < ResourceLoadingEnd) do;
  Gap := GetTimer - ResourceLoadingEnd;
  if Gap > 10 then begin
    FLog.LogTagged('D', 'WARNING: resource management took ' + IntToStr(Gap + QUOTA) + 'ms (quota is ' + IntToStr(QUOTA) + 'ms)', Time - StartupTime);
  end;
end;

procedure OEngine.Render;
var
  Dur: TMetricDuration;
begin
  {$IFDEF ANDROID}
  if not FWinApi.ReadyRender then
    Exit;
  if (not Loaded) and (@FInit <> nil) then begin
    FInit(@Self);
    Loaded := True;
    // PrintLogTagged('D', 'Application Init done');
  end;
  {$ENDIF}
  if {$IFNDEF ANDROID}@{$ENDIF}FRender <> nil then begin
    Dur.Init('render_dt');
    FRender(@Self);
    Dur.Done;
  end;
end;

procedure OEngine.InitLogPath(const LogPath: AnsiString);
begin
  FLog.SetFileName(LogPath);
end;

procedure OEngine.InitDisplayMode(Width, Height, Bits, Flags: LongInt);
begin
  FWidth := Width;
  FHeight := Height;
  FBits := Bits;
  FFlags := Flags;
end;

procedure OEngine.InitTitle(const Title: TString);
begin
  FTitle := Title;
end;

procedure OEngine.InitInitFunc(InitCallback: TCallback);
begin
  if @InitCallback = nil then begin
    FInit := @NopCallback;
  end else
    FInit := InitCallback;
end;

procedure OEngine.InitDoneFunc(DoneCallback: TCallback);
begin
  if @DoneCallback = nil then begin
    FDone := @NopCallback;
  end else
    FDone := DoneCallback;
end;

procedure OEngine.InitRenderFunc(RenderCallback: TCallback);
begin
  if @RenderCallback = nil then begin
    FRender := @NopCallback;
  end else
    FRender := RenderCallback;
end;

procedure OEngine.InitUpdateFunc(UpdateCallback: TCallback; Interval: LongInt);
begin
  if @UpdateCallback = nil then begin
    FUpdate := @NopCallback;
  end else
    FUpdate := UpdateCallback;
  FUpdateInterval := Interval;
end;

procedure OEngine.InitEventFunc(OnEvent: TOnWindowEvent);
begin
  FWinApi.SetOnEventHandler(OnEvent);
end;

procedure OEngine.InitEventFunc(OnEvent: TOnWindowEventFunc);
begin
  FWinApi.SetOnEventHandler(OnEvent);
end;

procedure OEngine.RegistSubsystem(Subsystem: PEngSubsystem);
begin
  SetLength(FSubsystems, Length(FSubsystems) + 1);
  FSubsystems[High(FSubsystems)] := Subsystem;
end;

procedure OEngine.OnInit;
begin
  FWinApi.OnInit(FWidth, FHeight, FBits, FFlags, PAnsiChar(FTitle));
  FDraw.OnInit;
  FControl.OnInit;
  FAudio.OnInit;
  FMusic.OnInit;
  {$IFNDEF ANDROID}
  if FInit <> nil then begin
    FInit(@Self);
    //PrintLogTagged('D', 'Application Init done');
  end;
  {$ENDIF}
  //PrintLogTagged('D', 'Init done');
end;

procedure OEngine.OnDone;
begin
  if FDone <> nil then
    FDone(@Self);
  FMusic.OnDone;
  FAudio.OnDone;
end;

procedure OEngine.StartLoop;
var
  IterationTime, FPSCounter, LastFPS: LongInt;
  CycleStart: LongInt;
begin
  FExiting := False;

  HistogramReset(FRenderHistogram);
  HistogramReset(FUpdateHistogram);
  HistogramReset(FEventHistogram);
  HistogramReset(FLoopCycleHistogram);

  FLastUpdateTime := GetTimer;
  LastFPS := GetTimer;
  FFPS := 0;
  FPSCounter := 0;
  CycleStart := GetTimer;
  while not FExiting do begin
    FLog.SetStage('U');
    IterationTime := GetTimer;
    FDurationStart := GetTimer;
    while IterationTime - FLastUpdateTime > FUpdateInterval do begin
      Update;
      HistogramAddDuration(FUpdateHistogram, FDurationStart, Eng.Time);
      FLastUpdateTime := FLastUpdateTime + FUpdateInterval;
    end;
    FLog.SetStage('R');
    FDurationStart := GetTimer;
    FDurationStartMicroSeconds := GetTimerMicroSeconds;
    Render;
    FFrameTimeMicroSeconds := Eng.TimeMicroSeconds - FLastRenderMicroSeconds;
    FLastRenderMicroSeconds := Eng.TimeMicroSeconds;
    HistogramAddDuration(FRenderHistogram, FDurationStart, Eng.Time);
    Inc(FPSCounter);
    if LastFPS + 1000 < GetTimer then begin
      FFPS := FPSCounter;
      Metrics.Send('fps', FFPS);
      FPSCounter := 0;
      LastFPS := GetTimer;
      {FLog.LogTagged('M', 'Render: ' + HistogramToString(FRenderHistogram));
      FLog.LogTagged('M', 'Update: ' + HistogramToString(FUpdateHistogram));
      FLog.LogTagged('M', 'Events: ' + HistogramToString(FEventHistogram));
      FLog.LogTagged('M', 'Cycles: ' + HistogramToString(FLoopCycleHistogram));}
      HistogramReset(FRenderHistogram);
      HistogramReset(FUpdateHistogram);
      HistogramReset(FEventHistogram);
      HistogramReset(FLoopCycleHistogram);
    end;
    FLog.SetStage('E');
    FDurationStart := GetTimer;
    FWinApi.ReadMessages;
    HistogramAddDuration(FEventHistogram, FDurationStart, Eng.Time);
    HistogramAddDuration(FLoopCycleHistogram, CycleStart, Eng.Time);
  end;
  FlushLog;
end;

procedure OEngine.StopLoop;
begin
  FExiting := True;
end;

procedure OEngine.Run;
begin
  FLog.SetStage('I');
  OnInit;
  StartLoop;
  FLog.SetStage('D');
  OnDone;
  FLog.SetStage('-');
end;

procedure OEngine.Run(Render, Update: TCallback;
      Interval: LongInt = 30;
      Init: TCallback = nil;
      Done: TCallback = nil);
begin
  InitInitFunc(Init);
  InitRenderFunc(Render);
  InitUpdateFunc(Update, Interval);
  InitDoneFunc(Done);
  Run;
end;

function OEngine.Time: LongInt;
begin
  Result := GetTimer;
end;

function OEngine.TimeMicroSeconds: Double;
begin
  Exit(GetTimerMicroSeconds);
end;

function OEngine.Delta: LongInt;
begin
  Result := FUpdateInterval;
end;

function OEngine.LastUpdateTime: LongInt;
begin
  Result := FLastUpdateTime;
end;

function OEngine.FrameTime: PtrInt;
begin
  Exit(Trunc(FFrameTimeMicroSeconds * 1000));
end;

procedure OEngine.PrintLogTagged(const Sub, S: TString);
begin
  FLog.LogTagged(Sub, S, Time - StartupTime);
end;

procedure OEngine.PrintLogTagged(const Sub, S: TString; Groups: LongInt);
var
  Prefix: TString;
begin
  // FIXME: do not use stdout in GUI application
  Prefix := ToStr(LastUpdateTime);
  while Length(Prefix) < 10 do
    Prefix := '0' + Prefix;
  Prefix := Prefix + ' ' + Sub + ' ';
{$IFDEF ANDROID}
  LOGW(PAnsiChar(Prefix + S));
{$ELSE}
  System.Writeln(Prefix + S);
{$ENDIF}
end;

procedure OEngine.FlushLog;
begin
  FLog.Flush;
end;

function OEngine.GetMouse: TVec2i;
begin
  Result := WinApi.Window.GetMouse;
end;

function OEngine.Width: LongInt;
begin
  Result := WinApi.Window.Size.X;
end;

function OEngine.Height: LongInt;
begin
  Result := WinApi.Window.Size.Y;
end;

{$IFDEF DENG_DEMO_MODE}
procedure OEngine.PlayDemo(const LogFileName: TString);
begin
end;
{$ENDIF}

procedure LogTagged(const G, S: AnsiString);
begin
  Eng.PrintLogTagged(G, S);
end;

procedure Log(const S: AnsiString);
begin
  Eng.PrintLogTagged(' ', S)
end;

{$IFDEF ANDROID}
procedure _SetAndroidState(State: Pointer);
begin
  DWinApi._AndroidState := State;
end;
{$ENDIF}

var
  Initialized: Boolean = False;
procedure CallInitialization;
begin
  if not Initialized then begin
    Eng.Init;
    Initialized := True;
  end;
end;

initialization
  CallInitialization;
finalization
  if Initialized then
    Eng.Done;
end.
