#pragma once

#include <maps/libs/common/include/exception.h>

#include <cstddef>
#include <chrono>
#include <memory>
#include <thread>
#include <type_traits>


namespace maps {
namespace mrc {
namespace common {

/*
 * A class representing retry policy.
 * Will perform at most maxRetry() retries,
 * exponentially increasing timeout between them
 * starting from initialTimeout(),
 * with the basement of timeoutBackoff()
 *
 * NOTE: with default values will perform exactly 1 try,
 * i. e. no waiting and retries will be made
 */
class RetryPolicy
{
public:
    //ctors and dtors
    RetryPolicy();
    RetryPolicy(RetryPolicy&&) = default;
    RetryPolicy& operator=(RetryPolicy&&) = default;
    RetryPolicy(const RetryPolicy& toCopy);
    RetryPolicy& operator=(const RetryPolicy& toCopy);
    ~RetryPolicy();

    //setters and getters
    std::chrono::milliseconds initialTimeout() const;
    RetryPolicy& setInitialTimeout(std::chrono::milliseconds timeout);
    size_t timeoutBackoff() const;
    RetryPolicy& setTimeoutBackoff(size_t backoff);
    size_t maxAttempts() const;
    RetryPolicy& setMaxAttempts(size_t attempts);

private:
    class Impl;
    std::unique_ptr<Impl> impl_;
};


class MaxRetryNumberReached: public Exception
{
    using Exception::Exception;
};


template<typename RetryException, typename Function, class... Args>
auto retryOnException(
    const RetryPolicy& retryPolicy,
    Function&& func,
    Args&&... args
) -> decltype(func(args...))
{
    std::chrono::milliseconds retryInterval = retryPolicy.initialTimeout();
    for (size_t retryNumber = 0; retryNumber < retryPolicy.maxAttempts(); ++retryNumber) {
        try {
            return func(args...);
        } catch (const RetryException&) {
            //pass
        }

        //do not sleep if the last retry has failed
        if (retryNumber == retryPolicy.maxAttempts() - 1) {
            throw MaxRetryNumberReached();
        }

        std::this_thread::sleep_for(retryInterval);
        retryInterval *= retryPolicy.timeoutBackoff();
    }

    throw MaxRetryNumberReached();
}


} // namespace common
} // namespace mrc
} // namespace maps
