unit XData.Query.Parser;

{$I XData.Inc}

interface

uses
  Generics.Collections,
  XData.Query.Expression,
  XData.Query.Method,
  XData.Query.Tokenizer;

type
  TQueryMethod = XData.Query.Method.TQueryMethod;

  TQueryParser = class
  strict private
    class var
      Operators: TDictionary<string, TQueryOperator>;
      OperatorNot: TQueryOperator;
      Methods: TDictionary<string, TQueryMethod>;
  strict private
    FTokenizer: TQueryTokenizer;
    FManaged: TList<TQueryExpression>;
    function Save(const AExpression: TQueryExpression): TQueryExpression;
    function GetOperator: TQueryOperator;
    function ParseClimbing(const Left: TQueryExpression; const MinPrecedence: TOperatorPrecedence): TQueryExpression;
    function ParseAtom: TQueryExpression;
    function ParseMethodCall(const AName: string): TMethodCallExpression;
    function ParseMember(const AName: string): TMemberExpression;
    function CreateBinary(const Oper: TQueryOperator; const Left, Right: TQueryExpression): TBinaryExpression;
    class constructor Create;
    class destructor Destroy;
  strict protected
    property Tokenizer: TQueryTokenizer read FTokenizer;
    function Extract(const AExpression: TQueryExpression): TQueryExpression;
    function ParseExpression: TQueryExpression;
  public
    constructor Create(const AExpression: string);
    destructor Destroy; override;
    function Parse: TQueryExpression;
    class procedure AddMethod(const Name: string; AMethod: TQueryMethod);
  end;

  TQueryOrderParser = class(TQueryParser)
  public
    function ParseOrders: TArray<TQueryOrderInfo>;
  end;

  TQueryExpandParser = class(TQueryParser)
  public
    function ParseItems: TArray<TQueryExpandInfo>;
  end;

implementation

uses
  SysUtils,
  XData.Query.Exceptions;

{ TXDataQueryParser }

function TQueryParser.ParseClimbing(const Left: TQueryExpression;
  const MinPrecedence: TOperatorPrecedence):  TQueryExpression;
var
  Op: TQueryOperator;
  OpInner: TQueryOperator;
  Right: TQueryExpression;
begin
  Result := Left;
  Op := GetOperator;
  while (Op <> nil) and Op.IsBinary and (Op.Precedence >= MinPrecedence) do
  begin
    FTokenizer.NextToken;
    Right := ParseAtom;
    OpInner := GetOperator;
    while (OpInner <> nil) and OpInner.IsBinary and (OpInner.Precedence > Op.Precedence) do
    begin
      Right := ParseClimbing(Right, OpInner.Precedence);
      OpInner := GetOperator;
    end;
    Result := CreateBinary(Op, Result, Right);
    Op := GetOperator;
  end;
end;

function TQueryParser.ParseExpression: TQueryExpression;
begin
  Result := ParseClimbing(ParseAtom, Low(TOperatorPrecedence));
end;

class procedure TQueryParser.AddMethod(const Name: string;
  AMethod: TQueryMethod);
begin
  Methods.AddOrSetValue(Name, AMethod);
end;

constructor TQueryParser.Create(const AExpression: string);
begin
  FTokenizer := TQueryTokenizer.Create(AExpression);
  FManaged := TObjectList<TQueryExpression>.Create(true);
end;

function TQueryParser.CreateBinary(const Oper: TQueryOperator; const Left,
  Right: TQueryExpression): TBinaryExpression;
begin
  if Oper.IsLogical then
    Result := TLogicalExpression.Create(Oper, Extract(Left), Extract(Right))
  else
  if Oper.IsRelational then
    Result := TComparisonExpression.Create(Oper, Extract(Left), Extract(Right))
  else
  if Oper.IsArithmetic then
    Result := TArithmeticExpression.Create(Oper, Extract(Left), Extract(Right))
  else
    raise EQueryInvalidOperatorSyntaxError.Create(FTokenizer, Oper.Name);
  Save(Result);
end;

class constructor TQueryParser.Create;
begin
  Operators := TObjectDictionary<string, TQueryOperator>.Create([doOwnsValues]);
  Operators.Add('not', TQueryOperator.Create('not', TOperatorType.opNot, TOperatorPrecedence.Unary));
  OperatorNot := Operators['not'];
  Operators.Add('mul', TQueryOperator.Create('mul', TOperatorType.opMul, TOperatorPrecedence.Multiplicative));
  Operators.Add('div', TQueryOperator.Create('div', TOperatorType.opDiv, TOperatorPrecedence.Multiplicative));
  Operators.Add('mod', TQueryOperator.Create('mod', TOperatorType.opMod, TOperatorPrecedence.Multiplicative));
  Operators.Add('add', TQueryOperator.Create('add', TOperatorType.opAdd, TOperatorPrecedence.Additive));
  Operators.Add('sub', TQueryOperator.Create('sub', TOperatorType.opSub, TOperatorPrecedence.Additive));
  Operators.Add('lt', TQueryOperator.Create('lt', TOperatorType.opLt, TOperatorPrecedence.Relational));
  Operators.Add('gt', TQueryOperator.Create('gt', TOperatorType.opGt, TOperatorPrecedence.Relational));
  Operators.Add('le', TQueryOperator.Create('le', TOperatorType.opLe, TOperatorPrecedence.Relational));
  Operators.Add('ge', TQueryOperator.Create('ge', TOperatorType.opGe, TOperatorPrecedence.Relational));
  Operators.Add('eq', TQueryOperator.Create('eq', TOperatorType.opEq, TOperatorPrecedence.Equality));
  Operators.Add('ne', TQueryOperator.Create('ne', TOperatorType.opNe, TOperatorPrecedence.Equality));
  Operators.Add('like', TQueryOperator.Create('like', TOperatorType.opLike, TOperatorPrecedence.Equality));
  Operators.Add('ilike', TQueryOperator.Create('ilike', TOperatorType.opILike, TOperatorPrecedence.Equality));
  Operators.Add('and', TQueryOperator.Create('and', TOperatorType.opAnd, TOperatorPrecedence.ConditionalAnd));
  Operators.Add('or', TQueryOperator.Create('or', TOperatorType.opOr, TOperatorPrecedence.ConditionalOr));

  Methods := TObjectDictionary<string, TQueryMethod>.Create([doOwnsValues]);

  Methods.Add('startswith', TQueryMethod.Create('startswith', 2, True));
  Methods.Add('endswith', TQueryMethod.Create('endswith', 2, True));
  Methods.Add('contains', TQueryMethod.Create('contains', 2, True));

  Methods.Add('year', TQueryMethod.Create('year', 1));
  Methods.Add('month', TQueryMethod.Create('month', 1));
  Methods.Add('day', TQueryMethod.Create('day', 1));
  Methods.Add('hour', TQueryMethod.Create('hour', 1));
  Methods.Add('minute', TQueryMethod.Create('minute', 1));
  Methods.Add('second', TQueryMethod.Create('second', 1));

  Methods.Add('concat', TQueryMethod.Create('concat', 2));
  Methods.Add('upper', TQueryMethod.Create('upper', 1));
  Methods.Add('lower', TQueryMethod.Create('lower', 1));
  Methods.Add('length', TQueryMethod.Create('length', 1));
  Methods.Add('bytelength', TQueryMethod.Create('bytelength', 1));
  Methods.Add('substring', TQueryMethod.Create('substring', 3));
  Methods.Add('position', TQueryMethod.Create('position', 2));
end;

class destructor TQueryParser.Destroy;
begin
  Methods.Free;
  Operators.Free;
end;

destructor TQueryParser.Destroy;
begin
  FTokenizer.Free;
  FManaged.Free;
  inherited;
end;

function TQueryParser.Extract(const AExpression: TQueryExpression): TQueryExpression;
begin
  Result := FManaged.Extract(AExpression);
end;

function TQueryParser.GetOperator: TQueryOperator;
begin
  if not ((FTokenizer.TokenType = TQueryTokenType.Identifier)
    and Operators.TryGetValue(Lowercase(FTokenizer.IdValue), Result)) then
    Result := nil;
end;

function TQueryParser.Parse: TQueryExpression;
begin
  FTokenizer.NextToken;
  Result := ParseExpression;
  if FTokenizer.TokenType <> TQueryTokenType.EOF then
    raise EQuerySyntaxError.Create(FTokenizer);
  Extract(Result);
end;

function TQueryParser.ParseAtom: TQueryExpression;
var
  Op: TQueryOperator;
  Id: string;
begin
  case FTokenizer.TokenType of
    TQueryTokenType.TextLiteral:
      begin
        Result := Save(TStringLiteralExpression.Create(FTokenizer.StringValue));
        FTokenizer.NextToken;
      end;
    TQueryTokenType.IntegerLiteral:
      begin
        Result := Save(TIntegerLiteralExpression.Create(FTokenizer.IntegerValue));
        FTokenizer.NextToken;
      end;
    TQueryTokenType.Int64Literal:
      begin
        Result := Save(TInt64LiteralExpression.Create(FTokenizer.Int64Value));
        FTokenizer.NextToken;
      end;
    TQueryTokenType.DoubleLiteral:
      begin
        Result := Save(TDoubleLiteralExpression.Create(FTokenizer.DoubleValue));
        FTokenizer.NextToken;
      end;
    TQueryTokenType.BooleanLiteral:
      begin
        Result := Save(TBooleanLiteralExpression.Create(FTokenizer.BooleanValue));
        FTokenizer.NextToken;
      end;
    TQueryTokenType.NullLiteral:
      begin
        Result := Save(TNullLiteralExpression.Create);
        FTokenizer.NextToken;
      end;
    TQueryTokenType.DateTimeLiteral:
      begin
        Result := Save(TDateTimeLiteralExpression.Create(FTokenizer.DateTimeValue));
        FTokenizer.NextToken;
      end;
    TQueryTokenType.GuidLiteral:
      begin
        Result := Save(TGuidLiteralExpression.Create(FTokenizer.GuidValue));
        FTokenizer.NextToken;
      end;

    TQueryTokenType.OpenParen:
      begin
        FTokenizer.NextToken;
        Result := Save(TParenthesisExpression.Create(Extract(ParseExpression)));
        if FTokenizer.TokenType <> TQueryTokenType.CloseParen then
          raise EQueryParenCloseExpectedError.Create(FTokenizer);
        FTokenizer.NextToken;
      end;
    TQueryTokenType.Negate:
      begin
        FTokenizer.NextToken;
        Result := Save(TArithmeticUnaryExpression.Create(OperatorNot, Extract(ParseAtom)));
      end;
    TQueryTokenType.Identifier:
      begin
        Op := GetOperator;
        if Op <> nil then
        begin
          if Op.IsUnary then
          begin
            FTokenizer.NextToken;
            Result := Save(TBooleanUnaryExpression.Create(Op, Extract(ParseAtom)));
          end else
            raise EQueryInvalidOperatorSyntaxError.Create(FTokenizer, Op.Name);
        end
        else
        begin
          Id := FTokenizer.IdValue;
          FTokenizer.NextToken;
          if FTokenizer.TokenType = TQueryTokenType.OpenParen then
            Result := ParseMethodCall(Id)
          else
            Result := ParseMember(Id);
        end;
      end;
    TQueryTokenType.EOF,
    TQueryTokenType.CloseParen,
    TQueryTokenType.Slash,
    TQueryTokenType.Comma:
      raise EQuerySyntaxError.Create(FTokenizer);
  else
    raise EQueryIndentifierExpectedError.Create(FTokenizer); // should never reach here
  end;
end;

function TQueryParser.ParseMember(const AName: string): TMemberExpression;
begin
  Result := TMemberExpression.Create(AName);
  Save(Result);
  while (FTokenizer.TokenType = TQueryTokenType.Slash) do
  begin
    FTokenizer.NextToken;
    if FTokenizer.TokenType <> TQueryTokenType.Identifier then
      raise EQueryIndentifierExpectedError.Create(FTokenizer);
    Result.Names.Add(FTokenizer.IdValue);
    FTokenizer.NextToken;
  end;
end;

function TQueryParser.ParseMethodCall(const AName: string): TMethodCallExpression;
var
  Method: TQueryMethod;
begin
  if not Methods.TryGetValue(Lowercase(AName), Method) then
    raise EQueryUnknownMethodError.Create(FTokenizer, AName);
  FTokenizer.NextToken; // skip open parentesis
  Result := TMethodCallExpression.Create(Method);
  Save(Result);
  if FTokenizer.TokenType <> TQueryTokenType.CloseParen then
  repeat
    Result.Arguments.Add(Extract(ParseExpression));
    case FTokenizer.TokenType of
      TQueryTokenType.Comma:
        FTokenizer.NextToken;
      TQueryTokenType.CloseParen:
        begin
          FTokenizer.NextToken;
          break;
        end;
    else
      raise EQuerySyntaxError.Create(FTokenizer);
    end;
  until false;

  if (Result.Arguments.Count < Method.ArgumentCount) or (Result.Arguments.Count > Method.MaxArgumentCount) then
    raise EQueryWrongArgumentCountError.Create(FTokenizer,
      Result.Arguments.Count, Method.ArgumentCount, Method.MaxArgumentCount);
end;

function TQueryParser.Save(
  const AExpression: TQueryExpression): TQueryExpression;
begin
  FManaged.Add(AExpression);
  Result := AExpression;
end;

{ TQueryOrderParser }

function TQueryOrderParser.ParseOrders: TArray<TQueryOrderInfo>;
var
  List: TObjectList<TQueryOrderInfo>;
  Expr: TQueryExpression;
  Ascending: boolean;
begin
  List := TObjectList<TQueryOrderInfo>.Create(true);
  try
    Tokenizer.NextToken;
    if Tokenizer.TokenType <> TQueryTokenType.EOF then
    repeat
      Expr := ParseExpression;

      Ascending := true;
      if Tokenizer.TokenType = TQueryTokenType.Identifier then
      begin
        if SameText(Tokenizer.IdValue, 'desc') then
          Ascending := false
        else
        if not SameText(Tokenizer.IdValue, 'asc') then
          raise EQuerySyntaxError.Create(Tokenizer);
        Tokenizer.NextToken;
      end;

      List.Add(TQueryOrderInfo.Create(Extract(Expr), Ascending));

      case Tokenizer.TokenType of
        TQueryTokenType.Comma:
          Tokenizer.NextToken;
        TQueryTokenType.EOF:
          break;
      else
        raise EQuerySyntaxError.Create(Tokenizer);
      end;
    until false;

    Result := List.ToArray;
    List.OwnsObjects := false;
  finally
    List.Free;
  end;
end;

{ TQueryExpandParser }

function TQueryExpandParser.ParseItems: TArray<TQueryExpandInfo>;
var
  List: TObjectList<TQueryExpandInfo>;
  Expr: TQueryExpression;
begin
  List := TObjectList<TQueryExpandInfo>.Create(true);
  try
    Tokenizer.NextToken;
    if Tokenizer.TokenType <> TQueryTokenType.EOF then
    repeat
      Expr := ParseExpression;

      List.Add(TQueryExpandInfo.Create(Extract(Expr)));

      case Tokenizer.TokenType of
        TQueryTokenType.Comma:
          Tokenizer.NextToken;
        TQueryTokenType.EOF:
          break;
      else
        raise EQuerySyntaxError.Create(Tokenizer);
      end;
    until false;

    Result := List.ToArray;
    List.OwnsObjects := false;
  finally
    List.Free;
  end;
end;

end.
