//////////////////////////////////////////////////////////////////////////////
//
// (C) Copyright Ion Gaztanaga 2015-2015. Distributed under the Boost
// Software License, Version 1.0. (See accompanying file
// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// See http://www.boost.org/libs/container for documentation.
//
//////////////////////////////////////////////////////////////////////////////

#include <boost/container/pmr/global_resource.hpp>
#include <boost/core/lightweight_test.hpp>

#include <boost/intrusive/detail/math.hpp>

#include "derived_from_memory_resource.hpp"
#include "memory_resource_logger.hpp"

using namespace boost::container::pmr;

template<class PoolResource>
struct derived_from_pool_resource
   : public PoolResource
{
   derived_from_pool_resource(const pool_options& opts, memory_resource* upstream)
      : PoolResource(opts, upstream)
   {}

   explicit derived_from_pool_resource(memory_resource *p)
      : PoolResource(p)
   {}

   explicit derived_from_pool_resource(const pool_options &opts)
      : PoolResource(opts)
   {}

   derived_from_pool_resource()
      : PoolResource()
   {}

   using PoolResource::do_allocate;
   using PoolResource::do_deallocate;
   using PoolResource::do_is_equal;
};

template<class PoolResource>
void test_default_constructor()
{
   //With default options/resource
   {
      derived_from_memory_resource dmr;
      dmr.reset();
      PoolResource m;
      //test postconditions
      BOOST_TEST(m.upstream_resource() == get_default_resource());
      BOOST_TEST(m.options().max_blocks_per_chunk == pool_options_default_max_blocks_per_chunk);
      BOOST_TEST(m.options().largest_required_pool_block == pool_options_default_largest_required_pool_block);
      //test it does not allocate any memory
      BOOST_TEST(dmr.do_allocate_called == false);
   }
}

template<class PoolResource>
void test_upstream_constructor()
{
   //With a resource, default options
   {
      derived_from_memory_resource dmr;
      dmr.reset();
      PoolResource m(&dmr);
      //test postconditions
      BOOST_TEST(m.upstream_resource() == &dmr);
      BOOST_TEST(m.options().max_blocks_per_chunk == pool_options_default_max_blocks_per_chunk);
      BOOST_TEST(m.options().largest_required_pool_block == pool_options_default_largest_required_pool_block);
      //test it does not allocate any memory
      BOOST_TEST(dmr.do_allocate_called == false);
   }
}

template<class PoolResource>
void test_options_constructor()
{
   //Default options
   {
      memory_resource_logger mrl;
      BOOST_TEST(mrl.m_info.size() == 0u);
      set_default_resource(&mrl);
      pool_options opts;
      PoolResource m(opts);
      //test postconditions
      BOOST_TEST(m.upstream_resource() == get_default_resource());
      BOOST_TEST(m.options().max_blocks_per_chunk == pool_options_default_max_blocks_per_chunk);
      BOOST_TEST(m.options().largest_required_pool_block == pool_options_default_largest_required_pool_block);
      //test it does not allocate any memory
      BOOST_TEST(mrl.m_info.size() == 0u);
   }
   //Too large option values
   {
      memory_resource_logger mrl;
      BOOST_TEST(mrl.m_info.size() == 0u);
      set_default_resource(&mrl);
      pool_options opts;
      opts.max_blocks_per_chunk = pool_options_default_max_blocks_per_chunk+1;
      opts.largest_required_pool_block = pool_options_default_largest_required_pool_block+1;
      PoolResource m(opts);
      //test postconditions
      BOOST_TEST(m.upstream_resource() == get_default_resource());
      BOOST_TEST(m.options().max_blocks_per_chunk == pool_options_default_max_blocks_per_chunk);
      BOOST_TEST(m.options().largest_required_pool_block == pool_options_default_largest_required_pool_block);
      //test it does not allocate any memory
      BOOST_TEST(mrl.m_info.size() == 0u);
   }
   //Too small option values
   {
      memory_resource_logger mrl;
      BOOST_TEST(mrl.m_info.size() == 0u);
      set_default_resource(&mrl);
      pool_options opts;
      opts.largest_required_pool_block = pool_options_minimum_largest_required_pool_block-1u;
      PoolResource m(opts);
      //test postconditions
      BOOST_TEST(m.upstream_resource() == get_default_resource());
      BOOST_TEST(m.options().max_blocks_per_chunk == pool_options_default_max_blocks_per_chunk);
      BOOST_TEST(m.options().largest_required_pool_block == pool_options_minimum_largest_required_pool_block);
      //test it does not allocate any memory
      BOOST_TEST(mrl.m_info.size() == 0u);
   }
   //In range option values
   {
      memory_resource_logger mrl;
      BOOST_TEST(mrl.m_info.size() == 0u);
      set_default_resource(&mrl);
      pool_options opts;
      opts.max_blocks_per_chunk = pool_options_default_max_blocks_per_chunk;
      opts.largest_required_pool_block = pool_options_minimum_largest_required_pool_block;
      PoolResource m(opts);
      //test postconditions
      BOOST_TEST(m.upstream_resource() == get_default_resource());
      BOOST_TEST(m.options().max_blocks_per_chunk == pool_options_default_max_blocks_per_chunk);
      BOOST_TEST(m.options().largest_required_pool_block == pool_options_minimum_largest_required_pool_block);
      //test it does not allocate any memory
      BOOST_TEST(mrl.m_info.size() == 0u);
   }
}

template<class PoolResource>
void test_options_upstream_constructor()
{
   //Default options
   {
      derived_from_memory_resource dmr;
      dmr.reset();
      pool_options opts;
      PoolResource m(opts, &dmr);
      //test postconditions
      BOOST_TEST(m.upstream_resource() == &dmr);
      BOOST_TEST(m.options().max_blocks_per_chunk == pool_options_default_max_blocks_per_chunk);
      BOOST_TEST(m.options().largest_required_pool_block == pool_options_default_largest_required_pool_block);
      //test it does not allocate any memory
      BOOST_TEST(dmr.do_allocate_called == false);
   }
   //Too large option values
   {
      derived_from_memory_resource dmr;
      dmr.reset();
      pool_options opts;
      opts.max_blocks_per_chunk = pool_options_default_max_blocks_per_chunk+1;
      opts.largest_required_pool_block = pool_options_default_largest_required_pool_block+1;
      PoolResource m(opts, &dmr);
      //test postconditions
      BOOST_TEST(m.upstream_resource() == &dmr);
      BOOST_TEST(m.options().max_blocks_per_chunk == pool_options_default_max_blocks_per_chunk);
      BOOST_TEST(m.options().largest_required_pool_block == pool_options_default_largest_required_pool_block);
      //test it does not allocate any memory
      BOOST_TEST(dmr.do_allocate_called == false);
   }
   //Too small option values
   {
      derived_from_memory_resource dmr;
      dmr.reset();
      pool_options opts;
      opts.largest_required_pool_block = pool_options_minimum_largest_required_pool_block-1u;
      PoolResource m(opts, &dmr);
      //test postconditions
      BOOST_TEST(m.upstream_resource() == &dmr);
      BOOST_TEST(m.options().max_blocks_per_chunk == pool_options_default_max_blocks_per_chunk);
      BOOST_TEST(m.options().largest_required_pool_block == pool_options_minimum_largest_required_pool_block);
      //test it does not allocate any memory
      BOOST_TEST(dmr.do_allocate_called == false);
   }
   //In range option values
   {
      derived_from_memory_resource dmr;
      dmr.reset();
      pool_options opts;
      opts.max_blocks_per_chunk = pool_options_default_max_blocks_per_chunk;
      opts.largest_required_pool_block = pool_options_minimum_largest_required_pool_block;
      PoolResource m(opts, &dmr);
      //test postconditions
      BOOST_TEST(m.upstream_resource() == &dmr);
      //max blocks is unchanged in this implementation
      BOOST_TEST(m.options().max_blocks_per_chunk == pool_options_default_max_blocks_per_chunk);
      //largest block is rounded to pow2
      BOOST_TEST(m.options().largest_required_pool_block == bi::detail::ceil_pow2(opts.largest_required_pool_block));
      //test it does not allocate any memory
      BOOST_TEST(dmr.do_allocate_called == false);
   }
}

template<class PoolResource>
void test_options()
{
   //In range option values
   {
      derived_from_memory_resource dmr;
      dmr.reset();
      pool_options opts;
      opts.max_blocks_per_chunk = pool_options_default_max_blocks_per_chunk/2u;
      opts.largest_required_pool_block = (pool_options_default_largest_required_pool_block
         - pool_options_minimum_largest_required_pool_block) | std::size_t(1); //guaranteed to be non power of 2.
      PoolResource m(opts, &dmr);
      //test postconditions
      BOOST_TEST(m.upstream_resource() == &dmr);
      //max blocks is unchanged in this implementation
      BOOST_TEST(m.options().max_blocks_per_chunk == opts.max_blocks_per_chunk);
      //largest block is rounded to pow2
      BOOST_TEST(m.options().largest_required_pool_block == bi::detail::ceil_pow2(opts.largest_required_pool_block));
      //test it does not allocate any memory
      BOOST_TEST(dmr.do_allocate_called == false);
   }
}

template<class PoolResource>
void test_do_allocate_deallocate()
{
   memory_resource_logger mrl;
   {
      derived_from_pool_resource<PoolResource> dmbr(&mrl);
      {
         //First block from pool 0
         dmbr.do_allocate(1, 1);
         //It should allocate the pool array plus an initial block
         BOOST_TEST(mrl.m_info.size() == 2u);
         //Second block from pool 0
         dmbr.do_allocate(1, 1);
         //It should allocate again (with 2 chunks per block)
         BOOST_TEST(mrl.m_info.size() == 3u);
         //Third block from pool 0
         dmbr.do_allocate(1, 1);
         //It should NOT allocate again (previous was a 2 block chunk)
         BOOST_TEST(mrl.m_info.size() == 3u);
      }
   }
   BOOST_TEST(mrl.m_mismatches == 0u);
   BOOST_TEST(mrl.m_info.size() == 0u);

   //Allocate and deallocate from the same chunk to test block caching
   {
      derived_from_pool_resource<PoolResource> dmbr(&mrl);
      {
         //First block from pool 0
         void *p = dmbr.do_allocate(1, 1);
         //It should allocate the pool array plus an initial block
         BOOST_TEST(mrl.m_info.size() == 2u);
         //No cached, as initial blocks per chunk is 1
         BOOST_TEST(dmbr.pool_cached_blocks(0u) == 0u);
         //Deallocate and allocate again
         dmbr.do_deallocate(p, 1, 1);
         //Cached
         BOOST_TEST(dmbr.pool_cached_blocks(0u) == 1u);
         p = dmbr.do_allocate(1, 1);
         //Reused
         BOOST_TEST(dmbr.pool_cached_blocks(0u) == 0u);
         //It should have NOT allocated (block reuse)
         BOOST_TEST(mrl.m_info.size() == 2u);

         //Allocate again 2 times (a 2 block chunk is exhausted)
         void *p2 = dmbr.do_allocate(1, 1);
         //1 left cached
         BOOST_TEST(dmbr.pool_cached_blocks(0u) == 1u);
         void *p3 = dmbr.do_allocate(1, 1);
         //Cache exhausted
         BOOST_TEST(dmbr.pool_cached_blocks(0u) == 0u);
         //Single chunk allocation happened
         BOOST_TEST(mrl.m_info.size() == 3u);

         //Now deallocate all (no memory is freed, all cached)
         dmbr.do_deallocate(p2, 1, 1);
         dmbr.do_deallocate(p3, 1, 1);
         dmbr.do_deallocate(p, 1, 1);
         BOOST_TEST(dmbr.pool_cached_blocks(0u) == 3u);
         BOOST_TEST(mrl.m_info.size() == 3u);
      }
   }
   BOOST_TEST(mrl.m_mismatches == 0u);
   BOOST_TEST(mrl.m_info.size() == 0u);

   //Now test max block per chunk
   {
      pool_options opts;
      //so after max_blocks_per_chunk*2-1 allocations, all new chunks must hold max_blocks_per_chunk blocks
      opts.max_blocks_per_chunk = 32u; 
      derived_from_pool_resource<PoolResource> dmbr(opts, &mrl);
      {
         std::size_t loops = opts.max_blocks_per_chunk*2-1u;
         while(loops--){
            dmbr.do_allocate(1, 1);
         }
         //pool array + log2(max_blocks_per_chunk)+1 chunks (sizes [1, 2, 4, ...])
         const std::size_t num_chunks = bi::detail::floor_log2(opts.max_blocks_per_chunk)+1u;
         BOOST_TEST(mrl.m_info.size() == 1u + num_chunks);
         //Next allocation should allocate max_blocks_per_chunk blocks in a chunk so max_blocks_per_chunk-1 should remain free
         dmbr.do_allocate(1, 1);
         BOOST_TEST(mrl.m_info.size() == 1u + num_chunks + 1u);
         BOOST_TEST(dmbr.pool_cached_blocks(0u) == (opts.max_blocks_per_chunk-1u));
         //Exhaust the chunk and allocate a new one, test max_blocks_per_chunk is not passed again
         loops = opts.max_blocks_per_chunk;
         while(loops--){
            dmbr.do_allocate(1, 1);
         }
         BOOST_TEST(mrl.m_info.size() == 1u + num_chunks + 2u);
         BOOST_TEST(dmbr.pool_cached_blocks(0u) == (opts.max_blocks_per_chunk-1u));
      }
   }
   BOOST_TEST(mrl.m_mismatches == 0u);
   BOOST_TEST(mrl.m_info.size() == 0u);

   //Now test max block per chunk
   {
      pool_options opts;
      //so after max_blocks_per_chunk*2-1 allocations, all new chunks must hold max_blocks_per_chunk blocks
      opts.max_blocks_per_chunk = 32u; 
      derived_from_pool_resource<PoolResource> dmbr(opts, &mrl);
      {
         std::size_t loops = opts.max_blocks_per_chunk*2-1u;
         while(loops--){
            dmbr.do_allocate(1, 1);
         }
         //pool array + log2(max_blocks_per_chunk)+1 chunks (sizes [1, 2, 4, ...])
         BOOST_TEST(dmbr.pool_next_blocks_per_chunk(0u) == opts.max_blocks_per_chunk);
         const std::size_t num_chunks = bi::detail::floor_log2(opts.max_blocks_per_chunk)+1u;
         BOOST_TEST(mrl.m_info.size() == 1u + num_chunks);
         //Next allocation should allocate max_blocks_per_chunk blocks in a chunk so max_blocks_per_chunk-1 should remain free
         dmbr.do_allocate(1, 1);
         BOOST_TEST(dmbr.pool_next_blocks_per_chunk(0u) == opts.max_blocks_per_chunk);
         BOOST_TEST(mrl.m_info.size() == 1u + num_chunks + 1u);
         BOOST_TEST(dmbr.pool_cached_blocks(0u) == (opts.max_blocks_per_chunk-1u));
      }
   }
   BOOST_TEST(mrl.m_mismatches == 0u);
   BOOST_TEST(mrl.m_info.size() == 0u);

   //Now test different pool sizes
   {
      pool_options opts;
      //so after max_blocks_per_chunk*2-1 allocations, all new chunks must hold max_blocks_per_chunk blocks
      opts.max_blocks_per_chunk = 1u; 
      derived_from_pool_resource<PoolResource> dmbr(opts, &mrl);
      const pool_options &final_opts = dmbr.options();

      //Force pool creation
      dmbr.do_deallocate(dmbr.do_allocate(1, 1), 1, 1);
      //pool array plus first pool's chunk allocation
      BOOST_TEST(mrl.m_info.size() == 2u);
      //pool count must be:
      // log2(the maximum block) - log2(the minimum block) + 1. Example if minimum block is 8, and maximum 32:
      // log(32) - log2(8) + 1u = 3 pools (block sizes: 8, 16, and 32)
      const std::size_t minimum_size = dmbr.pool_block(0u);
      const std::size_t maximum_size = final_opts.largest_required_pool_block;
      BOOST_TEST(dmbr.pool_count() == (1u + bi::detail::floor_log2(maximum_size) - bi::detail::floor_log2(minimum_size)));
      for(std::size_t i = 0, s = minimum_size, max = dmbr.pool_count(); i != max; ++i, s*=2){
         //Except in the first pool, each cache should be empty
         BOOST_TEST(dmbr.pool_cached_blocks(i) == std::size_t(i == 0));
         dmbr.do_deallocate(dmbr.do_allocate(s/2+1, 1), s/2+1, 1);
         dmbr.do_deallocate(dmbr.do_allocate(s-1, 1), s-1, 1);
         dmbr.do_deallocate(dmbr.do_allocate(s, 1), s, 1);
         //pool array plus each previous chunk allocation
         BOOST_TEST(mrl.m_info.size() == (1u + i + 1u));
         //as we limited max_blocks_per_chunk to 1, no cached blocks should be available except one
         BOOST_TEST(dmbr.pool_cached_blocks(i) == 1u);
      }
      //Now test out of maximum values, which should go directly to upstream
      //it should be directly deallocated.
      void *p = dmbr.do_allocate(maximum_size+1, 1);
      BOOST_TEST(mrl.m_info.size() == (1u + dmbr.pool_count() + 1u));
      dmbr.do_deallocate(p, maximum_size+1, 1);
      BOOST_TEST(mrl.m_info.size() == (1u + dmbr.pool_count()));
   }
   BOOST_TEST(mrl.m_mismatches == 0u);
   BOOST_TEST(mrl.m_info.size() == 0u);
}

template<class PoolResource>
void test_do_is_equal()
{
   //`this == dynamic_cast<const PoolResource*>(&other)`.
   memory_resource_logger mrl;
   derived_from_pool_resource<PoolResource> dmbr(&mrl);
   derived_from_pool_resource<PoolResource> dmbr2(&mrl);
   BOOST_TEST(true == dmbr.do_is_equal(dmbr));
   BOOST_TEST(false == dmbr.do_is_equal(dmbr2));
   //A different type should be always different
   derived_from_memory_resource dmr;
   BOOST_TEST(false == dmbr.do_is_equal(dmr));
}

template<class PoolResource>
void test_release()
{
   memory_resource_logger mrl;
   {
      pool_options opts;
      //so after max_blocks_per_chunk*2-1 allocations, all new chunks must hold max_blocks_per_chunk blocks
      opts.max_blocks_per_chunk = 4u; 
      derived_from_pool_resource<PoolResource> dmbr(opts, &mrl);
      const pool_options &final_opts = dmbr.options();
      const std::size_t minimum_size = dmbr.pool_block(0u);
      const std::size_t maximum_size = final_opts.largest_required_pool_block;
      const std::size_t pool_count = 1u + bi::detail::floor_log2(maximum_size) - bi::detail::floor_log2(minimum_size);

      std::size_t expected_memory_allocs = 0;
      for(std::size_t i = 0, imax = pool_count, s = minimum_size; i != imax; s*=2, ++i){
         for(std::size_t j = 0, j_max = opts.max_blocks_per_chunk*2u-1u; j != j_max; ++j){
            dmbr.do_allocate(s, 1);
         }
         //One due to the pool array, and for each pool, log2(max_blocks_per_chunk)+1 allocations
         expected_memory_allocs = 1 + (bid::floor_log2(opts.max_blocks_per_chunk) + 1u)*(i+1);
         //pool array plus each previous chunk allocation
         BOOST_TEST(mrl.m_info.size() == expected_memory_allocs);
      }
      //Now with out-of-pool sizes
      for(std::size_t j = 0, j_max = opts.max_blocks_per_chunk*2u-1u; j != j_max; ++j){
         dmbr.do_allocate(maximum_size+1, 1);
         BOOST_TEST(mrl.m_info.size() == ++expected_memory_allocs);
      }
      //Now release memory and check all memory allocated through do_allocate was deallocated to upstream      
      dmbr.release();
      BOOST_TEST(mrl.m_info.size() == 1u);
   }
   BOOST_TEST(mrl.m_mismatches == 0u);
   BOOST_TEST(mrl.m_info.size() == 0u);
}

template<class PoolResource>
void test_destructor()
{
   memory_resource_logger mrl;
   {
      pool_options opts;
      //so after max_blocks_per_chunk*2-1 allocations, all new chunks must hold max_blocks_per_chunk blocks
      opts.max_blocks_per_chunk = 4u; 
      derived_from_pool_resource<PoolResource> dmbr(opts, &mrl);
      const pool_options &final_opts = dmbr.options();
      const std::size_t minimum_size = dmbr.pool_block(0u);
      const std::size_t maximum_size = final_opts.largest_required_pool_block;
      const std::size_t pool_count = 1u + bi::detail::floor_log2(maximum_size) - bi::detail::floor_log2(minimum_size);

      std::size_t expected_memory_allocs = 0;
      for(std::size_t i = 0, imax = pool_count, s = minimum_size; i != imax; s*=2, ++i){
         for(std::size_t j = 0, j_max = opts.max_blocks_per_chunk*2u-1u; j != j_max; ++j){
            dmbr.do_allocate(s, 1);
         }
         //One due to the pool array, and for each pool, log2(max_blocks_per_chunk)+1 allocations
         expected_memory_allocs = 1 + (bid::floor_log2(opts.max_blocks_per_chunk) + 1u)*(i+1);
         //pool array plus each previous chunk allocation
         BOOST_TEST(mrl.m_info.size() == expected_memory_allocs);
      }
      //Now with out-of-pool sizes
      for(std::size_t j = 0, j_max = opts.max_blocks_per_chunk*2u-1u; j != j_max; ++j){
         dmbr.do_allocate(maximum_size+1, 1);
         BOOST_TEST(mrl.m_info.size() == ++expected_memory_allocs);
      }
      //Don't release, all memory, including internal allocations, should be automatically
      //released after the destructor is run
   }
   BOOST_TEST(mrl.m_mismatches == 0u);
   BOOST_TEST(mrl.m_info.size() == 0u);
}


template<class PoolResource>
void test_pool_resource()
{
   test_options_upstream_constructor<PoolResource>();
   test_default_constructor<PoolResource>();
   test_upstream_constructor<PoolResource>();
   test_options_constructor<PoolResource>();
   test_options<PoolResource>();
   test_do_allocate_deallocate<PoolResource>();
   test_do_is_equal<PoolResource>();
   test_release<PoolResource>();
   test_destructor<PoolResource>();
}
