/**
This module defines `TypedAllocator`, a statically-typed allocator that
aggregates multiple untyped allocators and uses them depending on the static
properties of the types allocated. For example, distinct allocators may be used
for thread-local vs. thread-shared data, or for fixed-size data (`struct`,
`class` objects) vs. resizable data (arrays).

Macros:
T2=$(TR <td style="text-align:left">$(D $1)</td> $(TD $(ARGS $+)))
*/

module std.experimental.allocator.typed;

import std.experimental.allocator;
import std.experimental.allocator.common;
import std.traits : isPointer, hasElaborateDestructor;
import std.typecons : Flag, Yes, No;
import std.range : isInputRange, isForwardRange, walkLength, save, empty,
    front, popFront;

/**
Allocation-related flags dictated by type characteristics. `TypedAllocator`
deduces these flags from the type being allocated and uses the appropriate
allocator accordingly.
*/
enum AllocFlag : uint
{
    init = 0,
    /**
    Fixed-size allocation (unlikely to get reallocated later). Examples: `int`,
    `double`, any `struct` or `class` type. By default it is assumed that the
    allocation is variable-size, i.e. susceptible to later reallocation
    (for example all array types). This flag is advisory, i.e. in-place resizing
    may be attempted for `fixedSize` allocations and may succeed. The flag is
    just a hint to the compiler it may use allocation strategies that work well
    with objects of fixed size.
    */
    fixedSize = 1,
    /**
    The type being allocated embeds no pointers. Examples: `int`, `int[]`, $(D
    Tuple!(int, float)). The implicit conservative assumption is that the type
    has members with indirections so it needs to be scanned if garbage
    collected. Example of types with pointers: `int*[]`, $(D Tuple!(int,
    string)).
    */
    hasNoIndirections = 4,
    /**
    By default it is conservatively assumed that allocated memory may be `cast`
    to `shared`, passed across threads, and deallocated in a different thread
    than the one that allocated it. If that's not the case, there are two
    options. First, `immutableShared` means the memory is allocated for
    `immutable` data and will be deallocated in the same thread it was
    allocated in. Second, `threadLocal` means the memory is not to be shared
    across threads at all. The two flags cannot be simultaneously present.
    */
    immutableShared = 8,
    /// ditto
    threadLocal = 16,
}

/**
`TypedAllocator` acts like a chassis on which several specialized allocators
can be assembled. To let the system make a choice about a particular kind of
allocation, use `Default` for the respective parameters.

There is a hierarchy of allocation kinds. When an allocator is implemented for
a given combination of flags, it is used. Otherwise, the next down the list is
chosen.

$(BOOKTABLE ,

$(TR $(TH `AllocFlag` combination) $(TH Description))

$(T2 AllocFlag.threadLocal |$(NBSP)AllocFlag.hasNoIndirections
|$(NBSP)AllocFlag.fixedSize,
This is the most specific allocation policy: the memory being allocated is
thread local, has no indirections at all, and will not be reallocated. Examples
of types fitting this description: `int`, `double`, $(D Tuple!(int, long)), but
not $(D Tuple!(int, string)), which contains an indirection.)

$(T2 AllocFlag.threadLocal |$(NBSP)AllocFlag.hasNoIndirections,
As above, but may be reallocated later. Examples of types fitting this
description are $(D int[]), $(D double[]), $(D Tuple!(int, long)[]), but not
$(D Tuple!(int, string)[]), which contains an indirection.)

$(T2 AllocFlag.threadLocal,
As above, but may embed indirections. Examples of types fitting this
description are $(D int*[]), $(D Object[]), $(D Tuple!(int, string)[]).)

$(T2 AllocFlag.immutableShared |$(NBSP)AllocFlag.hasNoIndirections
|$(NBSP)AllocFlag.fixedSize,
The type being allocated is `immutable` and has no pointers. The thread that
allocated it must also deallocate it. Example: `immutable(int)`.)

$(T2 AllocFlag.immutableShared |$(NBSP)AllocFlag.hasNoIndirections,
As above, but the type may be appended to in the future. Example: `string`.)

$(T2 AllocFlag.immutableShared,
As above, but the type may embed references. Example: `immutable(Object)[]`.)

$(T2 AllocFlag.hasNoIndirections |$(NBSP)AllocFlag.fixedSize,
The type being allocated may be shared across threads, embeds no indirections,
and has fixed size.)

$(T2 AllocFlag.hasNoIndirections,
The type being allocated may be shared across threads, may embed indirections,
and has variable size.)

$(T2 AllocFlag.fixedSize,
The type being allocated may be shared across threads, may embed indirections,
and has fixed size.)

$(T2 0, The most conservative/general allocation: memory may be shared,
deallocated in a different thread, may or may not be resized, and may embed
references.)
)

Params:
PrimaryAllocator = The default allocator.
Policies = Zero or more pairs consisting of an `AllocFlag` and an allocator
type.
*/
struct TypedAllocator(PrimaryAllocator, Policies...)
{
    import std.typecons : Tuple;
    import std.meta : AliasSeq;
    import std.algorithm.sorting : isSorted;

    static assert(Policies.length == 0 || isSorted([Stride2!Policies]));

    private template Stride2(T...)
    {
        static if (T.length >= 2)
        {
            alias Stride2 = AliasSeq!(T[0], Stride2!(T[2 .. $]));
        }
        else
        {
            alias Stride2 = AliasSeq!(T[0 .. $]);
        }
    }

    // state
    static if (stateSize!PrimaryAllocator) private PrimaryAllocator primary;
    else alias primary = PrimaryAllocator.instance;
    static if (Policies.length > 0)
        private Tuple!(Stride2!(Policies[1 .. $])) extras;

    private static bool match(uint have, uint want)
    {
        enum uint maskAway =
            ~(AllocFlag.immutableShared | AllocFlag.threadLocal);
        // Do we offer thread local?
        if (have & AllocFlag.threadLocal)
        {
            if (want & AllocFlag.threadLocal)
                return match(have & maskAway, want & maskAway);
            return false;
        }
        if (have & AllocFlag.immutableShared)
        {
            // Okay to ask for either thread local or immutable shared
            if (want & (AllocFlag.threadLocal
                    | AllocFlag.immutableShared))
                return match(have & maskAway, want & maskAway);
            return false;
        }
        // From here on we have full-blown thread sharing.
        if (have & AllocFlag.hasNoIndirections)
        {
            if (want & AllocFlag.hasNoIndirections)
                return match(have & ~AllocFlag.hasNoIndirections,
                    want & ~AllocFlag.hasNoIndirections);
            return false;
        }
        // Fixed size or variable size both match.
        return true;
    }

    /**
    Given `flags` as a combination of `AllocFlag` values, or a type `T`, returns
    the allocator that's a closest fit in capabilities.
    */
    auto ref allocatorFor(uint flags)()
    {
        static if (Policies.length == 0 || !match(Policies[0], flags))
        {
            return primary;
        }
        else static if (Policies.length && match(Policies[$ - 2], flags))
        {
            return extras[$ - 1];
        }
        else
        {
            foreach (i, choice; Stride2!Policies)
            {
                static if (!match(choice, flags))
                {
                    return extras[i - 1];
                }
            }
            assert(0);
        }
    }

    /// ditto
    auto ref allocatorFor(T)()
    {
        static if (is(T == void[]))
        {
            return primary;
        }
        else
        {
            return allocatorFor!(type2flags!T)();
        }
    }

    /**
    Given a type `T`, returns its allocation-related flags as a combination of
    `AllocFlag` values.
    */
    static uint type2flags(T)()
    {
        uint result;
        static if (is(T == immutable))
            result |= AllocFlag.immutableShared;
        else static if (is(T == shared))
            result |= AllocFlag.forSharing;
        static if (!is(T == U[], U))
            result |= AllocFlag.fixedSize;
        import std.traits : hasIndirections;
        static if (!hasIndirections!T)
            result |= AllocFlag.hasNoIndirections;
        return result;
    }

    /**
    Dynamically allocates (using the appropriate allocator chosen with
    `allocatorFor!T`) and then creates in the memory allocated an object of
    type `T`, using `args` (if any) for its initialization. Initialization
    occurs in the memory allocated and is otherwise semantically the same as
    `T(args)`. (Note that using `make!(T[])` creates a pointer to an
    (empty) array of `T`s, not an array. To allocate and initialize an
    array, use `makeArray!T` described below.)

    Params:
    T = Type of the object being created.
    args = Optional arguments used for initializing the created object. If not
    present, the object is default constructed.

    Returns: If `T` is a class type, returns a reference to the created `T`
    object. Otherwise, returns a `T*` pointing to the created object. In all
    cases, returns `null` if allocation failed.

    Throws: If `T`'s constructor throws, deallocates the allocated memory and
    propagates the exception.
    */
    auto make(T, A...)(auto ref A args)
    {
        return .make!T(allocatorFor!T, args);
    }

    /**
    Create an array of `T` with `length` elements. The array is either
    default-initialized, filled with copies of `init`, or initialized with
    values fetched from `range`.

    Params:
    T = element type of the array being created
    length = length of the newly created array
    init = element used for filling the array
    range = range used for initializing the array elements

    Returns:
    The newly-created array, or `null` if either `length` was `0` or
    allocation failed.

    Throws:
    The first two overloads throw only if the used allocator's primitives do.
    The overloads that involve copy initialization deallocate memory and propagate the exception if the copy operation throws.
    */
    T[] makeArray(T)(size_t length)
    {
        return .makeArray!T(allocatorFor!(T[]), length);
    }

    /// Ditto
    T[] makeArray(T)(size_t length, auto ref T init)
    {
        return .makeArray!T(allocatorFor!(T[]), init, length);
    }

    /// Ditto
    T[] makeArray(T, R)(R range)
    if (isInputRange!R)
    {
        return .makeArray!T(allocatorFor!(T[]), range);
    }

    /**
    Grows `array` by appending `delta` more elements. The needed memory is
    allocated using the same allocator that was used for the array type. The
    extra elements added are either default-initialized, filled with copies of
    `init`, or initialized with values fetched from `range`.

    Params:
    T = element type of the array being created
    array = a reference to the array being grown
    delta = number of elements to add (upon success the new length of `array`
    is $(D array.length + delta))
    init = element used for filling the array
    range = range used for initializing the array elements

    Returns:
    `true` upon success, `false` if memory could not be allocated. In the
    latter case `array` is left unaffected.

    Throws:
    The first two overloads throw only if the used allocator's primitives do.
    The overloads that involve copy initialization deallocate memory and
    propagate the exception if the copy operation throws.
    */
    bool expandArray(T)(ref T[] array, size_t delta)
    {
        return .expandArray(allocatorFor!(T[]), array, delta);
    }
    /// Ditto
    bool expandArray(T)(T[] array, size_t delta, auto ref T init)
    {
        return .expandArray(allocatorFor!(T[]), array, delta, init);
    }
    /// Ditto
    bool expandArray(T, R)(ref T[] array, R range)
    if (isInputRange!R)
    {
        return .expandArray(allocatorFor!(T[]), array, range);
    }

    /**
    Shrinks an array by `delta` elements using `allocatorFor!(T[])`.

    If $(D arr.length < delta), does nothing and returns `false`. Otherwise,
    destroys the last $(D arr.length - delta) elements in the array and then
    reallocates the array's buffer. If reallocation fails, fills the array with
    default-initialized data.

    Params:
    T = element type of the array being created
    arr = a reference to the array being shrunk
    delta = number of elements to remove (upon success the new length of
    `arr` is $(D arr.length - delta))

    Returns:
    `true` upon success, `false` if memory could not be reallocated. In the
    latter case $(D arr[$ - delta .. $]) is left with default-initialized
    elements.

    Throws:
    The first two overloads throw only if the used allocator's primitives do.
    The overloads that involve copy initialization deallocate memory and
    propagate the exception if the copy operation throws.
    */
    bool shrinkArray(T)(ref T[] arr, size_t delta)
    {
        return .shrinkArray(allocatorFor!(T[]), arr, delta);
    }

    /**
    Destroys and then deallocates (using `allocatorFor!T`) the object pointed
    to by a pointer, the class object referred to by a `class` or `interface`
    reference, or an entire array. It is assumed the respective entities had
    been allocated with the same allocator.
    */
    void dispose(T)(T* p)
    {
        return .dispose(allocatorFor!T, p);
    }
    /// Ditto
    void dispose(T)(T p)
    if (is(T == class) || is(T == interface))
    {
        return .dispose(allocatorFor!T, p);
    }
    /// Ditto
    void dispose(T)(T[] array)
    {
        return .dispose(allocatorFor!(T[]), array);
    }
}

///
@system unittest
{
    import std.experimental.allocator.gc_allocator : GCAllocator;
    import std.experimental.allocator.mallocator : Mallocator;
    import std.experimental.allocator.mmap_allocator : MmapAllocator;
    alias MyAllocator = TypedAllocator!(GCAllocator,
        AllocFlag.fixedSize | AllocFlag.threadLocal, Mallocator,
        AllocFlag.fixedSize | AllocFlag.threadLocal
                | AllocFlag.hasNoIndirections,
            MmapAllocator,
    );
    MyAllocator a;
    auto b = &a.allocatorFor!0();
    static assert(is(typeof(*b) == shared GCAllocator));
    enum f1 = AllocFlag.fixedSize | AllocFlag.threadLocal;
    auto c = &a.allocatorFor!f1();
    static assert(is(typeof(*c) == Mallocator));
    enum f2 = AllocFlag.fixedSize | AllocFlag.threadLocal;
    static assert(is(typeof(a.allocatorFor!f2()) == Mallocator));
    // Partial match
    enum f3 = AllocFlag.threadLocal;
    static assert(is(typeof(a.allocatorFor!f3()) == Mallocator));

    int* p = a.make!int;
    scope(exit) a.dispose(p);
    int[] arr = a.makeArray!int(42);
    scope(exit) a.dispose(arr);
    assert(a.expandArray(arr, 3));
    assert(a.shrinkArray(arr, 4));
}
