/*
 * 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/common/utils/arrow/arrow_utils.h"

#include "arrow/array/array_base.h"
#include "arrow/array/array_nested.h"
#include "arrow/array/concatenate.h"
#include "arrow/util/compression.h"
#include "fmt/format.h"
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/string_utils.h"

namespace paimon {

namespace {

bool HasNonZeroOffset(const std::shared_ptr<arrow::ArrayData>& data) {
    if (data->offset != 0) {
        return true;
    }
    for (const auto& child : data->child_data) {
        if (HasNonZeroOffset(child)) {
            return true;
        }
    }
    return false;
}

}  // namespace

const char* ArrowUtils::kArrowSchemaMetadataKey = "ARROW:schema";

Result<std::shared_ptr<arrow::Schema>> ArrowUtils::DataTypeToSchema(
    const std::shared_ptr<arrow::DataType>& data_type) {
    if (data_type->id() != arrow::Type::STRUCT) {
        return Status::Invalid(
            fmt::format("Expected struct data type, actual data type: {}", data_type->ToString()));
    }
    const auto& struct_type = std::static_pointer_cast<arrow::StructType>(data_type);
    return std::make_shared<arrow::Schema>(struct_type->fields());
}

Result<std::vector<int32_t>> ArrowUtils::CreateProjection(
    const std::shared_ptr<arrow::Schema>& src_schema, const arrow::FieldVector& target_fields) {
    std::vector<int32_t> target_to_src_mapping;
    target_to_src_mapping.reserve(target_fields.size());
    for (const auto& field : target_fields) {
        auto src_field_idx = src_schema->GetFieldIndex(field->name());
        if (src_field_idx < 0) {
            return Status::Invalid(
                fmt::format("Field '{}' not found or duplicate in src schema", field->name()));
        }
        target_to_src_mapping.push_back(src_field_idx);
    }
    return target_to_src_mapping;
}

Status ArrowUtils::CheckNullabilityMatch(const std::shared_ptr<arrow::Schema>& schema,
                                         const std::shared_ptr<arrow::Array>& data) {
    auto struct_array = arrow::internal::checked_pointer_cast<arrow::StructArray>(data);
    if (struct_array->num_fields() != schema->num_fields()) {
        return Status::Invalid(fmt::format(
            "CheckNullabilityMatch failed, data field count {} mismatch schema field count {}",
            struct_array->num_fields(), schema->num_fields()));
    }
    for (int32_t i = 0; i < schema->num_fields(); i++) {
        PAIMON_RETURN_NOT_OK(InnerCheckNullabilityMatch(schema->field(i), struct_array->field(i)));
    }
    return Status::OK();
}

void ArrowUtils::TraverseArray(const std::shared_ptr<arrow::Array>& array) {
    arrow::Type::type type = array->type()->id();
    switch (type) {
        case arrow::Type::type::DICTIONARY: {
            auto* dict_array = arrow::internal::checked_cast<arrow::DictionaryArray*>(array.get());
            [[maybe_unused]] auto dict = dict_array->dictionary();
            return;
        }
        case arrow::Type::type::STRUCT: {
            auto* struct_array = arrow::internal::checked_cast<arrow::StructArray*>(array.get());
            for (const auto& field : struct_array->fields()) {
                TraverseArray(field);
            }
            return;
        }
        case arrow::Type::type::MAP: {
            auto* map_array = arrow::internal::checked_cast<arrow::MapArray*>(array.get());
            TraverseArray(map_array->keys());
            TraverseArray(map_array->items());
            return;
        }
        case arrow::Type::type::LIST: {
            auto* list_array = arrow::internal::checked_cast<arrow::ListArray*>(array.get());
            TraverseArray(list_array->values());
            return;
        }
        default:
            return;
    }
}

bool ArrowUtils::EqualsIgnoreNullable(const std::shared_ptr<arrow::DataType>& type,
                                      const std::shared_ptr<arrow::DataType>& other_type) {
    if (type->id() != other_type->id() || type->num_fields() != other_type->num_fields()) {
        return false;
    }
    for (int32_t i = 0; i < type->num_fields(); ++i) {
        const auto& field = type->field(i);
        const auto& other_field = other_type->field(i);
        if (field->name() != other_field->name()) {
            return false;
        }
        if (!EqualsIgnoreNullable(field->type(), other_field->type())) {
            return false;
        }
    }
    return true;
}

Status ArrowUtils::InnerCheckNullabilityMatch(const std::shared_ptr<arrow::Field>& field,
                                              const std::shared_ptr<arrow::Array>& data) {
    if (PAIMON_UNLIKELY(!field->nullable() && data->null_count() != 0)) {
        return Status::Invalid(fmt::format(
            "CheckNullabilityMatch failed, field {} not nullable while data have null value",
            field->name()));
    }
    auto type = field->type();
    if (type->id() == arrow::Type::STRUCT) {
        auto struct_type = arrow::internal::checked_pointer_cast<arrow::StructType>(field->type());
        auto struct_array = arrow::internal::checked_pointer_cast<arrow::StructArray>(data);
        for (int32_t i = 0; i < struct_type->num_fields(); ++i) {
            PAIMON_RETURN_NOT_OK(
                InnerCheckNullabilityMatch(struct_type->field(i), struct_array->field(i)));
        }
    } else if (type->id() == arrow::Type::LIST) {
        auto list_type = arrow::internal::checked_pointer_cast<arrow::ListType>(field->type());
        auto list_array = arrow::internal::checked_pointer_cast<arrow::ListArray>(data);
        PAIMON_RETURN_NOT_OK(
            InnerCheckNullabilityMatch(list_type->value_field(), list_array->values()));
    } else if (type->id() == arrow::Type::MAP) {
        auto map_type = arrow::internal::checked_pointer_cast<arrow::MapType>(field->type());
        auto map_array = arrow::internal::checked_pointer_cast<arrow::MapArray>(data);
        PAIMON_RETURN_NOT_OK(InnerCheckNullabilityMatch(map_type->key_field(), map_array->keys()));
        PAIMON_RETURN_NOT_OK(
            InnerCheckNullabilityMatch(map_type->item_field(), map_array->items()));
    }
    return Status::OK();
}

Result<std::shared_ptr<arrow::StructArray>> ArrowUtils::RemoveFieldFromStructArray(
    const std::shared_ptr<arrow::StructArray>& struct_array, const std::string& field_name) {
    auto struct_type = std::static_pointer_cast<arrow::StructType>(struct_array->type());
    int32_t field_idx = struct_type->GetFieldIndex(field_name);
    if (field_idx == -1) {
        return struct_array;
    }
    std::vector<std::shared_ptr<arrow::Array>> new_arrays;
    std::vector<std::shared_ptr<arrow::Field>> new_fields;
    for (int32_t i = 0; i < struct_type->num_fields(); ++i) {
        if (i != field_idx) {
            new_arrays.emplace_back(struct_array->field(i));
            new_fields.emplace_back(struct_type->field(i));
        }
    }
    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
        std::shared_ptr<arrow::StructArray> array,
        arrow::StructArray::Make(new_arrays, new_fields, struct_array->null_bitmap(),
                                 struct_array->null_count(), struct_array->offset()));
    return array;
}

Result<std::shared_ptr<arrow::RecordBatch>> ArrowUtils::NormalizeRecordBatchOffsets(
    const std::shared_ptr<arrow::RecordBatch>& record_batch, arrow::MemoryPool* pool) {
    arrow::ArrayVector normalized_columns;
    for (int32_t i = 0; i < record_batch->num_columns(); ++i) {
        const std::shared_ptr<arrow::Array>& column = record_batch->column(i);
        if (!HasNonZeroOffset(column->data())) {
            continue;
        }
        if (normalized_columns.empty()) {
            normalized_columns = record_batch->columns();
        }
        PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> normalized_column,
                                          arrow::Concatenate({column}, pool));
        normalized_columns[i] = std::move(normalized_column);
    }
    if (normalized_columns.empty()) {
        return record_batch;
    }
    return arrow::RecordBatch::Make(record_batch->schema(), record_batch->num_rows(),
                                    std::move(normalized_columns));
}

Result<arrow::Compression::type> ArrowUtils::GetCompressionType(const std::string& compression) {
    std::string normalized = StringUtils::ToLowerCase(compression);
    if (normalized.empty() || normalized == "none") {
        normalized = "uncompressed";
    }
    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(arrow::Compression::type compression_type,
                                      arrow::util::Codec::GetCompressionType(normalized));
    return compression_type;
}

}  // namespace paimon
