unit XData.Criteria.AliasVisitor;

interface

uses
  Generics.Collections,
  Aurelius.Criteria.Base,
  XData.Aurelius.Model,
  XData.Model.Classes,
  XData.Query.Expression;

type
  TQueryAliasInfo = class
  private
    FAlias: string;
    FFetchMode: TFetchMode;
    FPathAlias: string;
    FPathMember: string;
    FNavProp: TXDataNavigationProperty;
    function GetFullPath: string;
  public
    constructor Create(const APathAlias, APathMember: string; const AAlias: string;
      AFetchMode: TFetchMode = TFetchMode.Default);
    property Alias: string read FAlias;
    property FetchMode: TFetchMode read FFetchMode write FFetchMode;
    property FullPath: string read GetFullPath;
    property PathAlias: string read FPathAlias;
    property PathMember: string read FPathMember;
    property NavProp: TXDataNavigationProperty read FNavProp write FNavProp;
  end;

  TQueryAliases = class
  private
    FItems: TList<TQueryAliasInfo>;
  public
    constructor Create;
    destructor Destroy; override;
    function AddOrSet(const APathAlias, APathMember: string; ANavProp: TXDataNavigationProperty;
      AFetchMode: TFetchMode = TFetchMode.Default): TQueryAliasInfo;
    function FindByPath(const APathAlias, APathMember: string): TQueryAliasInfo;
    function FindByAlias(const AAlias: string): TQueryAliasInfo;
    function GetEnumerator: TEnumerator<TQueryAliasInfo>;
  end;

  TQueryAliasVisitor = class(TStackQueryVisitor<TQueryExpression>)
  strict private
    FModel: TXDataAureliusModel;
    FType: TXDataEntityType;
    FAliases: TQueryAliases;
    function GetAliases: TQueryAliases;
  public
    procedure NullLiteral(const Expr: TNullLiteralExpression); override;
    procedure StringLiteral(const Expr: TStringLiteralExpression); override;
    procedure DateTimeLiteral(const Expr: TDateTimeLiteralExpression); override;
    procedure GuidLiteral(const Expr: TGuidLiteralExpression); override;
    procedure DoubleLiteral(const Expr: TDoubleLiteralExpression); override;
    procedure IntegerLiteral(const Expr: TIntegerLiteralExpression); override;
    procedure Int64Literal(const Expr: TInt64LiteralExpression); override;
    procedure BooleanLiteral(const Expr: TBooleanLiteralExpression); override;
    procedure Logical(const Expr: TLogicalExpression); override;
    procedure Comparison(const Expr: TComparisonExpression); override;
    procedure Arithmetic(const Expr: TArithmeticExpression); override;
    procedure ArithmeticUnary(const Expr: TArithmeticUnaryExpression); override;
    procedure Parenthesis(const Expr: TParenthesisExpression); override;
    procedure Member(const Expr: TMemberExpression); override;
    procedure MethodCall(const Expr: TMethodCallExpression); override;
    procedure BooleanUnary(const Expr: TBooleanUnaryExpression); override;
    procedure ResolvedMember(const Expr: TResolvedMemberExpression); override;
  public
    constructor Create(const AModel: TXDataAureliusModel; AType: TXDataEntityType);
    destructor Destroy; override;
    property Aliases: TQueryAliases read GetAliases;
  end;

// TODO: Refactor this function and type to some other place, here doesn't look good
type
  TNavPath = record
    NavProps: TArray<TXDataNavigationProperty>;
    FinalProp: TXDataProperty;
    class function CreatePath(EntityType: TXDataEntityType; ANames: TList<string>): TNavPath; static;
  end;

implementation

uses
  SysUtils,
  XData.Query.Exceptions;


{ TQueryAliasInfo }

constructor TQueryAliasInfo.Create(const APathAlias, APathMember: string;
  const AAlias: string; AFetchMode: TFetchMode = TFetchMode.Default);
begin
  FPathAlias := APathAlias;
  FPathMember := APathMember;
  FAlias := AAlias;
  FFetchMode := AFetchMode;
end;

function TQueryAliasInfo.GetFullPath: string;
begin
  Result := PathMember;
  if PathAlias <> ''  then
    Result := PathAlias + '.' + PathMember;
end;

{ TQueryAliasVisitor }

procedure TQueryAliasVisitor.Arithmetic(const Expr: TArithmeticExpression);
var
  Left, Right: TQueryExpression;
begin
  Expr.Right.Accept(Self);
  Expr.Left.Accept(Self);
  Left := Pop;
  Right := Pop;
  Push(TArithmeticExpression.Create(Expr.Oper, Left, Right));
end;

procedure TQueryAliasVisitor.ArithmeticUnary(
  const Expr: TArithmeticUnaryExpression);
begin
  Expr.Expression.Accept(Self);
  Push(TArithmeticUnaryExpression.Create(Expr.Oper, Pop));
end;

procedure TQueryAliasVisitor.BooleanLiteral(const Expr: TBooleanLiteralExpression);
begin
  Push(TBooleanLiteralExpression.Create(Expr.Value));
end;

procedure TQueryAliasVisitor.BooleanUnary(const Expr: TBooleanUnaryExpression);
begin
  Expr.Expression.Accept(Self);
  Push(TBooleanUnaryExpression.Create(Expr.Oper, Pop));
end;

procedure TQueryAliasVisitor.Comparison(const Expr: TComparisonExpression);
var
  Left, Right: TQueryExpression;
begin
  Expr.Right.Accept(Self);
  Expr.Left.Accept(Self);
  Left := Pop;
  Right := Pop;
  Push(TComparisonExpression.Create(Expr.Oper, Left, Right));
end;

constructor TQueryAliasVisitor.Create(const AModel: TXDataAureliusModel; AType: TXDataEntityType);
begin
  inherited Create;
  FModel := AModel;
  FType := AType;
  FAliases := TQueryAliases.Create;
end;

procedure TQueryAliasVisitor.DateTimeLiteral(const Expr: TDateTimeLiteralExpression);
begin
  Push(TDateTimeLiteralExpression.Create(Expr.Value));
end;

destructor TQueryAliasVisitor.Destroy;
begin
  FAliases.Free;
  inherited;
end;

procedure TQueryAliasVisitor.DoubleLiteral(const Expr: TDoubleLiteralExpression);
begin
  Push(TDoubleLiteralExpression.Create(Expr.Value));
end;

function TQueryAliasVisitor.GetAliases: TQueryAliases;
begin
  Result := FAliases;
end;

procedure TQueryAliasVisitor.GuidLiteral(const Expr: TGuidLiteralExpression);
begin
  Push(TGuidLiteralExpression.Create(Expr.Value));
end;

procedure TQueryAliasVisitor.Int64Literal(const Expr: TInt64LiteralExpression);
begin
  Push(TInt64LiteralExpression.Create(Expr.Value));
end;

procedure TQueryAliasVisitor.IntegerLiteral(const Expr: TIntegerLiteralExpression);
begin
  Push(TIntegerLiteralExpression.Create(Expr.Value));
end;

procedure TQueryAliasVisitor.Logical(const Expr: TLogicalExpression);
var
  Left, Right: TQueryExpression;
begin
  Expr.Right.Accept(Self);
  Expr.Left.Accept(Self);
  Left := Pop;
  Right := Pop;
  Push(TLogicalExpression.Create(Expr.Oper, Left, Right));
end;

procedure TQueryAliasVisitor.Member(const Expr: TMemberExpression);
var
  PathAlias: string;
  PathMember: string;
  NavProp: TXDataNavigationProperty;
  NavPath: TNavPath;
begin
  // Get the list of properties that makes up the full property path
  NavPath := TNavPath.CreatePath(Self.FType, Expr.Names);

  // For nested member names in queries (for example, Invoice/Customer/Country/Name)
  // create one Alias for each member that is not final (they are necessarily nav props)
  PathAlias := '';
  for NavProp in NavPath.NavProps do
  begin
    PathMember := FModel.GetNavPropInfo(NavProp).MemberName;
    PathAlias := FAliases.AddOrSet(PathAlias, PathMember, NavProp).Alias;
  end;

  if NavPath.FinalProp is TXDataSimpleProperty then
    PathMember := FModel.GetPropertyInfo(TXDataSimpleProperty(NavPath.FinalProp)).RttiInfo.MemberName
  else
    PathMember := FModel.GetNavPropInfo(TXDataNavigationProperty(NavPath.FinalProp)).MemberName;

  Push(TResolvedMemberExpression.Create(PathAlias, PathMember, NavPath.FinalProp));
end;

procedure TQueryAliasVisitor.MethodCall(const Expr: TMethodCallExpression);
var
  I: integer;
  NewExpr: TMethodCallExpression;
begin
  for I := Expr.Arguments.Count - 1 downto 0 do
    Expr.Arguments[I].Accept(Self);
  NewExpr := TMethodCallExpression.Create(Expr.Method);
  for I := 0 to Expr.Arguments.Count - 1 do
    NewExpr.Arguments.Add(Pop);
  Push(NewExpr);
end;

procedure TQueryAliasVisitor.NullLiteral(const Expr: TNullLiteralExpression);
begin
  Push(TNullLiteralExpression.Create);
end;

procedure TQueryAliasVisitor.Parenthesis(const Expr: TParenthesisExpression);
begin
  Expr.Expression.Accept(Self);
  Push(TParenthesisExpression.Create(Pop));
end;

procedure TQueryAliasVisitor.ResolvedMember(
  const Expr: TResolvedMemberExpression);
begin
  Push(TResolvedMemberExpression.Create(Expr.Alias, Expr.Member, Expr.Prop));
end;

procedure TQueryAliasVisitor.StringLiteral(
  const Expr: TStringLiteralExpression);
begin
  Push(TStringLiteralExpression.Create(Expr.Value));
end;

{ TQueryAliases }

function TQueryAliases.AddOrSet(const APathAlias, APathMember: string; ANavProp: TXDataNavigationProperty;
  AFetchMode: TFetchMode): TQueryAliasInfo;
var
  Alias: string;
begin
  Result := FindByPath(APathAlias, APathMember);
  if Result = nil then
  begin
    Alias := 'a' + IntToStr(FItems.Count + 1);
    Result := TQueryAliasInfo.Create(APathAlias, APathMember, Alias);
    FItems.Add(Result);
  end;
  Result.FetchMode := AFetchMode;
  Result.NavProp := ANavProp;
end;

constructor TQueryAliases.Create;
begin
  FItems := TObjectList<TQueryAliasInfo>.Create(True);
end;

destructor TQueryAliases.Destroy;
begin
  FItems.Free;
  inherited;
end;

function TQueryAliases.FindByAlias(const AAlias: string): TQueryAliasInfo;
var
  Info: TQueryAliasInfo;
begin
  for Info in FItems do
    if SameText(AAlias, Info.Alias) then
      Exit(Info);
  Result := nil;
end;

function TQueryAliases.FindByPath(const APathAlias, APathMember: string): TQueryAliasInfo;
var
  Info: TQueryAliasInfo;
begin
  for Info in FItems do
    if SameText(APathMember, Info.PathMember) and SameText(APathAlias, Info.PathAlias) then
      Exit(Info);
  Result := nil;
end;

function TQueryAliases.GetEnumerator: TEnumerator<TQueryAliasInfo>;
begin
  Result := FItems.GetEnumerator;
end;

{ TNavPath }

class function TNavPath.CreatePath(EntityType: TXDataEntityType; ANames: TList<string>): TNavPath;

  function _GetNavProp(AType: TXDataEntityType; const AName: string): TXDataNavigationProperty;
  begin
    Result := AType.FindNavigationProperty(AName);
    if Result = nil then
      raise EQueryUnknownProperty.Create(AName);
  end;

var
  I: integer;
  NavProp: TXDataNavigationProperty;
  SimpleProp: TXDataSimpleProperty;
begin
  Assert(ANames.Count > 0);
  SetLength(Result.NavProps, ANames.Count - 1);

  for I := 0 to ANames.Count - 2 do
  begin
    // Find the navigation property (raise error if not found)
    NavProp := _GetNavProp(EntityType, ANames[I]);
    EntityType := NavProp.Target;
    if EntityType is TXDataEntityCollectionType then
      EntityType := TXDataEntityCollectionType(EntityType).ItemType;
    Result.NavProps[I] := NavProp;
  end;

  // Get final property. Either try simple property, or navigation property. Error if not found
  SimpleProp := EntityType.FindProperty(ANames.Last);
  if SimpleProp <> nil then
    Result.FinalProp := SimpleProp
  else
    Result.FinalProp := _GetNavProp(EntityType, ANames.Last);
end;

end.
