/*
 * 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/table/source/table_scan.h"

#include <map>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "fmt/format.h"
#include "paimon/common/predicate/predicate_validator.h"
#include "paimon/common/types/data_field.h"
#include "paimon/common/utils/fields_comparator.h"
#include "paimon/common/utils/options_utils.h"
#include "paimon/core/core_options.h"
#include "paimon/core/index/index_file_handler.h"
#include "paimon/core/manifest/index_manifest_file.h"
#include "paimon/core/manifest/manifest_file.h"
#include "paimon/core/manifest/manifest_list.h"
#include "paimon/core/operation/append_only_file_store_scan.h"
#include "paimon/core/operation/data_evolution_file_store_scan.h"
#include "paimon/core/operation/file_store_scan.h"
#include "paimon/core/operation/key_value_file_store_scan.h"
#include "paimon/core/schema/schema_manager.h"
#include "paimon/core/schema/schema_validation.h"
#include "paimon/core/schema/table_schema.h"
#include "paimon/core/table/bucket_mode.h"
#include "paimon/core/table/source/abstract_table_scan.h"
#include "paimon/core/table/source/append_only_split_generator.h"
#include "paimon/core/table/source/data_evolution_batch_scan.h"
#include "paimon/core/table/source/data_evolution_split_generator.h"
#include "paimon/core/table/source/data_table_batch_scan.h"
#include "paimon/core/table/source/data_table_stream_scan.h"
#include "paimon/core/table/source/merge_tree_split_generator.h"
#include "paimon/core/table/source/read_optimized_scan_options.h"
#include "paimon/core/table/source/snapshot/snapshot_reader.h"
#include "paimon/core/table/source/split_generator.h"
#include "paimon/core/table/system/system_table.h"
#include "paimon/core/utils/branch_manager.h"
#include "paimon/core/utils/field_mapping.h"
#include "paimon/core/utils/file_store_path_factory.h"
#include "paimon/core/utils/index_file_path_factories.h"
#include "paimon/core/utils/snapshot_manager.h"
#include "paimon/format/file_format.h"
#include "paimon/result.h"
#include "paimon/scan_context.h"
#include "paimon/status.h"

namespace arrow {
class Schema;
}  // namespace arrow

namespace paimon {
class Executor;
class MemoryPool;

namespace {

class TableScanImpl {
 public:
    static Result<std::unique_ptr<FileStoreScan>> CreateFileStoreScan(
        const std::shared_ptr<FileStorePathFactory>& path_factory,
        const std::shared_ptr<arrow::Schema>& arrow_schema,
        const std::shared_ptr<TableSchema>& table_schema, const CoreOptions& core_options,
        const std::shared_ptr<Executor>& executor, const std::shared_ptr<MemoryPool>& memory_pool,
        const ScanContext* context) {
        auto fs = core_options.GetFileSystem();
        auto manifest_file_format = core_options.GetManifestFormat();
        std::string branch = BranchManager::NormalizeBranch(core_options.GetBranch());
        auto snapshot_manager = std::make_shared<SnapshotManager>(fs, context->GetPath(), branch);
        // TODO(liancheng.lsz): support fallback branch in scan
        auto schema_manager = std::make_shared<SchemaManager>(fs, context->GetPath(), branch);
        PAIMON_ASSIGN_OR_RAISE(
            std::shared_ptr<ManifestList> manifest_list,
            ManifestList::Create(fs, manifest_file_format, core_options.GetManifestCompression(),
                                 path_factory, core_options.GetCache(), memory_pool));
        PAIMON_ASSIGN_OR_RAISE(
            std::shared_ptr<arrow::Schema> partition_schema,
            FieldMapping::GetPartitionSchema(arrow_schema, table_schema->PartitionKeys()));
        PAIMON_ASSIGN_OR_RAISE(
            std::shared_ptr<ManifestFile> manifest_file,
            ManifestFile::Create(fs, manifest_file_format, core_options.GetManifestCompression(),
                                 path_factory, core_options.GetManifestTargetFileSize(),
                                 memory_pool, core_options, partition_schema));
        std::unique_ptr<FileStoreScan> scan;
        if (table_schema->PrimaryKeys().empty()) {
            if (core_options.DataEvolutionEnabled()) {
                PAIMON_ASSIGN_OR_RAISE(
                    scan, DataEvolutionFileStoreScan::Create(
                              snapshot_manager, schema_manager, manifest_list, manifest_file,
                              table_schema, arrow_schema, context->GetScanFilters(), core_options,
                              executor, memory_pool));
            } else {
                PAIMON_ASSIGN_OR_RAISE(
                    scan, AppendOnlyFileStoreScan::Create(
                              snapshot_manager, schema_manager, manifest_list, manifest_file,
                              table_schema, arrow_schema, context->GetScanFilters(), core_options,
                              executor, memory_pool));
            }
        } else {
            PAIMON_ASSIGN_OR_RAISE(
                scan, KeyValueFileStoreScan::Create(snapshot_manager, schema_manager, manifest_list,
                                                    manifest_file, table_schema, arrow_schema,
                                                    context->GetScanFilters(), core_options,
                                                    executor, memory_pool));
        }
        return WithTablePath(std::move(scan), context);
    }

    static std::unique_ptr<FileStoreScan> WithTablePath(std::unique_ptr<FileStoreScan>&& scan,
                                                        const ScanContext* context) {
        scan->WithTablePath(context->GetPath());
        return std::move(scan);
    }

    static Result<std::unique_ptr<SplitGenerator>> CreateSplitGenerator(
        const std::shared_ptr<TableSchema>& table_schema, const CoreOptions& core_options,
        const ScanContext* context) {
        auto source_split_target_size = core_options.GetSourceSplitTargetSize();
        auto source_split_open_file_cost = core_options.GetSourceSplitOpenFileCost();
        if (table_schema->PrimaryKeys().empty()) {
            if (core_options.DataEvolutionEnabled()) {
                return std::make_unique<DataEvolutionSplitGenerator>(
                    source_split_target_size, source_split_open_file_cost,
                    core_options.BlobSplitByFileSize());
            }
            BucketMode bucket_mode = (core_options.GetBucket() == -1 ? BucketMode::BUCKET_UNAWARE
                                                                     : BucketMode::HASH_FIXED);
            return std::make_unique<AppendOnlySplitGenerator>(
                source_split_target_size, source_split_open_file_cost, bucket_mode);
        } else {
            // TODO(liancheng.lsz): support evolution
            PAIMON_ASSIGN_OR_RAISE(std::vector<std::string> trimmed_primary_keys,
                                   table_schema->TrimmedPrimaryKeys());
            PAIMON_ASSIGN_OR_RAISE(std::vector<DataField> trimmed_pk_fields,
                                   table_schema->GetFields(trimmed_primary_keys));
            PAIMON_ASSIGN_OR_RAISE(
                std::shared_ptr<FieldsComparator> key_comparator,
                FieldsComparator::Create(trimmed_pk_fields, /*is_ascending_order=*/true));
            return std::make_unique<MergeTreeSplitGenerator>(
                source_split_target_size, source_split_open_file_cost,
                core_options.DeletionVectorsEnabled(), core_options.GetMergeEngine(),
                key_comparator);
        }
    }

    static Result<std::unique_ptr<IndexFileHandler>> CreateIndexFileHandler(
        const CoreOptions& core_options, const std::shared_ptr<FileStorePathFactory>& path_factory,
        const std::shared_ptr<MemoryPool>& memory_pool) {
        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<IndexManifestFile> index_manifest_file,
                               IndexManifestFile::Create(
                                   core_options.GetFileSystem(), core_options.GetManifestFormat(),
                                   core_options.GetManifestCompression(), path_factory,
                                   core_options.GetBucket(), memory_pool, core_options));
        return std::make_unique<IndexFileHandler>(
            core_options.GetFileSystem(), std::move(index_manifest_file),
            std::make_shared<IndexFilePathFactories>(path_factory),
            core_options.DeletionVectorsBitmap64(), memory_pool);
    }
};

Result<std::unique_ptr<TableScan>> NewDataTableScan(const std::shared_ptr<ScanContext>& context);

}  // namespace

Result<std::unique_ptr<TableScan>> TableScan::Create(std::unique_ptr<ScanContext> context) {
    if (context == nullptr) {
        return Status::Invalid("scan context is null pointer");
    }
    if (context->GetMemoryPool() == nullptr) {
        return Status::Invalid("memory pool is null pointer");
    }
    if (context->GetExecutor() == nullptr) {
        return Status::Invalid("executor is null pointer");
    }

    std::shared_ptr<ScanContext> shared_context = std::move(context);
    // load schema
    PAIMON_ASSIGN_OR_RAISE(CoreOptions tmp_options,
                           CoreOptions::FromMap(shared_context->GetOptions(),
                                                shared_context->GetSpecificFileSystem(), {}));
    PAIMON_ASSIGN_OR_RAISE(std::optional<SystemTablePath> system_table_path,
                           SystemTableLoader::TryParsePath(shared_context->GetPath()));
    if (system_table_path) {
        PAIMON_ASSIGN_OR_RAISE(
            std::shared_ptr<SystemTable> system_table,
            SystemTableLoader::LoadFromPath(tmp_options.GetFileSystem(), shared_context->GetPath(),
                                            shared_context->GetOptions()));
        return system_table->NewScan(shared_context);
    }
    return NewDataTableScan(shared_context);
}

namespace {

Result<std::unique_ptr<TableScan>> NewDataTableScan(const std::shared_ptr<ScanContext>& context) {
    PAIMON_ASSIGN_OR_RAISE(
        CoreOptions tmp_options,
        CoreOptions::FromMap(context->GetOptions(), context->GetSpecificFileSystem(), {}));
    std::string branch = BranchManager::NormalizeBranch(tmp_options.GetBranch());
    std::shared_ptr<TableSchema> table_schema;
    const auto& specific_table_schema = context->GetSpecificTableSchema();
    if (branch == BranchManager::DEFAULT_MAIN_BRANCH && specific_table_schema) {
        PAIMON_ASSIGN_OR_RAISE(table_schema,
                               TableSchema::CreateFromJson(specific_table_schema.value()));
    } else {
        SchemaManager schema_manager(tmp_options.GetFileSystem(), context->GetPath(), branch);
        PAIMON_ASSIGN_OR_RAISE(std::optional<std::shared_ptr<TableSchema>> latest_table_schema,
                               schema_manager.Latest());
        if (latest_table_schema == std::nullopt) {
            return Status::Invalid("not found latest schema");
        }
        table_schema = latest_table_schema.value();
    }
    PAIMON_ASSIGN_OR_RAISE(bool read_optimized,
                           OptionsUtils::GetValueFromMap<bool>(context->GetOptions(),
                                                               kReadOptimizedScanOption, false));
    // merge options
    auto options = table_schema->Options();
    for (const auto& [key, value] : context->GetOptions()) {
        options[key] = value;
    }
    PAIMON_ASSIGN_OR_RAISE(CoreOptions core_options,
                           CoreOptions::FromMap(options, context->GetSpecificFileSystem(), {}));
    core_options.WithCache(context->GetCache());
    // validate options
    if (core_options.GetBucket() == -1) {
        if (!table_schema->PrimaryKeys().empty()) {
            return Status::NotImplemented(fmt::format(
                "do not support pk table bucket={} in scan process", core_options.GetBucket()));
        }
    } else if (core_options.GetBucket() < 1 &&
               !SchemaValidation::IsPostponeBucketTable(*table_schema, core_options.GetBucket())) {
        return Status::Invalid(
            fmt::format("do not support bucket={} in scan process", core_options.GetBucket()));
    }

    // validate schema and scan filter
    auto arrow_schema = DataField::ConvertDataFieldsToArrowSchema(table_schema->Fields());
    if (context->GetScanFilters() && context->GetScanFilters()->GetPredicate()) {
        PAIMON_RETURN_NOT_OK(PredicateValidator::ValidatePredicateWithSchema(
            *arrow_schema, context->GetScanFilters()->GetPredicate(),
            /*validate_field_idx=*/false));
        PAIMON_RETURN_NOT_OK(PredicateValidator::ValidatePredicateWithLiterals(
            context->GetScanFilters()->GetPredicate()));
    }
    PAIMON_ASSIGN_OR_RAISE(std::vector<std::string> external_paths,
                           core_options.CreateExternalPaths());
    PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> global_index_external_path,
                           core_options.CreateGlobalIndexExternalPath());

    PAIMON_ASSIGN_OR_RAISE(
        std::shared_ptr<FileStorePathFactory> path_factory,
        FileStorePathFactory::Create(
            context->GetPath(), arrow_schema, table_schema->PartitionKeys(),
            core_options.GetPartitionDefaultName(), core_options.GetFileFormat()->Identifier(),
            core_options.DataFilePrefix(), core_options.LegacyPartitionNameEnabled(),
            external_paths, global_index_external_path, core_options.IndexFileInDataFileDir(),
            context->GetMemoryPool()));

    PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<FileStoreScan> file_store_scan,
                           TableScanImpl::CreateFileStoreScan(
                               path_factory, arrow_schema, table_schema, core_options,
                               context->GetExecutor(), context->GetMemoryPool(), context.get()));
    PAIMON_ASSIGN_OR_RAISE(
        std::unique_ptr<SplitGenerator> split_generator,
        TableScanImpl::CreateSplitGenerator(table_schema, core_options, context.get()));
    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<IndexFileHandler> index_file_handler,
                           TableScanImpl::CreateIndexFileHandler(core_options, path_factory,
                                                                 context->GetMemoryPool()));
    auto snapshot_reader = std::make_shared<SnapshotReader>(
        file_store_scan, path_factory, std::move(split_generator), std::move(index_file_handler));
    const bool pk_table = !table_schema->PrimaryKeys().empty();
    if (read_optimized && pk_table && context->IsStreamingMode()) {
        return Status::NotImplemented(
            "read-optimized system table does not support streaming scan for primary key table");
    }
    if (context->IsStreamingMode()) {
        return std::make_unique<DataTableStreamScan>(core_options, snapshot_reader);
    }
    auto batch_scan = std::make_unique<DataTableBatchScan>(
        /*pk_table=*/pk_table, core_options, snapshot_reader, read_optimized, context->GetLimit());
    if (!core_options.DataEvolutionEnabled()) {
        return batch_scan;
    }
    return std::make_unique<DataEvolutionBatchScan>(
        context->GetPath(), snapshot_reader, std::move(batch_scan), context->GetGlobalIndexResult(),
        core_options, context->GetMemoryPool(), context->GetExecutor());
}

}  // namespace

}  // namespace paimon
