#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <string>
#include <fstream>
#include <stdint.h>

#include <QMessageBox>
#include <QMap>
#include <QAbstractButton>
#include <QFile>
#include <QTextStream>
#include <QDir>
#include <QStringList>

#include "resolution.h"
#include "clworker.h"
#include "platformdialog.h"

#define SUCCESS 0
#define FAILURE 1

using namespace std;

/**
 * @brief clWorker::clWorker
 * @param resolution
 * @param useGpu
 */

clWorker::clWorker(resolution resolution,bool useGpu)
{
    m_resolution = resolution;
    m_useGpu = useGpu;
}

/**
 * @brief clWorker::~clWorker
 */

clWorker::~clWorker()
{
}

/**
 * @brief clWorker::init
 * @param workBuffer
 * @param workBufferLength
 * @param imageMaterialId
 * @param kernelConfigs
 * @return
 */

bool clWorker::init(uint32_t *workBuffer,uint32_t workBufferLength,QList<clKernelConfig> kernelConfigs)
{
    m_kernels = kernelConfigs;

    // get platforms

    cl_uint numPlatforms;	//the NO. of platforms
    cl_platform_id platform = NULL;	//the chosen platform
    cl_int	status = clGetPlatformIDs(0, NULL, &numPlatforms);
    if (status != CL_SUCCESS)
    {
        print("Error: Getting platforms!\n");
        return false;
    }

    print(QString("Found %1 OpenCl platforms\n").arg(numPlatforms));

    // get ids

    cl_platform_id* platformIds = (cl_platform_id*)malloc(numPlatforms * sizeof(cl_platform_id));
    status = clGetPlatformIDs(numPlatforms, platformIds, NULL);

    QMap<QString,int> platformMap;
    for(int i=0;i<(int)numPlatforms;i++)
    {
        char pName[8192] = {0};
        clGetPlatformInfo(platformIds[i],CL_PLATFORM_NAME,8192,pName,NULL);
        print(QString("Platform #%1: %2\n").arg(i).arg(pName));

        platformMap[QString(pName)] = i;
    }

    PlatformDialog pfd;
    for(auto e : platformMap.keys())
    {
        pfd.addPlatform(e);
    }
    pfd.exec();
    m_useGpu = !pfd.useCpu();

    QString platformName = pfd.currentPlatform();

    print(QString("Selected platform is '%1'\n").arg(platformName));

    platform = platformIds[platformMap[platformName]];
    free(platformIds);

    // get device

    cl_uint				numDevices = 0;
    status = clGetDeviceIDs(platform, m_useGpu?CL_DEVICE_TYPE_GPU:CL_DEVICE_TYPE_CPU, 0, NULL, &numDevices);
    if (numDevices == 0)	// no GPU
    {
        print("No GPU device available.\n");
        print("Choose CPU as default device.\n");

        status = clGetDeviceIDs(platform, CL_DEVICE_TYPE_CPU, 0, NULL, &numDevices);
        m_devices = (cl_device_id*)malloc(numDevices * sizeof(cl_device_id));
        status = clGetDeviceIDs(platform, CL_DEVICE_TYPE_CPU, numDevices, m_devices, NULL);
    }
    else
    {
        m_devices = (cl_device_id*)malloc(numDevices * sizeof(cl_device_id));
        status = clGetDeviceIDs(platform, m_useGpu?CL_DEVICE_TYPE_GPU:CL_DEVICE_TYPE_CPU, numDevices, m_devices, NULL);
    }

    // print device names

    for(int i=0;i<(int)numDevices;i++)
    {
        size_t valueSize;
        clGetDeviceInfo(m_devices[i], CL_DEVICE_NAME, 0, NULL, &valueSize);
        char* value = (char*) malloc(valueSize);
        clGetDeviceInfo(m_devices[i], CL_DEVICE_NAME, valueSize, value, NULL);
        print(QString("Device #%1: %2 %3\n").arg(i).arg(value).arg(i==0?"use this one":""));
        free(value);
    }

    // context

    m_context = clCreateContext(NULL, 1, m_devices, NULL, NULL, NULL);

    // queue

    m_commandQueue = clCreateCommandQueue(m_context, m_devices[0], 0, NULL);

    // program

    QString filename = ":/PongKernel.cl";
    QFile file(filename);
    file.open(QIODevice::ReadOnly);
    if(!file.isOpen())
    {
        print(QString("Can't open file '%1'\n").arg(filename));
        return false;
    }
    QTextStream fileTestStream(&file);
    QString kernelSource = fileTestStream.readAll();
    QByteArray kernelSourceBA = kernelSource.toUtf8();

    const char *source = kernelSourceBA.data();
    size_t sourceSize[] = { static_cast<size_t>(kernelSource.length()) };
    m_program = clCreateProgramWithSource(m_context, 1, &source, sourceSize, NULL);

    QStringList buildFlags;

    if(pfd.fastMath())
    {
        buildFlags.append("-cl-mad-enable");
        buildFlags.append("-cl-no-signed-zeros");
        buildFlags.append("-cl-denorms-are-zero");
        buildFlags.append("-cl-fast-relaxed-math");
    }

    if(pfd.wError())
    {
        buildFlags.append("-Werror");
    }

    if(pfd.saveTemps())
    {
        QString currentPath = QDir::currentPath();
        print(QString("Try to save temps to %1\n").arg(currentPath));
        buildFlags += QString(" -save-temps=%1").arg(currentPath);
    }

    QByteArray buildFlagsBA = buildFlags.join(" ").toUtf8();
    const char *buildFlagsCS = buildFlagsBA.data();

    print(QString("Use compile flags = '%1'\n").arg(buildFlagsCS));

    // build

    status = clBuildProgram(m_program, 1, m_devices, buildFlagsCS , NULL, NULL);
    if(status != CL_SUCCESS)
    {
        print(QString("CLProgram was not successfull because (Returncode %1)\n").arg(status));
        print(QString("Error code as String '%1'\n").arg(getErrorString(status)));

        size_t length;
        char buffer[2048];

        for(int i=0;i<(int)numDevices;i++)
        {
            print(QString("Status for device '%1'\n").arg(i));

            buffer[0] = 0;
            clGetProgramBuildInfo(m_program, m_devices[i], CL_PROGRAM_BUILD_LOG, sizeof(buffer), buffer, &length);
            print(QString("Build-Log '%1'\n").arg(buffer));

            buffer[0] = 0;
            clGetProgramBuildInfo(m_program, m_devices[i], CL_PROGRAM_BUILD_OPTIONS, sizeof(buffer), buffer, &length);
            print(QString("Build-Opt '%1'\n").arg(buffer));
        }

        return false;
    }

    // buffers etc.

    const uint32_t dataLength = m_resolution.width*m_resolution.height;

    m_workBuffer = clCreateBuffer(m_context, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, (workBufferLength) * sizeof(uint32_t), (void *)workBuffer, NULL);
    m_outputBuffer = clCreateBuffer(m_context, CL_MEM_READ_WRITE, (dataLength) * sizeof(uint32_t), NULL, NULL);

    // kernel object

    for (QList<clKernelConfig>::iterator it = m_kernels.begin(); it != m_kernels.end(); ++it)
    {
        QString name = it->getName();
        cl_kernel kernel = clCreateKernel(m_program,name.toLatin1().data(), NULL);
        it->setKernel(kernel);

    }

    return status==SUCCESS;
}

/**
 * @brief clWorker::run
 * @param sliderX
 * @param sliderY
 * @param sliderZ
 * @param kernelName
 * @return
 */

bool clWorker::run(unsigned int sliderX,unsigned int sliderY,unsigned int sliderZ,QString kernelName)
{
    cl_int	status;

    clKernelConfig config;

    for (QList<clKernelConfig>::iterator it = m_kernels.begin(); it != m_kernels.end(); ++it)
    {
        if(it->getName() == kernelName)
        {
            config = *it;
            break;
        }
    }

    cl_kernel kernel = config.getKernel();

    if(kernel == NULL)
    {
        print("unknown kernel\n");
        return false;
    }

    /* sets Kernel arguments */
    status = clSetKernelArg(kernel, 0, sizeof(cl_mem),  (void *)&m_workBuffer);
    status = clSetKernelArg(kernel, 1, sizeof(cl_mem),  (void *)&m_outputBuffer);
    status = clSetKernelArg(kernel, 2, sizeof(cl_uint), (void *)&m_resolution.width);
    status = clSetKernelArg(kernel, 3, sizeof(cl_uint), (void *)&m_resolution.height);
    status = clSetKernelArg(kernel, 4, sizeof(cl_uint), (void *)&sliderX);
    status = clSetKernelArg(kernel, 5, sizeof(cl_uint), (void *)&sliderY);
    status = clSetKernelArg(kernel, 6, sizeof(cl_uint), (void *)&sliderZ);

    size_t global_work_size[] = {config.getWorkSize(0),config.getWorkSize(1)};

    status = clEnqueueNDRangeKernel(m_commandQueue, kernel, 2, NULL, global_work_size, NULL, 0, NULL, NULL);
    clFinish(m_commandQueue);

    return status==SUCCESS;
}

/**
 * @brief clWorker::copyBack
 * @param outData
 * @return
 */

bool clWorker::copyBack(uint32_t *outData)
{
    cl_int	status;

    const uint32_t dataLength = m_resolution.width*m_resolution.height;

    status = clEnqueueReadBuffer(m_commandQueue, m_outputBuffer, CL_TRUE, 0, dataLength * sizeof(uint32_t), outData, 0, NULL, NULL);
    return status==SUCCESS;
}

bool clWorker::writeWorkBuffer(uint32_t *workBuffer,uint32_t workBufferLength)
{
    cl_int status = clEnqueueWriteBuffer(m_commandQueue,m_workBuffer,CL_TRUE,0, (workBufferLength) * sizeof(uint32_t), (void *)workBuffer,0,NULL,NULL);
    return status==SUCCESS;
}

bool clWorker::clearOutputBuffer()
{
    const uint32_t dataLength = m_resolution.width*m_resolution.height;
    uint32_t pattern = 0xff000000;
    cl_int status = clEnqueueFillBuffer(m_commandQueue,m_outputBuffer,&pattern,sizeof(uint32_t),0, (dataLength) * sizeof(uint32_t),0,NULL,NULL);
    return status==SUCCESS;
}

/**
 * @brief clWorker::cleanup
 * @return
 */

bool clWorker::cleanup()
{
    cl_int	status = SUCCESS;

    // Clean the resources

    for (QList<clKernelConfig>::iterator it = m_kernels.begin(); it != m_kernels.end(); ++it)
    {
        status &= clReleaseKernel(it->getKernel());				//Release kernel.
    }
    status &= clReleaseProgram(m_program);				//Release the program object.
    status &= clReleaseMemObject(m_workBuffer);				//Release mem object.
    status &= clReleaseMemObject(m_outputBuffer);
    status &= clReleaseMemObject(m_inputImage);
    status &= clReleaseSampler(m_inputImageSampler);
    status &= clReleaseCommandQueue(m_commandQueue);	//Release  Command queue.
    status &= clReleaseContext(m_context);				//Release context.

    if (m_devices != NULL)
    {
        free(m_devices);
        m_devices = NULL;
    }
    return status==SUCCESS;
}

/**
 * @brief clWorker::getErrorString
 * @param error
 * @return
 */

const char *clWorker::getErrorString(cl_int error)
{
    switch(error)
    {
    // run-time and JIT compiler errors
    case 0: return "CL_SUCCESS";
    case -1: return "CL_DEVICE_NOT_FOUND";
    case -2: return "CL_DEVICE_NOT_AVAILABLE";
    case -3: return "CL_COMPILER_NOT_AVAILABLE";
    case -4: return "CL_MEM_OBJECT_ALLOCATION_FAILURE";
    case -5: return "CL_OUT_OF_RESOURCES";
    case -6: return "CL_OUT_OF_HOST_MEMORY";
    case -7: return "CL_PROFILING_INFO_NOT_AVAILABLE";
    case -8: return "CL_MEM_COPY_OVERLAP";
    case -9: return "CL_IMAGE_FORMAT_MISMATCH";
    case -10: return "CL_IMAGE_FORMAT_NOT_SUPPORTED";
    case -11: return "CL_BUILD_PROGRAM_FAILURE";
    case -12: return "CL_MAP_FAILURE";
    case -13: return "CL_MISALIGNED_SUB_BUFFER_OFFSET";
    case -14: return "CL_EXEC_STATUS_ERROR_FOR_EVENTS_IN_WAIT_LIST";
    case -15: return "CL_COMPILE_PROGRAM_FAILURE";
    case -16: return "CL_LINKER_NOT_AVAILABLE";
    case -17: return "CL_LINK_PROGRAM_FAILURE";
    case -18: return "CL_DEVICE_PARTITION_FAILED";
    case -19: return "CL_KERNEL_ARG_INFO_NOT_AVAILABLE";

        // compile-time errors
    case -30: return "CL_INVALID_VALUE";
    case -31: return "CL_INVALID_DEVICE_TYPE";
    case -32: return "CL_INVALID_PLATFORM";
    case -33: return "CL_INVALID_DEVICE";
    case -34: return "CL_INVALID_CONTEXT";
    case -35: return "CL_INVALID_QUEUE_PROPERTIES";
    case -36: return "CL_INVALID_COMMAND_QUEUE";
    case -37: return "CL_INVALID_HOST_PTR";
    case -38: return "CL_INVALID_MEM_OBJECT";
    case -39: return "CL_INVALID_IMAGE_FORMAT_DESCRIPTOR";
    case -40: return "CL_INVALID_IMAGE_SIZE";
    case -41: return "CL_INVALID_SAMPLER";
    case -42: return "CL_INVALID_BINARY";
    case -43: return "CL_INVALID_BUILD_OPTIONS";
    case -44: return "CL_INVALID_PROGRAM";
    case -45: return "CL_INVALID_PROGRAM_EXECUTABLE";
    case -46: return "CL_INVALID_KERNEL_NAME";
    case -47: return "CL_INVALID_KERNEL_DEFINITION";
    case -48: return "CL_INVALID_KERNEL";
    case -49: return "CL_INVALID_ARG_INDEX";
    case -50: return "CL_INVALID_ARG_VALUE";
    case -51: return "CL_INVALID_ARG_SIZE";
    case -52: return "CL_INVALID_KERNEL_ARGS";
    case -53: return "CL_INVALID_WORK_DIMENSION";
    case -54: return "CL_INVALID_WORK_GROUP_SIZE";
    case -55: return "CL_INVALID_WORK_ITEM_SIZE";
    case -56: return "CL_INVALID_GLOBAL_OFFSET";
    case -57: return "CL_INVALID_EVENT_WAIT_LIST";
    case -58: return "CL_INVALID_EVENT";
    case -59: return "CL_INVALID_OPERATION";
    case -60: return "CL_INVALID_GL_OBJECT";
    case -61: return "CL_INVALID_BUFFER_SIZE";
    case -62: return "CL_INVALID_MIP_LEVEL";
    case -63: return "CL_INVALID_GLOBAL_WORK_SIZE";
    case -64: return "CL_INVALID_PROPERTY";
    case -65: return "CL_INVALID_IMAGE_DESCRIPTOR";
    case -66: return "CL_INVALID_COMPILER_OPTIONS";
    case -67: return "CL_INVALID_LINKER_OPTIONS";
    case -68: return "CL_INVALID_DEVICE_PARTITION_COUNT";

        // extension errors
    case -1000: return "CL_INVALID_GL_SHAREGROUP_REFERENCE_KHR";
    case -1001: return "CL_PLATFORM_NOT_FOUND_KHR";
    case -1002: return "CL_INVALID_D3D10_DEVICE_KHR";
    case -1003: return "CL_INVALID_D3D10_RESOURCE_KHR";
    case -1004: return "CL_D3D10_RESOURCE_ALREADY_ACQUIRED_KHR";
    case -1005: return "CL_D3D10_RESOURCE_NOT_ACQUIRED_KHR";
    default: return "Unknown OpenCL error";
    }
}
