/***************************************************************************
  qgsopenclutils.h - QgsOpenClUtils

 ---------------------
 begin                : 11.4.2018
 copyright            : (C) 2018 by Alessandro Pasotti
 email                : elpaso at itopen dot it
 ***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
#ifndef QGSOPENCLUTILS_H
#define QGSOPENCLUTILS_H


#define CL_HPP_ENABLE_EXCEPTIONS

#include <QString>
#include <QtGlobal>

#define SIP_NO_FILE

using namespace Qt::StringLiterals;

#ifdef Q_OS_MAC
#define CL_HPP_MINIMUM_OPENCL_VERSION 120
#define CL_HPP_TARGET_OPENCL_VERSION 120
#define CL_TARGET_OPENCL_VERSION 120
#else
#define CL_USE_DEPRECATED_OPENCL_1_1_APIS
#define CL_HPP_TARGET_OPENCL_VERSION 200
#define CL_TARGET_OPENCL_VERSION 200
#endif

#include "qgsconfig.h"

#ifdef OPENCL_USE_NEW_HEADER
#include <CL/opencl.hpp>
#else
#include <CL/cl2.hpp>
#endif

#include "qgis_core.h"
#include "qgis.h"

#include "cpl_conv.h"

class QgsSettingsEntryBool;
class QgsSettingsEntryString;

/**
 * \ingroup core
 * \class QgsOpenClUtils
 * \brief Utilities responsible for common OpenCL operations.
 *
 * These operations include:
 *
 * - enable/disable opencl
 * - store and retrieve preferences for the default device
 * - check opencl device availability and automatically choose the first GPU device
 * - creating the default context
 * - loading program sources from standard locations
 * - build programs and log errors
 *
 * Usage:
 *
 * \code{.cpp}
 * // This will check if OpenCL is enabled in user options and if there is a suitable
 * // device, if a device is found it is initialized.
 * if ( QgsOpenClUtils::enabled() && QgsOpenClUtils::available() )
 * {
 *    // Use the default context
 *    cl::Context ctx = QgsOpenClUtils::context();
 *    cl::CommandQueue queue( ctx );
 *    // Load the program from a standard location and build it
 *    cl::Program program = QgsOpenClUtils::buildProgram( ctx, QgsOpenClUtils::sourceFromBaseName( u"hillshade"_s ) );
 *    // Continue with the usual OpenCL buffer, kernel and execution
 *    ...
 * }
 * \endcode
 *
 * \note not available in Python bindings
 * \since QGIS 3.4
 */
class CORE_EXPORT QgsOpenClUtils
{
    Q_GADGET

  public:
    static const QgsSettingsEntryBool *settingsOpenClEnabled;
    static const QgsSettingsEntryString *settingsOpenClDefaultDevice;
    /**
     * The ExceptionBehavior enum define how exceptions generated by OpenCL should be treated
     */
    enum ExceptionBehavior
    {
      Catch, //!< Write errors in the message log and silently fail
      Throw  //!< Write errors in the message log and re-throw exceptions
    };

    /**
     * The Type enum represent OpenCL device type
     */
    enum HardwareType
    {
      CPU,
      GPU,
      Other
    };

    Q_ENUM( HardwareType )

    /**
     * The Info enum maps to OpenCL info constants
     *
     * \see deviceInfo()
     */
    enum Info
    {
      Name = CL_DEVICE_NAME,
      Vendor = CL_DEVICE_VENDOR,
      Version = CL_DEVICE_VERSION,
      Profile = CL_DEVICE_PROFILE,
      ImageSupport = CL_DEVICE_IMAGE_SUPPORT,
      Image2dMaxWidth = CL_DEVICE_IMAGE2D_MAX_WIDTH,
      Image2dMaxHeight = CL_DEVICE_IMAGE2D_MAX_HEIGHT,
      MaxMemAllocSize = CL_DEVICE_MAX_MEM_ALLOC_SIZE,
      Type = CL_DEVICE_TYPE // CPU/GPU etc.
    };

    /**
     * Checks whether a suitable OpenCL platform and device is available on this system
     * and initialize the QGIS OpenCL system by activating the preferred device
     * if specified in the user the settings, if no preferred device was set or
     * the preferred device could not be found the first GPU device is activated,
     * the first CPU device acts as a fallback if none of the previous could be found.
     *
     * This function must always be called before using QGIS OpenCL utils
     */
    static bool available();

    //! Returns TRUE if OpenCL is enabled in the user settings
    static bool enabled();

    //! Returns a list of OpenCL devices found on this sysytem
    static const std::vector<cl::Device> devices();

    /**
     * Returns the active device.
     *
     * The active device is set as the default device for all OpenCL operations,
     * once it is set it cannot be changed until QGIS is restarted (this is
     * due to the way the underlying OpenCL library is built).
     */
    static cl::Device activeDevice();

    /**
     * Returns the active platform OpenCL version string (e.g. 1.1, 2.0 etc.)
     * or a blank string if there is no active platform.
     * \since QGIS 3.6
     */
    static QString activePlatformVersion();

    //! Store in the settings the preferred \a deviceId device identifier
    static void storePreferredDevice( const QString deviceId );

    //! Read from the settings the preferred device identifier
    static QString preferredDevice();

    //! Create a string identifier from a \a device
    static QString deviceId( const cl::Device device );

    /**
     * Returns a formatted description for the \a device
     */
    static QString deviceDescription( const cl::Device device );

    /**
     * Returns a formatted description for the device identified by \a deviceId
     */
    static QString deviceDescription( const QString deviceId );

    //! Set the OpenCL user setting to \a enabled
    static void setEnabled( bool enabled );

    //! Extract and return the build log error from \a error
    static QString buildLog( cl::BuildError &error );

    //! Read an OpenCL source file from \a path
    static QString sourceFromPath( const QString &path );

    //! Returns the full path to a an OpenCL source file from the \a baseName ('.cl' extension is automatically appended)
    static QString sourceFromBaseName( const QString &baseName );

    //! OpenCL string for message logs
    static QLatin1String LOGMESSAGE_TAG;

    //! Returns a string representation from an OpenCL \a errorCode
    static QString errorText( const int errorCode );

    /**
     * Create an OpenCL command queue from the default context.
     *
     * This wrapper is required in order to prevent a crash when
     * running on OpenCL platforms < 2
     */
    static cl::CommandQueue commandQueue();

    /**
     * Build the program from \a source in the given \a context and depending on \a exceptionBehavior
     * can throw or catch the exceptions
     * \return the built program
     * \deprecated QGIS 3.6
     */
    Q_DECL_DEPRECATED static cl::Program buildProgram( const cl::Context &context, const QString &source, ExceptionBehavior exceptionBehavior = Catch );

    /**
     * Build the program from \a source, depending on \a exceptionBehavior can throw or catch the exceptions
     * \return the built program
     */
    static cl::Program buildProgram( const QString &source, ExceptionBehavior exceptionBehavior = Catch );


    /**
     * Context factory
     *
     * \return a new context for the default device or an invalid context if
     *         no device were identified or OpenCL support is not available
     *         and enabled
     */
    static cl::Context context();

    //! Returns the base path to OpenCL program directory
    static QString sourcePath();

    //! Set the  base path to OpenCL program directory
    static void setSourcePath( const QString &value );

    //! Returns \a infoType information about the active (default) device
    static QString activeDeviceInfo( const Info infoType = Info::Name );

    //! Returns \a infoType information about the \a device
    static QString deviceInfo( const Info infoType, cl::Device device );

    /**
     * Tiny smart-pointer-like wrapper around CPLMalloc and CPLFree: this is needed because
     * OpenCL C++ API may throw exceptions
     */
    template<typename T> struct CPLAllocator
    {
      public:
        explicit CPLAllocator( unsigned long size )
          : mMem( static_cast<T *>( CPLMalloc( sizeof( T ) * size ) ) )
        {}

        ~CPLAllocator() { CPLFree( static_cast<void *>( mMem ) ); }

        void reset( T *newData )
        {
          if ( mMem )
            CPLFree( static_cast<void *>( mMem ) );
          mMem = newData;
        }

        void reset( unsigned long size ) { reset( static_cast<T *>( CPLMalloc( sizeof( T ) * size ) ) ); }

        T &operator*()
        {
          // cppcheck-suppress returnTempReference
          return &mMem[0];
        }

        T *release()
        {
          T *tmpMem = mMem;
          mMem = nullptr;
          return tmpMem;
        }

        T &operator[]( const int index ) { return mMem[index]; }

        T *get() { return mMem; }

      private:
        T *mMem = nullptr;
    };


  private:
    QgsOpenClUtils();

    /**
     * Activate a device identified by its \a preferredDeviceId by making it the default device
     * if the device does not exists or deviceId is empty, the first GPU device will be
     * activated, if a GPU device is not found, the first CPU device will be chosen instead.
     *
     * Called once by init() when OpenCL is used for the first time in a QGIS working session.
     *
     * \return TRUE if the device could be found and activated. Return FALSE if the device was already
     * the active one or if a device could not be activated.
     *
     * \see init()
     * \see available()
     */
    static bool activate( const QString &preferredDeviceId = QString() );

    static bool activateInternal( const QString &preferredDeviceId );

    /**
     * Initialize the OpenCL system by setting and activating the default device.
     */
    static void init();

    static bool sAvailable;
};


#endif // QGSOPENCLUTILS_H
