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

// Adapted from Apache ORC
// https://github.com/apache/orc/blob/main/c%2B%2B/src/io/Cache.cc

#include "paimon/utils/read_ahead_cache.h"

#include <algorithm>
#include <cassert>
#include <future>
#include <shared_mutex>

#include "paimon/common/utils/byte_range_combiner.h"
#include "paimon/common/utils/math.h"

namespace paimon {

struct RangeCacheEntry {
    ByteRange range;
    std::shared_ptr<Bytes> buffer;
    std::shared_future<Status> future;  // use shared_future in case of multiple get calls

    RangeCacheEntry() = default;
    RangeCacheEntry(const ByteRange& range, std::shared_ptr<Bytes> buffer,
                    std::future<Status> future)
        : range(range), buffer(std::move(buffer)), future(std::move(future).share()) {}

    friend bool operator<(const RangeCacheEntry& left, const RangeCacheEntry& right) {
        return left.range.offset < right.range.offset;
    }
};

CacheConfig::CacheConfig(uint64_t buffer_size_limit, uint64_t range_size_limit,
                         uint64_t hole_size_limit, uint64_t pre_buffer_limit)
    : buffer_size_limit_(buffer_size_limit),
      range_size_limit_(range_size_limit),
      hole_size_limit_(hole_size_limit),
      pre_buffer_limit_(pre_buffer_limit) {}

CacheConfig::CacheConfig()
    : CacheConfig(/*buffer_size_limit=*/512 * 1024 * 1024,
                  /*range_size_limit=*/16 * 1024 * 1024,
                  /*hole_size_limit=*/8 * 1024,
                  /*pre_buffer_limit=*/128 * 1024 * 1024) {}

class ReadAheadCache::Impl {
 public:
    Impl(const std::shared_ptr<InputStream>& stream, const CacheConfig& config,
         const std::shared_ptr<MemoryPool>& memory_pool);
    ~Impl();

    Status Init(std::vector<ByteRange>&& ranges);
    Result<ByteSlice> Read(const ByteRange& range);
    void Reset();

 private:
    std::vector<RangeCacheEntry> MakeCacheEntries(const std::vector<ByteRange>& ranges) const;
    void PreBuffer(uint64_t offset);

    /// Cache the given ranges in the background.
    ///
    /// The caller must ensure that the ranges do not overlap with each other,
    /// nor with previously cached ranges.  Otherwise, behaviour will be undefined.
    void Cache(std::vector<ByteRange> ranges);

    std::shared_ptr<InputStream> stream_;
    CacheConfig config_;
    // Ordered by offset (so as to find a matching region by binary search)
    std::vector<RangeCacheEntry> entries_;
    std::shared_ptr<MemoryPool> memory_pool_;
    std::shared_mutex rw_mutex_;
    std::vector<std::atomic<bool>> is_cached_;
    std::vector<ByteRange> pending_ranges_;
    bool is_initialized_ = false;
};

void ReadAheadCache::Impl::Cache(std::vector<ByteRange> ranges) {
    std::sort(ranges.begin(), ranges.end(),
              [](const ByteRange& a, const ByteRange& b) { return a.offset < b.offset; });
    std::vector<RangeCacheEntry> new_entries = MakeCacheEntries(ranges);
    // Add new entries, themselves ordered by offset
    std::unique_lock<std::shared_mutex> lock(rw_mutex_);
    if (entries_.size() > 0) {
        size_t new_entries_size = 0;
        for (const auto& e : new_entries) {
            new_entries_size += e.range.length;
        }

        size_t total_size = 0;
        for (const auto& e : entries_) {
            total_size += e.range.length;
        }
        size_t limit = config_.GetBufferSizeLimit();
        while (!entries_.empty() && total_size + new_entries_size > limit) {
            auto iter = entries_.begin();
            total_size -= entries_.front().range.length;
            entries_.erase(iter);
        }

        std::vector<RangeCacheEntry> merged(entries_.size() + new_entries.size());
        std::merge(entries_.begin(), entries_.end(), new_entries.begin(), new_entries.end(),
                   merged.begin());
        entries_ = std::move(merged);
    } else {
        entries_ = std::move(new_entries);
    }
}

Status ReadAheadCache::Impl::Init(std::vector<ByteRange>&& ranges) {
    if (is_initialized_) {
        return Status::Invalid("Cache has already been initialized");
    }
    PAIMON_ASSIGN_OR_RAISE(
        std::vector<ByteRange> pending_ranges,
        ByteRangeCombiner::CoalesceByteRanges(std::move(ranges), config_.GetHoleSizeLimit(),
                                              config_.GetRangeSizeLimit()));
    for (const auto& pending_range : pending_ranges) {
        PAIMON_RETURN_NOT_OK(ValidateValueInRange<int64_t>(pending_range.offset, "range offset"));
        PAIMON_RETURN_NOT_OK(ValidateValueInRange<int64_t>(pending_range.length, "range length"));
    }
    pending_ranges_ = pending_ranges;
    is_cached_ = std::vector<std::atomic<bool>>(pending_ranges_.size());
    for (auto& is_cached : is_cached_) {
        is_cached.store(false);
    }
    is_initialized_ = true;
    return Status::OK();
}

void ReadAheadCache::Impl::PreBuffer(uint64_t offset) {
    auto it = std::lower_bound(pending_ranges_.begin(), pending_ranges_.end(), offset,
                               [](const ByteRange& range, uint64_t offset) {
                                   return range.offset + range.length <= offset;
                               });
    if (it == pending_ranges_.end() || it->offset > offset) {
        return;
    }

    size_t start_idx = std::distance(pending_ranges_.begin(), it);
    std::vector<ByteRange> ranges;
    size_t total_bytes = 0;
    for (size_t i = start_idx; i < pending_ranges_.size(); ++i) {
        size_t range_size = pending_ranges_[i].length;
        total_bytes += range_size;
        if (total_bytes > config_.GetPreBufferLimit()) {
            break;
        }
        if (is_cached_[i].exchange(true)) {
            continue;
        }
        ranges.emplace_back(pending_ranges_[i]);
    }

    if (!ranges.empty()) {
        Cache(std::move(ranges));
    }
}

ReadAheadCache::Impl::Impl(const std::shared_ptr<InputStream>& stream, const CacheConfig& config,
                           const std::shared_ptr<MemoryPool>& memory_pool)
    : stream_(stream), config_(config), memory_pool_(memory_pool) {}

ReadAheadCache::Impl::~Impl() {
    std::unique_lock<std::shared_mutex> lock(rw_mutex_);
    for (auto& entry : entries_) {
        entry.future.wait();
    }
}

void ReadAheadCache::Impl::Reset() {
    std::unique_lock<std::shared_mutex> lock(rw_mutex_);
    for (auto& entry : entries_) {
        entry.future.wait();
    }
    entries_.clear();
    is_cached_.clear();
    pending_ranges_.clear();
    is_initialized_ = false;
}

Result<ByteSlice> ReadAheadCache::Impl::Read(const ByteRange& range) {
    if (range.length == 0) {
        return ByteSlice{std::make_shared<Bytes>(0, memory_pool_.get()), 0, 0};
    }
    PreBuffer(range.offset);
    ByteSlice result{};
    {
        std::shared_lock<std::shared_mutex> lock(rw_mutex_);
        auto it = std::lower_bound(entries_.begin(), entries_.end(), range.offset,
                                   [](const RangeCacheEntry& e, uint64_t offset) {
                                       return e.range.offset + e.range.length <= offset;
                                   });
        if (it != entries_.end() && it->range.Contains(range)) {
            PAIMON_RETURN_NOT_OK(it->future.get());
            result = ByteSlice{it->buffer, range.offset - it->range.offset, range.length};
            return result;
        }
    }
    return result;
}

std::vector<RangeCacheEntry> ReadAheadCache::Impl::MakeCacheEntries(
    const std::vector<ByteRange>& ranges) const {
    std::vector<RangeCacheEntry> new_entries;
    new_entries.reserve(ranges.size());
    for (const auto& range : ranges) {
        auto promise = std::make_shared<std::promise<Status>>();
        auto future = promise->get_future();
        auto buffer = std::make_shared<Bytes>(range.length, memory_pool_.get());
        auto read_size = static_cast<int64_t>(buffer->size());
        auto read_offset = static_cast<int64_t>(range.offset);
        stream_->ReadAsync(
            buffer->data(), read_size, read_offset,
            [promise, buffer](Status status) mutable { promise->set_value(status); });
        new_entries.emplace_back(range, std::move(buffer), std::move(future));
    }
    return new_entries;
}

ReadAheadCache::ReadAheadCache(const std::shared_ptr<InputStream>& stream,
                               const CacheConfig& config,
                               const std::shared_ptr<MemoryPool>& memory_pool)
    : impl_(std::make_unique<Impl>(stream, config, memory_pool)) {}

ReadAheadCache::~ReadAheadCache() = default;

Status ReadAheadCache::Init(std::vector<ByteRange>&& ranges) {
    return impl_->Init(std::move(ranges));
}

Result<ByteSlice> ReadAheadCache::Read(const ByteRange& range) {
    return impl_->Read(range);
}

void ReadAheadCache::Reset() {
    return impl_->Reset();
}

}  // namespace paimon
