// Description: Testing for named parameters
// Documentation: named_parameter.txt
#include "test/test_init.h"
#include <pastel/sys/function/identity_function.h>
#include <string>
namespace
{
    enum class Enum
    {
        Off,
        On
    };
    struct Offset
    {
        explicit Offset(integer id_ = 0)
            : amount(id_)
        {
        }
        Offset(const Offset& that)
            : amount(0)
        {
        }
        Offset(Offset&& that)
            : amount(that.amount)
        {
            that.amount = 0;
        }
        integer amount;
    };
    struct Euclidean_Metric
    {
        template <typename Type>
        decltype(auto) operator()(Type x, Type y) const
        {
            return ((x - y) * (x - y));
        }
    };
    struct Manhattan_Metric
    {
        template <typename Type>
        decltype(auto) operator()(Type x, Type y) const
        {
            return std::abs(x - y);
        }
    };
    template <
        typename Point,
        typename... ArgumentSet,
        Requires<
            std::is_integral<Point>
        > = 0
    >
    float distance(
        const Point& a, const Point& b, 
        ArgumentSet&&... argumentSet)
    {
        float scaling = 
            PASTEL_ARG(
                scaling,
                [](){return 1.0;},
                [](auto input) 
                {
                    return std::is_convertible<decltype(input), float>();
                }
            );
        auto&& metric = 
            PASTEL_ARG(
                metric, 
                [](){return Euclidean_Metric();}, 
                [](auto input) {return
                    implicitArgument(Or<
                        std::is_same<decltype(input), Euclidean_Metric>,
                        std::is_same<decltype(input), Manhattan_Metric>
                    >());} 
            );
        bool negate = 
            PASTEL_ARG(
                negate, 
                [](){return false;},
                [](auto input) {return std::is_same<decltype(input), bool>();} 
            );
        auto offset =
            PASTEL_ARG_S(offset, Offset());
        Enum enumValue =
            PASTEL_ARG_ENUM(enumValue, Enum::Off);
        return metric(a, b) * scaling * (negate ? -1 : 1) + offset.amount + (integer)enumValue;
    }
    struct A {};
}
TEST_CASE("Tag (named_parameter)")
{
    // The tag hashing uses the Fowler-Noll-Vo 1a hash-function 
    // (FNV-1a) for 32-bit integers. Check the hashes of some
    // known strings. Initially I had a bug in the implementation 
    // which I noticed only because I had a hash collision between
    // these strings.
    PASTEL_STATIC_ASSERT(tagHash("translation") == 3419592236UL);
    PASTEL_STATIC_ASSERT(tagHash("orientation") == 3309681697UL);
    PASTEL_STATIC_ASSERT("translation"_tag == 3419592236UL);
    PASTEL_STATIC_ASSERT("orientation"_tag == 3309681697UL);
    // These are the only english words which collide under the
    // FNV1a hash function.
    PASTEL_STATIC_ASSERT(tagHash("liquid") == tagHash("costarring"));
    PASTEL_STATIC_ASSERT("liquid"_tag == "costarring"_tag);
}
TEST_CASE("Empty (Empty)")
{
    REQUIRE(distance(1, 6) == 5 * 5);
}
TEST_CASE("Explicit (named_parameter)")
{
    REQUIRE(distance(1, 6, PASTEL_TAG(scaling), 2.0) == 2 * 5 * 5);
    REQUIRE(distance(1, 6, PASTEL_TAG(scaling), 3.5) == 3.5 * 5 * 5);
    REQUIRE(distance(1, 6, PASTEL_TAG(metric), Manhattan_Metric()) == 5);
    REQUIRE(distance(1, 6, PASTEL_TAG(scaling), 3.5, PASTEL_TAG(metric), Manhattan_Metric()) == 3.5 * 5);
}
TEST_CASE("Enum (named_parameter)")
{
    REQUIRE(distance(1, 6, Enum::Off) == 5 * 5 + 0);
    REQUIRE(distance(1, 6, Enum::On) == 5 * 5 + 1);
}
TEST_CASE("Forwarding (named_parameter)")
{
    // Test that arguments are perfectly forwarded
    {
        Offset offset(543);
        REQUIRE(distance(1, 6, PASTEL_TAG(offset), std::move(offset)) == 5 * 5 + 543);
        REQUIRE(offset.amount == 0);
    }
    {
        Offset offset(543);
        REQUIRE(distance(1, 6, PASTEL_TAG(offset), offset) == 5 * 5 + 0);
        REQUIRE(offset.amount == 543);
    }
}
TEST_CASE("Flag (named_parameter)")
{
    REQUIRE(distance(1, 6, PASTEL_TAG(negate)) == (-1) * 5 * 5);
    REQUIRE(distance(1, 6, PASTEL_TAG(scaling)) == 1 * 5 * 5);
    REQUIRE(distance(1, 6, PASTEL_TAG(scaling), 2.0, PASTEL_TAG(negate), PASTEL_TAG(metric), Manhattan_Metric()) == 2 * (-1) * 5);
}
TEST_CASE("Implicit (named_parameter)")
{
    REQUIRE(distance(1, 6, Manhattan_Metric()) == 5);
    // Since 'negate' is not an implicit parameter,
    // the 'true' will not bind to it.
    REQUIRE(distance(1, 6, Manhattan_Metric(), true) == 5);
}
TEST_CASE("Erroneous (named_parameter)")
{
    // These should give errors at compile-time.
    // Error: Multiple arguments for 'negate'.
    //distance(1, 6, PASTEL_TAG(negate), PASTEL_TAG(negate));
    //distance(1, 6, PASTEL_TAG(scaling));
    //distance(1, 6, true, true);
    // Error: 'metric' is required to be either Manhattan_Metric or Euclidean_Metric.
    //distance(1, 6, PASTEL_TAG(metric), A());
    //distance(1, 6, PASTEL_TAG(metric));
    // Error: 'negate' is required to be of type bool.
    //distance(1, 6, PASTEL_TAG(negate), 4.0f);
}