///
module std.experimental.allocator.building_blocks.bucketizer;

/**

A $(D Bucketizer) uses distinct allocators for handling allocations of sizes in
the intervals $(D [min, min + step - 1]), $(D [min + step, min + 2 * step - 1]),
$(D [min + 2 * step, min + 3 * step - 1]), $(D ...), $(D [max - step + 1, max]).

$(D Bucketizer) holds a fixed-size array of allocators and dispatches calls to
them appropriately. The size of the array is $(D (max + 1 - min) / step), which
must be an exact division.

Allocations for sizes smaller than $(D min) or larger than $(D max) are illegal
for $(D Bucketizer). To handle them separately, $(D Segregator) may be of use.

*/
struct Bucketizer(Allocator, size_t min, size_t max, size_t step)
{
    import common = std.experimental.allocator.common : roundUpToMultipleOf,
           alignedAt;
    import std.traits : hasMember;
    import std.typecons : Ternary;

    static assert((max - (min - 1)) % step == 0,
        "Invalid limits when instantiating " ~ Bucketizer.stringof);

    // state
    /**
    The array of allocators is publicly available for e.g. initialization and
    inspection.
    */
    Allocator[(max + 1 - min) / step] buckets;

    pure nothrow @safe @nogc
    private Allocator* allocatorFor(size_t n)
    {
        const i = (n - min) / step;
        return i < buckets.length ? &buckets[i] : null;
    }

    /**
    The alignment offered is the same as $(D Allocator.alignment).
    */
    enum uint alignment = Allocator.alignment;

    /**
    Rounds up to the maximum size of the bucket in which $(D bytes) falls.
    */
    pure nothrow @safe @nogc
    size_t goodAllocSize(size_t bytes) const
    {
        // round up bytes such that bytes - min + 1 is a multiple of step
        assert(bytes >= min);
        const min_1 = min - 1;
        return min_1 + roundUpToMultipleOf(bytes - min_1, step);
    }

    /**
    Directs the call to either one of the $(D buckets) allocators.
    */
    void[] allocate(size_t bytes)
    {
        if (!bytes) return null;
        if (auto a = allocatorFor(bytes))
        {
            const actual = goodAllocSize(bytes);
            auto result = a.allocate(actual);
            return result.ptr ? result.ptr[0 .. bytes] : null;
        }
        return null;
    }

    /**
    Allocates the requested `bytes` of memory with specified `alignment`.
    Directs the call to either one of the $(D buckets) allocators. Defined only
    if `Allocator` defines `alignedAllocate`.
    */
    static if (hasMember!(Allocator, "alignedAllocate"))
    void[] alignedAllocate(size_t bytes, uint alignment)
    {
        if (!bytes) return null;
        if (auto a = allocatorFor(bytes))
        {
            const actual = goodAllocSize(bytes);
            auto result = a.alignedAllocate(actual, alignment);
            return result !is null ? (() @trusted => (&result[0])[0 .. bytes])() : null;
        }
        return null;
    }

    /**
    This method allows expansion within the respective bucket range. It succeeds
    if both $(D b.length) and $(D b.length + delta) fall in a range of the form
    $(D [min + k * step, min + (k + 1) * step - 1]).
    */
    bool expand(ref void[] b, size_t delta)
    {
        if (!b || delta == 0) return delta == 0;
        assert(b.length >= min && b.length <= max);
        const available = goodAllocSize(b.length);
        const desired = b.length + delta;
        if (available < desired) return false;
        b = (() @trusted => b.ptr[0 .. desired])();
        return true;
    }

    /**
    This method allows reallocation within the respective bucket range. If both
    $(D b.length) and $(D size) fall in a range of the form $(D [min + k *
    step, min + (k + 1) * step - 1]), then reallocation is in place. Otherwise,
    reallocation with moving is attempted.
    */
    bool reallocate(ref void[] b, size_t size)
    {
        if (size == 0)
        {
            deallocate(b);
            b = null;
            return true;
        }
        if (size >= b.length && expand(b, size - b.length))
        {
            return true;
        }
        assert(b.length >= min && b.length <= max);
        if (goodAllocSize(size) == goodAllocSize(b.length))
        {
            b = b.ptr[0 .. size];
            return true;
        }
        // Move cross buckets
        return common.reallocate(this, b, size);
    }

    /**
    Similar to `reallocate`, with alignment. Defined only if `Allocator`
    defines `alignedReallocate`.
    */
    static if (hasMember!(Allocator, "alignedReallocate"))
    bool alignedReallocate(ref void[] b, size_t size, uint a)
    {
        if (size == 0)
        {
            deallocate(b);
            b = null;
            return true;
        }
        if (size >= b.length && b.ptr.alignedAt(a) && expand(b, size - b.length))
        {
            return true;
        }
        assert(b.length >= min && b.length <= max);
        if (goodAllocSize(size) == goodAllocSize(b.length) && b.ptr.alignedAt(a))
        {
            b = b.ptr[0 .. size];
            return true;
        }
        // Move cross buckets
        return common.alignedReallocate(this, b, size, a);
    }

    /**
    Defined only if `Allocator` defines `owns`. Finds the owner of `b` and forwards the call to it.
    */
    static if (hasMember!(Allocator, "owns"))
    Ternary owns(void[] b)
    {
        if (!b.ptr) return Ternary.no;
        if (auto a = allocatorFor(b.length))
        {
            const actual = goodAllocSize(b.length);
            return a.owns(b.ptr[0 .. actual]);
        }
        return Ternary.no;
    }

    /**
    This method is only defined if $(D Allocator) defines $(D deallocate).
    */
    static if (hasMember!(Allocator, "deallocate"))
    bool deallocate(void[] b)
    {
        if (!b.ptr) return true;
        if (auto a = allocatorFor(b.length))
        {
            a.deallocate(b.ptr[0 .. goodAllocSize(b.length)]);
        }
        return true;
    }

    /**
    This method is only defined if all allocators involved define $(D
    deallocateAll), and calls it for each bucket in turn. Returns `true` if all
    allocators could deallocate all.
    */
    static if (hasMember!(Allocator, "deallocateAll"))
    bool deallocateAll()
    {
        bool result = true;
        foreach (ref a; buckets)
        {
            if (!a.deallocateAll()) result = false;
        }
        return result;
    }

    /**
    This method is only defined if all allocators involved define $(D
    resolveInternalPointer), and tries it for each bucket in turn.
    */
    static if (hasMember!(Allocator, "resolveInternalPointer"))
    Ternary resolveInternalPointer(const void* p, ref void[] result)
    {
        foreach (ref a; buckets)
        {
            Ternary r = a.resolveInternalPointer(p, result);
            if (r == Ternary.yes) return r;
        }
        return Ternary.no;
    }
}

///
@system unittest
{
    import std.algorithm.comparison : max;
    import std.experimental.allocator.building_blocks.allocator_list : AllocatorList;
    import std.experimental.allocator.building_blocks.free_list : FreeList;
    import std.experimental.allocator.building_blocks.region : Region;
    import std.experimental.allocator.common : unbounded;
    import std.experimental.allocator.mallocator : Mallocator;
    import std.typecons : Ternary;
    Bucketizer!(
        FreeList!(
            AllocatorList!(
                (size_t n) => Region!Mallocator(max(n, 1024 * 1024))),
            0, unbounded),
        65, 512, 64) a;
    auto b = a.allocate(400);
    assert(b.length == 400);
    assert(a.owns(b) == Ternary.yes);
    a.deallocate(b);
}

@system unittest
{
    import std.algorithm.comparison : max;
    import std.experimental.allocator.building_blocks.allocator_list : AllocatorList;
    import std.experimental.allocator.building_blocks.free_list : FreeList;
    import std.experimental.allocator.building_blocks.region : Region;
    import std.experimental.allocator.common : unbounded;
    import std.experimental.allocator.mallocator : Mallocator;
    import std.typecons : Ternary;

    Bucketizer!(
        FreeList!(
            AllocatorList!(
                (size_t n) => Region!Mallocator(max(n, 1024 * 1024)), Mallocator),
            0, unbounded),
        65, 512, 64) a;

    assert((() pure nothrow @safe @nogc => a.goodAllocSize(65))() == 128);

    auto b = a.allocate(100);
    assert(b.length == 100);
    // Make reallocate use extend
    assert((() nothrow @nogc => a.reallocate(b, 101))());
    assert(b.length == 101);
    // Move cross buckets
    assert((() nothrow @nogc => a.reallocate(b, 200))());
    assert(b.length == 200);
    // Free through realloc
    assert((() nothrow @nogc => a.reallocate(b, 0))());
    assert(b is null);
    // Ensure deallocate inherits from parent allocators
    assert((() nothrow @nogc => a.deallocate(b))());
    assert((() nothrow @nogc => a.deallocateAll())());
}

// Test alignedAllocate
@system unittest
{
    import std.experimental.allocator.building_blocks.bitmapped_block : BitmappedBlock;
    import std.experimental.allocator.gc_allocator : GCAllocator;

    Bucketizer!(BitmappedBlock!(64, 8, GCAllocator), 65, 512, 64) a;
    foreach (ref bucket; a.buckets)
    {
        bucket = BitmappedBlock!(64, 8, GCAllocator)(new ubyte[1024]);
    }

    auto b = a.alignedAllocate(100, 16);
    assert(b.length == 100);
    assert(a.alignedAllocate(42, 16) is null);
    assert(a.alignedAllocate(0, 16) is null);
    assert((() pure nothrow @safe @nogc => a.expand(b, 0))());
    assert(b.length == 100);
    assert((() pure nothrow @safe @nogc => a.expand(b, 28))());
    assert(b.length == 128);
    assert((() pure nothrow @safe @nogc => !a.expand(b, 1))());
}

@system unittest
{
    import std.experimental.allocator.building_blocks.bitmapped_block : BitmappedBlock;
    import std.experimental.allocator.gc_allocator : GCAllocator;

    Bucketizer!(BitmappedBlock!(64, 8, GCAllocator), 1, 512, 64) a;
    foreach (ref bucket; a.buckets)
    {
        bucket = BitmappedBlock!(64, 8, GCAllocator)(new ubyte[1024]);
    }

    auto b = a.alignedAllocate(1, 4);
    assert(b.length == 1);
    // Make reallocate use extend
    assert(a.alignedReallocate(b, 11, 4));
    assert(b.length == 11);
    // Make reallocate use use realloc because of alignment change
    assert(a.alignedReallocate(b, 21, 16));
    assert(b.length == 21);
    // Make reallocate use extend
    assert(a.alignedReallocate(b, 22, 16));
    assert(b.length == 22);
    // Move cross buckets
    assert(a.alignedReallocate(b, 101, 16));
    assert(b.length == 101);
    // Free through realloc
    assert(a.alignedReallocate(b, 0, 16));
    assert(b is null);
}
