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

#include <cerrno>
#include <concepts>
#include <cstdint>
#include <cstring>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <utility>
#include <vector>

#include "iceberg/arrow_c_data.h"
#include "iceberg/iceberg_export.h"
#include "iceberg/result.h"
#include "iceberg/type_fwd.h"

namespace iceberg {

/// \brief Cached state for ProjectBatch over one input/output schema pair.
///
/// Exported because this internal utility is shared across library translation units.
class ICEBERG_EXPORT ProjectionContext {
 public:
  using ProjectBatchState = std::shared_ptr<void>;

  using ProjectBatchFunction = auto (*)(ArrowArray* input_batch,
                                        std::span<const int32_t> row_indices,
                                        ProjectionContext& projection)
      -> Result<ArrowArray>;

  /// \brief Register a custom implementation for ProjectBatch.
  ///
  /// Registration is process-wide. If multiple implementations are registered,
  /// the last non-null implementation wins.
  static void RegisterProjectBatchFunction(ProjectBatchFunction project_batch_function);

  /// \brief Returns true when a custom implementation has been registered.
  static bool HasProjectBatchFunction();

  /// \brief Resolve the registered ProjectBatch implementation.
  static auto ResolveProjectBatchFunction() -> ProjectBatchFunction;

  /// \brief Build reusable projection state for a validated schema pair.
  ///
  /// \param input_schema Schema that describes every input batch.
  /// \param output_schema Final schema and column order requested by the caller.
  /// \param project_batch_function Optional implementation returned by
  ///   ProjectionContext::ResolveProjectBatchFunction, or nullptr to use the nanoarrow
  ///   path.
  /// \note It validates that output_schema selects or reorders complete top-level fields
  /// by field id. Nested pruning and type changes are rejected. The input_schema and
  /// output_schema passed to Make must outlive the context. ProjectBatch may lazily
  /// initialize backend cache; do not share one context across concurrent calls.
  static Result<ProjectionContext> Make(const Schema& input_schema,
                                        const Schema& output_schema,
                                        ProjectBatchFunction project_batch_function);

  ProjectionContext(ProjectionContext&&) noexcept;
  ProjectionContext& operator=(ProjectionContext&&) noexcept;
  ~ProjectionContext();

  ProjectionContext(const ProjectionContext&) = delete;
  ProjectionContext& operator=(const ProjectionContext&) = delete;

  const Schema& input_schema() const;

  const Schema& output_schema() const;

  const ArrowSchema& input_arrow_schema() const;

  const ArrowSchema& output_arrow_schema() const;

  std::span<const int32_t> selected_field_indices() const;

  ProjectBatchFunction project_batch_function() const;

  ProjectBatchState& project_batch_state();

 private:
  ProjectionContext() = default;

  // Raw schema pointers are borrowed from caller-owned schemas. FileScanTaskReader
  // keeps those schema objects alive in the same stream source that owns this context.
  const Schema* input_schema_ = nullptr;
  const Schema* output_schema_ = nullptr;
  std::vector<int32_t> selected_field_indices_;
  ArrowSchema input_arrow_schema_{};
  ArrowSchema output_arrow_schema_{};
  ProjectBatchFunction project_batch_function_ = nullptr;
  ProjectBatchState project_batch_state_;
};

/// \brief Concept for sources that can be wrapped as ArrowArrayStreams.
template <typename Source>
concept ArrowArrayStreamProvider = requires(Source& source) {
  { source.Close() } -> std::same_as<Status>;
  { source.Next() } -> std::same_as<Result<std::optional<ArrowArray>>>;
  { source.Schema() } -> std::same_as<Result<ArrowSchema>>;
};

namespace detail {

template <ArrowArrayStreamProvider Source>
struct ArrowArrayStreamPrivateData {
  std::unique_ptr<Source> source;
  std::string last_error;

  explicit ArrowArrayStreamPrivateData(std::unique_ptr<Source> src)
      : source(std::move(src)) {}

  ~ArrowArrayStreamPrivateData() {
    if (source != nullptr) {
      std::ignore = source->Close();
    }
  }
};

template <ArrowArrayStreamProvider Source>
int GetSchema(struct ArrowArrayStream* stream, struct ArrowSchema* out) {
  if (stream == nullptr || stream->private_data == nullptr) {
    return EINVAL;
  }

  auto* private_data =
      static_cast<ArrowArrayStreamPrivateData<Source>*>(stream->private_data);
  auto schema_result = private_data->source->Schema();
  if (!schema_result.has_value()) {
    private_data->last_error = schema_result.error().message;
    std::memset(out, 0, sizeof(ArrowSchema));
    return EIO;
  }

  *out = std::move(schema_result.value());
  return 0;
}

template <ArrowArrayStreamProvider Source>
int GetNext(struct ArrowArrayStream* stream, struct ArrowArray* out) {
  if (stream == nullptr || stream->private_data == nullptr) {
    return EINVAL;
  }

  auto* private_data =
      static_cast<ArrowArrayStreamPrivateData<Source>*>(stream->private_data);
  auto next_result = private_data->source->Next();
  if (!next_result.has_value()) {
    private_data->last_error = next_result.error().message;
    std::memset(out, 0, sizeof(ArrowArray));
    return EIO;
  }

  auto& optional_array = next_result.value();
  if (optional_array.has_value()) {
    *out = std::move(optional_array.value());
  } else {
    std::memset(out, 0, sizeof(ArrowArray));
  }

  return 0;
}

template <ArrowArrayStreamProvider Source>
const char* GetLastError(struct ArrowArrayStream* stream) {
  if (stream == nullptr || stream->private_data == nullptr) {
    return nullptr;
  }

  auto* private_data =
      static_cast<ArrowArrayStreamPrivateData<Source>*>(stream->private_data);
  return private_data->last_error.empty() ? nullptr : private_data->last_error.c_str();
}

template <ArrowArrayStreamProvider Source>
void Release(struct ArrowArrayStream* stream) {
  if (stream == nullptr || stream->private_data == nullptr) {
    return;
  }

  delete static_cast<ArrowArrayStreamPrivateData<Source>*>(stream->private_data);
  stream->private_data = nullptr;
  stream->release = nullptr;
}

}  // namespace detail

/// \brief Wrap an object with Close, Next, and Schema as an ArrowArrayStream.
template <ArrowArrayStreamProvider Source>
Result<ArrowArrayStream> MakeArrowArrayStream(std::unique_ptr<Source> source) {
  if (source == nullptr) {
    return InvalidArgument("Cannot make ArrowArrayStream from null source");
  }

  auto private_data =
      std::make_unique<detail::ArrowArrayStreamPrivateData<Source>>(std::move(source));
  ArrowArrayStream stream{.get_schema = detail::GetSchema<Source>,
                          .get_next = detail::GetNext<Source>,
                          .get_last_error = detail::GetLastError<Source>,
                          .release = detail::Release<Source>,
                          .private_data = private_data.release()};
  return stream;
}

/// \brief Wrap a Reader as an ArrowArrayStream.
ICEBERG_EXPORT Result<ArrowArrayStream> MakeArrowArrayStream(
    std::unique_ptr<Reader> reader);

/// \brief Project selected rows from a batch into complete top-level fields.
///
/// `input_batch` is consumed by this function. If the projection carries a registered
/// implementation, the call is delegated to it; otherwise the built-in nanoarrow
/// implementation is used. The projection must have been created for the stable schema
/// pair that describes the input batch and requested output. This function does not
/// revalidate schema compatibility on each batch.
///
/// \param input_batch Owned Arrow C Data batch to project.
/// \param row_indices Zero-based row positions to copy from `input_batch`.
/// \param projection Reusable schema/projection state created by
///   ProjectionContext::Make.
/// \return A newly owned ArrowArray matching `projection.output_schema()`.
ICEBERG_EXPORT Result<ArrowArray> ProjectBatch(ArrowArray* input_batch,
                                               std::span<const int32_t> row_indices,
                                               ProjectionContext& projection);

}  // namespace iceberg
