/*
This is experimental code still:
- the code is raw still
- it lacks commentes
- it does the bare minimum (append one item and extract the array),
  extend is absent still
- unit tests are messy still
*/


import std.stdio: put = writef, putr = writefln;
import std.gc: gcmalloc = malloc, gcrealloc = realloc, hasNoPointers;
import std.conv: toInt;


//---------------------------------------------------------------
// Correct starting point
class ArrayBuilderBasic(T) {
    private T[] data;

    void opCatAssign(T x) {
        this.data ~= x;
    }

    T[] toarray() {
        return this.data;
    }
}


//---------------------------------------------------------------
// useless naive version
class ArrayBuilderNaive(T) {
    private T* data;
    private int len;

    this() {
        this.len = 0;
        this.data = cast(T*)malloc(this.len * T.sizeof);
    }

    void opCatAssign(T x) {
        this.len++;
        this.data = cast(T*)realloc(this.data, this.len * T.sizeof);
        this.data[this.len - 1] = x;
    }

    T[] toarray() {
        return this.data[0 .. this.len];
    }
}


//=============================================================
// Minimal potentially usabe version

import std.traits: FieldTypeTuple;

class ArgumentException: Exception {
    this(string msg) {
        super(msg);
    }
}

template IsType(T, Types...) {
    // Original idea by Burton Radons, modified
    static if (Types.length == 0)
        const bool IsType = false;
    else
        const bool IsType = is(T == Types[0]) || IsType!(T, Types[1 .. $]);
}


// HasReferences and IsReferenceType may be merged in a single template
template HasReferences(Types...) {
    static if (Types.length == 0)
        const bool HasReferences = false;
    else
        const bool HasReferences = IsReferenceType!(Types[0]) |
                                   HasReferences!(Types[1 .. $]);
}

template IsReferenceType(T) {
    static if (IsType!(T, bool, byte, ubyte, short, ushort, int, uint, long,
                          ulong, float, double, real, ifloat, idouble, ireal,
                          cfloat, cdouble, creal, char, dchar, wchar) ) {
        const bool IsReferenceType = false;
    } else static if ( is(T == struct) ) {
        const bool IsReferenceType = HasReferences!(FieldTypeTuple!(T));
    } else
        const bool IsReferenceType = true;
}


template IsArray(T) {
    const bool IsArray = is(typeof(T.length)) && is(typeof(T.sort)) &&
                         is(typeof(T.reverse)) && is(typeof(T.dup));
}

template IsDynamicArray(T) {
    // Adapted from the module tango.core.Traits, (C) 2005-2006 Sean Kelly, BSD style licence
    const bool IsDynamicArray = is( typeof(T.init[0])[] == T );
}

template IsStaticArray(T) {
    const bool IsStaticArray = IsArray!(T) && (!IsDynamicArray!(T));
}

///
static struct ArrayBuilder(T) {
    private T* data;
    private int len, _capacity;

    void opCatAssign(T x) {
        if (this.len >= this._capacity) {
            int old_capacity = this._capacity;

            if (this._capacity == 0)
                this._capacity = 4; // starting point not too much small
            else if (this._capacity < (1 << 26)) // 67_108_864
                this._capacity *= 2; // for amortized O(1) and less RAM fragmentation
            else
                this._capacity *= 1.3; // slower growing rate to save RAM

            if (old_capacity != 0)
                this.data = cast(T*)gcrealloc(this.data, this._capacity * T.sizeof);
            else
                this.data = cast(T*)gcmalloc(this._capacity * T.sizeof);

            // to not leave random dangling pointers to
            // the GC if T contains references or pointers
            static if (IsReferenceType!(T)) {
                static if (IsStaticArray!(T)) {
                    // This can't be used, DMD BUG:
                    // this.data[old_capacity .. this._capacity][] = T.init;
                    T Tinit;
                    this.data[old_capacity .. this._capacity][] = Tinit;
                } else
                    this.data[old_capacity .. this._capacity] = T.init;
            } else {
                // is this necessary at every realloc or doing it once is enough?
                hasNoPointers(this.data);
            }
        }

        static if (IsStaticArray!(T))
            this.data[this.len][] = x;
        else
            this.data[this.len] = x;

        this.len++;
    }

    // void opCatAssign(T[] arr) { }  // TODO
    // generic iterable, the following code must be moved inside
    // opCatAssign, and managed with static ifs, this will make things
    // quite hairy
    // void opCatAssign(TyIter)(TyIter iter) { ... } // TODO

    ///
    void clear(bool reclaim=true) {
        // the 'reclaim' argument may be overkill, maybe only one of the
        // two possibilities can be kept

        if (reclaim) {
            gcrealloc(this.data, 0);
            this.data = null;
            this._capacity = 0;
        } else {
            static if (IsReferenceType!(T)) {
                static if (IsStaticArray!(T)) {
                    // This can't be used, DMD BUG:
                    // this.data[old_capacity .. this._capacity][] = T.init;
                    T Tinit;
                    this.data[0 .. this._capacity][] = Tinit;
                } else
                    this.data[0 .. this._capacity] = T.init;
            }
        }

        this.len = 0;
    }

    ///
    int length() {
        return this.len;
    }

    //void length(int newlen) { } // TODO

    ///
    int capacity() {
        return this._capacity;
    }

    //void capacity(int newlen) { } // TODO

    ///
    T[] toarray() {
        return this.data[0 .. this.len];
    }
} // end ArrayBuilder!()



unittest { // tests of ArrayBuilder!()
    //alias ArrayBuilderBasic AB;
    //alias ArrayBuilderNaive AB;
    alias ArrayBuilder AB;

    // First test ------------------------------------
    AB!(int) ab1;
    assert(ab1.toarray == new int[0]);
    ab1 ~= 1;
    ab1 ~= 3;
    ab1 ~= 5;
    assert(ab1.toarray == [1, 3, 5]);
    ab1.clear;
    assert(ab1.toarray == new int[0]);

    auto array1 = new int[1000];
    foreach (i, ref el; array1) {
        ab1 ~= i;
        el = i;
    }
    assert(array1 == ab1.toarray);



    // Second test ------------------------------------
    class Foo {
        int i;
        this(int i) { this.i = i; }
        int opEquals(Foo o) { /* putr("* ", this.i, " ", o.i); */ return this.i == o.i; }
    }

    AB!(Foo) ab2;
    assert(ab2.toarray == new Foo[0]);
    ab2 ~= new Foo(1);
    ab2 ~= new Foo(3);
    ab2 ~= new Foo(5);
    Foo[] r1 = [new Foo(1), new Foo(3), new Foo(5)];
    //putr(typeid(typeof(ab2.toarray)));
    //putr(typeid(typeof(ab2.toarray()))); // their type is different!
    //assert(ab2.toarray == r1); // doesn't work! I don't know why
    foreach (i, el; ab2.toarray())
        assert(el == r1[i]);

    ab2.clear;
    assert(ab2.toarray == new Foo[0]);
    auto array2 = new Foo[1000];
    foreach (i, ref el; array2) {
        ab2 ~= new Foo(i);
        el = new Foo(i);
    }

    //assert(array2 == ab2.toarray);  // doesn't work! I don't know why
    foreach (i, el; ab2.toarray())
        assert(el == array2[i]);


    // Third test ------------------------------------
    AB!(int[2]) ab3;
    assert(ab3.toarray == new int[2][0]);
    ab3 ~= [1, 2];
    ab3 ~= [3, 4];
    ab3 ~= [5, 6];
    auto r2 = ab3.toarray;
    assert(r2[0] == [1, 2]);
    assert(r2[1] == [3, 4]);
    assert(r2[2] == [5, 6]);

    ab3.clear;
    assert(ab3.toarray == new int[2][0]);
    auto array3 = new int[2][](1000);

    foreach (i, ref el; array3) {
        ab3 ~= [i, i];
        el[] = [i, i];
    }
    foreach (i, el; ab3.toarray())
        assert(el == array3[i]);
} // end tests of AB!()



unittest { printf(__FILE__ " unittest performed.\n"); }


// benchmarks ====================================================

import std.string: join, toString;
import std.math: abs;


version (Win32) {
    import std.c.windows.windows: QueryPerformanceCounter, QueryPerformanceFrequency;

    /*********************************
    Return the seconds (a double) from the start of the program.
    */
    double clock() {
        long t;
        QueryPerformanceCounter(&t);

        return cast(double)t / queryPerformanceFrequency;
    }

    long queryPerformanceFrequency;

    static this() {
        QueryPerformanceFrequency(&queryPerformanceFrequency);
    }
}

version (linux) { // this may give too few decimals?
    import std.c.linux.linux: time;

    /// ditto
    double clock() {
        return cast(double)time(null);
    }
}


string thousands(TyIntegral)(TyIntegral n, string separator="_") {
    string ns = toString(abs(n)).reverse;
    string[] parts;
    for (int i = 0; i < ns.length; i += 3)
        parts ~= ns[i .. (i+3) < length ? i+3 : length];
    return (n < 0 ? "-" : "") ~ parts.join(separator).reverse;
}


void benchmark1(alias AB)(int ntest, int N=2_000_000) {
    putr("\nbenchmark 1, N=", thousands(N), ":");

    if (!ntest) {
        auto t0 = clock();
        int[] a1;
        for (int i; i < N; i++)
            a1 ~= i;
        auto last1 = a1[$ - 1];
        auto t2 = clock() - t0;
        putr("  Array append: %.3f", t2, " s   ", last1);
    } else {
        auto t0 = clock();
        AB!(int) a2;
        for (int i; i < N; i++)
            a2 ~= i;
        auto last2 = a2.toarray[$ - 1];
        auto t2 = clock() - t0;
        putr("  AB: %.3f", t2, " s   ", last2);
    }
}


void benchmark2(alias AB)(int ntest, int N=2_000_000) {
    putr("\nbenchmark 2, N=", thousands(N), ":");

    if (!ntest) {
        auto t0 = clock();
        int[] a1a;
        int[] a1b;
        for (int i; i < N; i++) {
            a1a ~= i;
            a1b ~= i;
        }
        auto last1a = a1a[$ - 1];
        auto last1b = a1b[$ - 1];
        auto t2 = clock() - t0;
        putr("  Array append: %.3f", t2, " s   ", last1a, " ", last1b);
    } else {
        auto t0 = clock();
        AB!(int) a2a;
        AB!(int) a2b;
        for (int i; i < N; i++) {
            a2a ~= i;
            a2b ~= i;
        }
        auto last2a = a2a.toarray[$ - 1];
        auto last2b = a2b.toarray[$ - 1];
        auto t2 = clock() - t0;
        putr("  AB: %.3f", t2, " s   ", last2a, " ", last2b);
    }
}


void benchmark3(alias AB)(int ntest, int N=2_000_000) {
    putr("\nbenchmark 3, N=", thousands(N), ":");

    class Foo {
        int i;
        this(int i) { this.i = i; }
    }

    if (!ntest) {
        auto t0 = clock();
        Foo[] a1;
        for (int i; i < N; i++)
            a1 ~= new Foo(i);
        auto last1 = a1[$ - 1];
        auto t2 = clock() - t0;
        putr("  Array append: %.3f", t2, " s   ", last1.i);
    } else {
        auto t0 = clock();
        AB!(Foo) a2;
        for (int i; i < N; i++)
            a2 ~= new Foo(i);
        auto last2 = a2.toarray[$ - 1];
        auto t2 = clock() - t0;
        putr("  AB: %.3f", t2, " s   ", last2.i);
    }
}


void benchmark4(alias AB)(int ntest, int N=2_000_000) {
    putr("\nbenchmark 4, N=", thousands(N), ":");

    class Foo {
        int i;
        this(int i) { this.i = i; }
    }

    if (!ntest) {
        auto t0 = clock();
        Foo[] a1a;
        Foo[] a1b;
        for (int i; i < N; i++) {
            a1a ~= new Foo(i);
            a1b ~= new Foo(i);
        }
        auto last1a = a1a[$ - 1];
        auto last1b = a1b[$ - 1];
        auto t2 = clock() - t0;
        putr("  Array append: %.3f", t2, " s   ", last1a.i, " ", last1b.i);
    } else {
        auto t0 = clock();
        AB!(Foo) a2a;
        AB!(Foo) a2b;
        for (int i; i < N; i++) {
            a2a ~= new Foo(i);
            a2b ~= new Foo(i);
        }
        auto last2a = a2a.toarray[$ - 1];
        auto last2b = a2b.toarray[$ - 1];
        auto t2 = clock() - t0;
        putr("  AB: %.3f", t2, " s   ", last2a.i, " ", last2b.i);
    }
}




void main(string[] args) {
    int bench = args.length >= 2 ? toInt(args[1]) : 1;
    int ntest = args.length >= 3 ? toInt(args[2]) : 0;
    int N = args.length >= 4 ? toInt(args[3]) : 10_000;

    //alias ArrayBuilderBasic AB;
    //alias ArrayBuilderNaive AB;
    alias ArrayBuilder AB;

    switch (bench) {
        case 1: benchmark1!(AB)(ntest, N); break;
        case 2: benchmark2!(AB)(ntest, N); break;
        case 3: benchmark3!(AB)(ntest, N); break;
        case 4: benchmark4!(AB)(ntest, N); break;
        default: throw new Error("wrong bench number");
    }
}

/*
Timings, on a Core Duo @ 2 GHz, 2 GB RAM:

benchmark 1, N=2_000_000:
  Array append: 0.160 s
  ArrayBuilder: 0.026 s
benchmark 1, N=20_000_000:
  Array append: 1.837 s
  ArrayBuilder: 0.325 s
benchmark 1, N=50_000_000:
  Array append: 4.489 s
  ArrayBuilder: 0.731 s
benchmark 1, N=200_000_000:
  Array append: 32.293 s
  ArrayBuilder: Out of memory

benchmark 2, N=200_000:
  Array append: 0.099 s
  ArrayBuilder: 0.004 s
benchmark 2, N=2_000_000:
  Array append: 6.896 s
  ArrayBuilder: 0.050 s

benchmark 3, N=200_000:
  Array append: 0.090 s
  ArrayBuilder: 0.076 s
benchmark 3, N=2_000_000:
  Array append: 1.014 s
  ArrayBuilder: 0.923 s (why is this so slow?)
benchmark 3, N=6_000_000:
  Array append: 5.295 s
  ArrayBuilder: 4.431 s

benchmark 4, N=500_000:
  Array append: 1.109 s
  ArrayBuilder: 0.414 s
benchmark 4, N=1_000_000:
  Array append: 3.103 s
  ArrayBuilder: 0.962 s

*/

// -O -release -inline