/*
 * 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.
 */

#include "iceberg/expression/term.h"

#include <format>

#include "iceberg/result.h"
#include "iceberg/row/struct_like.h"
#include "iceberg/schema.h"
#include "iceberg/transform.h"
#include "iceberg/util/checked_cast.h"
#include "iceberg/util/macros.h"

namespace iceberg {

Bound::~Bound() = default;

BoundTerm::~BoundTerm() = default;

Reference::~Reference() = default;

template <typename B>
Result<std::shared_ptr<B>> Unbound<B>::Bind(const Schema& schema) const {
  return Bind(schema, /*case_sensitive=*/true);
}

// NamedReference implementation
Result<std::unique_ptr<NamedReference>> NamedReference::Make(std::string field_name) {
  if (field_name.empty()) [[unlikely]] {
    return InvalidExpression("NamedReference cannot have empty field name");
  }
  return std::unique_ptr<NamedReference>(new NamedReference(std::move(field_name)));
}

NamedReference::NamedReference(std::string field_name)
    : field_name_(std::move(field_name)) {
  ICEBERG_DCHECK(!field_name_.empty(), "NamedReference cannot have empty field name");
}

NamedReference::~NamedReference() = default;

Result<std::shared_ptr<BoundReference>> NamedReference::Bind(const Schema& schema,
                                                             bool case_sensitive) const {
  ICEBERG_ASSIGN_OR_RAISE(auto field_opt,
                          schema.FindFieldByName(field_name_, case_sensitive));
  if (!field_opt.has_value()) [[unlikely]] {
    return InvalidExpression("Cannot find field '{}' in struct: {}", field_name_,
                             schema.ToString());
  }

  int32_t field_id = field_opt.value().get().field_id();
  ICEBERG_ASSIGN_OR_RAISE(auto accessor, schema.GetAccessorById(field_id));

  return BoundReference::Make(field_opt.value().get(), std::move(accessor));
}

std::string NamedReference::ToString() const {
  return std::format("ref(name=\"{}\")", field_name_);
}

// BoundReference implementation
Result<std::unique_ptr<BoundReference>> BoundReference::Make(
    SchemaField field, std::unique_ptr<StructLikeAccessor> accessor) {
  if (auto status = field.Validate(); !status.has_value()) [[unlikely]] {
    return InvalidExpression("Cannot create BoundReference with invalid field: {}",
                             status.error().message);
  }
  if (!accessor) [[unlikely]] {
    return InvalidExpression("Cannot create BoundReference without accessor");
  }
  return std::unique_ptr<BoundReference>(
      new BoundReference(std::move(field), std::move(accessor)));
}

BoundReference::BoundReference(SchemaField field,
                               std::unique_ptr<StructLikeAccessor> accessor)
    : field_(std::move(field)), accessor_(std::move(accessor)) {
  ICEBERG_DCHECK(field_.Validate().has_value(),
                 "Cannot create BoundReference with invalid field");
  ICEBERG_DCHECK(accessor_ != nullptr, "Cannot create BoundReference without accessor");
}

BoundReference::~BoundReference() = default;

std::string BoundReference::ToString() const {
  return std::format("ref(id={}, type={})", field_.field_id(), field_.type()->ToString());
}

Result<Literal> BoundReference::Evaluate(const StructLike& data) const {
  return accessor_->GetLiteral(data);
}

bool BoundReference::Equals(const BoundTerm& other) const {
  if (other.kind() != Term::Kind::kReference) {
    return false;
  }

  const auto& other_ref = internal::checked_cast<const BoundReference&>(other);
  return field_.field_id() == other_ref.field_.field_id() &&
         field_.optional() == other_ref.field_.optional() &&
         *field_.type() == *other_ref.field_.type();
}

// UnboundTransform implementation
Result<std::unique_ptr<UnboundTransform>> UnboundTransform::Make(
    std::shared_ptr<NamedReference> ref, std::shared_ptr<Transform> transform) {
  if (!ref || !transform) [[unlikely]] {
    return InvalidExpression(
        "Cannot create UnboundTransform with null reference or transform");
  }
  return std::unique_ptr<UnboundTransform>(
      new UnboundTransform(std::move(ref), std::move(transform)));
}

UnboundTransform::UnboundTransform(std::shared_ptr<NamedReference> ref,
                                   std::shared_ptr<Transform> transform)
    : ref_(std::move(ref)), transform_(std::move(transform)) {
  ICEBERG_DCHECK(!ref || !transform,
                 "Cannot create UnboundTransform with null reference or transform");
}

UnboundTransform::~UnboundTransform() = default;

std::string UnboundTransform::ToString() const {
  return std::format("{}({})", transform_->ToString(), ref_->ToString());
}

Result<std::shared_ptr<BoundTransform>> UnboundTransform::Bind(
    const Schema& schema, bool case_sensitive) const {
  ICEBERG_ASSIGN_OR_RAISE(auto bound_ref, ref_->Bind(schema, case_sensitive));
  ICEBERG_ASSIGN_OR_RAISE(auto transform_func, transform_->Bind(bound_ref->type()));
  return BoundTransform::Make(std::move(bound_ref), transform_,
                              std::move(transform_func));
}

// BoundTransform implementation
Result<std::unique_ptr<BoundTransform>> BoundTransform::Make(
    std::shared_ptr<BoundReference> ref, std::shared_ptr<Transform> transform,
    std::shared_ptr<TransformFunction> transform_func) {
  if (!ref || !transform || !transform_func) [[unlikely]] {
    return InvalidExpression(
        "Cannot create BoundTransform with null reference or transform");
  }
  return std::unique_ptr<BoundTransform>(new BoundTransform(
      std::move(ref), std::move(transform), std::move(transform_func)));
}

BoundTransform::BoundTransform(std::shared_ptr<BoundReference> ref,
                               std::shared_ptr<Transform> transform,
                               std::shared_ptr<TransformFunction> transform_func)
    : ref_(std::move(ref)),
      transform_(std::move(transform)),
      transform_func_(std::move(transform_func)) {
  ICEBERG_DCHECK(ref_ && transform_ && transform_func_,
                 "Cannot create BoundTransform with null reference or transform");
}

BoundTransform::~BoundTransform() = default;

std::string BoundTransform::ToString() const {
  return std::format("{}({})", transform_->ToString(), ref_->ToString());
}

Result<Literal> BoundTransform::Evaluate(const StructLike& data) const {
  ICEBERG_ASSIGN_OR_RAISE(auto literal, ref_->Evaluate(data));
  return transform_func_->Transform(literal);
}

bool BoundTransform::MayProduceNull() const {
  // transforms must produce null for null input values
  // transforms may produce null for non-null inputs when not order-preserving
  return ref_->MayProduceNull() || !transform_->PreservesOrder();
}

std::shared_ptr<Type> BoundTransform::type() const {
  return transform_func_->ResultType();
}

bool BoundTransform::Equals(const BoundTerm& other) const {
  if (other.kind() == Term::Kind::kTransform) {
    const auto& other_transform = internal::checked_cast<const BoundTransform&>(other);
    return *ref_ == *other_transform.ref_ && *transform_ == *other_transform.transform_;
  }

  if (transform_->transform_type() == TransformType::kIdentity &&
      other.kind() == Term::Kind::kReference) {
    return *ref_ == other;
  }

  return false;
}

// Explicit template instantiations
template Result<std::shared_ptr<BoundReference>> Unbound<BoundReference>::Bind(
    const Schema& schema) const;
template Result<std::shared_ptr<BoundTransform>> Unbound<BoundTransform>::Bind(
    const Schema& schema) const;

}  // namespace iceberg
