#include "tile/base/object_pool/thread_local.h"

#include "tile/base/chrono.h"
#include "tile/base/deferred.h"

#include <algorithm>

namespace tile {
namespace object_pool {
namespace detail {
namespace tls {

constexpr std::size_t kMinimumFreePerWash = 32;
constexpr auto kMinmumWashInterval        = std::chrono::milliseconds(5);

namespace {
// return value in [upto/2, upto]
std::size_t
GetFreeCount(std::size_t upto)
{
    return std::min(upto, std::max(upto / 2, kMinimumFreePerWash));
}

void
WashOutCache(PoolDescriptor *pool)
{
    auto now = ReadCoarseSteadyClock();
    if (now < pool->last_wash + kMinmumWashInterval) {
        return;
    } else {
        pool->last_wash = now;
    }

    auto &&primary                 = pool->primary_cache;
    auto &&secondary               = pool->secondary_cache;
    auto move_to_secondary_or_free = [&](std::size_t count) {
        while (count-- > 0) {
            if (secondary.size() < pool->low_water_mark) { secondary.push_back(std::move(primary.front())); }
            primary.pop_front();
        }
    };

    if (primary.size() > pool->high_water_mark) {
        auto upto = GetFreeCount(primary.size() - pool->high_water_mark);
        move_to_secondary_or_free(upto);
        if (upto >= kMinimumFreePerWash) { return; }
    }

#ifndef NDEBUG
    std::size_t objects_had = pool->primary_cache.size() + pool->secondary_cache.size();
#endif

    // move idle objects to secondary cache or free
    auto idle_objects = std::find_if(primary.begin(), primary.end(),
                                     [&](const TimestampedObject &e) { return now - e.last_used < pool->max_idle; })
        - primary.begin();
    move_to_secondary_or_free(GetFreeCount(idle_objects));
#ifndef NDEBUG
    if (objects_had >= pool->low_water_mark) {
        TILE_CHECK_GE(pool->primary_cache.size() + pool->secondary_cache.size(), pool->low_water_mark);
    }
#endif
}

}// namespace

void *
Get(const TypeDescriptor &desc, PoolDescriptor *pool)
{
    if (pool->primary_cache.empty()) {
        if (!pool->secondary_cache.empty()) {
            // primary empty, secondary not empty
            pool->primary_cache = std::move(pool->secondary_cache);
            for (auto &&e : pool->primary_cache) { e.last_used = ReadCoarseSteadyClock(); }
        } else {
            // primary empty, secondary_cache empty
            pool->primary_cache.push_back(TimestampedObject{
                .ptr       = {desc.create(), desc.destroy},
                .last_used = ReadCoarseSteadyClock()
            });
        }
    }

    TILE_DCHECK(!pool->primary_cache.empty());

    auto rc = std::move(pool->primary_cache.back());
    pool->primary_cache.pop_back();
    return rc.ptr.Leak();
}

void
Put(const TypeDescriptor &desc, PoolDescriptor *pool, void *ptr)
{
    ScopedDeferred _([&] { WashOutCache(pool); });
    pool->primary_cache.push_back(TimestampedObject{
        .ptr       = {ptr, desc.destroy},
        .last_used = ReadCoarseSteadyClock()
    });
}

}// namespace tls
}// namespace detail
}// namespace object_pool
}// namespace tile
