{
  Модуль для загрузки и рендера анимированных спрайтов.
  Является надстройкой над DEng Texture Manager.

    TSprite.TFrame позволяет вырезать из текстуры определённый фрагмент
                   и рендерить его.

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

    TSprite.TState можно использовать для "клонирования" спрайтов:
        State.Init(@Sprite);
        ...

  Json TFrame может иметь следующие формы:
    * "filename"   имя файла с текстурой (если текстура полностью содержит кадр)
    * (
          "filename": "filename", // имя файла с текстурой
          "x": x, "y": y, "w": w, "h": h // координаты фрагмента с кадром
          "xywh": [x, y, w, h], // альтернативный вариант задания фрагмента
          "dt": dt // время в миллисекундах сколько длится кадр
      )

  Json TSprite может иметь следующие формы:
    * Json TFrame (если спрайт состоит из одного кадра)
    * (
        "filename": "filename", // текстура по умолчанию
        "dt": dt, // длительность кадра по умолчанию
        "frames": [
          "sprite": ()
        ]
      )

  Основные способы использования спрайта:
    * Sprite.State.StartAnimation(Eng.Time);
      ...
      Sprite.State.UpdateAnimation(Eng.Time);
      Sprite.State.Draw(...);
    * Sprite.State.StartAnimation;
      ...
      Sprite.State.Animate(DeltaTime);
      Sprite.State.Draw(...);
}
unit dsprite;

interface

uses
  dstrnum,
  dstream,
  dstreamfile,
  dansiview,
  dvector2i,
  dvector2f,
  dvector3f,
  {$I deng_app_units.inc},
  DDrawTex,
  djson,
  dnode,
  dnodeview;

type
PSprite = ^TSprite;
TSprite = object
public type
  PFrame = ^TFrame;
  TFrame = object
    Tex: TDrawTex;
    P, S: TVec2i;
    Dt: LongInt;
    FlipD, FlipH, FlipV: Boolean;
    TexCoords: TDrawTex.TTexCoords4;
    constructor Init; overload;
    constructor Init(Man: PDrawTexManager; Eng: TEngine; Json: PJsonValue; const DefaultFilename: AnsiString = ''; DefaultDt: LongInt = 100); overload;
    constructor Init(Man: PDrawTexManager; Eng: TEngine; Node: PNodeValue; const DefaultFilename: AnsiString = ''; DefaultDt: LongInt = 100); overload;
    constructor Init(Man: PDrawTexManager; Eng: TEngine; const NodeView: TNodeView; const DefaultFilename: AnsiString = ''; DefaultDt: LongInt = 100); overload;
    constructor InitClone(Frame: PFrame);
    procedure Reinit;
    destructor Done; virtual;
    procedure Draw(Eng: TEngine; const Pos: TVec2i); overload;
    procedure Draw(Eng: TEngine; const Pos: TVec2f; FlipX: Boolean = False; FlipY: Boolean = False); overload;
    procedure Draw(Eng: TEngine; const Pos, Size: TVec2i); overload;
    procedure Draw(Eng: TEngine; const Pos, Size: TVec2f); overload;
    procedure Draw(Eng: TEngine; const Pos, Right, Up: TVec3f; const Pivot: TVec2f);
    procedure LoadFromJson(Man: PDrawTexManager; Eng: TEngine; Json: PJsonValue);
    procedure ComputeTexCoords;
  end;
  PState = ^TState;
  TState = object
  public
    FSprite: PSprite; // frames
    FFrame: LongInt; // current frame
    StartedTime: LongInt; // global time of started animation
    CurrentTime: LongInt; // time of last UpdateAnimation
    //AnimationTime: LongInt; // time of animation
    FrameTime: LongInt; // duration of current frame
    Loop: Boolean; // is animation should be loop?
    Finished: Boolean; // is animation finished?
    constructor Init(OriginalSprite: PSprite); // clone sprite
    destructor Done; virtual;

    // Animation
    procedure StartAnimation(aCurrentTime: LongInt = 0;
                             aAnimationTime: LongInt = 0);
    procedure UpdateAnimation(GlobalTime: LongInt);
    procedure Animate(Delta: LongInt);
    procedure Draw(Eng: TEngine; const Pos: TVec2i); overload;
    procedure Draw(Eng: TEngine; const Pos: TVec2f; FlipX: Boolean = False; FlipY: Boolean = False); overload;
    procedure Draw(Eng: TEngine; const Pos, Size: TVec2i); overload;
    procedure Draw(Eng: TEngine; const Pos, Size: TVec2f); overload;
    procedure Draw(Eng: TEngine; const Pos, Right, Up: TVec3f; const Pivot: TVec2f);
    function Tex: PDrawTex; inline;
    function Size: TVec2i; inline;
    function Frame: PFrame; inline;
  end;
private
  FFrames: array of TFrame;
  FState: TState;
public
  constructor Init;
  destructor Done; virtual;

  // Require resources
  procedure Require;

  // Constructing
  procedure AddFrame(Man: PDrawTexManager; Eng: TEngine; Data: TData; Dt: LongInt; const P, S: TVec2i); overload;
  procedure AddFrame(Man: PDrawTexManager; Eng: TEngine; Json: PJsonValue; const DefaultFilename: AnsiString = ''; DefaultDt: LongInt = 100); overload;
  procedure LoadFromJson(Man: PDrawTexManager; Eng: TEngine; Json: PJsonValue);
  procedure AddFrameFromNode(Man: PDrawTexManager; Eng: TEngine; Node: PNodeValue; const DefaultFilename: AnsiString = ''; DefaultDt: LongInt = 100); overload;
  procedure LoadFromNode(Man: PDrawTexManager; Eng: TEngine; Node: PNodeValue);

  function State: PState; inline;
end;

implementation

constructor TSprite.TFrame.Init;
begin
  Tex.Init;
  P.X := 0;
  P.Y := 0;
  S.X := 0;
  S.Y := 0;
  Dt := 0;
end;

constructor TSprite.TFrame.Init(Man: PDrawTexManager; Eng: TEngine; Json: PJsonValue; const DefaultFilename: AnsiString = ''; DefaultDt: LongInt = 100);
var
  O: PJson;
  A: PJsonArray;
  TempString: AnsiString;
begin
  if Json^._Type = JSON_OBJECT then begin
    O := Json^.O;
    // TODO we should take mipmapping from global settings (e.g. for Al Dente)
    Tex.Init(Man, Eng, O^.Get('filename', DefaultFilename), True);
    S.X := Tex.Size.X;
    S.Y := Tex.Size.Y;
    A := O^.Get('xywh', EmptyJsonArray);
    if A^.Length >= 4 then begin
      P.X := O^.Get('x', A^.Get(0, 0));
      P.Y := O^.Get('y', A^.Get(1, 0));
      S.X := O^.Get('w', A^.Get(2, S.X));
      S.Y := O^.Get('h', A^.Get(3, S.Y));
    end else begin
      P.X := O^.Get('x', 0);
      P.Y := O^.Get('y', 0);
      S.X := O^.Get('w', S.X);
      S.Y := O^.Get('h', S.Y);
    end;
    Dt := O^.Get('dt', DefaultDt);
    FlipD := O^.Get('flipd', 0) <> 0;
    FlipH := O^.Get('fliph', 0) <> 0;
    FlipV := O^.Get('flipv', 0) <> 0;
  end else if Json^._Type = JSON_STRING then begin
    // TODO we should take mipmapping from global settings (e.g. for Al Dente)
    Tex.Init(Man, Eng, Json^.S, True);
    P.X := 0;
    P.Y := 0;
    S.X := Tex.Size.X;
    S.Y := Tex.Size.Y;
    Dt := DefaultDt;
  end else
    Init;
  ComputeTexCoords;
end;

constructor TSprite.TFrame.Init(Man: PDrawTexManager; Eng: TEngine; Node: PNodeValue; const DefaultFilename: AnsiString = ''; DefaultDt: LongInt = 100);
var
  O: PNodeObject;
  A: PNodeItem;
begin
  if Node^.Type_ = NODE_OBJECT then begin
    O := Node^.O;
    Tex.Init(Man, Eng, O^.Get('filename', DefaultFilename).ToString);
    S.X := Tex.Size.X;
    S.Y := Tex.Size.Y;
    A := O^.Get('xywh', NodeEmptyList);
    if A <> nil then begin
      P.X := O^.Get('x', A^.Value^.GetInt(0)); // TODO
      P.Y := O^.Get('y', A^.Next^.Value^.GetInt(0));
      S.X := O^.Get('w', A^.Next^.Next^.Value^.GetInt(S.X));
      S.Y := O^.Get('h', A^.Next^.Next^.Next^.Value^.GetInt(S.Y));
    end else begin
      P.X := O^.Get('x', 0);
      P.Y := O^.Get('y', 0);
      S.X := O^.Get('w', S.X);
      S.Y := O^.Get('h', S.Y);
    end;
    Dt := O^.Get('dt', DefaultDt);
    FlipD := O^.Get('flipd', 0) <> 0;
    FlipH := O^.Get('fliph', 0) <> 0;
    FlipV := O^.Get('flipv', 0) <> 0;
  end else if Node^.Type_ = NODE_STRING then begin
    Tex.Init(Man, Eng, Node^.S.ToString);
    P.X := 0;
    P.Y := 0;
    S.X := Tex.Size.X;
    S.Y := Tex.Size.Y;
    Dt := DefaultDt;
  end else
    Init;
  ComputeTexCoords;
end;

function GetStr(const Node: TNodeView;
                Key: TAnsiView;
                Def: TAnsiView): TAnsiView;
var
  HashedKey: TNodeHashedString;
  N: TNodeView;
begin
  HashedKey.SetValue(Key.Data, Key.Length);
  if not N.FindPair(HashedKey, N) then
    Exit(Def);
  if N._Type = dnodeview.NODE_STRING then begin
    Exit(AnsiView(N.S, N.Count));
  end else
    Exit(Def);
end;

constructor TSprite.TFrame.Init(Man: PDrawTexManager; Eng: TEngine; const NodeView: TNodeView; const DefaultFilename: AnsiString = ''; DefaultDt: LongInt = 100); overload;
var
  O: PNodeViewHashMap;
  A: PNodeViewArray;
begin
  if NodeView._Type = NODE_OBJECT then begin
    Tex.Init(Man, Eng, GetStr(NodeView, 'filename', DefaultFilename).ToString);
    S.X := Tex.Size.X;
    S.Y := Tex.Size.Y;
    A := NodeView.GetArr('xywh', nil);
    if A <> nil then begin
      P.X := NodeView.GetInt('x', A^.A[0].GetInt(0)); // TODO
      P.Y := NodeView.GetInt('y', A^.A[1].GetInt(0));
      S.X := NodeView.GetInt('w', A^.A[2].GetInt(S.X));
      S.Y := NodeView.GetInt('h', A^.A[3].GetInt(S.Y));
    end else begin
      P.X := NodeView.GetInt('x', 0);
      P.Y := NodeView.GetInt('y', 0);
      S.X := NodeView.GetInt('w', S.X);
      S.Y := NodeView.GetInt('h', S.Y);
    end;
    Dt := NodeView.GetInt('dt', DefaultDt);
    FlipD := NodeView.GetInt('flipd', 0) <> 0;
    FlipH := NodeView.GetInt('fliph', 0) <> 0;
    FlipV := NodeView.GetInt('flipv', 0) <> 0;
  end else if NodeView._Type = dnodeview.NODE_STRING then begin
    Tex.Init(Man, Eng, AnsiView(NodeView.S, NodeView.Count).ToString);
    P.X := 0;
    P.Y := 0;
    S.X := Tex.Size.X;
    S.Y := Tex.Size.Y;
    Dt := DefaultDt;
  end else
    Init;
  ComputeTexCoords;
end;

constructor TSprite.TFrame.InitClone(Frame: PFrame);
begin
  Tex.InitClone(@Frame.Tex);
  P := Frame.P;
  S := Frame.S;
  Dt := Frame.Dt;
end;

procedure TSprite.TFrame.Reinit;
begin
  Tex.Done;
  Tex.Init;
end;

destructor TSprite.TFrame.Done;
begin
  Tex.Done;
end;

procedure TSprite.TFrame.Draw(Eng: TEngine; const Pos: TVec2i);
begin
  Tex.Draw(Eng, Vec2f(Pos), Vec2f(S), TexCoords);
end;

procedure TSprite.TFrame.Draw(Eng: TEngine; const Pos: TVec2f; FlipX: Boolean = False; FlipY: Boolean = False);
begin
  Tex.Draw(Eng, Pos, Vec2f(S), TexCoords, FlipX, FlipY);
end;

procedure TSprite.TFrame.Draw(Eng: TEngine; const Pos, Size: TVec2i);
begin
  Tex.Draw(Eng, Vec2f(Pos), Vec2f(Size), TexCoords);
end;

procedure TSprite.TFrame.Draw(Eng: TEngine; const Pos, Size: TVec2f);
begin
  Tex.Draw(Eng, Pos, Size, TexCoords);
end;

procedure TSprite.TFrame.Draw(Eng: TEngine; const Pos, Right, Up: TVec3f; const Pivot: TVec2f);
begin
  Tex.Draw(Eng, Pos, Right, Up, Pivot);
end;

procedure TSprite.TFrame.LoadFromJson(Man: PDrawTexManager; Eng: TEngine; Json: PJsonValue);
begin
  Done;
  Init(Man, Eng, Json);
end;

procedure TSprite.TFrame.ComputeTexCoords;
var
  A, B: TVec2f;
begin
  if FlipD then begin
    A := Vec2f((P.Y      ) / Tex.Size.Y, (P.X      ) / Tex.Size.X);
    B := Vec2f((P.Y + S.Y) / Tex.Size.Y, (P.X + S.X) / Tex.Size.X);
  end else begin
    A := Vec2f((P.X      ) / Tex.Size.X, (P.Y      ) / Tex.Size.Y);
    B := Vec2f((P.X + S.X) / Tex.Size.X, (P.Y + S.Y) / Tex.Size.Y);
  end;
  if FlipH then begin
    TexCoords[0].X := B.X;
    TexCoords[1].X := A.X;
    TexCoords[2].X := A.X;
    TexCoords[3].X := B.X;
  end else begin
    TexCoords[0].X := A.X;
    TexCoords[1].X := B.X;
    TexCoords[2].X := B.X;
    TexCoords[3].X := A.X;
  end;
  if FlipV then begin
    TexCoords[0].Y := B.Y;
    TexCoords[1].Y := B.Y;
    TexCoords[2].Y := A.Y;
    TexCoords[3].Y := A.Y;
  end else begin
    TexCoords[0].Y := A.Y;
    TexCoords[1].Y := A.Y;
    TexCoords[2].Y := B.Y;
    TexCoords[3].Y := B.Y;
  end;
end;

constructor TSprite.TState.Init(OriginalSprite: PSprite);
begin
  FSprite := OriginalSprite;
  Loop := True;
  StartAnimation(0);
end;

destructor TSprite.TState.Done;
begin
end;

procedure TSprite.TState.StartAnimation(aCurrentTime: LongInt = 0; aAnimationTime: LongInt = 0);
begin
  FFrame := 0;
  StartedTime := aCurrentTime - aAnimationTime;
  CurrentTime := aCurrentTime;
  FrameTime := 0;
  Finished := False;
  if aAnimationTime <> 0 then
    Animate(aAnimationTime);
end;

procedure TSprite.TState.UpdateAnimation(GlobalTime: LongInt);
begin
  Animate(GlobalTime - CurrentTime);
  CurrentTime := GlobalTime;
end;

procedure TSprite.TState.Animate(Delta: LongInt);
var
  F: PFrame;
begin
  if Finished then
    Exit;
  F := @FSprite.FFrames[FFrame];
  Delta := Delta + FrameTime;
  while F^.Dt < Delta do begin
    //Log(IntToStr(Ord(Loop)) + ' ' + IntToStr(FFrame) + '/' + IntToStr(High(FSprite.FFrames)));
    if FFrame >= High(FSprite.FFrames) then begin
      if not Loop then begin
        Finished := True;
        FrameTime := F^.Dt;
        Exit;
      end;
      FFrame := 0;
    end else
      Inc(FFrame);
    Dec(Delta, F^.Dt);
    if FFrame <= High(FSprite.FFrames) then
      F := @FSprite.FFrames[FFrame];
  end;
  FrameTime := Delta;
end;

procedure TSprite.TState.Draw(Eng: TEngine; const Pos: TVec2i);
begin
  FSprite.FFrames[FFrame].Draw(Eng, Pos);
end;

procedure TSprite.TState.Draw(Eng: TEngine; const Pos: TVec2f; FlipX: Boolean = False; FlipY: Boolean = False);
begin
  FSprite.FFrames[FFrame].Draw(Eng, Pos, FlipX, FlipY);
end;

procedure TSprite.TState.Draw(Eng: TEngine; const Pos, Size: TVec2i);
begin
  FSprite.FFrames[FFrame].Draw(Eng, Pos, Size);
end;

procedure TSprite.TState.Draw(Eng: TEngine; const Pos, Size: TVec2f);
begin
  FSprite.FFrames[FFrame].Draw(Eng, Pos, Size);
end;

procedure TSprite.TState.Draw(Eng: TEngine; const Pos, Right, Up: TVec3f; const Pivot: TVec2f);
begin
  FSprite.FFrames[FFrame].Draw(Eng, Pos, Right, Up, Pivot);
end;

function TSprite.TState.Tex: PDrawTex;
begin
  Result := @FSprite.FFrames[FFrame].Tex;
end;

function TSprite.TState.Size: TVec2i;
begin
  Result := FSprite.FFrames[FFrame].S;
end;

function TSprite.TState.Frame: PFrame;
begin
  Result := @FSprite.FFrames[FFrame];
end;

constructor TSprite.Init;
begin
  SetLength(FFrames, 0);
  FState.Init(@Self);
end;

destructor TSprite.Done;
var
  I: LongInt;
begin
  for I := 0 to High(FFrames) do
    FFrames[I].Tex.Done;
  SetLength(FFrames, 0);
end;

procedure TSprite.Require;
var
  I: LongInt;
begin
  for I := 0 to High(FFrames) do
    FFrames[I].Tex.Require;
end;

procedure TSprite.AddFrame(Man: PDrawTexManager; Eng: TEngine; Data: TData; Dt: LongInt; const P, S: TVec2i);
begin
  SetLength(FFrames, Length(FFrames) + 1);
  FFrames[High(FFrames)].Tex.Init(Man, Eng, Data);
  FFrames[High(FFrames)].Dt := Dt;
  FFrames[High(FFrames)].P := P;
  FFrames[High(FFrames)].S := S;
end;

procedure TSprite.AddFrame(Man: PDrawTexManager; Eng: TEngine; Json: PJsonValue; const DefaultFilename: AnsiString = ''; DefaultDt: LongInt = 100);
var
  S: AnsiString;
begin
  SetLength(FFrames, Length(FFrames) + 1);
  S := DefaultFilename;
  UniqueString(S);
  FFrames[High(FFrames)].Init(Man, Eng, Json, S, DefaultDt);
end;

procedure TSprite.LoadFromJson(Man: PDrawTexManager; Eng: TEngine; Json: PJsonValue);
var
  DefaultFilename: AnsiString;
  DefaultDt: LongInt;
  Item: PJsonItem;
  Frames: PJsonArray;
begin
  if Json^._Type = JSON_OBJECT then begin
    FState.Loop := Json^.O^.Get('loop', 1) <> 0;
    Frames := Json^.O^.Get('frames', EmptyJsonArray);
    if Frames = EmptyJsonArray then begin
      AddFrame(Man, Eng, Json);
    end else begin
      DefaultFilename := Json^.O^.Get('filename', '');
      DefaultDt := Json^.O^.Get('dt', 100);
      for Item in Frames^ do
        AddFrame(Man, Eng, @Item^.Value, DefaultFilename, DefaultDt);
    end;
  end else if Json^._Type = JSON_STRING then begin
    AddFrame(Man, Eng, Json);
  end;
end;

procedure TSprite.AddFrameFromNode(Man: PDrawTexManager; Eng: TEngine; Node: PNodeValue; const DefaultFilename: AnsiString = ''; DefaultDt: LongInt = 100);
begin
  SetLength(FFrames, Length(FFrames) + 1);
  FFrames[High(FFrames)].Init(Man, Eng, Node, DefaultFilename, DefaultDt);
end;

procedure TSprite.LoadFromNode(Man: PDrawTexManager; Eng: TEngine; Node: PNodeValue);
var
  DefaultFilename: AnsiString;
  DefaultDt: LongInt;
  Item: PNodeItem;
  Frames: PNodeItem;
begin
  if Node^.Type_ = NODE_OBJECT then begin
    FState.Loop := Node^.O^.Get('loop', 1) <> 0;
    Frames := Node^.O^.Get('frames', NodeEmptyList);
    if Frames = NodeEmptyList then begin
      AddFrameFromNode(Man, Eng, Node);
    end else begin
      DefaultFilename := Node^.O^.Get('filename', '').ToString;
      DefaultDt := Node^.O^.Get('dt', 100);
      while Frames <> nil do begin
        AddFrameFromNode(Man, Eng, Frames^.Value, DefaultFilename, DefaultDt);
        Frames := Frames^.Next;
      end;
    end;
  end else if Node^.Type_ = NODE_STRING then begin
    AddFrameFromNode(Man, Eng, Node);
  end;
end;

function TSprite.State: PState;
begin
  Result := @FState;
end;

end.
