#pragma once
#include <memory>
#include <stdexcept>

/**
 * @brief A class for generating a range of values.
 * @details
 *  Returns `nSteps` values with `start` being the first and `stop` being the last value. 
 *  The same value is returned `countsPerStep` times before the next value is returned.
 *  After the last `stop` value has been returned, the cycle is restarted from the first `start` value.
 */
template<typename T>
struct Range {
    using value_type = T;
    Range(T start, T stop, unsigned nSteps, unsigned countsPerStep=1) : start(start), stop(stop), nSteps(nSteps), stepCounter(0), countsPerStep(countsPerStep), counter(0) {
        if (nSteps <= 1) {
            throw std::invalid_argument("nSteps must be greater than 1");
        }
        step = (stop - start) / (nSteps - 1);
    };
    T getNext() {
        T result = start + stepCounter * step;
        if (++counter >= countsPerStep) {
            counter = 0;
            if (++stepCounter >= nSteps) {
                stepCounter = 0;
            }
        }
        return result;
    };
    private:
        T start;
        T stop;
        T step;
        unsigned nSteps;
        unsigned stepCounter;
        unsigned countsPerStep;
        unsigned counter;
};

/**
 * This ugly mechanism lets the javascript pass
 * any combination of Range and basic types to a function.
 * Every time the function is called, the Range classes will return a new value while the rest stays the same.
 */
template<class T, template<class> class U>
inline constexpr bool is_instance_of_v = std::false_type{};
template<template<class> class U, class V>
inline constexpr bool is_instance_of_v<U<V>,U> = std::true_type{};

template<typename T>
concept isRange = is_instance_of_v<std::remove_reference_t<T>, Range>;


// All types except Range
template <typename T, typename Enable = void>
struct transform_type {
    using type = T;
};
// Range
template <typename T>
struct transform_type<T, std::enable_if_t<isRange<T>>> {
    using type = typename std::remove_reference_t<T>::value_type;  // If T is a Range, return its value_type
};


static_assert(isRange<Range<int>>);
static_assert(isRange<Range<double>&>);
static_assert(!isRange<double>);

// Dispatcher
template<isRange T>
std::remove_reference_t<T>::value_type processType(T&& obj) {
    return obj.getNext();
}
template<typename T>
requires(!isRange<T>)
T processType(T obj) {
    return obj;
}

/// Make a shared model pointer from a arguments, which may contain Range objects
template<typename Model, typename... Args>
std::shared_ptr<Model> makeModel(Args&&... args) {
    std::tuple<typename transform_type<Args>::type...> results = std::make_tuple(processType(std::forward<Args>(args))...);
    return std::apply([](auto&&... args) { return std::make_shared<Model>(args...); }, results);
}

