/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#pragma once

/// \file iceberg/expression/expressions.h
/// Factory methods for creating expressions.

#include <initializer_list>
#include <memory>
#include <string>
#include <vector>

#include "iceberg/expression/aggregate.h"
#include "iceberg/expression/literal.h"
#include "iceberg/expression/predicate.h"
#include "iceberg/expression/term.h"
#include "iceberg/iceberg_export.h"
#include "iceberg/util/macros.h"

namespace iceberg {

/// \brief Fluent APIs to create expressions.
///
/// \throw `ExpressionError` for invalid expression.
class ICEBERG_EXPORT Expressions {
 public:
  // Logical operations

  /// \brief Create an AND expression.
  template <typename... Args>
  static std::shared_ptr<Expression> And(std::shared_ptr<Expression> left,
                                         std::shared_ptr<Expression> right,
                                         Args&&... args)
    requires std::conjunction_v<std::is_same<Args, std::shared_ptr<Expression>>...>
  {
    ICEBERG_ASSIGN_OR_THROW(auto and_expr,
                            iceberg::And::MakeFolded(std::move(left), std::move(right),
                                                     std::forward<Args>(args)...));
    return and_expr;
  }

  /// \brief Create an OR expression.
  template <typename... Args>
  static std::shared_ptr<Expression> Or(std::shared_ptr<Expression> left,
                                        std::shared_ptr<Expression> right, Args&&... args)
    requires std::conjunction_v<std::is_same<Args, std::shared_ptr<Expression>>...>
  {
    ICEBERG_ASSIGN_OR_THROW(auto or_expr,
                            iceberg::Or::MakeFolded(std::move(left), std::move(right),
                                                    std::forward<Args>(args)...));
    return or_expr;
  }

  /// \brief Create a NOT expression.
  ///
  /// \param child The expression to negate
  /// \return A negated expression with optimizations applied:
  ///   - not(true) returns false
  ///   - not(false) returns true
  ///   - not(not(x)) returns x
  static std::shared_ptr<Expression> Not(std::shared_ptr<Expression> child);

  // Transform functions

  /// \brief Create a bucket transform term.
  static std::shared_ptr<UnboundTransform> Bucket(std::string name, int32_t num_buckets);

  /// \brief Create a year transform term.
  static std::shared_ptr<UnboundTransform> Year(std::string name);

  /// \brief Create a month transform term.
  static std::shared_ptr<UnboundTransform> Month(std::string name);

  /// \brief Create a day transform term.
  static std::shared_ptr<UnboundTransform> Day(std::string name);

  /// \brief Create an hour transform term.
  static std::shared_ptr<UnboundTransform> Hour(std::string name);

  /// \brief Create a truncate transform term.
  static std::shared_ptr<UnboundTransform> Truncate(std::string name, int32_t width);

  /// \brief Create a transform expression.
  static std::shared_ptr<UnboundTransform> Transform(
      std::string name, std::shared_ptr<Transform> transform);

  // Aggregates

  /// \brief Create a COUNT aggregate for a field name.
  static std::shared_ptr<UnboundAggregateImpl<BoundReference>> Count(std::string name);

  /// \brief Create a COUNT aggregate for an unbound term.
  static std::shared_ptr<UnboundAggregateImpl<BoundReference>> Count(
      std::shared_ptr<UnboundTerm<BoundReference>> expr);

  /// \brief Create a COUNT_NULL aggregate for a field name.
  static std::shared_ptr<UnboundAggregateImpl<BoundReference>> CountNull(
      std::string name);

  /// \brief Create a COUNT_NULL aggregate for an unbound term.
  static std::shared_ptr<UnboundAggregateImpl<BoundReference>> CountNull(
      std::shared_ptr<UnboundTerm<BoundReference>> expr);

  /// \brief Create a COUNT_NOT_NULL aggregate for a field name.
  static std::shared_ptr<UnboundAggregateImpl<BoundReference>> CountNotNull(
      std::string name);

  /// \brief Create a COUNT_NOT_NULL aggregate for an unbound term.
  static std::shared_ptr<UnboundAggregateImpl<BoundReference>> CountNotNull(
      std::shared_ptr<UnboundTerm<BoundReference>> expr);

  /// \brief Create a COUNT(*) aggregate.
  static std::shared_ptr<UnboundAggregateImpl<BoundReference>> CountStar();

  /// \brief Create a MAX aggregate for a field name.
  static std::shared_ptr<UnboundAggregateImpl<BoundReference>> Max(std::string name);

  /// \brief Create a MAX aggregate for an unbound term.
  static std::shared_ptr<UnboundAggregateImpl<BoundReference>> Max(
      std::shared_ptr<UnboundTerm<BoundReference>> expr);
  /// \brief Create a MAX aggregate for an unbound transform term.
  static std::shared_ptr<UnboundAggregateImpl<BoundTransform>> Max(
      std::shared_ptr<UnboundTerm<BoundTransform>> expr);

  /// \brief Create a MIN aggregate for a field name.
  static std::shared_ptr<UnboundAggregateImpl<BoundReference>> Min(std::string name);

  /// \brief Create a MIN aggregate for an unbound term.
  static std::shared_ptr<UnboundAggregateImpl<BoundReference>> Min(
      std::shared_ptr<UnboundTerm<BoundReference>> expr);
  /// \brief Create a MIN aggregate for an unbound transform term.
  static std::shared_ptr<UnboundAggregateImpl<BoundTransform>> Min(
      std::shared_ptr<UnboundTerm<BoundTransform>> expr);

  // Unary predicates

  /// \brief Create an IS NULL predicate for a field name.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> IsNull(std::string name);

  /// \brief Create an IS NULL predicate for an unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> IsNull(
      std::shared_ptr<UnboundTerm<B>> expr) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred,
        UnboundPredicateImpl<B>::Make(Expression::Operation::kIsNull, std::move(expr)));
    return pred;
  }

  /// \brief Create a NOT NULL predicate for a field name.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> NotNull(std::string name);

  /// \brief Create a NOT NULL predicate for an unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> NotNull(
      std::shared_ptr<UnboundTerm<B>> expr) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred,
        UnboundPredicateImpl<B>::Make(Expression::Operation::kNotNull, std::move(expr)));
    return pred;
  }

  /// \brief Create an IS NaN predicate for a field name.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> IsNaN(std::string name);

  /// \brief Create an IS NaN predicate for an unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> IsNaN(
      std::shared_ptr<UnboundTerm<B>> expr) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred,
        UnboundPredicateImpl<B>::Make(Expression::Operation::kIsNan, std::move(expr)));
    return pred;
  }

  /// \brief Create a NOT NaN predicate for a field name.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> NotNaN(std::string name);

  /// \brief Create a NOT NaN predicate for an unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> NotNaN(
      std::shared_ptr<UnboundTerm<B>> expr) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred,
        UnboundPredicateImpl<B>::Make(Expression::Operation::kNotNan, std::move(expr)));
    return pred;
  }

  // Comparison predicates

  /// \brief Create a less than predicate for a field name.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> LessThan(std::string name,
                                                                        Literal value);

  /// \brief Create a less than predicate for an unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> LessThan(
      std::shared_ptr<UnboundTerm<B>> expr, Literal value) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred, UnboundPredicateImpl<B>::Make(Expression::Operation::kLt,
                                                 std::move(expr), std::move(value)));
    return pred;
  }

  /// \brief Create a less than or equal predicate for a field name.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> LessThanOrEqual(
      std::string name, Literal value);

  /// \brief Create a less than or equal predicate for an unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> LessThanOrEqual(
      std::shared_ptr<UnboundTerm<B>> expr, Literal value) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred, UnboundPredicateImpl<B>::Make(Expression::Operation::kLtEq,
                                                 std::move(expr), std::move(value)));
    return pred;
  }

  /// \brief Create a greater than predicate for a field name.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> GreaterThan(
      std::string name, Literal value);

  /// \brief Create a greater than predicate for an unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> GreaterThan(
      std::shared_ptr<UnboundTerm<B>> expr, Literal value) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred, UnboundPredicateImpl<B>::Make(Expression::Operation::kGt,
                                                 std::move(expr), std::move(value)));
    return pred;
  }

  /// \brief Create a greater than or equal predicate for a field name.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> GreaterThanOrEqual(
      std::string name, Literal value);

  /// \brief Create a greater than or equal predicate for an unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> GreaterThanOrEqual(
      std::shared_ptr<UnboundTerm<B>> expr, Literal value) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred, UnboundPredicateImpl<B>::Make(Expression::Operation::kGtEq,
                                                 std::move(expr), std::move(value)));
    return pred;
  }

  /// \brief Create an equal predicate for a field name.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> Equal(std::string name,
                                                                     Literal value);

  /// \brief Create an equal predicate for an unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> Equal(
      std::shared_ptr<UnboundTerm<B>> expr, Literal value) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred, UnboundPredicateImpl<B>::Make(Expression::Operation::kEq,
                                                 std::move(expr), std::move(value)));
    return pred;
  }

  /// \brief Create a not equal predicate for a field name.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> NotEqual(std::string name,
                                                                        Literal value);

  /// \brief Create a not equal predicate for an unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> NotEqual(
      std::shared_ptr<UnboundTerm<B>> expr, Literal value) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred, UnboundPredicateImpl<B>::Make(Expression::Operation::kNotEq,
                                                 std::move(expr), std::move(value)));
    return pred;
  }

  // String predicates

  /// \brief Create a starts with predicate for a field name.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> StartsWith(
      std::string name, std::string value);

  /// \brief Create a starts with predicate for an unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> StartsWith(
      std::shared_ptr<UnboundTerm<B>> expr, std::string value) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred,
        UnboundPredicateImpl<B>::Make(Expression::Operation::kStartsWith, std::move(expr),
                                      Literal::String(std::move(value))));
    return pred;
  }

  /// \brief Create a not starts with predicate for a field name.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> NotStartsWith(
      std::string name, std::string value);

  /// \brief Create a not starts with predicate for an unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> NotStartsWith(
      std::shared_ptr<UnboundTerm<B>> expr, std::string value) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred, UnboundPredicateImpl<B>::Make(Expression::Operation::kNotStartsWith,
                                                 std::move(expr),
                                                 Literal::String(std::move(value))));
    return pred;
  }

  // Set predicates

  /// \brief Create an IN predicate for a field name.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> In(
      std::string name, std::vector<Literal> values);

  /// \brief Create an IN predicate for an unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> In(std::shared_ptr<UnboundTerm<B>> expr,
                                                     std::vector<Literal> values) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred, UnboundPredicateImpl<B>::Make(Expression::Operation::kIn,
                                                 std::move(expr), std::move(values)));
    return pred;
  }

  /// \brief Create an IN predicate for a field name with initializer list.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> In(
      std::string name, std::initializer_list<Literal> values);

  /// \brief Create an IN predicate for an unbound term with initializer list.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> In(
      std::shared_ptr<UnboundTerm<B>> expr, std::initializer_list<Literal> values) {
    return In<B>(std::move(expr), std::vector<Literal>(values));
  }

  /// \brief Create a NOT IN predicate for a field name.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> NotIn(
      std::string name, std::vector<Literal> values);

  /// \brief Create a NOT IN predicate for an unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> NotIn(
      std::shared_ptr<UnboundTerm<B>> expr, std::vector<Literal> values) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred, UnboundPredicateImpl<B>::Make(Expression::Operation::kNotIn,
                                                 std::move(expr), std::move(values)));
    return pred;
  }

  /// \brief Create a NOT IN predicate for a field name with initializer list.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> NotIn(
      std::string name, std::initializer_list<Literal> values);

  /// \brief Create a NOT IN predicate for an unbound term with initializer list.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> NotIn(
      std::shared_ptr<UnboundTerm<B>> expr, std::initializer_list<Literal> values) {
    return NotIn<B>(expr, std::vector<Literal>(values));
  }

  // Generic predicate factory

  /// \brief Create a predicate with operation and single value.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> Predicate(
      Expression::Operation op, std::string name, Literal value);

  /// \brief Create a predicate with operation and multiple values.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> Predicate(
      Expression::Operation op, std::string name, std::vector<Literal> values);

  /// \brief Create a predicate with operation and multiple values.
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> Predicate(
      Expression::Operation op, std::string name, std::initializer_list<Literal> values);

  /// \brief Create a unary predicate (no values).
  static std::shared_ptr<UnboundPredicateImpl<BoundReference>> Predicate(
      Expression::Operation op, std::string name);

  /// \brief Create a predicate for unbound term with multiple values.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> Predicate(
      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> expr,
      std::vector<Literal> values) {
    ICEBERG_ASSIGN_OR_THROW(
        auto pred, UnboundPredicateImpl<B>::Make(op, std::move(expr), std::move(values)));
    return pred;
  }

  /// \brief Create a predicate with operation and multiple values.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> Predicate(
      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> expr,
      std::initializer_list<Literal> values) {
    return Predicate<B>(op, std::move(expr), std::vector<Literal>(values));
  }

  /// \brief Create a unary predicate for unbound term.
  template <typename B>
  static std::shared_ptr<UnboundPredicateImpl<B>> Predicate(
      Expression::Operation op, std::shared_ptr<UnboundTerm<B>> expr) {
    ICEBERG_ASSIGN_OR_THROW(auto pred,
                            UnboundPredicateImpl<B>::Make(op, std::move(expr)));
    return pred;
  }

  // Constants

  /// \brief Return the always true expression.
  static std::shared_ptr<True> AlwaysTrue();

  /// \brief Return the always false expression.
  static std::shared_ptr<False> AlwaysFalse();

  // Utilities

  /// \brief Create a named reference to a field.
  static std::shared_ptr<NamedReference> Ref(std::string name);

  /// \brief Create a literal from a value.
  static Literal Lit(Literal::Value value, std::shared_ptr<PrimitiveType> type);
};

}  // namespace iceberg
