/************************************************************************
 *
 * allocator.cpp - definitions of allocator testsuite helpers
 *
 * $Id$
 *
 ************************************************************************
 *
 * 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.
 * 
 **************************************************************************/

// expand _TEST_EXPORT macros
#define _RWSTD_TEST_SRC

#include <rw_allocator.h>
#include <rw_new.h>         // for operator_new()
#include <rw_driver.h>         // for rw_assert()
#include <rw_exception.h>   // for rw_throw()

#include <new>              // for bad_alloc, placement new
#include <stdarg.h>         // for va_arg(), va_list
#include <string.h>         // for memset()

/**************************************************************************/

static const char* const
_rw_func_names[] = {
    "UserAlloc ()",
    "UserAlloc (const UserAlloc&)",
    "UserAlloc (const UserAlloc<U>&)",
    "~UserAlloc ()",
    "operator= (const UserAlloc&)",
    "operator=<U>(const UserAlloc<U>&)",
    "allocate (size_type, void*)",
    "deallocate (pointer, size_type)",
    "construct (pointer, const_reference)",
    "destroy (pointer)",
    "address (reference) const",
    "max_size () const"
};

static int _rw_id_gen;   // generates unique ids


/**************************************************************************/

SharedAlloc::
SharedAlloc (size_t nbytes /* = MAX_SIZE */, size_t nblocks /* = MAX_SIZE */)
    : max_bytes_ (nbytes), max_blocks_ (nblocks),
      n_bytes_ (0), n_blocks_ (0), n_refs_ (0), id_ (0)
{
    memset (n_calls_, 0, sizeof n_calls_);
    memset (n_throws_, 0, sizeof n_throws_);
    memset (throw_at_calls_, 0, sizeof throw_at_calls_);
}


/* virtual */ SharedAlloc::
~SharedAlloc ()
{
    static size_t deadbeef = 0;
    if (0 == deadbeef) {
        deadbeef = size_t (0xdeadbeefU);
        if (deadbeef < deadbeef << 1) {
            // assume 64-bit size_t
            deadbeef <<= 31;
            deadbeef <<=  1;
            deadbeef  |= size_t (0xdeadbeefU);
        }
    }

    // invalidate
    size_t n = sizeof n_calls_ / sizeof *n_calls_;
    for (size_t i = 0; i != n; ++i)
        n_calls_ [i] = deadbeef;

    n = sizeof n_throws_ / sizeof *n_throws_;
    for (size_t i = 0; i != n; ++i)
        n_throws_ [i] = deadbeef;

    n = sizeof throw_at_calls_ / sizeof *throw_at_calls_;
    for (size_t i = 0; i != n; ++i)
        throw_at_calls_ [i] = deadbeef;
}


/* virtual */ void* SharedAlloc::
allocate (size_t nelems, size_t size, const void* /* = 0 */)
{
    funcall (m_allocate);

    const size_t nbytes = nelems * size;

    if (max_blocks_ < n_blocks_ + nelems)
        rw_throw (ex_bad_alloc, __FILE__, __LINE__, 0,
                  "allocate (%zu):  reached block limit of %zu",
                  nbytes, max_blocks_);

    if (max_bytes_ < n_bytes_ + nbytes)
        rw_throw (ex_bad_alloc, __FILE__, __LINE__, 0,
                  "allocate (%zu):  reached size limit of %zu",
                  nbytes, max_bytes_);

    return operator_new (nbytes, false);
}


/* virtual */ void SharedAlloc::
deallocate (void *ptr, size_t /* nelems */, size_t /* size */)
{
    funcall (m_deallocate);

    return operator_delete (ptr, false);
}


/* virtual */ size_t SharedAlloc::
max_size (size_t size /* = 1 */)
{
    funcall (m_max_size);

    return max_blocks_ / size;
}


/* virtual */ void SharedAlloc::
funcall (MemFun mf, const SharedAlloc *other /* = 0 */)
{
    // increment the number of calls regardless of success
    ++n_calls_ [mf];

    if (m_ctor == mf) {
        // ordinary (not a copy or converting) ctor
        if (id_ <= 0) {
            RW_ASSERT (0 == n_refs_);
            id_ = ++_rw_id_gen;
        }

        // increment the number of references to this allocator
        ++n_refs_;
    }
    else if (m_cpy_ctor == mf || m_cvt_ctor == mf) {
        // copy or converting ctor
        RW_ASSERT (0 < id_);
        RW_ASSERT (0 < n_refs_);

        // increment the number of references to this allocator
        ++n_refs_;
    }
    else if (m_cpy_assign == mf || m_cvt_assign == mf) {
        // assignment operator
        RW_ASSERT (0 <= id_ && id_ <= _rw_id_gen);
        RW_ASSERT (0 < n_refs_);

        RW_ASSERT (0 != other);

        if (this != other) {
            // decrement the number of references and invalidate
            // id if no object refers to *this
            if (0 == --n_refs_)
                id_ = -1;

            SharedAlloc* const po = _RWSTD_CONST_CAST (SharedAlloc*, other);

            if (other->id_ <= 0)
                po->id_ = ++_rw_id_gen;

            ++po->n_refs_;
        }
    }
    else if (m_dtor == mf) {
        // dtor
        RW_ASSERT (0 <= id_ && id_ <= _rw_id_gen);
        RW_ASSERT (0 < n_refs_);

        // decrement the number of references and invalidate
        // id if no object refers to *this
        if (0 == --n_refs_)
            id_ = -1;
    }

    // check the number of calls and throw an exception
    // if the specified limit has been reached
    if (n_calls_ [mf] == throw_at_calls_ [mf]) {
        // increment the exception counter for this function
        ++n_throws_ [mf];

        rw_throw (ex_bad_alloc, __FILE__, __LINE__, 0,
                  "UserAlloc::%s: reached call limit of %zu",
                  _rw_func_names [mf], throw_at_calls_);

        RW_ASSERT (!"logic error: should not reach");
    }
}


/* static */ SharedAlloc* SharedAlloc::
instance (SharedAlloc *pinst /* = 0 */)
{
    // get or a set a pointer to the global allocator object
    static SharedAlloc* pglobal = 0;

    if (0 == pglobal) {
        // construct the global allocator object in the static buffer
        static size_t instbuf [sizeof (SharedAlloc) / sizeof (size_t) + 1];
        static SharedAlloc* const pbuf = new (instbuf) SharedAlloc ();

        pglobal = pbuf;
    }

    if (pinst) {
        // set the global allocator object and return a pointer
        // to the previous one
        SharedAlloc* const tmp = pglobal;
        pglobal = pinst;
        pinst   = tmp;
    }
    else
        pinst = pglobal;

    RW_ASSERT (0 != pinst);

    return pinst;
}


void SharedAlloc::
reset_call_counters ()
{
    memset (n_calls_, 0, sizeof n_calls_);
}


/* static */ const char* SharedAlloc::
func_name (MemFun mf)
{
    return _rw_func_names [mf];
}

/**************************************************************************/

static void
_rw_check_leaks (int line, size_t expect_blocks, size_t expect_bytes)
{
    if (0 < line) {
        // determine and report memory leaks since last checkpoint

        /* const */ size_t nbytes;
        const       size_t nblocks = rwt_check_leaks (&nbytes, 0);

        if (_RWSTD_SIZE_MAX == expect_blocks)
            expect_blocks = nblocks;

        if (_RWSTD_SIZE_MAX == expect_bytes)
            expect_bytes = nbytes;

        rw_assert (nblocks == expect_blocks && nbytes == expect_bytes,
                   0, line,
                   "line %d. %{$FUNCALL} operator new allocated "
                   "%zu bytes in %zu blocks, expected %zu bytes "
                   "in %zu blocks",
                   __LINE__, nbytes, nblocks,
                   expect_bytes, expect_blocks);
    }
    else {
        // establish a checkpoint for memory leaks
        rwt_check_leaks (0, 0);
    }
}


_TEST_EXPORT void
rw_check_leaks (const SharedAlloc *palloc        /* =  0 */,
                int                line          /* =  0 */, 
                size_t             expect_blocks /* =  0 */,
                size_t             expect_bytes  /* = -1 */)
{
    _rw_check_leaks (line, expect_blocks, expect_bytes);

    if (0 == palloc)
        return;

    static size_t nbytes;
    static size_t nblocks;

    if (0 < line) {
        // determine and report memory leaks since last checkpoint
        const size_t leaked_bytes  = palloc->n_bytes_ - nbytes;
        const size_t leaked_blocks = palloc->n_blocks_ - nblocks;

        if (_RWSTD_SIZE_MAX == expect_blocks)   // don't care
            expect_blocks = leaked_blocks;

        if (_RWSTD_SIZE_MAX == expect_bytes)   // don't care
            expect_bytes = nbytes;

        rw_assert (   leaked_blocks == expect_blocks
                   && leaked_bytes == expect_bytes, 0, line,
                   "line %d. %{$FUNCALL} UserAlloc allocated "
                   "%zu bytes in %zu blocks, expected %zu bytes "
                   "in %zu blocks",
                   __LINE__, leaked_bytes, leaked_blocks,
                   expect_bytes, expect_blocks);
    }
    else {
        // establish a checkpoint for memory leaks
        nbytes  = palloc->n_bytes_;
        nblocks = palloc->n_blocks_;
    }
}
