#include <CL/cl.h>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <string>
#include <fstream>

using namespace std;

const char *getCLErrorString(cl_int err);
bool checkErrors(cl_int err, const char *message);
bool checkErrors(cl_int err, const string &message);
void getPlatformIDs(cl_uint *numPlatforms, cl_platform_id **platforms);
void deletePlatformIDs(cl_platform_id *platforms);
void getGPUDeviceIDs(cl_uint numPlatforms, cl_platform_id *platforms, cl_uint **numDevices, cl_device_id ***devices);
void getGPUDeviceIDsForPlatform(cl_platform_id platform, cl_uint *numDevices, cl_device_id **devices);
void deleteGPUDeviceIDs(cl_uint numPlatforms, cl_uint *numDevices, cl_device_id **devices);
void printDeviceInfo(cl_device_id device);
bool createContext(const cl_device_id *device, cl_context *context);
bool createCommandQueue(cl_context context, cl_device_id device, cl_command_queue *commandQueue);
void printProgramBuildInfo(cl_program program, cl_device_id device);
bool createAndBuildProgram(const char *filename, cl_device_id device, cl_context context, cl_program *program);
bool createKernel(cl_program program, const char *kernelName, cl_kernel *kernel);

int main(int argc, char **argv)
{
    cl_uint num_platforms = 0;
    cl_platform_id *platforms = NULL;
    cl_uint *num_devices = NULL;
    cl_device_id **devices = NULL;

    getPlatformIDs(&num_platforms, &platforms);
    getGPUDeviceIDs(num_platforms, platforms, &num_devices, &devices);

    int use_platform = 0;
    int use_device = 0;
    cl_platform_id platform = platforms[use_platform];
    cl_device_id device = devices[use_platform][use_device];
    cl_context context = 0;
    cl_command_queue command_queue = 0;

    if (!createContext(&device, &context))
    {
        deleteGPUDeviceIDs(num_platforms, num_devices, devices);
        deletePlatformIDs(platforms);
        exit(EXIT_FAILURE);
    }

    if (!createCommandQueue(context, device, &command_queue))
    {
        clReleaseContext(context);
        deleteGPUDeviceIDs(num_platforms, num_devices, devices);
        deletePlatformIDs(platforms);
        exit(EXIT_FAILURE);
    }

    //cl_program program = 0;
    //cl_kernel kernel = 0;
    //createAndBuildProgram("kernel.cl", device, context, &program);
    //createKernel(program, "myKernel", &kernel);

    // ::TODO::

    //clReleaseKernel(kernel);
    //clReleaseProgram(program);
    clReleaseCommandQueue(command_queue);
    clReleaseContext(context);
    deleteGPUDeviceIDs(num_platforms, num_devices, devices);
    deletePlatformIDs(platforms);
    system("pause");
    return 0;
}

const char *getCLErrorString(cl_int err)
{
    switch (err)
    {
    case CL_SUCCESS: return "CL_SUCCESS";
    case CL_DEVICE_NOT_FOUND: return "CL_DEVICE_NOT_FOUND";
    case CL_DEVICE_NOT_AVAILABLE: return "CL_DEVICE_NOT_AVAILABLE";
    case CL_COMPILER_NOT_AVAILABLE: return "CL_COMPILER_NOT_AVAILABLE";
    case CL_MEM_OBJECT_ALLOCATION_FAILURE: return "CL_MEM_OBJECT_ALLOCATION_FAILURE";
    case CL_OUT_OF_RESOURCES: return "CL_OUT_OF_RESOURCES";
    case CL_OUT_OF_HOST_MEMORY: return "CL_OUT_OF_HOST_MEMORY";
    case CL_PROFILING_INFO_NOT_AVAILABLE: return "CL_PROFILING_INFO_NOT_AVAILABLE";
    case CL_MEM_COPY_OVERLAP: return "CL_MEM_COPY_OVERLAP";
    case CL_IMAGE_FORMAT_MISMATCH: return "CL_IMAGE_FORMAT_MISMATCH";
    case CL_IMAGE_FORMAT_NOT_SUPPORTED: return "CL_IMAGE_FORMAT_NOT_SUPPORTED";
    case CL_BUILD_PROGRAM_FAILURE: return "CL_BUILD_PROGRAM_FAILURE";
    case CL_MAP_FAILURE: return "CL_MAP_FAILURE";
    case CL_MISALIGNED_SUB_BUFFER_OFFSET: return "CL_MISALIGNED_SUB_BUFFER_OFFSET";
    case CL_EXEC_STATUS_ERROR_FOR_EVENTS_IN_WAIT_LIST: return "CL_EXEC_STATUS_ERROR_FOR_EVENTS_IN_WAIT_LIST";
    case CL_INVALID_VALUE: return "CL_INVALID_VALUE";
    case CL_INVALID_DEVICE_TYPE: return "CL_INVALID_DEVICE_TYPE";
    case CL_INVALID_PLATFORM: return "CL_INVALID_PLATFORM";
    case CL_INVALID_DEVICE: return "CL_INVALID_DEVICE";
    case CL_INVALID_CONTEXT: return "CL_INVALID_CONTEXT";
    case CL_INVALID_QUEUE_PROPERTIES: return "CL_INVALID_QUEUE_PROPERTIES";
    case CL_INVALID_COMMAND_QUEUE: return "CL_INVALID_COMMAND_QUEUE";
    case CL_INVALID_HOST_PTR: return "CL_INVALID_HOST_PTR";
    case CL_INVALID_MEM_OBJECT: return "CL_INVALID_MEM_OBJECT";
    case CL_INVALID_IMAGE_FORMAT_DESCRIPTOR: return "CL_INVALID_IMAGE_FORMAT_DESCRIPTOR";
    case CL_INVALID_IMAGE_SIZE: return "CL_INVALID_IMAGE_SIZE";
    case CL_INVALID_SAMPLER: return "CL_INVALID_SAMPLER";
    case CL_INVALID_BINARY: return "CL_INVALID_BINARY";
    case CL_INVALID_BUILD_OPTIONS: return "CL_INVALID_BUILD_OPTIONS";
    case CL_INVALID_PROGRAM: return "CL_INVALID_PROGRAM";
    case CL_INVALID_PROGRAM_EXECUTABLE: return "CL_INVALID_PROGRAM_EXECUTABLE";
    case CL_INVALID_KERNEL_NAME: return "CL_INVALID_KERNEL_NAME";
    case CL_INVALID_KERNEL_DEFINITION: return "CL_INVALID_KERNEL_DEFINITION";
    case CL_INVALID_KERNEL: return "CL_INVALID_KERNEL";
    case CL_INVALID_ARG_INDEX: return "CL_INVALID_ARG_INDEX";
    case CL_INVALID_ARG_VALUE: return "CL_INVALID_ARG_VALUE";
    case CL_INVALID_ARG_SIZE: return "CL_INVALID_ARG_SIZE";
    case CL_INVALID_KERNEL_ARGS: return "CL_INVALID_KERNEL_ARGS";
    case CL_INVALID_WORK_DIMENSION: return "CL_INVALID_WORK_DIMENSION";
    case CL_INVALID_WORK_GROUP_SIZE: return "CL_INVALID_WORK_GROUP_SIZE";
    case CL_INVALID_WORK_ITEM_SIZE: return "CL_INVALID_WORK_ITEM_SIZE";
    case CL_INVALID_GLOBAL_OFFSET: return "CL_INVALID_GLOBAL_OFFSET";
    case CL_INVALID_EVENT_WAIT_LIST: return "CL_INVALID_EVENT_WAIT_LIST";
    case CL_INVALID_EVENT: return "CL_INVALID_EVENT";
    case CL_INVALID_OPERATION: return "CL_INVALID_OPERATION";
    case CL_INVALID_GL_OBJECT: return "CL_INVALID_GL_OBJECT";
    case CL_INVALID_BUFFER_SIZE: return "CL_INVALID_BUFFER_SIZE";
    case CL_INVALID_MIP_LEVEL: return "CL_INVALID_MIP_LEVEL";
    case CL_INVALID_GLOBAL_WORK_SIZE: return "CL_INVALID_GLOBAL_WORK_SIZE";
    case CL_INVALID_PROPERTY: return "CL_INVALID_PROPERTY";
    default: return "Unknown OpenCL error!";
    }
}

bool checkErrors(cl_int err, const char *message)
{
    if (CL_SUCCESS == err)
    {
        return false;
    }

    cerr << message << endl;
    cerr << "Error code: " << getCLErrorString(err) << endl;
    system("pause > nul");
    return true;
}

bool checkErrors(cl_int err, const string &message)
{
    return checkErrors(err, message.c_str());
}

void getPlatformIDs(cl_uint *numPlatforms, cl_platform_id **platforms)
{
    cl_int err;

    err = clGetPlatformIDs(0, NULL, numPlatforms);
    if (checkErrors(err, "Error while obtaining the number of OpenCL platforms!"))
    {
        exit(EXIT_FAILURE);
    }

    cout << "The number of available OpenCL platforms: " << *numPlatforms << endl;

    *platforms = new cl_platform_id[*numPlatforms];
    err = clGetPlatformIDs(*numPlatforms, *platforms, NULL);
    if (checkErrors(err, "Error while obtaining the IDs of OpenCL platforms!"))
    {
        deletePlatformIDs(*platforms);
        exit(EXIT_FAILURE);
    }

    cout << "The IDs of OpenCL platforms obtained." << endl << endl;
}

void deletePlatformIDs(cl_platform_id *platforms)
{
    delete[] platforms;
}

void getGPUDeviceIDs(cl_uint numPlatforms, cl_platform_id *platforms, cl_uint **numDevices, cl_device_id ***devices)
{
    cl_uint sum_num_devices = 0;

    *numDevices = new cl_uint[numPlatforms];
    *devices = new cl_device_id*[numPlatforms];
    for (cl_uint i = 0; i < numPlatforms; ++i)
    {
        cout << ">> OpenCL platform #" << i << endl;
        (*numDevices)[i] = 0;
        (*devices)[i] = NULL;
        getGPUDeviceIDsForPlatform(platforms[i], *numDevices + i, *devices + i);
        sum_num_devices += (*numDevices)[i];
        for (cl_uint j = 0; j < (*numDevices)[i]; ++j)
        {
            cout << "> OpenCL GPU device #" << j << ":" << endl;
            printDeviceInfo((*devices)[i][j]);
        }
        cout << endl;
    }
    if (0 == sum_num_devices)
    {
        cerr << "OpenCL GPU devices were not found!" << endl;
        deleteGPUDeviceIDs(numPlatforms, *numDevices, *devices);
        deletePlatformIDs(platforms);
        system("pause > nul");
        exit(EXIT_FAILURE);
    }
}

void getGPUDeviceIDsForPlatform(cl_platform_id platform, cl_uint *numDevices, cl_device_id **devices)
{
    cl_int err;

    err = clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 0, NULL, numDevices);
    if (checkErrors(err, "Error while obtaining the number of available OpenCL GPU devices!"))
    {
        return;
    }

    cout << "The number of available OpenCL GPU devices: " << *numDevices << endl;
    if (0 == *numDevices)
    {
        return;
    }

    *devices = new cl_device_id[*numDevices];
    err = clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, *numDevices, *devices, NULL);
    if (checkErrors(err, "Error while obtaining the IDs of OpenCL GPU devices!"))
    {
        delete[] * devices;
        *devices = NULL;
        *numDevices = 0;
        return;
    }

    cout << "The IDs of OpenCL GPU devices obtained." << endl;
}

void deleteGPUDeviceIDs(cl_uint numPlatforms, cl_uint *numDevices, cl_device_id **devices)
{
    for (cl_uint i = 0; i < numPlatforms; ++i)
    {
        delete[] devices[i];
    }
    delete[] devices;
    delete[] numDevices;
}

void printDeviceInfo(cl_device_id device)
{
    cl_int err;
    size_t param_value_size = 0;
    char *buffer = NULL;

    // CL_DEVICE_NAME
    err = clGetDeviceInfo(device, CL_DEVICE_NAME, 0, NULL, &param_value_size);
    if (!checkErrors(err, "Error while obtaining the size of device name string!"))
    {
        buffer = new char[param_value_size];
        clGetDeviceInfo(device, CL_DEVICE_NAME, param_value_size, static_cast<void *>(buffer), NULL);
        cout << "CL_DEVICE_NAME: " << buffer << endl;
        delete[] buffer;
        buffer = NULL;
    }

    // CL_DEVICE_VENDOR
    err = clGetDeviceInfo(device, CL_DEVICE_VENDOR, 0, NULL, &param_value_size);
    if (!checkErrors(err, "Error while obtaining the size of vendor name string!"))
    {
        buffer = new char[param_value_size];
        clGetDeviceInfo(device, CL_DEVICE_VENDOR, param_value_size, static_cast<void *>(buffer), NULL);
        cout << "CL_DEVICE_VENDOR: " << buffer << endl;
        delete[] buffer;
        buffer = NULL;
    }

    // CL_DEVICE_VERSION
    err = clGetDeviceInfo(device, CL_DEVICE_VERSION, 0, NULL, &param_value_size);
    if (!checkErrors(err, "Error while obtaining the size of OpenCL version string!"))
    {
        buffer = new char[param_value_size];
        clGetDeviceInfo(device, CL_DEVICE_VERSION, param_value_size, static_cast<void *>(buffer), NULL);
        cout << "CL_DEVICE_VERSION: " << buffer << endl;
        delete[] buffer;
        buffer = NULL;
    }

    // CL_DEVICE_PROFILE
    err = clGetDeviceInfo(device, CL_DEVICE_PROFILE, 0, NULL, &param_value_size);
    if (!checkErrors(err, "Error while obtaining the size of OpenCL profile string!"))
    {
        buffer = new char[param_value_size];
        clGetDeviceInfo(device, CL_DEVICE_PROFILE, param_value_size, static_cast<void *>(buffer), NULL);
        cout << "CL_DEVICE_PROFILE: " << buffer << endl;
        delete[] buffer;
        buffer = NULL;
    }

    // CL_DRIVER_VERSION
    err = clGetDeviceInfo(device, CL_DRIVER_VERSION, 0, NULL, &param_value_size);
    if (!checkErrors(err, "Error while obtaining the size of OpenCL software driver version string!"))
    {
        buffer = new char[param_value_size];
        clGetDeviceInfo(device, CL_DRIVER_VERSION, param_value_size, static_cast<void *>(buffer), NULL);
        cout << "CL_DRIVER_VERSION: " << buffer << endl;
        delete[] buffer;
        buffer = NULL;
    }
}

bool createContext(const cl_device_id *device, cl_context *context)
{
    cl_int err;

    *context = clCreateContext(NULL, 1, device, NULL, NULL, &err);
    if (checkErrors(err, "Error while creating an OpenCL context!"))
    {
        *context = 0;
        return false;
    }

    cout << "An OpenCL context created." << endl << endl;
    return true;
}

bool createCommandQueue(cl_context context, cl_device_id device, cl_command_queue *commandQueue)
{
    cl_int err;

    *commandQueue = clCreateCommandQueue(context, device, 0, &err);
    if (checkErrors(err, "Error while creating a command-queue!"))
    {
        *commandQueue = 0;
        return false;
    }

    cout << "A command-queue created." << endl << endl;
    return true;
}

void printProgramBuildInfo(cl_program program, cl_device_id device)
{
	cl_int err;
	size_t param_value_size = 0;
	char *buffer = NULL;

	err = clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 0, NULL, &param_value_size);
	if (!checkErrors(err, "Error while obtaining the size of build log string!"))
	{
		buffer = new char[param_value_size];
		clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, param_value_size, static_cast<void *>(buffer), NULL);
		cout << "Build log:" << endl << buffer << endl;
		delete[] buffer;
		buffer = NULL;
	}
}

bool createAndBuildProgram(const char *filename, cl_device_id device, cl_context context, cl_program *program)
{
	cl_int err;
	ifstream file_cl(filename);
	string source_code;
	string line;
	char *buffer = NULL;

	if (!file_cl.is_open())
	{
		cerr << "Error while opening the file '" << filename << "'!" << endl;
		return false;
	}

	while (file_cl.good())
	{
		getline(file_cl, line);
		source_code += line;
	}
	file_cl.close();
	buffer = new char[source_code.length() + 1];
	sprintf(buffer, "%s", source_code.c_str());
	*program = clCreateProgramWithSource(context, 1, const_cast<const char **>(&buffer), NULL, &err);
	delete[] buffer;
	buffer = NULL;
	if (checkErrors(err, string("Error while creating a program object for the source code '") + filename + "'!"))
	{
		*program = 0;
		return false;
	}

	err = clBuildProgram(*program, 0, NULL, NULL, NULL, NULL);
	if (checkErrors(err, string("Error while building a program executable from the source code '") + filename + "'!"))
	{
		printProgramBuildInfo(*program, device);
		clReleaseProgram(*program);
		*program = 0;
		return false;
	}

	cout << "An OpenCL program created and built from the source code '" << filename << "'." << endl << endl;
	return true;
}

bool createKernel(cl_program program, const char *kernelName, cl_kernel *kernel)
{
    cl_int err;

    *kernel = clCreateKernel(program, kernelName, &err);
    if (checkErrors(err, string("Error while creating a kernel object for a function '") + kernelName + "'!"))
    {
        *kernel = 0;
        return false;
    }

    cout << "A kernel object for a function '" << kernelName << "' created." << endl << endl;
    return true;
}
