//
// Copyright 2021 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// CLEventVk.cpp: Implements the class methods for CLEventVk.
//

#ifdef UNSAFE_BUFFERS_BUILD
#    pragma allow_unsafe_libc_calls
#endif

#include "libANGLE/renderer/vulkan/CLEventVk.h"
#include "libANGLE/renderer/vulkan/CLCommandQueueVk.h"

#include "libANGLE/cl_utils.h"

namespace rx
{

CLEventVk::CLEventVk(const cl::Event &event,
                     const cl::ExecutionStatus initialStatus,
                     const QueueSerial eventSerial)
    : CLEventImpl(event),
      mStatus(cl::ToCLenum(initialStatus)),
      mProfilingTimestamps(ProfilingTimestamps{}),
      mQueueSerial(eventSerial)
{
    ANGLE_CL_IMPL_TRY(setTimestamp(*mStatus));
}

CLEventVk::~CLEventVk() {}

angle::Result CLEventVk::onEventCreate()
{
    ASSERT(!isUserEvent());
    ASSERT(mQueueSerial.valid());

    if (cl::FromCLenum<cl::ExecutionStatus>(*mStatus) == cl::ExecutionStatus::Complete)
    {
        // Submission finished at this point, just set event to complete
        ANGLE_TRY(setStatusAndExecuteCallback(CL_COMPLETE));
    }
    else
    {
        getFrontendObject().getCommandQueue()->getImpl<CLCommandQueueVk>().addEventReference(*this);
        if (getFrontendObject().getCommandQueue()->getProperties().intersects(
                CL_QUEUE_PROFILING_ENABLE))
        {
            // Block for profiling so that we get timestamps per-command
            ANGLE_TRY(getFrontendObject().getCommandQueue()->getImpl<CLCommandQueueVk>().finish());
        }
    }
    return angle::Result::Continue;
}

angle::Result CLEventVk::getCommandExecutionStatus(cl_int &executionStatus)
{
    executionStatus = *mStatus;
    return angle::Result::Continue;
}

angle::Result CLEventVk::setUserEventStatus(cl_int executionStatus)
{
    ASSERT(isUserEvent());

    // Not much to do here other than storing the user supplied state.
    // Error checking and single call enforcement is responsibility of the front end.
    ANGLE_TRY(setStatusAndExecuteCallback(executionStatus));

    // User event set and callback(s) finished - notify those waiting
    mUserEventCondition.notify_all();

    return angle::Result::Continue;
}

angle::Result CLEventVk::setCallback(cl::Event &event, cl_int commandExecCallbackType)
{
    ASSERT(commandExecCallbackType >= CL_COMPLETE);
    ASSERT(commandExecCallbackType < CL_QUEUED);

    // Not much to do, acknowledge the presence of callback and returns
    mHaveCallbacks->at(commandExecCallbackType) = true;

    return angle::Result::Continue;
}

angle::Result CLEventVk::getProfilingInfo(cl::ProfilingInfo name,
                                          size_t valueSize,
                                          void *value,
                                          size_t *valueSizeRet)
{
    cl_ulong valueUlong   = 0;
    size_t copySize       = 0;
    const void *copyValue = nullptr;

    auto profilingTimestamps = mProfilingTimestamps.synchronize();

    switch (name)
    {
        case cl::ProfilingInfo::CommandQueued:
            valueUlong = profilingTimestamps->commandQueuedTS;
            break;
        case cl::ProfilingInfo::CommandSubmit:
            valueUlong = profilingTimestamps->commandSubmitTS;
            break;
        case cl::ProfilingInfo::CommandStart:
            valueUlong = profilingTimestamps->commandStartTS;
            break;
        case cl::ProfilingInfo::CommandEnd:
            valueUlong = profilingTimestamps->commandEndTS;
            break;
        case cl::ProfilingInfo::CommandComplete:
            valueUlong = profilingTimestamps->commandCompleteTS;
            break;
        default:
            UNREACHABLE();
    }
    copyValue = &valueUlong;
    copySize  = sizeof(valueUlong);

    if ((value != nullptr) && (copyValue != nullptr))
    {
        memcpy(value, copyValue, std::min(valueSize, copySize));
    }

    if (valueSizeRet != nullptr)
    {
        *valueSizeRet = copySize;
    }

    return angle::Result::Continue;
}

angle::Result CLEventVk::waitForUserEventStatus()
{
    ASSERT(isUserEvent());

    cl_int status = CL_QUEUED;
    std::unique_lock<std::mutex> ul(mUserEventMutex);
    ANGLE_TRY(getCommandExecutionStatus(status));
    if (status > CL_COMPLETE)
    {
        // User is responsible for setting the user-event object, we need to wait for that event
        // (We dont care what the outcome is, just need to wait until that event triggers)
        INFO() << "Waiting for user-event (" << &mEvent
               << ") to be set! (aka clSetUserEventStatus)";
        mUserEventCondition.wait(ul);
    }

    return angle::Result::Continue;
}

angle::Result CLEventVk::setStatusAndExecuteCallback(cl_int status)
{
    auto statusHandle        = mStatus.synchronize();
    auto haveCallbacksHandle = mHaveCallbacks.synchronize();

    // we might skip states in some cases i.e. move from QUEUED to COMPLETE, so
    // make sure we are setting time stamps for all transitions
    ASSERT(*statusHandle >= status);
    while (*statusHandle > status)
    {
        (*statusHandle)--;
        ANGLE_TRY(setTimestamp(*statusHandle));
        if (*statusHandle >= CL_COMPLETE && *statusHandle < CL_QUEUED &&
            haveCallbacksHandle->at(*statusHandle))
        {
            getFrontendObject().callback(*statusHandle);
            haveCallbacksHandle->at(*statusHandle) = false;
        }
    }

    return angle::Result::Continue;
}

angle::Result CLEventVk::setTimestamp(cl_int status)
{
    if (!isUserEvent() &&
        mEvent.getCommandQueue()->getProperties().intersects(CL_QUEUE_PROFILING_ENABLE))
    {
        // TODO(aannestrand) Just get current CPU timestamp for now, look into Vulkan GPU device
        // timestamp query instead and later make CPU timestamp a fallback if GPU timestamp cannot
        // be queried http://anglebug.com/357902514
        cl_ulong cpuTS =
            std::chrono::time_point_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now())
                .time_since_epoch()
                .count();

        auto profilingTimestamps = mProfilingTimestamps.synchronize();

        switch (status)
        {
            case CL_QUEUED:
                profilingTimestamps->commandQueuedTS = cpuTS;
                break;
            case CL_SUBMITTED:
                profilingTimestamps->commandSubmitTS = cpuTS;
                break;
            case CL_RUNNING:
                profilingTimestamps->commandStartTS = cpuTS;
                break;
            case CL_COMPLETE:
                profilingTimestamps->commandEndTS = cpuTS;

                // Returns a value equivalent to passing CL_PROFILING_COMMAND_END if the device
                // associated with event does not support device-side enqueue.
                // https://registry.khronos.org/OpenCL/specs/3.0-unified/html/OpenCL_API.html#_device_side_enqueue
                profilingTimestamps->commandCompleteTS = cpuTS;
                break;
            default:
                UNREACHABLE();
        }
    }

    return angle::Result::Continue;
}

}  // namespace rx
