//
// Copyright (c) 2017 The Khronos Group Inc.
// 
// 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
//
// 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.
//
#include "testBase.h"
#include "../../test_common/harness/typeWrappers.h"
#include "../../test_common/harness/conversions.h"

const char *zero_sized_enqueue_test_kernel[] = {
"__kernel void foo_kernel(__global float *src, __global int *dst)\n"
"{\n"
"    int  tid = get_global_id(0);\n"
"\n"
"    dst[tid] = (int)src[tid];\n"
"\n"
"}\n" };

const int bufSize = 128;

cl_int test_zero_sized_enqueue_and_test_output_buffer(cl_command_queue queue, clKernelWrapper& kernel, clMemWrapper& buf, size_t dim, size_t ndrange[])
{
    cl_int error = clEnqueueNDRangeKernel(queue, kernel, dim, NULL, ndrange, NULL, 0, NULL, NULL);
    if (error != CL_SUCCESS)
    {
        return error;
    }

    clFinish(queue);

    // check output buffer has not changed.
    int* output = reinterpret_cast<int*>(clEnqueueMapBuffer(queue, buf, CL_TRUE, CL_MAP_READ, 0, sizeof(int) * bufSize, 0, NULL, NULL, &error));
    if (error != CL_SUCCESS)
    {
        return error;
    }

    for (int i = 0; i < bufSize; ++i)
    {
        if (output[i] != 0)
        {
            log_error( "ERROR: output buffer value has changed.\n" );
            return CL_INVALID_OPERATION;
        }
    }

    return clEnqueueUnmapMemObject(queue, buf, output, 0, NULL, NULL);
}

int test_zero_sized_enqueue_helper(cl_device_id deviceID, cl_context context, cl_command_queue queue, int num_elements)
{
    int error;
    clProgramWrapper program;
    clKernelWrapper kernel;
    clMemWrapper            streams[2];
    size_t    ndrange1 = 0;
    size_t    ndrange20[2] = {0, 0};
    size_t    ndrange21[2] = {1, 0};
    size_t    ndrange22[2] = {0, 1};

    size_t    ndrange30[3] = {0, 0, 0};
    size_t    ndrange31[3] = {1, 0, 0};
    size_t    ndrange32[3] = {0, 1, 0};
    size_t    ndrange33[3] = {0, 0, 1};
    size_t    ndrange34[3] = {0, 1, 1};
    size_t    ndrange35[3] = {1, 0, 1};
    size_t    ndrange36[3] = {1, 1, 0};

    streams[0] = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_ALLOC_HOST_PTR, bufSize * sizeof(int), NULL, &error);
    streams[1] = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_ALLOC_HOST_PTR, bufSize * sizeof(int), NULL, &error);

    int* buf = new int[bufSize];
    memset(buf, 0, sizeof(int) * bufSize);

    // update output buffer
    error = clEnqueueWriteBuffer(queue, streams[1], CL_TRUE, 0, sizeof(int) * bufSize, buf, 0, NULL, NULL);


    /* Create a kernel to test with */
    if( create_single_kernel_helper( context, &program, &kernel, 1, zero_sized_enqueue_test_kernel, "foo_kernel" ) != 0 )
    {
        return -1;
    }

    error = clSetKernelArg(kernel, 0, sizeof(cl_mem), &streams[0]);
    test_error( error, "clSetKernelArg failed." );
    error = clSetKernelArg(kernel, 1, sizeof(cl_mem), &streams[1]);
    test_error( error, "clSetKernelArg failed." );

    // Simple API return code tests for 1D, 2D and 3D zero sized ND range.
    error = test_zero_sized_enqueue_and_test_output_buffer(queue, kernel, streams[1], 1, &ndrange1);
    test_error( error, "1D zero sized kernel enqueue failed." );

    error = test_zero_sized_enqueue_and_test_output_buffer(queue, kernel, streams[1], 2, ndrange20);
    test_error( error, "2D zero sized kernel enqueue failed." );

    error = test_zero_sized_enqueue_and_test_output_buffer(queue, kernel, streams[1], 2, ndrange21);
    test_error( error, "2D zero sized kernel enqueue failed." );

    error = test_zero_sized_enqueue_and_test_output_buffer(queue, kernel, streams[1], 2, ndrange22);
    test_error( error, "2D zero sized kernel enqueue failed." );


    error = test_zero_sized_enqueue_and_test_output_buffer(queue, kernel, streams[1], 3, ndrange30);
    test_error( error, "3D zero sized kernel enqueue failed." );

    error = test_zero_sized_enqueue_and_test_output_buffer(queue, kernel, streams[1], 3, ndrange31);
    test_error( error, "3D zero sized kernel enqueue failed." );

    error = test_zero_sized_enqueue_and_test_output_buffer(queue, kernel, streams[1], 3, ndrange32);
    test_error( error, "3D zero sized kernel enqueue failed." );

    error = test_zero_sized_enqueue_and_test_output_buffer(queue, kernel, streams[1], 3, ndrange33);
    test_error( error, "3D zero sized kernel enqueue failed." );

    error = test_zero_sized_enqueue_and_test_output_buffer(queue, kernel, streams[1], 3, ndrange34);
    test_error( error, "3D zero sized kernel enqueue failed." );

    error = test_zero_sized_enqueue_and_test_output_buffer(queue, kernel, streams[1], 3, ndrange35);
    test_error( error, "3D zero sized kernel enqueue failed." );

    error = test_zero_sized_enqueue_and_test_output_buffer(queue, kernel, streams[1], 3, ndrange36);
    test_error( error, "3D zero sized kernel enqueue failed." );

    // Verify zero-sized ND range kernel still satisfy event wait list and correct event object
    // is returned
    cl_event ev = NULL;
    clEventWrapper user_ev = clCreateUserEvent(context, &error);
    test_error( error, "user event creation failed." );
    error = clEnqueueNDRangeKernel(queue, kernel, 1, NULL, ndrange30, NULL, 1, &user_ev, &ev);
    test_error( error, "3D zero sized kernel enqueue failed." );
    if (ev == NULL)
    {
        log_error( "ERROR: failed to create an event object\n" );
        return -1;
    }

    cl_int sta;
    error = clGetEventInfo(ev, CL_EVENT_COMMAND_EXECUTION_STATUS, sizeof(cl_int), &sta, NULL);
    test_error( error, "Failed to get event status.");

    if (sta != CL_QUEUED)
    {
        log_error( "ERROR: incorrect zero sized kernel enqueue event status.\n" );
        return -1;
    }

    // now unblock zero-sized enqueue
    error = clSetUserEventStatus(user_ev, CL_COMPLETE);
    test_error( error, "Failed to set user event status.");

    clFinish(queue);

    // now check zero sized enqueue event status
    error = clGetEventInfo(ev, CL_EVENT_COMMAND_EXECUTION_STATUS, sizeof(cl_int), &sta, NULL);
    test_error( error, "Failed to get event status.");

    if (sta != CL_COMPLETE)
    {
        log_error( "ERROR: incorrect zero sized kernel enqueue event status.\n" );
        return -1;
    }

    delete [] buf;

    return 0;
}


int test_zero_sized_enqueue(cl_device_id deviceID, cl_context context, cl_command_queue queue, int num_elements)
{
    int res = test_zero_sized_enqueue_helper(deviceID, context, queue, num_elements);
    if (res != 0)
    {
        return res;
    }

    // now test out of order queue
    cl_command_queue_properties props;
    cl_int error = clGetDeviceInfo(deviceID, CL_DEVICE_QUEUE_PROPERTIES, sizeof(cl_command_queue_properties), &props, NULL);
    test_error( error, "clGetDeviceInfo failed.");

    if (props & CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE)
    {
        // test out of order queue
        cl_queue_properties queue_prop_def[] =
        {
            CL_QUEUE_PROPERTIES, CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE,
            0
        };

        clCommandQueueWrapper ooqueue = clCreateCommandQueueWithProperties(context, deviceID, queue_prop_def, &error);
        test_error( error, "clCreateCommandQueueWithProperties failed.");

        res = test_zero_sized_enqueue_helper(deviceID, context, ooqueue, num_elements);
    }

    return res;
}
