/*
 * 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 <cstdint>
#include <limits>
#include <memory>
#include <mutex>
#include <span>
#include <utility>
#include <vector>

#include <nanoarrow/nanoarrow.h>

#include "iceberg/arrow/nanoarrow_status_internal.h"
#include "iceberg/arrow_c_data_guard_internal.h"
#include "iceberg/arrow_c_data_util_internal.h"
#include "iceberg/file_reader.h"
#include "iceberg/result.h"
#include "iceberg/schema.h"
#include "iceberg/schema_field.h"
#include "iceberg/schema_internal.h"
#include "iceberg/util/macros.h"

namespace iceberg {

Result<ArrowArrayStream> MakeArrowArrayStream(std::unique_ptr<Reader> reader) {
  return MakeArrowArrayStream<Reader>(std::move(reader));
}

namespace {

Result<size_t> FindFieldIndexById(std::span<const SchemaField> fields, int32_t field_id) {
  for (size_t index = 0; index < fields.size(); ++index) {
    if (fields[index].field_id() == field_id) {
      return index;
    }
  }
  return InvalidArgument("Required schema does not contain projected field id {}",
                         field_id);
}

std::mutex g_project_batch_function_mutex;
ProjectionContext::ProjectBatchFunction g_project_batch_function = nullptr;

ProjectionContext::ProjectBatchFunction GetProjectBatchFunction() {
  std::lock_guard lock(g_project_batch_function_mutex);
  return g_project_batch_function;
}

Result<std::vector<int32_t>> BuildSelectedFieldIndices(
    std::span<const SchemaField> input_fields,
    std::span<const SchemaField> output_fields) {
  std::vector<int32_t> selected_field_indices;
  selected_field_indices.reserve(output_fields.size());

  for (const auto& output_field : output_fields) {
    ICEBERG_ASSIGN_OR_RAISE(auto input_index,
                            FindFieldIndexById(input_fields, output_field.field_id()));
    const auto& input_field = input_fields[input_index];
    if (*input_field.type() != *output_field.type()) {
      return InvalidArgument(
          "ProjectBatch only supports complete top-level fields, but field id "
          "{} changes type from {} to {}",
          output_field.field_id(), input_field.type()->ToString(),
          output_field.type()->ToString());
    }
    ICEBERG_PRECHECK(
        input_index <= static_cast<size_t>(std::numeric_limits<int32_t>::max()),
        "Input field index {} exceeds int32 range", input_index);
    selected_field_indices.push_back(static_cast<int32_t>(input_index));
  }

  return selected_field_indices;
}

Status AppendValue(const ArrowSchema& input_schema, const ArrowArray& input_array,
                   const ArrowArrayView& input_view, int64_t row_index,
                   ArrowArray* output_array);

Status AppendListValues(const ArrowSchema& input_schema, const ArrowArray& input_array,
                        const ArrowArrayView& input_view, int64_t row_index,
                        ArrowArray* output_array) {
  const int64_t begin = ArrowArrayViewListChildOffset(&input_view, row_index);
  const int64_t end = ArrowArrayViewListChildOffset(&input_view, row_index + 1);
  for (int64_t element_index = begin; element_index < end; ++element_index) {
    ICEBERG_RETURN_UNEXPECTED(
        AppendValue(*input_schema.children[0], *input_array.children[0],
                    *input_view.children[0], element_index, output_array->children[0]));
  }
  ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayFinishElement(output_array));
  return {};
}

Status AppendMapValues(const ArrowSchema& input_schema, const ArrowArray& input_array,
                       const ArrowArrayView& input_view, int64_t row_index,
                       ArrowArray* output_array) {
  const int64_t begin = ArrowArrayViewListChildOffset(&input_view, row_index);
  const int64_t end = ArrowArrayViewListChildOffset(&input_view, row_index + 1);
  for (int64_t entry_index = begin; entry_index < end; ++entry_index) {
    ICEBERG_RETURN_UNEXPECTED(
        AppendValue(*input_schema.children[0], *input_array.children[0],
                    *input_view.children[0], entry_index, output_array->children[0]));
  }
  ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayFinishElement(output_array));
  return {};
}

Status AppendDecimal(const ArrowSchema& input_schema, const ArrowArrayView& input_view,
                     int64_t row_index, ArrowArray* output_array) {
  ArrowError error;
  ArrowSchemaView schema_view;
  ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR(
      ArrowSchemaViewInit(&schema_view, &input_schema, &error), error);

  ArrowDecimal value;
  ArrowDecimalInit(&value, schema_view.decimal_bitwidth, schema_view.decimal_precision,
                   schema_view.decimal_scale);
  ArrowArrayViewGetDecimalUnsafe(&input_view, row_index, &value);
  ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayAppendDecimal(output_array, &value));
  return {};
}

Status AppendValue(const ArrowSchema& input_schema, const ArrowArray& input_array,
                   const ArrowArrayView& input_view, int64_t row_index,
                   ArrowArray* output_array) {
  if (ArrowArrayViewIsNull(&input_view, row_index)) {
    ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayAppendNull(output_array, 1));
    return {};
  }

  switch (input_view.storage_type) {
    case NANOARROW_TYPE_NA:
      ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayAppendNull(output_array, 1));
      return {};
    case NANOARROW_TYPE_BOOL:
    case NANOARROW_TYPE_INT8:
    case NANOARROW_TYPE_INT16:
    case NANOARROW_TYPE_INT32:
    case NANOARROW_TYPE_INT64:
    case NANOARROW_TYPE_DATE32:
    case NANOARROW_TYPE_DATE64:
    case NANOARROW_TYPE_TIME32:
    case NANOARROW_TYPE_TIME64:
    case NANOARROW_TYPE_TIMESTAMP:
    case NANOARROW_TYPE_DURATION:
      ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayAppendInt(
          output_array, ArrowArrayViewGetIntUnsafe(&input_view, row_index)));
      return {};
    case NANOARROW_TYPE_UINT8:
    case NANOARROW_TYPE_UINT16:
    case NANOARROW_TYPE_UINT32:
    case NANOARROW_TYPE_UINT64:
      ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayAppendUInt(
          output_array, ArrowArrayViewGetUIntUnsafe(&input_view, row_index)));
      return {};
    case NANOARROW_TYPE_HALF_FLOAT:
    case NANOARROW_TYPE_FLOAT:
    case NANOARROW_TYPE_DOUBLE:
      ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayAppendDouble(
          output_array, ArrowArrayViewGetDoubleUnsafe(&input_view, row_index)));
      return {};
    case NANOARROW_TYPE_STRING:
    case NANOARROW_TYPE_LARGE_STRING:
    case NANOARROW_TYPE_STRING_VIEW: {
      auto value = ArrowArrayViewGetStringUnsafe(&input_view, row_index);
      ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayAppendString(output_array, value));
      return {};
    }
    case NANOARROW_TYPE_BINARY:
    case NANOARROW_TYPE_LARGE_BINARY:
    case NANOARROW_TYPE_FIXED_SIZE_BINARY:
    case NANOARROW_TYPE_BINARY_VIEW: {
      auto value = ArrowArrayViewGetBytesUnsafe(&input_view, row_index);
      ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayAppendBytes(output_array, value));
      return {};
    }
    case NANOARROW_TYPE_DECIMAL128:
    case NANOARROW_TYPE_DECIMAL256:
      return AppendDecimal(input_schema, input_view, row_index, output_array);
    case NANOARROW_TYPE_STRUCT: {
      for (int64_t child_index = 0; child_index < input_schema.n_children;
           ++child_index) {
        ICEBERG_RETURN_UNEXPECTED(AppendValue(
            *input_schema.children[child_index], *input_array.children[child_index],
            *input_view.children[child_index], row_index,
            output_array->children[child_index]));
      }
      ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayFinishElement(output_array));
      return {};
    }
    case NANOARROW_TYPE_LIST:
    case NANOARROW_TYPE_LARGE_LIST:
    case NANOARROW_TYPE_FIXED_SIZE_LIST:
      return AppendListValues(input_schema, input_array, input_view, row_index,
                              output_array);
    case NANOARROW_TYPE_MAP:
      return AppendMapValues(input_schema, input_array, input_view, row_index,
                             output_array);
    default:
      return NotImplemented("Unsupported Arrow type for merge-on-read projection: {}",
                            static_cast<int>(input_view.storage_type));
  }
}

}  // namespace

ProjectionContext::ProjectionContext(ProjectionContext&& other) noexcept
    : input_schema_(std::exchange(other.input_schema_, nullptr)),
      output_schema_(std::exchange(other.output_schema_, nullptr)),
      selected_field_indices_(std::move(other.selected_field_indices_)),
      input_arrow_schema_(other.input_arrow_schema_),
      output_arrow_schema_(other.output_arrow_schema_),
      project_batch_function_(std::exchange(other.project_batch_function_, nullptr)),
      project_batch_state_(std::move(other.project_batch_state_)) {
  other.input_arrow_schema_.release = nullptr;
  other.output_arrow_schema_.release = nullptr;
}

ProjectionContext& ProjectionContext::operator=(ProjectionContext&& other) noexcept {
  if (this == &other) {
    return *this;
  }

  if (input_arrow_schema_.release != nullptr) {
    input_arrow_schema_.release(&input_arrow_schema_);
  }
  if (output_arrow_schema_.release != nullptr) {
    output_arrow_schema_.release(&output_arrow_schema_);
  }

  input_schema_ = std::exchange(other.input_schema_, nullptr);
  output_schema_ = std::exchange(other.output_schema_, nullptr);
  selected_field_indices_ = std::move(other.selected_field_indices_);
  input_arrow_schema_ = other.input_arrow_schema_;
  other.input_arrow_schema_.release = nullptr;
  output_arrow_schema_ = other.output_arrow_schema_;
  other.output_arrow_schema_.release = nullptr;
  project_batch_function_ = std::exchange(other.project_batch_function_, nullptr);
  project_batch_state_ = std::move(other.project_batch_state_);
  return *this;
}

ProjectionContext::~ProjectionContext() {
  if (input_arrow_schema_.release != nullptr) {
    input_arrow_schema_.release(&input_arrow_schema_);
  }
  if (output_arrow_schema_.release != nullptr) {
    output_arrow_schema_.release(&output_arrow_schema_);
  }
}

Result<ProjectionContext> ProjectionContext::Make(
    const Schema& input_schema, const Schema& output_schema,
    ProjectionContext::ProjectBatchFunction project_batch_function) {
  ICEBERG_ASSIGN_OR_RAISE(
      auto selected_field_indices,
      BuildSelectedFieldIndices(input_schema.fields(), output_schema.fields()));

  ProjectionContext context;
  context.input_schema_ = &input_schema;
  context.output_schema_ = &output_schema;
  context.selected_field_indices_ = std::move(selected_field_indices);
  ICEBERG_RETURN_UNEXPECTED(ToArrowSchema(input_schema, &context.input_arrow_schema_));
  ICEBERG_RETURN_UNEXPECTED(ToArrowSchema(output_schema, &context.output_arrow_schema_));
  context.project_batch_function_ = project_batch_function;

  return context;
}

const Schema& ProjectionContext::input_schema() const { return *input_schema_; }

const Schema& ProjectionContext::output_schema() const { return *output_schema_; }

const ArrowSchema& ProjectionContext::input_arrow_schema() const {
  return input_arrow_schema_;
}

const ArrowSchema& ProjectionContext::output_arrow_schema() const {
  return output_arrow_schema_;
}

std::span<const int32_t> ProjectionContext::selected_field_indices() const {
  return selected_field_indices_;
}

ProjectionContext::ProjectBatchFunction ProjectionContext::project_batch_function()
    const {
  return project_batch_function_;
}

ProjectionContext::ProjectBatchState& ProjectionContext::project_batch_state() {
  return project_batch_state_;
}

void ProjectionContext::RegisterProjectBatchFunction(
    ProjectionContext::ProjectBatchFunction project_batch_function) {
  ICEBERG_DCHECK(project_batch_function != nullptr,
                 "ProjectBatch implementation must not be null");
  if (project_batch_function == nullptr) {
    return;
  }
  std::lock_guard lock(g_project_batch_function_mutex);
  g_project_batch_function = project_batch_function;
}

bool ProjectionContext::HasProjectBatchFunction() {
  return GetProjectBatchFunction() != nullptr;
}

auto ProjectionContext::ResolveProjectBatchFunction()
    -> ProjectionContext::ProjectBatchFunction {
  return GetProjectBatchFunction();
}

namespace {

Result<ArrowArray> ProjectBatchNanoarrow(
    const ArrowSchema& input_arrow_schema, const ArrowArray& input_batch,
    std::span<const int32_t> row_indices, const ArrowSchema& output_arrow_schema,
    std::span<const int32_t> selected_field_indices) {
  ArrowArrayView input_view;
  ArrowError error;
  ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR(
      ArrowArrayViewInitFromSchema(&input_view, &input_arrow_schema, &error), error);
  internal::ArrowArrayViewGuard input_view_guard(&input_view);
  ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR(
      ArrowArrayViewSetArray(&input_view, &input_batch, &error), error);
  ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR(
      ArrowArrayViewValidate(&input_view, NANOARROW_VALIDATION_LEVEL_DEFAULT, &error),
      error);

  ArrowArray output_array;
  ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR(
      ArrowArrayInitFromSchema(&output_array, &output_arrow_schema, &error), error);
  internal::ArrowArrayGuard output_array_guard(&output_array);
  ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayStartAppending(&output_array));
  ICEBERG_NANOARROW_RETURN_UNEXPECTED(
      ArrowArrayReserve(&output_array, static_cast<int64_t>(row_indices.size())));

  for (int64_t row_index : row_indices) {
    ICEBERG_PRECHECK(row_index >= 0 && row_index < input_batch.length,
                     "Row index {} out of range for batch length {}", row_index,
                     input_batch.length);
    for (size_t output_index = 0; output_index < selected_field_indices.size();
         ++output_index) {
      const int32_t input_index = selected_field_indices[output_index];
      ICEBERG_RETURN_UNEXPECTED(AppendValue(*input_arrow_schema.children[input_index],
                                            *input_batch.children[input_index],
                                            *input_view.children[input_index], row_index,
                                            output_array.children[output_index]));
    }
    ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayFinishElement(&output_array));
  }

  ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR(
      ArrowArrayFinishBuildingDefault(&output_array, &error), error);

  return std::exchange(output_array, ArrowArray{});
}

}  // namespace

Result<ArrowArray> ProjectBatch(ArrowArray* input_batch,
                                std::span<const int32_t> row_indices,
                                ProjectionContext& projection) {
  ICEBERG_PRECHECK(input_batch != nullptr, "input_batch must not be null");
  internal::ArrowArrayGuard input_batch_guard(input_batch);

  auto project_batch_function = projection.project_batch_function();
  if (project_batch_function != nullptr) {
    // ProjectBatch owns input_batch. Arrow-backed implementations import it and clear
    // release, so input_batch_guard becomes a no-op instead of double-releasing.
    return project_batch_function(input_batch, row_indices, projection);
  }

  return ProjectBatchNanoarrow(projection.input_arrow_schema(), *input_batch, row_indices,
                               projection.output_arrow_schema(),
                               projection.selected_field_indices());
}

}  // namespace iceberg
