/**
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

#pragma once

#include <aws/core/Core_EXPORTS.h>
#include <aws/core/utils/memory/AWSMemory.h>
#include <aws/core/utils/memory/stl/AWSAllocator.h>
#include <aws/core/utils/threading/Executor.h>
#include <functional>
#include <future>

namespace Aws
{
namespace Client
{
    /**
     * A template function that is used to create an Async Operation function body for AWS Operations
     */
    template<typename ClientT,
             typename RequestT,
             typename HandlerT,
             typename HandlerContextT,
             typename OperationFuncT,
             typename ExecutorT>
    inline void AWS_CORE_LOCAL MakeAsyncOperation(OperationFuncT&& operationFunc,
                                   const ClientT* clientThis,
                                   const RequestT& request,
                                   const HandlerT& handler,
                                   const HandlerContextT& context,
                                   ExecutorT* pExecutor)
    {
        std::function<void()> asyncTask =
            [operationFunc, clientThis, request, handler, context]() // note capture by value
            {
                handler(clientThis,
                        request,
                        (clientThis->*operationFunc)(request),
                        context);
            };

        pExecutor->Submit(std::move(asyncTask));
    }

    /**
     * A template function that is used to create an Async Operation function body for AWS Streaming Operations
     *   The only difference compared to a regular non-streaming Operation is that
     *   the request is passed by non-const reference, therefore virtual copy constructor is not needed.
     *   However, caller code must ensure the life time of the request object is maintained during the Async execution.
     */
    template<typename ClientT,
             typename RequestT,
             typename HandlerT,
             typename HandlerContextT,
             typename OperationFuncT,
             typename ExecutorT>
    inline void AWS_CORE_LOCAL MakeAsyncStreamingOperation(OperationFuncT&& operationFunc,
                                            const ClientT* clientThis,
                                            RequestT& request, // note non-const ref
                                            const HandlerT& handler,
                                            const HandlerContextT& context,
                                            ExecutorT* pExecutor)
    {
        std::function<void()> asyncTask =
            [operationFunc, clientThis, &request, handler, context]() // note capture by ref
            {
                handler(clientThis,
                        request,
                        (clientThis->*operationFunc)(request),
                        context);
            };

        pExecutor->Submit(std::move(asyncTask));
    }

    /**
     * A template function to create an Async Operation function body for AWS Operation without a request on input.
     */
    template<typename ClientT,
             typename HandlerT,
             typename HandlerContextT,
             typename OperationFuncT,
             typename ExecutorT>
    inline void AWS_CORE_LOCAL MakeAsyncOperation(OperationFuncT&& operationFunc,
                                            const ClientT* clientThis,
                                            const HandlerT& handler,
                                            const HandlerContextT& context,
                                            ExecutorT* pExecutor)
    {
        std::function<void()> asyncTask =
            [operationFunc, clientThis, handler, context]()
            {
                handler(clientThis,
                        (clientThis->*operationFunc)(),
                        context);
            };

        pExecutor->Submit(std::move(asyncTask));
    }

    /**
     * A template function that is used to create a Callable Operation function body for AWS Operations
     */
    template<typename ClientT,
             typename RequestT,
             typename OperationFuncT,
             typename ExecutorT>
    inline auto AWS_CORE_LOCAL MakeCallableOperation(const char* ALLOCATION_TAG,
                                      OperationFuncT&& operationFunc,
                                      const ClientT* clientThis,
                                      const RequestT& request,
                                      ExecutorT* pExecutor) -> std::future<decltype((clientThis->*operationFunc)(request))>
    {
        using OperationOutcomeT = decltype((clientThis->*operationFunc)(request));

        auto task = Aws::MakeShared< std::packaged_task< OperationOutcomeT() > >(
                ALLOCATION_TAG,
                [clientThis, operationFunc, request]() // note capture by value
                {
                    auto futureOutcome = (clientThis->*operationFunc)(request);
                    return futureOutcome;
                } );

        std::function<void()> packagedFunction =
                [task]() { (*task)(); };
        pExecutor->Submit(std::move(packagedFunction));
        return task->get_future();
    }

    /**
     * A template function that is used to create a Callable Operation function body for AWS Streaming Operations
     *   The only difference compared to a regular non-streaming Operation is that
     *   the request is passed by non-const reference, therefore virtual copy constructor is not needed.
     *   However, caller code must ensure the life time of the request object is maintained during the Async execution.
     */
    template<typename ClientT,
             typename RequestT,
             typename OperationFuncT,
             typename ExecutorT>
    inline auto AWS_CORE_LOCAL MakeCallableStreamingOperation(const char* ALLOCATION_TAG,
                                               OperationFuncT&& operationFunc,
                                               const ClientT* clientThis,
                                               RequestT& request,  // note non-const ref
                                               ExecutorT* pExecutor) -> std::future<decltype((clientThis->*operationFunc)(request))>
    {
        using OperationOutcomeT = decltype((clientThis->*operationFunc)(request));

        auto task = Aws::MakeShared< std::packaged_task< OperationOutcomeT() > >(
                ALLOCATION_TAG,
                [clientThis, operationFunc, &request]()  // note capture by ref
                {
                    return (clientThis->*operationFunc)(request);
                } );

        std::function<void()> packagedFunction =
                [task]() { (*task)(); };
        pExecutor->Submit(std::move(packagedFunction));
        return task->get_future();
    }

    /**
     * A template function that is used to create a Callable Operation function body for AWS Operation without a request on input.
     */
    template<typename ClientT,
             typename OperationFuncT,
             typename ExecutorT>
    inline auto AWS_CORE_LOCAL MakeCallableOperation(const char* ALLOCATION_TAG,
                                               OperationFuncT&& operationFunc,
                                               const ClientT* clientThis,
                                               ExecutorT* pExecutor) -> std::future<decltype((clientThis->*operationFunc)())>
    {
        using OperationOutcomeT = decltype((clientThis->*operationFunc)());

        auto task = Aws::MakeShared< std::packaged_task< OperationOutcomeT() > >(
                ALLOCATION_TAG,
                [clientThis, operationFunc]()
                {
                    return (clientThis->*operationFunc)();
                } );

        std::function<void()> packagedFunction =
                [task]() { (*task)(); };
        pExecutor->Submit(std::move(packagedFunction));
        return task->get_future();
    }
} // namespace Client
} // namespace Aws