/*
 * Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
 *
 * SPDX-License-Identifier: MIT
 */

#include "OCLBufferFromImage.h"

#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

#define GROUP_SIZE 256

const static char strKernel[] =
    "__kernel void buffer2bufferCopy(                                          "
    "         \n"
    "    __global char* input,                                                 "
    "          \n"
    "    __global char* output)                                                "
    "          \n"
    "{                                                                         "
    "         \n"
    "    int coord = (int)(get_global_id(0));                                  "
    "          \n"
    "    output[coord] = input[coord];                                         "
    "          \n"
    "}                                                                         "
    "         \n";

typedef CL_API_ENTRY cl_mem(CL_API_CALL* clCreateBufferFromImageAMD_fn)(cl_context context,
                                                                        cl_mem image,
                                                                        cl_int* errcode_ret);
clCreateBufferFromImageAMD_fn clCreateBufferFromImageAMD;

OCLBufferFromImage::OCLBufferFromImage() : OCLTestImp() {
  _numSubTests = 2;
  blockSizeX = GROUP_SIZE;
  blockSizeY = 1;
}

OCLBufferFromImage::~OCLBufferFromImage() {}

void OCLBufferFromImage::open(unsigned int test, char* units, double& conversion,
                              unsigned int deviceId) {
  buffer = bufferImage = clImage2D = bufferOut = NULL;
  done = false;
  pitchAlignment = 0;
  bufferSize = 0;

  _openTest = test;
  // Initialize random number seed
  srand((unsigned int)time(NULL));

  OCLTestImp::open(test, units, conversion, deviceId);
  if (_errorFlag) return;

  cl_device_type deviceType;
  error_ = _wrapper->clGetDeviceInfo(devices_[deviceId], CL_DEVICE_TYPE, sizeof(deviceType),
                                     &deviceType, NULL);
  CHECK_RESULT((error_ != CL_SUCCESS), "CL_DEVICE_TYPE failed");

  if (!(deviceType & CL_DEVICE_TYPE_GPU)) {
    testDescString = "GPU device is required for this test!\n";
    done = true;
    return;
  }

  cl_bool imageSupport;
  size_t size;
  _wrapper->clGetDeviceInfo(devices_[deviceId], CL_DEVICE_IMAGE_SUPPORT, sizeof(imageSupport),
                            &imageSupport, &size);
  if (!imageSupport) {
    testDescString = "Image not supported, skipping this test! ";
    done = true;
    return;
  }

  clCreateBufferFromImageAMD =
      (clCreateBufferFromImageAMD_fn)clGetExtensionFunctionAddressForPlatform(
          platform_, "clCreateBufferFromImageAMD");
  if (clCreateBufferFromImageAMD == NULL) {
    testDescString = "clCreateBufferFromImageAMD not found!\n";
    done = true;
    return;
  }

  CompileKernel();
  AllocateOpenCLBuffer();
}

void OCLBufferFromImage::run(void) {
  if (_errorFlag || done) {
    return;
  }

  if ((_openTest % 2) == 0) {
    testReadBuffer(bufferImage);
  } else {
    testKernel();
  }
}

void OCLBufferFromImage::AllocateOpenCLBuffer() {
  cl_int status = 0;

  size_t size = 0;
  pitchAlignment = 0;
  status = _wrapper->clGetDeviceInfo(devices_[_deviceId], CL_DEVICE_IMAGE_PITCH_ALIGNMENT,
                                     sizeof(cl_uint), &pitchAlignment, &size);
  pitchAlignment--;

  const unsigned int requiredPitch = ((imageWidth + pitchAlignment) & ~pitchAlignment);
  const unsigned int pitch = requiredPitch;
  bufferSize = pitch * imageHeight;

  unsigned char* sourceData = new unsigned char[bufferSize];

  // init data
  for (unsigned int y = 0; y < bufferSize; y++) {
    *(sourceData + y) = y;
  }
  buffer = _wrapper->clCreateBuffer(context_, CL_MEM_COPY_HOST_PTR | CL_MEM_READ_WRITE, bufferSize,
                                    sourceData, &status);

  delete[] sourceData;

  const cl_image_format format = {CL_RGBA, CL_UNSIGNED_INT8};
#if defined(CL_VERSION_2_0)
  const cl_image_desc desc = {
      CL_MEM_OBJECT_IMAGE2D, imageWidth / 4, imageHeight, 0, 0, pitch, 0, 0, 0, {buffer}};
#else
  const cl_image_desc desc = {
      CL_MEM_OBJECT_IMAGE2D, imageWidth / 4, imageHeight, 0, 0, pitch, 0, 0, 0, buffer};
#endif
  clImage2D = _wrapper->clCreateImage(context_, CL_MEM_READ_WRITE, &format, &desc, NULL, &status);
  CHECK_RESULT(clImage2D == NULL || status != CL_SUCCESS, "AllocateOpenCLImage() failed");

  bufferImage = clCreateBufferFromImageAMD(context_, clImage2D, &status);
  char c[1024];
  _wrapper->clGetDeviceInfo(devices_[_deviceId], CL_DRIVER_VERSION, sizeof(c), &c, NULL);
  if (status == CL_INVALID_OPERATION) {
    testDescString = "clCreateBufferFromImageAMD not supported on this device!\n";
    done = true;
    return;
  }
  CHECK_RESULT(bufferImage == NULL || status != CL_SUCCESS,
               "clCreateBufferFromImage(bufferOut) failed");

  bufferOut = _wrapper->clCreateBuffer(context_, CL_MEM_READ_WRITE, bufferSize, NULL, &status);
  CHECK_RESULT(bufferOut == NULL || status != CL_SUCCESS, "clCreateBuffer(bufferOut) failed");
}

void OCLBufferFromImage::testReadBuffer(cl_mem buffer) {
  cl_int status = 0;
  unsigned char* dstData = new unsigned char[bufferSize];

  status = clEnqueueReadBuffer(cmdQueues_[_deviceId], buffer, 1, 0, bufferSize, dstData, 0, 0, 0);

  ::clFinish(cmdQueues_[_deviceId]);

  for (unsigned int y = 0; y < bufferSize; y++) {
    if (*(dstData + y) != (unsigned char)y) {
      CHECK_RESULT_NO_RETURN(true, "CheckCLBuffer: *(dstData+y)!=y => %i != %i", *(dstData + y), y);
      goto cleanup;
    }
  }
cleanup:

  delete[] dstData;
}

void OCLBufferFromImage::testKernel() {
  CopyOpenCLBuffer(bufferImage);

  testReadBuffer(bufferOut);
}

unsigned int OCLBufferFromImage::close(void) {
  if (bufferImage != NULL) clReleaseMemObject(bufferImage);
  if (clImage2D != NULL) clReleaseMemObject(clImage2D);
  if (buffer != NULL) clReleaseMemObject(buffer);
  if (bufferOut != NULL) clReleaseMemObject(bufferOut);
  return OCLTestImp::close();
}

void OCLBufferFromImage::CopyOpenCLBuffer(cl_mem buffer) {
  cl_int status = 0;

  // Set appropriate arguments to the kernel2D

  // input buffer image
  status = clSetKernelArg(kernel_, 0, sizeof(cl_mem), &buffer);
  CHECK_RESULT((status != CL_SUCCESS),
               "CopyOpenCLBuffer() failed at "
               "clSetKernelArg(kernel_,0,sizeof(cl_mem),&buffer)");
  status = clSetKernelArg(kernel_, 1, sizeof(cl_mem), &bufferOut);
  CHECK_RESULT((status != CL_SUCCESS),
               "CopyOpenCLBuffer() failed at "
               "clSetKernelArg(kernel_,1,sizeof(cl_mem),&bufferOut)");

  // Enqueue a kernel run call.
  size_t global_work_offset[] = {0};
  size_t globalThreads[] = {bufferSize};
  size_t localThreads[] = {blockSizeX};

  status = clEnqueueNDRangeKernel(cmdQueues_[_deviceId], kernel_, 1, NULL, globalThreads, NULL, 0,
                                  NULL, 0);
  CHECK_RESULT((status != CL_SUCCESS), "CopyOpenCLBuffer() failed at clEnqueueNDRangeKernel");

  status = clFinish(cmdQueues_[_deviceId]);
  CHECK_RESULT((status != CL_SUCCESS), "CopyOpenCLBuffer() failed at clFinish");
}

void OCLBufferFromImage::CompileKernel() {
  cl_int status = 0;

  size_t kernelSize = sizeof(strKernel);
  const char* strs = (const char*)&strKernel[0];

  program_ = _wrapper->clCreateProgramWithSource(context_, 1, &strs, &kernelSize, &status);

  status = _wrapper->clBuildProgram(program_, 1, &devices_[_deviceId], NULL, NULL, NULL);
  if (status != CL_SUCCESS) {
    if (status == CL_BUILD_PROGRAM_FAILURE) {
      cl_int logStatus;
      size_t buildLogSize = 0;
      logStatus = clGetProgramBuildInfo(program_, devices_[_deviceId], CL_PROGRAM_BUILD_LOG,
                                        buildLogSize, NULL, &buildLogSize);
      std::string buildLog;
      buildLog.resize(buildLogSize);

      logStatus = clGetProgramBuildInfo(program_, devices_[_deviceId], CL_PROGRAM_BUILD_LOG,
                                        buildLogSize, &buildLog[0], NULL);
      printf("%s", buildLog.c_str());
    }
    return;
  }
  // get a kernel object handle for a kernel with the given name
  kernel_ = _wrapper->clCreateKernel(program_, "buffer2bufferCopy", &status);

  size_t kernel2DWorkGroupSize = 0;
  status = clGetKernelWorkGroupInfo(kernel_, devices_[_deviceId], CL_KERNEL_WORK_GROUP_SIZE,
                                    sizeof(size_t), &kernel2DWorkGroupSize, 0);

  if ((blockSizeX * blockSizeY) > kernel2DWorkGroupSize) {
    if (blockSizeX > kernel2DWorkGroupSize) {
      blockSizeX = kernel2DWorkGroupSize;
      blockSizeY = 1;
    }
  }
}
