/*
 * 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 "paimon/format/avro/avro_file_batch_reader.h"

#include <limits>
#include <memory>
#include <utility>

#include "arrow/c/bridge.h"
#include "fmt/format.h"
#include "paimon/common/metrics/metrics_impl.h"
#include "paimon/common/utils/arrow/arrow_utils.h"
#include "paimon/common/utils/arrow/mem_utils.h"
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/scope_guard.h"
#include "paimon/core/utils/nested_projection_utils.h"
#include "paimon/format/avro/avro_input_stream_impl.h"
#include "paimon/format/avro/avro_schema_converter.h"
#include "paimon/reader/batch_reader.h"

namespace paimon::avro {

AvroFileBatchReader::AvroFileBatchReader(const std::shared_ptr<InputStream>& input_stream,
                                         const std::shared_ptr<::arrow::DataType>& file_data_type,
                                         std::unique_ptr<::avro::DataFileReaderBase>&& reader,
                                         std::unique_ptr<arrow::ArrayBuilder>&& array_builder,
                                         std::unique_ptr<arrow::MemoryPool>&& arrow_pool,
                                         int32_t batch_size,
                                         const std::shared_ptr<MemoryPool>& pool)
    : pool_(pool),
      arrow_pool_(std::move(arrow_pool)),
      input_stream_(input_stream),
      file_data_type_(file_data_type),
      reader_(std::move(reader)),
      array_builder_(std::move(array_builder)),
      batch_size_(batch_size),
      metrics_(std::make_shared<MetricsImpl>()) {}

AvroFileBatchReader::~AvroFileBatchReader() {
    DoClose();
}

void AvroFileBatchReader::DoClose() {
    if (!close_) {
        reader_->close();
        close_ = true;
    }
}

Result<std::unique_ptr<AvroFileBatchReader>> AvroFileBatchReader::Create(
    const std::shared_ptr<InputStream>& input_stream, int32_t batch_size,
    const std::shared_ptr<MemoryPool>& pool) {
    if (batch_size <= 0) {
        return Status::Invalid(
            fmt::format("invalid batch size {}, must be larger than 0", batch_size));
    }
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<::avro::DataFileReaderBase> reader,
                           CreateDataFileReader(input_stream, pool));
    const auto& avro_file_schema = reader->dataSchema();
    PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<::arrow::DataType> file_data_type,
                           AvroSchemaConverter::AvroSchemaToArrowDataType(avro_file_schema));
    auto arrow_pool = GetArrowPool(pool);
    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::unique_ptr<arrow::ArrayBuilder> array_builder,
                                      arrow::MakeBuilder(file_data_type, arrow_pool.get()));
    return std::unique_ptr<AvroFileBatchReader>(
        new AvroFileBatchReader(input_stream, file_data_type, std::move(reader),
                                std::move(array_builder), std::move(arrow_pool), batch_size, pool));
}

Result<std::unique_ptr<::avro::DataFileReaderBase>> AvroFileBatchReader::CreateDataFileReader(
    const std::shared_ptr<InputStream>& input_stream, const std::shared_ptr<MemoryPool>& pool) {
    PAIMON_RETURN_NOT_OK(input_stream->Seek(0, SeekOrigin::FS_SEEK_SET));
    try {
        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<::avro::InputStream> in,
                               AvroInputStreamImpl::Create(input_stream, BUFFER_SIZE, pool));
        auto reader = std::make_unique<::avro::DataFileReaderBase>(std::move(in));
        reader->init();
        return reader;
    } catch (const ::avro::Exception& e) {
        return Status::Invalid(fmt::format("build avro reader failed. {}", e.what()));
    } catch (const std::exception& e) {
        return Status::Invalid(fmt::format("build avro reader failed. {}", e.what()));
    } catch (...) {
        return Status::Invalid("build avro reader failed. unknown error");
    }
}

Result<BatchReader::ReadBatch> AvroFileBatchReader::NextBatch() {
    if (next_row_to_read_ == std::numeric_limits<uint64_t>::max()) {
        next_row_to_read_ = 0;
    }
    try {
        while (array_builder_->length() < batch_size_) {
            if (!reader_->hasMore()) {
                break;
            }
            reader_->decr();
            PAIMON_RETURN_NOT_OK(AvroDirectDecoder::DecodeAvroToBuilder(
                reader_->dataSchema().root(), read_fields_projection_, &reader_->decoder(),
                array_builder_.get(), &decode_context_));
        }
        previous_first_row_ = next_row_to_read_;
        next_row_to_read_ += array_builder_->length();
        if (array_builder_->length() == 0) {
            previous_batch_row_count_ = 0;
            return BatchReader::MakeEofBatch();
        }
        PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
                                          array_builder_->Finish());
        PAIMON_RETURN_NOT_OK_FROM_ARROW(array->Validate());
        std::unique_ptr<ArrowArray> c_array = std::make_unique<ArrowArray>();
        std::unique_ptr<ArrowSchema> c_schema = std::make_unique<ArrowSchema>();
        PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, c_array.get(), c_schema.get()));
        previous_batch_row_count_ = c_array->length;
        return make_pair(std::move(c_array), std::move(c_schema));
    } catch (const ::avro::Exception& e) {
        return Status::Invalid(fmt::format("avro reader next batch failed. {}", e.what()));
    } catch (const std::exception& e) {
        return Status::Invalid(fmt::format("avro reader next batch failed. {}", e.what()));
    } catch (...) {
        return Status::Invalid("avro reader next batch failed. unknown error");
    }
}

Status AvroFileBatchReader::SetReadSchema(::ArrowSchema* read_schema,
                                          const std::shared_ptr<Predicate>& predicate,
                                          const std::optional<RoaringBitmap32>& selection_bitmap) {
    if (!read_schema) {
        return Status::Invalid("SetReadSchema failed: read schema cannot be nullptr");
    }
    // TODO(menglingda.mld): support predicate
    if (selection_bitmap) {
        // TODO(menglingda.mld): support bitmap
    }
    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Schema> arrow_read_schema,
                                      arrow::ImportSchema(read_schema));
    PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Schema> file_schema,
                           ArrowUtils::DataTypeToSchema(file_data_type_));
    PAIMON_ASSIGN_OR_RAISE(
        bool has_nested_projection,
        NestedProjectionUtils::HasNestedSubfieldProjection(file_schema, arrow_read_schema));
    if (has_nested_projection) {
        return Status::Invalid(
            "SetReadSchema failed: avro reader does not support nested sub-field projection");
    }
    PAIMON_ASSIGN_OR_RAISE(read_fields_projection_,
                           CalculateReadFieldsProjection(file_schema, arrow_read_schema->fields()));
    std::shared_ptr<::arrow::DataType> read_data_type = arrow::struct_(arrow_read_schema->fields());
    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::unique_ptr<arrow::ArrayBuilder> array_builder,
                                      arrow::MakeBuilder(read_data_type, arrow_pool_.get()));
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<::avro::DataFileReaderBase> reader,
                           CreateDataFileReader(input_stream_, pool_));

    if (reader_) {
        reader_->close();
    }
    reader_ = std::move(reader);
    array_builder_ = std::move(array_builder);
    previous_first_row_ = std::numeric_limits<uint64_t>::max();
    previous_batch_row_count_ = 0;
    next_row_to_read_ = std::numeric_limits<uint64_t>::max();
    close_ = false;
    return Status::OK();
}

Result<std::set<size_t>> AvroFileBatchReader::CalculateReadFieldsProjection(
    const std::shared_ptr<::arrow::Schema>& file_schema, const arrow::FieldVector& read_fields) {
    std::set<size_t> projection_set;
    PAIMON_ASSIGN_OR_RAISE(std::vector<int32_t> projection,
                           ArrowUtils::CreateProjection(file_schema, read_fields));
    int32_t prev_index = -1;
    for (auto& index : projection) {
        if (index <= prev_index) {
            return Status::Invalid(
                "SetReadSchema failed: read schema fields order is different from file schema");
        }
        prev_index = index;
        projection_set.insert(index);
    }
    return projection_set;
}

Result<std::unique_ptr<::ArrowSchema>> AvroFileBatchReader::GetFileSchema() const {
    assert(reader_);
    auto c_schema = std::make_unique<::ArrowSchema>();
    PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportType(*file_data_type_, c_schema.get()));
    return c_schema;
}

Result<uint64_t> AvroFileBatchReader::GetNumberOfRows() const {
    if (!total_rows_) {
        PAIMON_ASSIGN_OR_RAISE(int64_t current_pos, input_stream_->GetPos());
        ScopeGuard stream_guard([this, current_pos]() -> void {
            // reset input stream position to original position
            Status status = input_stream_->Seek(current_pos, SeekOrigin::FS_SEEK_SET);
            (void)status;
        });
        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<::avro::DataFileReaderBase> reader,
                               CreateDataFileReader(input_stream_, pool_));
        ScopeGuard reader_guard([&reader]() -> void { reader->close(); });
        try {
            while (reader->hasMore()) {
                reader->decr();
                total_rows_ = total_rows_.value_or(0) + 1;
            }
        } catch (const ::avro::Exception& e) {
            return Status::Invalid(fmt::format("avro reader GetNumberOfRows failed. {}", e.what()));
        } catch (const std::exception& e) {
            return Status::Invalid(fmt::format("avro reader GetNumberOfRows failed. {}", e.what()));
        } catch (...) {
            return Status::Invalid("avro reader GetNumberOfRows failed. unknown error");
        }
    }
    return *total_rows_;
}

}  // namespace paimon::avro
