/*=========================================================================
 *
 *  Copyright UMC Utrecht and contributors
 *
 *  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.txt
 *
 *  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 __itkOpenCLEvent_h
#define __itkOpenCLEvent_h

#include "itkOpenCL.h"
#include "itkOpenCLExtension.h"
#include <ostream>

namespace itk
{
/** \class OpenCLEvent
 * \brief OpenCLEvent class represents an OpenCL event object.
 *
 * An event object can be used to track the execution status of a command.
 * The API calls that enqueue commands to a command queue create a new event
 * object that is returned in the event argument. In case of an error enqueuing
 * the command in the command queue the event argument does not
 * return an event object.
 *
 * \table
 * \row \o IsQueued() \o The command command has been enqueued in the command-queue.
 * \row \o IsSubmitted() \o The enqueued command has been submitted by the host
 * to the device associated with the command-queue.
 * \row \o IsRunning() \o The command is currently executing on the OpenCL device.
 * \row \o IsComplete() \o The command has successfully completed.
 * \row \o IsError() \o The command has finished execution due to an error.
 * \endtable
 *
 * The method WaitForFinished() waits on the host thread for commands identified
 * by event objects in event list to complete. A command is considered complete
 * if its execution status is \c{CL_COMPLETE} or a negative value.
 *
 * \code
 * OpenCLBuffer buffer = context->CreateBufferDevice(size, OpenCLMemoryObject::WriteOnly);
 * OpenCLEvent event = buffer.ReadAsync(offset, data, size);
 * ...
 * event.WaitForFinished();
 * \endcode
 *
 * The events specified in OpenCLEventList act as synchronization points.
 * The OpenCLEventList are used to control execution order:
 *
 * \code
 * OpenCLBuffer buffer = context->CreateBufferDevice(size, OpenCLMemoryObject::WriteOnly);
 * OpenCLEvent event1 = buffer.ReadAsync(offset1, data1, size1);
 * OpenCLEvent event2 = buffer.ReadAsync(offset2, data2, size2);
 *
 * OpenCLEventList events;
 * events << event1 << event2;
 * OpenCLEvent event3 = buffer.ReadAsync(offset3, data3, size3, events);
 * ...
 * event3.WaitForFinished();
 * \endcode
 *
 * The OpenCL functions that are submitted to a command-queue are enqueued
 * in the order the calls are made but can be configured to execute in-order
 * or out-of-order. The properties argument in context->CreateCommandQueue()
 * can be used to specify the execution order.
 *
 * If the \c{CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE} property of a command-queue
 * is not set, the commands enqueued to a command-queue execute in order.
 * For example, if an application calls buffer.ReadAsync() to execute event1
 * followed by a buffer.ReadAsync() to execute event2, the application can
 * assume that event1 finishes first and then event2 is executed.
 * If the OpenCLCommandQueue::IsOutOfOrder() property of a command-queue
 * is set, then there is no guarantee that event1 will finish before event2
 * starts execution.
 *
 * \ingroup OpenCL
 * \sa OpenCLCommandQueue::IsOutOfOrder(), OpenCLEventList, OpenCLUserEvent
 */

// Forward declaration
class OpenCLUserEvent;

class ITKOpenCL_EXPORT OpenCLEvent
{
public:

  /** Standard class typedefs. */
  typedef OpenCLEvent Self;

  /** Constructs a null OpenCL event object. */
  OpenCLEvent();

  /** Constructs an OpenCL event object from the identifier id.
   * This class takes over ownership of id and will release it in
   * the destructor. */
  OpenCLEvent( const cl_event id );

  /** Constructs a copy of other. The \c{clRetainEvent()} function
   * will be called to update the reference count on GetEventId(). */
  OpenCLEvent( const OpenCLEvent & other );

  /** Releases this OpenCL event object by calling \c{clReleaseEvent()}. */
  ~OpenCLEvent();

  /** Assigns other to this OpenCL event object. The current GetEventId() will
   * be released with \c{clReleaseEvent()}, and the new GetEventId() will be
   * retained with \c{clRetainEvent()}. */
  OpenCLEvent & operator=( const OpenCLEvent & other );

  /** Returns true if this OpenCL event object is null, false otherwise. */
  bool IsNull() const { return this->m_Id == 0; }

  /** Returns the OpenCL identifier for this event. */
  cl_event GetEventId() const { return this->m_Id; }

  /** Returns true if the command associated with this OpenCL event has been
   * queued for execution on the host, but has not yet been submitted to
   * the device yet.
   * \sa IsSubmitted(), IsRunning(), IsComplete(), IsError(), GetStatus() */
  bool IsQueued() const { return GetStatus() == CL_QUEUED; }

  /** Returns true if the command associated with this OpenCL event has been
   * submitted for execution on the device yet, but is not yet running.
   * \sa IsQueued(), IsRunning(), IsComplete(), IsError(), GetStatus() */
  bool IsSubmitted() const { return GetStatus() == CL_SUBMITTED; }

  /** Returns true if the command associated with this OpenCL event is
   * running on the device, but has not yet finished.
   * \sa IsQueued(), IsSubmitted(), IsComplete(), IsError(), GetStatus() */
  bool IsRunning() const { return GetStatus() == CL_RUNNING; }

  /** Returns true if the command associated with this OpenCL event
   * has completed execution on the device.
   * \sa IsQueued(), IsSubmitted(), IsRunning(), IsError(), GetStatus() */
  bool IsComplete() const { return GetStatus() == CL_COMPLETE; }

  /** Returns true if an error has occurred on this OpenCL event.
   * \sa IsQueued(), IsSubmitted(), IsRunning(), IsComplete(), GetStatus() */
  bool IsError() const { return GetStatus() < 0; }

  /** Returns the status of this event, which is an error code or one
   * of \c{CL_QUEUED}, \c{CL_SUBMITTED}, \c{CL_RUNNING} or \c{CL_COMPLETE}.
   * \sa IsQueued(), IsSubmitted(), IsRunning(), IsComplete(), IsError() */
  cl_int GetStatus() const;

  /** Returns the type of command that generated this event. */
  cl_command_type GetCommandType() const;

  /** Waits for this event to be signaled as finished. The calling thread
   * is blocked until the event is signaled. This function returns immediately
   * if the event is null.
   * \sa IsComplete(), OpenCLEventList::WaitForFinished() */
  cl_int WaitForFinished();

  /** Registers a user callback function for a specific command execution status.
   * If the execution of a command is terminated, the command-queue associated
   * with this terminated command, and the associated context (and all other
   * command-queues in this context) may no longer be available. The behavior of
   * OpenCL API calls that use this context (and command-queues associated with
   * this context) are now considered to be implementation defined. The user
   * registered callback function specified when context is created can be used
   * to report appropriate error information. */
  cl_int SetCallback( cl_int type,
  void( CL_CALLBACK * pfn_notify )( cl_event, cl_int, void * ),
  void * user_data = NULL );

  /** Returns the device time in nanoseconds when the command was queued for
   * execution on the host. The return value is only valid if the command has
   * finished execution and profiling was enabled on the command queue.
   * \sa GetSubmitTime(), GetRunTime(), GetFinishTime(), IsQueued() */
  cl_ulong GetQueueTime() const;

  /** Returns the device time in nanoseconds when the command was submitted by
   * the host for execution on the device. The return value is only valid if
   * the command has finished execution and profiling was enabled on the command queue.
   * \sa GetQueueTime(), GetRunTime(), GetFinishTime(), IsSubmitted() */
  cl_ulong GetSubmitTime() const;

  /** Returns the device time in nanoseconds when the command started
   * running on the device. The return value is only valid if the command
   * has finished execution and profiling was enabled on the command queue.
   * \sa GetQueueTime(), GetSubmitTime(), GetFinishTime(), IsRunning() */
  cl_ulong GetRunTime() const;

  /** Returns the device time in nanoseconds when the command finished running
   * on the device. The return value is only valid if the command has finished
   * execution and profiling was enabled on the command queue.
   * \sa GetQueueTime(), GetSubmitTime(), GetRunTime(), IsComplete() */
  cl_ulong GetFinishTime() const;

private:

  cl_event m_Id;

  /** friends from OpenCL core */
  friend class OpenCLUserEvent;
};

/** Operator ==
* Returns true if \a lhs OpenCL event is the same as \a rhs, false otherwise.
* \sa operator!= */
bool ITKOpenCL_EXPORT operator==( const OpenCLEvent & lhs, const OpenCLEvent & rhs );

/** Operator !=
* Returns true if \a lhs OpenCL event is not the same as \a rhs, false otherwise.
* \sa operator== */
bool ITKOpenCL_EXPORT operator!=( const OpenCLEvent & lhs, const OpenCLEvent & rhs );

/** Stream out operator for OpenCLEvent */
template< typename charT, typename traits >
inline
std::basic_ostream< charT, traits > &
operator<<( std::basic_ostream< charT, traits > & strm,
  const OpenCLEvent & event )
{
  const cl_event id = event.GetEventId();

  if( !id )
  {
    strm << "OpenCLEvent()";
    return strm;
  }

  const cl_command_type command = event.GetCommandType();
  const cl_int          status  = event.GetStatus();

  // Get command name, check for clGetEventInfo() specification
  const char * commandName;
  switch( command )
  {
    case CL_COMMAND_NDRANGE_KERNEL:
      commandName = "clEnqueueNDRangeKernel"; break;
    case CL_COMMAND_TASK:
      commandName = "clEnqueueTask"; break;
    case CL_COMMAND_NATIVE_KERNEL:
      commandName = "clEnqueueNativeKernel"; break;
    case CL_COMMAND_READ_BUFFER:
      commandName = "clEnqueueReadBuffer"; break;
    case CL_COMMAND_WRITE_BUFFER:
      commandName = "clEnqueueWriteBuffer"; break;
    case CL_COMMAND_COPY_BUFFER:
      commandName = "clEnqueueCopyBuffer"; break;
    case CL_COMMAND_READ_IMAGE:
      commandName = "clEnqueueReadImage"; break;
    case CL_COMMAND_WRITE_IMAGE:
      commandName = "clEnqueueWriteImage"; break;
    case CL_COMMAND_COPY_IMAGE:
      commandName = "clEnqueueCopyImage"; break;
    case CL_COMMAND_COPY_IMAGE_TO_BUFFER:
      commandName = "clEnqueueCopyImageToBuffer"; break;
    case CL_COMMAND_COPY_BUFFER_TO_IMAGE:
      commandName = "clEnqueueCopyBufferToImage"; break;
    case CL_COMMAND_MAP_BUFFER:
      commandName = "clEnqueueMapBuffer"; break;
    case CL_COMMAND_MAP_IMAGE:
      commandName = "clEnqueueMapImage"; break;
    case CL_COMMAND_UNMAP_MEM_OBJECT:
      commandName = "clEnqueueUnmapMemObject"; break;
    case CL_COMMAND_MARKER:
      commandName = "clEnqueueMarker"; break;
    case CL_COMMAND_ACQUIRE_GL_OBJECTS:
      commandName = "clEnqueueAcquireGLObjects"; break;
    case CL_COMMAND_RELEASE_GL_OBJECTS:
      commandName = "clEnqueueReleaseGLObjects"; break;
    // OpenCL 1.1 event types.
    case CL_COMMAND_READ_BUFFER_RECT:
      commandName = "clEnqueueReadBufferRect"; break;
    case CL_COMMAND_WRITE_BUFFER_RECT:
      commandName = "clEnqueueWriteBufferRect"; break;
    case CL_COMMAND_COPY_BUFFER_RECT:
      commandName = "clEnqueueCopyBufferRect"; break;
    case CL_COMMAND_USER:
      commandName = "clCreateUserEvent"; break;
    // OpenCL 1.2 event types.
    case CL_COMMAND_BARRIER:
      commandName = "clEnqueueBarrierWithWaitList"; break;
    case CL_COMMAND_MIGRATE_MEM_OBJECTS:
      commandName = "clEnqueueFillImage"; break;
    case CL_COMMAND_FILL_BUFFER:
      commandName = "clEnqueueFillBuffer"; break;
    case CL_COMMAND_FILL_IMAGE:
      commandName = "clEnqueueFillImage"; break;
    default:
      commandName = "Unknown"; break;
  }

  // Get command status
  const char * statusName;
  switch( status )
  {
    case CL_COMPLETE:
      statusName = "completed"; break;
    case CL_RUNNING:
      statusName = "running"; break;
    case CL_SUBMITTED:
      statusName = "submitted"; break;
    case CL_QUEUED:
      statusName = "queued"; break;
    default:
      statusName = "Unknown"; break;
  }
  if( status != CL_COMPLETE )
  {
    // Command is not complete : no profiling information available yet.
    strm << "OpenCLEvent(id:" << reinterpret_cast< long >( id )
         << " command:" << commandName
         << " status:" << statusName
         << ")";
  }
  else
  {
    cl_ulong queueTime, runTime, finishTime;
    if( clGetEventProfilingInfo
        ( id, CL_PROFILING_COMMAND_QUEUED,
      sizeof( queueTime ), &queueTime, 0 ) != CL_SUCCESS
      || clGetEventProfilingInfo
        ( id, CL_PROFILING_COMMAND_START,
      sizeof( runTime ), &runTime, 0 ) != CL_SUCCESS
      || clGetEventProfilingInfo
        ( id, CL_PROFILING_COMMAND_END,
      sizeof( finishTime ), &finishTime, 0 ) != CL_SUCCESS )
    {
      // Profiling information is not available, probably
      // because it was not enabled on the command queue.
      strm << "OpenCLEvent(id:" << reinterpret_cast< long >( id )
           << " command:" << commandName
           << " status:" << statusName
           << ")";
    }
    else
    {
      // Include profiling information in the debug output.
      const double fullDuration = ( finishTime - queueTime ) / 1000000.0f;
      const double runDuration  = ( finishTime - runTime ) / 1000000.0f;
      strm << "OpenCLEvent(id:" << reinterpret_cast< long >( id )
           << " command:" << commandName
           << " status:" << statusName
           << " full-time:" << fullDuration
           << " ms running-time:" << runDuration
           << "ms)";
    }
  }

  return strm;
}


} // end namespace itk

#endif /* __itkOpenCLEvent_h */
