/**
For testing only.
Used with the dummy ranges for testing higher order ranges.
*/
module std.internal.test.dummyrange;

import std.meta;
import std.range.primitives;
import std.typecons;

enum RangeType
{
    Input,
    Forward,
    Bidirectional,
    Random
}

enum Length
{
    Yes,
    No
}

enum ReturnBy
{
    Reference,
    Value
}

import std.traits : isArray;

// Range that's useful for testing other higher order ranges,
// can be parametrized with attributes.  It just dumbs down an array of
// numbers 1 .. 10.
struct DummyRange(ReturnBy _r, Length _l, RangeType _rt, T = uint[])
if (isArray!T)
{
    private static immutable uinttestData =
        [1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U];
    // These enums are so that the template params are visible outside
    // this instantiation.
    enum r = _r;
    enum l = _l;
    enum rt = _rt;

    static if (is(T == uint[]))
    {
        T arr = uinttestData;
    }
    else
    {
        T arr;
    }

    alias RetType = ElementType!(T);
    alias RetTypeNoAutoDecoding = ElementEncodingType!(T);

    void reinit()
    {
        // Workaround for DMD bug 4378
        static if (is(T == uint[]))
        {
            arr = uinttestData.dup;
        }
    }

    void popFront()
    {
        arr = arr[1..$];
    }

    @property bool empty() const
    {
        return arr.length == 0;
    }

    static if (r == ReturnBy.Reference)
    {
        @property ref inout(RetType) front() inout
        {
            return arr[0];
        }
    }
    else
    {
        @property RetType front() const
        {
            return arr[0];
        }

        @property void front(RetTypeNoAutoDecoding val)
        {
            arr[0] = val;
        }
    }

    static if (rt >= RangeType.Forward)
    {
        @property typeof(this) save()
        {
            return this;
        }
    }

    static if (rt >= RangeType.Bidirectional)
    {
        void popBack()
        {
            arr = arr[0..$ - 1];
        }

        static if (r == ReturnBy.Reference)
        {
            @property ref inout(RetType) back() inout
            {
                return arr[$ - 1];
            }
        }
        else
        {
            @property RetType back() const
            {
                return arr[$ - 1];
            }

            @property void back(RetTypeNoAutoDecoding val)
            {
                arr[$ - 1] = val;
            }
        }
    }

    static if (rt >= RangeType.Random)
    {
        static if (r == ReturnBy.Reference)
        {
            ref inout(RetType) opIndex(size_t index) inout
            {
                return arr[index];
            }
        }
        else
        {
            RetType opIndex(size_t index) const
            {
                return arr[index];
            }

            RetType opIndexAssign(RetTypeNoAutoDecoding val, size_t index)
            {
                return arr[index] = val;
            }

            RetType opIndexOpAssign(string op)(RetTypeNoAutoDecoding value, size_t index)
            {
                mixin("return arr[index] " ~ op ~ "= value;");
            }

            RetType opIndexUnary(string op)(size_t index)
            {
                mixin("return " ~ op ~ "arr[index];");
            }
        }

        typeof(this) opSlice(size_t lower, size_t upper)
        {
            auto ret = this;
            ret.arr = arr[lower .. upper];
            return ret;
        }

        typeof(this) opSlice()
        {
            return this;
        }
    }

    static if (l == Length.Yes)
    {
        @property size_t length() const
        {
            return arr.length;
        }

        alias opDollar = length;
    }
}

enum dummyLength = 10;

alias AllDummyRanges = AliasSeq!(
    DummyRange!(ReturnBy.Reference, Length.Yes, RangeType.Forward),
    DummyRange!(ReturnBy.Reference, Length.Yes, RangeType.Bidirectional),
    DummyRange!(ReturnBy.Reference, Length.Yes, RangeType.Random),
    DummyRange!(ReturnBy.Reference, Length.No, RangeType.Forward),
    DummyRange!(ReturnBy.Reference, Length.No, RangeType.Bidirectional),
    DummyRange!(ReturnBy.Value, Length.Yes, RangeType.Input),
    DummyRange!(ReturnBy.Value, Length.Yes, RangeType.Forward),
    DummyRange!(ReturnBy.Value, Length.Yes, RangeType.Bidirectional),
    DummyRange!(ReturnBy.Value, Length.Yes, RangeType.Random),
    DummyRange!(ReturnBy.Value, Length.No, RangeType.Input),
    DummyRange!(ReturnBy.Value, Length.No, RangeType.Forward),
    DummyRange!(ReturnBy.Value, Length.No, RangeType.Bidirectional)
);

template AllDummyRangesType(T)
{
    alias AllDummyRangesType = AliasSeq!(
        DummyRange!(ReturnBy.Reference, Length.Yes, RangeType.Forward, T),
        DummyRange!(ReturnBy.Reference, Length.Yes, RangeType.Bidirectional, T),
        DummyRange!(ReturnBy.Reference, Length.Yes, RangeType.Random, T),
        DummyRange!(ReturnBy.Reference, Length.No, RangeType.Forward, T),
        DummyRange!(ReturnBy.Reference, Length.No, RangeType.Bidirectional, T),
        DummyRange!(ReturnBy.Value, Length.Yes, RangeType.Input, T),
        DummyRange!(ReturnBy.Value, Length.Yes, RangeType.Forward, T),
        DummyRange!(ReturnBy.Value, Length.Yes, RangeType.Bidirectional, T),
        DummyRange!(ReturnBy.Value, Length.Yes, RangeType.Random, T),
        DummyRange!(ReturnBy.Value, Length.No, RangeType.Input, T),
        DummyRange!(ReturnBy.Value, Length.No, RangeType.Forward, T),
        DummyRange!(ReturnBy.Value, Length.No, RangeType.Bidirectional, T)
    );
}

/**
Tests whether forward, bidirectional and random access properties are
propagated properly from the base range(s) R to the higher order range
H.  Useful in combination with DummyRange for testing several higher
order ranges.
*/
template propagatesRangeType(H, R...)
{
    static if (allSatisfy!(isRandomAccessRange, R))
        enum bool propagatesRangeType = isRandomAccessRange!H;
    else static if (allSatisfy!(isBidirectionalRange, R))
        enum bool propagatesRangeType = isBidirectionalRange!H;
    else static if (allSatisfy!(isForwardRange, R))
        enum bool propagatesRangeType = isForwardRange!H;
    else
        enum bool propagatesRangeType = isInputRange!H;
}

template propagatesLength(H, R...)
{
    static if (allSatisfy!(hasLength, R))
        enum bool propagatesLength = hasLength!H;
    else
        enum bool propagatesLength = !hasLength!H;
}

/**
Reference type input range
*/
class ReferenceInputRange(T)
{
    import std.array : array;

    this(Range)(Range r)
    if (isInputRange!Range) {_payload = array(r);}
    final @property ref T front() {return _payload.front;}
    final void popFront() {_payload.popFront();}
    final @property bool empty() {return _payload.empty;}
    protected T[] _payload;
}

/**
Infinite input range
*/
class ReferenceInfiniteInputRange(T)
{
    this(T first = T.init) {_val = first;}
    final @property T front() {return _val;}
    final void popFront() {++_val;}
    enum bool empty = false;
    protected T _val;
}

/**
Reference forward range
*/
class ReferenceForwardRange(T) : ReferenceInputRange!T
{
    this(Range)(Range r)
    if (isInputRange!Range) {super(r);}
    final @property auto save(this This)() {return new This( _payload);}
}

/**
Infinite forward range
*/
class ReferenceInfiniteForwardRange(T) : ReferenceInfiniteInputRange!T
{
    this(T first = T.init) {super(first);}
    final @property ReferenceInfiniteForwardRange save()
    {return new ReferenceInfiniteForwardRange!T(_val);}
}

/**
Reference bidirectional range
*/
class ReferenceBidirectionalRange(T) : ReferenceForwardRange!T
{
    this(Range)(Range r)
    if (isInputRange!Range) {super(r);}
    final @property ref T back() {return _payload.back;}
    final void popBack() {_payload.popBack();}
}

@safe unittest
{
    static assert(isInputRange!(ReferenceInputRange!int));
    static assert(isInputRange!(ReferenceInfiniteInputRange!int));

    static assert(isForwardRange!(ReferenceForwardRange!int));
    static assert(isForwardRange!(ReferenceInfiniteForwardRange!int));

    static assert(isBidirectionalRange!(ReferenceBidirectionalRange!int));
}

private:

pure struct Cmp(T)
if (is(T == uint))
{
    static auto iota(size_t low = 1, size_t high = 11)
    {
        import std.range : iota;
        return iota(cast(uint) low, cast(uint) high);
    }

    static void initialize(ref uint[] arr)
    {
        import std.array : array;
        arr = iota().array;
    }

    static bool function(uint,uint) cmp = function(uint a, uint b) { return a == b; };

    enum dummyValue = 1337U;
    enum dummyValueRslt = 1337U * 2;
}

pure struct Cmp(T)
if (is(T == double))
{
    import std.math.operations : isClose;

    static auto iota(size_t low = 1, size_t high = 11)
    {
        import std.range : iota;
        return iota(cast(double) low, cast(double) high, 1.0);
    }

    static void initialize(ref double[] arr)
    {
        import std.array : array;
        arr = iota().array;
    }

    alias cmp = isClose!(double,double,double);

    enum dummyValue = 1337.0;
    enum dummyValueRslt = 1337.0 * 2.0;
}

struct TestFoo
{
    int a;

    bool opEquals(const ref TestFoo other) const
    {
        return this.a == other.a;
    }

    TestFoo opBinary(string op)(TestFoo other)
    {
        TestFoo ret = this;
        mixin("ret.a " ~ op ~ "= other.a;");
        return ret;
    }

    TestFoo opOpAssign(string op)(TestFoo other)
    {
        mixin("this.a " ~ op ~ "= other.a;");
        return this;
    }
}

pure struct Cmp(T)
if (is(T == TestFoo))
{
    static auto iota(size_t low = 1, size_t high = 11)
    {
        import std.algorithm.iteration : map;
        import std.range : iota;
        return iota(cast(int) low, cast(int) high).map!(a => TestFoo(a));
    }

    static void initialize(ref TestFoo[] arr)
    {
        import std.array : array;
        arr = iota().array;
    }

    static bool function(TestFoo,TestFoo) cmp = function(TestFoo a, TestFoo b)
    {
        return a.a == b.a;
    };

    @property static TestFoo dummyValue()
    {
        return TestFoo(1337);
    }

    @property static TestFoo dummyValueRslt()
    {
        return TestFoo(1337 * 2);
    }
}

@system unittest
{
    import std.algorithm.comparison : equal;
    import std.range : iota, retro, repeat;

    static void testInputRange(T,Cmp)()
    {
        T it;
        Cmp.initialize(it.arr);
        for (size_t numRuns = 0; numRuns < 2; ++numRuns)
        {
            if (numRuns == 1)
            {
                static if (is(immutable ElementType!(T) == immutable uint))
                {
                    it.reinit();
                }

                Cmp.initialize(it.arr);
            }

            assert(equal!(Cmp.cmp)(it, Cmp.iota(1, 11)));

            static if (hasLength!T)
            {
                assert(it.length == 10);
            }

            assert(!Cmp.cmp(it.front, Cmp.dummyValue));
            auto s = it.front;
            it.front = Cmp.dummyValue;
            assert(Cmp.cmp(it.front, Cmp.dummyValue));
            it.front = s;

            auto cmp = Cmp.iota(1,11);

            size_t jdx = 0;
            while (!it.empty && !cmp.empty)
            {
                static if (hasLength!T)
                {
                    assert(it.length == 10 - jdx);
                }

                assert(Cmp.cmp(it.front, cmp.front));
                it.popFront();
                cmp.popFront();

                ++jdx;
            }

            assert(it.empty);
            assert(cmp.empty);
        }

    }

    static void testForwardRange(T,Cmp)()
    {
        T it;
        Cmp.initialize(it.arr);
        auto s = it.save();
        s.popFront();
        assert(!Cmp.cmp(s.front, it.front));
    }

    static void testBidirectionalRange(T,Cmp)()
    {
        T it;
        Cmp.initialize(it.arr);
        assert(equal!(Cmp.cmp)(it.retro, Cmp.iota().retro));

        auto s = it.back;
        assert(!Cmp.cmp(s, Cmp.dummyValue));
        it.back = Cmp.dummyValue;
        assert( Cmp.cmp(it.back, Cmp.dummyValue));
        it.back = s;
    }

    static void testRandomAccessRange(T,Cmp)()
    {
        T it;
        Cmp.initialize(it.arr);
        size_t idx = 0;
        foreach (jt; it)
        {
            assert(it[idx] == jt);

            T copy = it[idx .. $];
            auto cmp = Cmp.iota(idx + 1, it.length + 1);
            assert(equal!(Cmp.cmp)(copy, cmp));

            ++idx;
        }

        {
            auto copy = it;
            copy.arr = it.arr.dup;
            for (size_t i = 0; i < copy.length; ++i)
            {
                copy[i] = Cmp.dummyValue;
                copy[i] += Cmp.dummyValue;
            }
            assert(equal!(Cmp.cmp)(copy, Cmp.dummyValueRslt.repeat(copy.length)));
        }

        static if (it.r == ReturnBy.Reference)
        {
            T copy;
            copy.arr = it.arr.dup;
            for (size_t i = 0; i < copy.length; ++i)
            {
                copy[i] = Cmp.dummyValue;
                copy[i] += Cmp.dummyValue;
            }

            assert(equal!(Cmp.cmp)(copy, Cmp.dummyValueRslt.repeat(copy.length)));
        }
    }

    import std.meta : AliasSeq;

    static foreach (S; AliasSeq!(uint, double, TestFoo))
    {
        foreach (T; AllDummyRangesType!(S[]))
        {
            testInputRange!(T,Cmp!S)();

            static if (isForwardRange!T)
            {
                testForwardRange!(T,Cmp!S)();
            }

            static if (isBidirectionalRange!T)
            {
                testBidirectionalRange!(T,Cmp!S)();
            }

            static if (isRandomAccessRange!T)
            {
                testRandomAccessRange!(T,Cmp!S)();
            }
        }
    }
}
