/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#ifndef ANDROID_HARDWARE_BROADCASTRADIO_VTS_MOCK_TIMEOUT
#define ANDROID_HARDWARE_BROADCASTRADIO_VTS_MOCK_TIMEOUT

#include <gmock/gmock.h>
#include <thread>

#ifndef EGMOCK_VERBOSE
#define EGMOCK_VERBOSE 0
#endif

/**
 * Print log message.
 *
 * INTERNAL IMPLEMENTATION - don't use in user code.
 */
#if EGMOCK_VERBOSE
#define EGMOCK_LOG_(...) LOG(VERBOSE) << "egmock: " << __VA_ARGS__
#else
#define EGMOCK_LOG_(...)
#endif

/**
 * Common helper objects for gmock timeout extension.
 *
 * INTERNAL IMPLEMENTATION - don't use in user code.
 */
#define EGMOCK_TIMEOUT_METHOD_DEF_(Method, ...) \
    std::atomic<bool> egmock_called_##Method;   \
    std::mutex egmock_mut_##Method;             \
    std::condition_variable egmock_cond_##Method;

/**
 * Function similar to comma operator, to make it possible to return any value returned by mocked
 * function (which may be void) and discard the result of the other operation (notification about
 * a call).
 *
 * We need to invoke the mocked function (which result is returned) before the notification (which
 * result is dropped) - that's exactly the opposite of comma operator.
 *
 * INTERNAL IMPLEMENTATION - don't use in user code.
 */
template <typename T>
static T EGMockFlippedComma_(std::function<T()> returned, std::function<void()> discarded) {
    auto ret = returned();
    discarded();
    return ret;
}

template <>
inline void EGMockFlippedComma_(std::function<void()> returned, std::function<void()> discarded) {
    returned();
    discarded();
}

/**
 * Common method body for gmock timeout extension.
 *
 * INTERNAL IMPLEMENTATION - don't use in user code.
 */
#define EGMOCK_TIMEOUT_METHOD_BODY_(Method, ...)                      \
    auto invokeMock = [&]() { return egmock_##Method(__VA_ARGS__); }; \
    auto notify = [&]() {                                             \
        std::lock_guard<std::mutex> lk(egmock_mut_##Method);          \
        EGMOCK_LOG_(#Method " called");                               \
        egmock_called_##Method = true;                                \
        egmock_cond_##Method.notify_all();                            \
    };                                                                \
    return EGMockFlippedComma_<decltype(invokeMock())>(invokeMock, notify);

// We define this as a variadic macro in case F contains unprotected
// commas (the same reason that we use variadic macros in other places
// in this file).
#define EGMOCK_RESULT_(tn, ...) \
    tn ::testing::internal::Function<__VA_ARGS__>::Result

// The type of argument N of the given function type.
// INTERNAL IMPLEMENTATION - DON'T USE IN USER CODE!!!
#define EGMOCK_ARG_(tn, N, ...) \
    tn ::testing::internal::Function<__VA_ARGS__>::template Arg<N-1>::type

/**
 * Gmock MOCK_METHOD0 timeout-capable extension.
 */
#define MOCK_TIMEOUT_METHOD0(Method, ...)       \
    MOCK_METHOD0(egmock_##Method, __VA_ARGS__); \
    EGMOCK_TIMEOUT_METHOD_DEF_(Method);         \
    virtual EGMOCK_RESULT_(, __VA_ARGS__) Method() { EGMOCK_TIMEOUT_METHOD_BODY_(Method); }

/**
 * Gmock MOCK_METHOD1 timeout-capable extension.
 */
#define MOCK_TIMEOUT_METHOD1(Method, ...)                                                   \
    MOCK_METHOD1(egmock_##Method, __VA_ARGS__);                                             \
    EGMOCK_TIMEOUT_METHOD_DEF_(Method);                                                     \
    virtual EGMOCK_RESULT_(, __VA_ARGS__) Method(EGMOCK_ARG_(, 1, __VA_ARGS__) egmock_a1) { \
        EGMOCK_TIMEOUT_METHOD_BODY_(Method, egmock_a1);                                     \
    }

/**
 * Gmock MOCK_METHOD2 timeout-capable extension.
 */
#define MOCK_TIMEOUT_METHOD2(Method, ...)                                                          \
    MOCK_METHOD2(egmock_##Method, __VA_ARGS__);                                                    \
    EGMOCK_TIMEOUT_METHOD_DEF_(Method);                                                            \
    virtual EGMOCK_RESULT_(, __VA_ARGS__)                                                          \
        Method(EGMOCK_ARG_(, 1, __VA_ARGS__) egmock_a1, EGMOCK_ARG_(, 2, __VA_ARGS__) egmock_a2) { \
        EGMOCK_TIMEOUT_METHOD_BODY_(Method, egmock_a1, egmock_a2);                                 \
    }

/**
 * Gmock EXPECT_CALL timeout-capable extension.
 *
 * It has slightly different syntax from the original macro, to make method name accessible.
 * So, instead of typing
 *     EXPECT_CALL(account, charge(100, Currency::USD));
 * you need to inline arguments
 *     EXPECT_TIMEOUT_CALL(account, charge, 100, Currency::USD);
 */
#define EXPECT_TIMEOUT_CALL(obj, Method, ...) \
    EGMOCK_LOG_(#Method " expected to call"); \
    (obj).egmock_called_##Method = false;     \
    EXPECT_CALL(obj, egmock_##Method(__VA_ARGS__))

/**
 * Waits for an earlier EXPECT_TIMEOUT_CALL to execute.
 *
 * It does not fully support special constraints of the EXPECT_CALL clause, just proceeds when the
 * first call to a given method comes. For example, in the following code:
 *     EXPECT_TIMEOUT_CALL(account, charge, 100, _);
 *     account.charge(50, Currency::USD);
 *     EXPECT_TIMEOUT_CALL_WAIT(account, charge, 500ms);
 * the wait clause will just continue, as the charge method was called.
 *
 * @param obj object for a call
 * @param Method the method to wait for
 * @param timeout the maximum time for waiting
 */
#define EXPECT_TIMEOUT_CALL_WAIT(obj, Method, timeout)                      \
    {                                                                       \
        EGMOCK_LOG_("waiting for " #Method " call");                        \
        std::unique_lock<std::mutex> lk((obj).egmock_mut_##Method);         \
        if (!(obj).egmock_called_##Method) {                                \
            auto status = (obj).egmock_cond_##Method.wait_for(lk, timeout); \
            EXPECT_EQ(std::cv_status::no_timeout, status);                  \
        }                                                                   \
    }

#endif  // ANDROID_HARDWARE_BROADCASTRADIO_VTS_MOCK_TIMEOUT
