{
  В каждой игре есть такое понятие, как экраны, состояния, или, как они названы
  в данном случае, сцены. Это может быть сама игра как таковая, меню,
  экран загрузки, какое-нибудь меню настроек и т.д. Зачастую удобно отделить
  саму игровую логику от всего этого.
  
  Для этого и реализован данный модуль. Далее кратенько описаны основные
  возможности.

  Каждая сцена реализуется наследником от объекта TScene.

  В каждый момент времени есть одна конкретная активная сцена, в которую
  перенаправляются все вызовы и события игрового цикла.

  Для переключения активной сцены используется метод Switch

        SceneManager.Switch(Next)

  Замечу, что это не приводит к моментальной смене сцены, сперва будет ожидаться
  окончание всех событий текущей сцены.

  Допустим, что есть активная сцена Active, и сцена Next, на которую
  происходит переключение. Тогда последовательность переключения будет такой:

        ожидаем завершения всех вызовов Active (Render, Update, OnEven etc)
          |
          v
        вызываем Next.Load
          |
          v
        вызываем Active.Unload
          |
          v
        делаем Next активным и продолжаем игровой цикл как обычно

  Замечу, что если Next.Load отрабатывает долго, то всё это время программа
  будет висеть. Чтобы этого избежать, есть специальная фишка с загрузочной
  сценой.

  Загрузочная сцена реализует загрузочный экран, который выполняется параллельно
  Next.Load. В нём возможно отрисовать всё что угодно, в т.ч. и прогресс бар,
  отработать события пользователя и т.д. При этом предусмотрен механизм для
  взаимодействия загрузочной сцены с процессом загрузки.


        // Устанавливает сцену как загрузочную. Для неё сразу будут вызваны
        // Load и для неё будут вызываться Render и Update при смене сцен.
        // В дальнейшем можно переключать загрузочную сцену перед любой
        // сменой экрана.
        SceneManager.SetLoadingScene(Scene, Async=True);
        
        // Переключаем сцену не просто так, а используя загрузочный экран
        // обычно заранее известно, что какое-то переключение экрана может
        // занять длительное время
        SceneManager.SwitchWithLoading(Next)

        // выставляет текущий прогресс
        // при старте загрузки неявно вызывается с параметрами (0, 100)
        SceneManager.SetLoadingProgress(Current, Maximum);
        // возвращает Current/Maximum
        SceneManager.GetLoadingProgress: Double;
        // возвращает Round(RelativeMaximum * Current/Maximum)
        SceneManager.GetLoadingProgress(RelativeMaximum): LongInt;

  С загрузочкой сценой обработка немного меняется:

        ожидаем завершения всех вызовов Active (Render, Update, OnEven etc)
            |
            |\__________________
            v                   |
      Loading.Update            v
      Loading.Render      Active.Unload
      Loading.Update            |
      Loading.Render            v
      Loading.Update        Next.Load
      Loading.Render            |
      Loading.Update            v
      Loading.Render       Переключаем цикл обработки на Next
            | __________________|
            |/
            v

  Наконец, сцены могут накладываться друг на друга стеком. При выходе из
  верхней сцены, активной станет предыдущая. Это может быть полезно, например,
  для внутриигрового меню, которое вызывается по Esc.

        // положить сцену поверх предыдущих
        SceneManager.Add(Scene);
        // выйти из текущей сцены наверх (или вообще из программы, если
        // сцена только одна)
        SceneManager.Quit;

  Отмечу, что Switch сбрасывает весь стек сцен и активной становится только
  одна.

  Обращаю внимание, что TSceneManager инкапсулирует в себя всё взаимодействие
  с фреймворком, касающееся регистрации методов и грамотного их переключения.
  Таким образом, не нужно самостоятельно регистрировать Render и Update,
  достаточно зарегистрировать сцену и сделать её активной, а также не нужно
  прописывать Init и Done, вместо этого нужно реализовать правильные Load и
  Unload.
}
unit dscene;

interface

uses
  dmutex,
  deng;

type
PScene = ^TScene;
TScene = object
private
  // maybe in some future dscene will not be dependent on DEng, then I will
  // implement this unit as generic and dscene_deng as specific DEng scenes
  // but for now it is much more simplier just use DEng
  FEng: TEngine;
  FLoaded: Boolean;
  FLoading: Boolean;
protected
  procedure DoLoad; virtual;
  procedure DoUnload; virtual;
  procedure DoRender; virtual;
  procedure DoUpdate; virtual;
  function DoOnEvent(Event: PWindowEvent): Boolean; virtual;
public
  constructor Init(Eng: TEngine);
  destructor Done; virtual;
  procedure Load;
  procedure Unload;
  procedure Reload;
  procedure Render; inline;
  procedure Update; inline;
  function OnEvent(Event: PWindowEvent): Boolean; inline;
  property Eng: TEngine read FEng;
  property Loaded: Boolean read FLoaded;
  property Loading: Boolean read FLoading;
end;

PSceneManager = ^TSceneManager;
TSceneManager = object
private
  // текущее состояние
  FActive: array of PScene;
  // предыдущее состояние для разгрузки
  FPrev: array of PScene;
  // состояние, в которое переходим, для загрузки
  FNext: array of PScene;
  // loading
  FLoading: Boolean;
  FLoadingProgress: LongInt;
  FLoadingMaximum: LongInt;
  FLoadingScene: PScene;
  FLoadingAsync: Boolean;
  FProgressCS: TMutex;
  // Scene to switch at init stage
  FSceneOnInit: PScene;
  procedure UnloadAndLoad;
public
  procedure Init;
  procedure Done;
  procedure Run(Scene: PScene);
  procedure Switch(Scene: PScene; Loading: Boolean = False);
  procedure SwitchWithLoading(Scene: PScene);
  procedure Add(Scene: PScene; Loading: PScene = nil);
  procedure SetLoadingScene(Scene: PScene; Async: Boolean = True);
  procedure SetLoadingProgress(Progress, Maximum: LongInt);
  function GetLoadingProgress(RelativeMaximum: LongInt): LongInt; overload;
  function GetLoadingProgress: Double; overload;
  procedure Quit;
  procedure Load; inline;
  procedure Unload; inline;
  procedure Render; //inline;
  procedure Update; inline;
  function OnEvent(Event: PWindowEvent): Boolean; inline;
end;

procedure SceneRun(Scene: PScene); inline; overload;
procedure SceneRun(var Scene: TScene); inline; overload;
// Устанавливает сцену, которая активна во время загрузки
procedure SceneSetLoading(Loading: PScene); inline; overload;
procedure SceneSetLoading(var Loading: TScene); inline; overload;
// Переключиться на одну-единственную сцену, выгрузить все текущие
// Если указано Loading, выполняет загрузку в отдельном потоке, а в основном
// рисует сцену
procedure SceneSwitch(Scene: PScene); inline; overload;
procedure SceneSwitch(var Scene: TScene); inline; overload;
procedure SceneSwitchWithLoading(Scene: PScene); inline; overload;
procedure SceneSwitchWithLoading(var Scene: TScene); inline; overload;
procedure SceneSwitchWithLoading(Scene: PScene; Loading: PScene); inline; overload;
procedure SceneSwitchWithLoading(var Scene: TScene; var Loading: TScene); inline; overload;
// Добавляет сцену поверх предыдущей
procedure SceneAdd(Scene: PScene; Loading: PScene = nil);
// Выходит из верхней сцены. Если это была последняя сцена -- выходит из всего
// приложения целиком
procedure SceneQuit;

// for callbacks in DEng
procedure GlobalSceneInit(Eng: TEngine);
procedure GlobalSceneRender(Eng: TEngine);
procedure GlobalSceneUpdate(Eng: TEngine);
function GlobalSceneOnEvent(Event: PWindowEvent): Boolean;

var
  SceneManager: TSceneManager;

implementation

procedure TScene.DoLoad;
begin
end;

procedure TScene.DoUnload;
begin
end;

procedure TScene.DoRender;
begin
end;

procedure TScene.DoUpdate;
begin
end;

function TScene.DoOnEvent(Event: PWindowEvent): Boolean;
begin
  Result := False;
end;

constructor TScene.Init(Eng: TEngine);
begin
  FEng := Eng;
  FLoaded := False;
  FLoading := False;
end;

destructor TScene.Done;
begin
  Unload;
end;

procedure TScene.Load;
begin
  if not FLoaded then begin
    FLoading := True;
    DoLoad;
    FLoading := False;
    FLoaded := True;
  end;
end;

procedure TScene.Unload;
begin
  if FLoaded then begin
    DoUnload;
    FLoaded := False;
  end;
end;

procedure TScene.Reload;
begin
  Unload;
  Load;
end;

function TScene.OnEvent(Event: PWindowEvent): Boolean;
begin
  Result := DoOnEvent(Event);
end;

procedure TScene.Render;
begin
  DoRender;
end;

procedure TScene.Update;
begin
  DoUpdate;
end;

procedure TSceneManager.UnloadAndLoad;
var
  I: LongInt;
begin
  for I := 0 to High(FPrev) do
    FPrev[I].Unload;
  for I := 0 to High(FNext) do
    FNext[I].Load;
end;

procedure TSceneManager.Init;
begin
  FProgressCS.Init;
end;

procedure TSceneManager.Done;
begin
  // TODO unload all scenes
  FProgressCS.Done;
end;

function BackgroundUnloadAndLoad(SceneManager: Pointer): PtrInt;
begin
  PSceneManager(SceneManager)^.UnloadAndLoad;
  PSceneManager(SceneManager)^.FActive := Copy(PSceneManager(SceneManager)^.FNext, 0, Length(PSceneManager(SceneManager)^.FNext));
  Result := 0;
end;

procedure TSceneManager.Run(Scene: PScene);
begin
  FSceneOnInit := Scene;
  Eng.Run;
end;

procedure TSceneManager.Switch(Scene: PScene; Loading: Boolean = False);
begin
  FPrev := Copy(FActive, 0, Length(FActive));
  SetLength(FNext, 1);
  FNext[0] := Scene;
  if Loading then begin
    BeginThread(@BackgroundUnloadAndLoad, @Self);
    SetLength(FActive, 1);
    FActive[0] := FLoadingScene;
  end else begin
    UnloadAndLoad;
    FActive := Copy(FNext, 0, Length(FNext));
  end;
end;

procedure TSceneManager.SwitchWithLoading(Scene: PScene);
begin
  Switch(Scene, True);
end;

procedure TSceneManager.SetLoadingScene(Scene: PScene; Async: Boolean = True);
begin
  if FLoadingScene <> nil then
    FLoadingScene^.Unload;

  FLoadingScene := Scene;
  FLoadingAsync := Async;

  if FLoadingScene <> nil then begin
    FLoadingScene^.Load;
    if FLoading then begin
      // TODO while loading we should change Render and Update callbacks in deng
    end;
  end;
end;

procedure TSceneManager.SetLoadingProgress(Progress, Maximum: LongInt);
begin
  FProgressCS.Enter;
  FLoadingProgress := Progress;
  FLoadingMaximum := Maximum;
  if FLoadingProgress < 0 then
    FLoadingProgress := 0;
  if FLoadingMaximum < 0 then
    FLoadingMaximum := 0;
  if FLoadingProgress > FLoadingMaximum then
    FLoadingProgress := FLoadingMaximum;
  FProgressCS.Leave;
end;

function TSceneManager.GetLoadingProgress(RelativeMaximum: LongInt): LongInt;
begin
  FProgressCS.Enter;
  Result := Round(RelativeMaximum * (FLoadingProgress / FLoadingMaximum));
  FProgressCS.Leave;
end;

function TSceneManager.GetLoadingProgress: Double;
begin
  FProgressCS.Enter;
  Result := FLoadingProgress / FLoadingMaximum;
  FProgressCS.Leave;
end;

procedure TSceneManager.Add(Scene: PScene; Loading: PScene = nil);
begin
  // TODO setup loading scene
  SetLength(FActive, Length(FActive) + 1);
  FActive[High(FActive)] := Scene;
end;

procedure TSceneManager.Quit;
begin
end;

procedure TSceneManager.Load;
begin
end;

procedure TSceneManager.Unload;
begin
end;

procedure TSceneManager.Render;
var
  I: LongInt;
begin
  for I := 0 to High(FActive) do
    FActive[I].Render;
end;

procedure TSceneManager.Update;
var
  I: LongInt;
begin
  for I := 0 to High(FActive) do
    FActive[I].Update;
end;

function TSceneManager.OnEvent(Event: PWindowEvent): Boolean;
var
  I: LongInt;
begin
  for I := High(FActive) downto 0 do begin
    if not FActive[I].OnEvent(Event) then
      Exit(False);
  end;
  Exit(True);
end;

procedure SceneRun(Scene: PScene);
begin
  SceneManager.Run(Scene);
end;

procedure SceneRun(var Scene: TScene);
begin
  SceneManager.Run(@Scene);
end;

procedure SceneSetLoading(Loading: PScene);
begin
  SceneManager.SetLoadingScene(Loading);
end;

procedure SceneSetLoading(var Loading: TScene);
begin
  SceneManager.SetLoadingScene(@Loading);
end;

procedure SceneSwitch(Scene: PScene);
begin
  SceneManager.Switch(Scene);
end;

procedure SceneSwitch(var Scene: TScene);
begin
  SceneManager.Switch(@Scene);
end;

procedure SceneSwitchWithLoading(Scene: PScene);
begin
  SceneManager.SwitchWithLoading(Scene);
end;

procedure SceneSwitchWithLoading(var Scene: TScene);
begin
  SceneManager.SwitchWithLoading(@Scene);
end;

procedure SceneSwitchWithLoading(Scene: PScene; Loading: PScene);
begin
  SceneManager.SetLoadingScene(Loading);
  SceneManager.SwitchWithLoading(Scene);
end;

procedure SceneSwitchWithLoading(var Scene: TScene; var Loading: TScene);
begin
  SceneSwitchWithLoading(@Scene, @Loading);
end;

procedure SceneAdd(Scene: PScene; Loading: PScene = nil);
begin
  SceneManager.Add(Scene, Loading);
end;

procedure SceneQuit;
begin
  SceneManager.Quit;
end;

procedure GlobalSceneInit(Eng: TEngine);
begin
  if SceneManager.FSceneOnInit <> nil then begin
    if SceneManager.FLoadingScene <> nil then begin
      SceneManager.SwitchWithLoading(SceneManager.FSceneOnInit);
    end else
      SceneManager.Switch(SceneManager.FSceneOnInit);
  end;
end;

procedure GlobalSceneRender(Eng: TEngine);
begin
  SceneManager.Render;
end;

procedure GlobalSceneUpdate(Eng: TEngine);
begin
  SceneManager.Update;
end;

function GlobalSceneOnEvent(Event: PWindowEvent): Boolean;
begin
  Result := SceneManager.OnEvent(Event);
end;

initialization
  SceneManager.Init;
  Eng.InitInitFunc(@GlobalSceneInit);
  Eng.InitRenderFunc(@GlobalSceneRender);
  Eng.InitUpdateFunc(@GlobalSceneUpdate, 30);
  Eng.InitEventFunc(@GlobalSceneOnEvent);
finalization
  SceneManager.Done;
end.
