//
// 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/testHarness.h"

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

    "__kernel void sample_test_2(__global int *src, __global int *dst)\n"
    "{\n"
    "    int  tid = get_global_id(0);\n"
    "    dst[tid] = src[tid] * 2;\n"
    "\n"
    "}\n"

    "__kernel void sample_test_3(__global int *src, __global int *dst)\n"
    "{\n"
    "    int  tid = get_global_id(0);\n"
    "    dst[tid] = src[tid] / 2;\n"
    "\n"
    "}\n"

    "__kernel void sample_test_4(__global int *src, __global int *dst)\n"
    "{\n"
    "    int  tid = get_global_id(0);\n"
    "    dst[tid] = src[tid] /3;\n"
    "\n"
    "}\n"
};

int sampleAction1( int source ) { return source; }
int sampleAction2( int source ) { return source * 2; }
int sampleAction3( int source ) { return source / 2; }
int sampleAction4( int source ) { return source / 3; }


typedef int (*sampleActionFn)( int source );

sampleActionFn    sampleActions[4] = { sampleAction1, sampleAction2, sampleAction3, sampleAction4 };

#define BUFFER_COUNT 2
#define TEST_SIZE    512

typedef struct TestItem
{
    struct TestItem     *next;
    cl_context          c;
    cl_command_queue    q;
    cl_program          p;
    cl_kernel           k[4];
    cl_mem              m[BUFFER_COUNT];
    MTdata              d;
}TestItem;

static void DestroyTestItem( TestItem *item );

// Attempt to create a context and associated objects
TestItem *CreateTestItem( cl_device_id deviceID, cl_int *err )
{
    cl_int error = 0;
    size_t i;

    // Allocate the TestItem struct
    TestItem *item = (TestItem *) malloc( sizeof(TestItem ) );
    if( NULL == item  )
    {
        if( err )
        {
            log_error( "FAILURE: Failed to allocate TestItem -- out of host memory!\n" );
            *err = CL_OUT_OF_HOST_MEMORY;
        }
        return NULL;
    }
    //zero so we know which fields we have initialized
    memset( item, 0, sizeof( *item ) );

    item->d = init_genrand( gRandomSeed );
    if( NULL == item->d )
    {
        if( err )
        {
            log_error( "FAILURE: Failed to allocate mtdata om CreateTestItem -- out of host memory!\n" );
            *err = CL_OUT_OF_HOST_MEMORY;
        }
        DestroyTestItem( item );
        return NULL;
    }


    // Create a context
    item->c = clCreateContext(NULL, 1, &deviceID, notify_callback, NULL, &error );
    if( item->c == NULL || error != CL_SUCCESS)
    {
        if (err) {
            log_error( "FAILURE: clCreateContext failed in CreateTestItem: %d\n", error);
            *err = error;
        }
        DestroyTestItem( item );
        return NULL;
    }

    // Create a queue
    item->q = clCreateCommandQueueWithProperties( item->c, deviceID, 0, &error);
    if( item->q == NULL || error != CL_SUCCESS)
    {
        if (err) {
            log_error( "FAILURE: clCreateCommandQueue failed in CreateTestItem: %d\n", error );
            *err = error;
        }
        DestroyTestItem( item );
        return NULL;
    }

    // Create a program
    error = create_single_kernel_helper_create_program(item->c, &item->p, 1, context_test_kernels);
    if( NULL == item->p || CL_SUCCESS != error )
    {
        if( err )
        {
            log_error( "FAILURE: clCreateProgram failed in CreateTestItem: %d\n", error );
            *err = error;
        }
        DestroyTestItem( item );
        return NULL;
    }

    error = clBuildProgram( item->p, 1, &deviceID, "", NULL, NULL );
    if( error )
    {
        if( err )
        {
            log_error( "FAILURE: clBuildProgram failed in CreateTestItem: %d\n", error );
            *err = error;
        }
        DestroyTestItem( item );
        return NULL;
    }

    // create some kernels
    for( i = 0; i < sizeof( item->k ) / sizeof( item->k[0] ); i++ )
    {
        static const char *kernelNames[] = { "sample_test_1", "sample_test_2", "sample_test_3", "sample_test_4" };
        item->k[i] = clCreateKernel( item->p, kernelNames[i], &error );
        if( NULL == item->k[i] || CL_SUCCESS != error )
        {
            if( err )
            {
                log_error( "FAILURE: clCreateKernel( \"%s\" ) failed in CreateTestItem: %d\n", kernelNames[i], error );
                *err = error;
            }
            DestroyTestItem( item );
            return NULL;
        }
    }

    // create some mem objects
    for( i = 0; i < BUFFER_COUNT; i++ )
    {
        item->m[i] = clCreateBuffer( item->c, CL_MEM_READ_WRITE, TEST_SIZE * sizeof(cl_int), NULL, &error );
        if( NULL == item->m[i] || CL_SUCCESS != error )
        {
            if( err )
            {
                log_error( "FAILURE: clCreateBuffer( %ld bytes ) failed in CreateTestItem: %d\n", TEST_SIZE * sizeof(cl_int), error );
                *err = error;
            }
            DestroyTestItem( item );
            return NULL;
        }
    }


    return item;
}

// Destroy a context and associate objects
static void DestroyTestItem( TestItem *item )
{
    size_t i;

    if( NULL == item )
        return;

    if( item->d )
        free_mtdata( item->d );
    if( item->c)
        clReleaseContext( item->c );
    if( item->q)
        clReleaseCommandQueue( item->q );
    if( item->p)
        clReleaseProgram( item->p );
    for( i = 0; i < sizeof( item->k ) / sizeof( item->k[0] ); i++ )
    {
        if( item->k[i])
            clReleaseKernel( item->k[i] );
    }
    for( i = 0; i < BUFFER_COUNT; i++ )
    {
        if( item->m[i])
            clReleaseMemObject( item->m[i] );
    }
    free(item );
}


cl_int UseTestItem( const TestItem *item, cl_int *err )
{
    size_t i, j;
    cl_int error = CL_SUCCESS;

    // Fill buffer 0 with random numbers
    cl_int *mapped = (cl_int*) clEnqueueMapBuffer( item->q, item->m[0], CL_TRUE, CL_MAP_WRITE, 0, TEST_SIZE * sizeof( cl_int ), 0, NULL, NULL, &error );
    if( NULL == mapped || CL_SUCCESS != error )
    {
        if( err )
        {
            log_error( "FAILURE: Failed to map buffer 0 for writing: %d\n", error );
            *err = error;
        }
        return error;
    }

    for( j = 0; j < TEST_SIZE; j++ )
        mapped[j] = genrand_int32(item->d);

    error = clEnqueueUnmapMemObject( item->q, item->m[0], mapped, 0, NULL, NULL );
    if( CL_SUCCESS != error )
    {
        if( err )
        {
            log_error( "FAILURE: failure to unmap buffer 0 for writing: %d\n", error );
            *err = error;
        }
        return error;
    }

    // try each kernel in turn.
    for( j = 0; j < sizeof(item->k) / sizeof( item->k[0] ); j++ )
    {
        // Fill buffer 1 with 0xdeaddead
        mapped = (cl_int*) clEnqueueMapBuffer( item->q, item->m[1], CL_TRUE, CL_MAP_WRITE, 0, TEST_SIZE * sizeof( cl_int ), 0, NULL, NULL, &error );
        if( NULL == mapped || CL_SUCCESS != error )
        {
            if( err )
            {
                log_error( "Failed to map buffer 1 for writing: %d\n", error );
                *err = error;
            }
            return error;
        }

        for( i = 0; i < TEST_SIZE; i++ )
            mapped[i] = 0xdeaddead;

        error = clEnqueueUnmapMemObject( item->q, item->m[1], mapped, 0, NULL, NULL );
        if( CL_SUCCESS != error )
        {
            if( err )
            {
                log_error( "Failed to unmap buffer 1 for writing: %d\n", error );
                *err = error;
            }
            return error;
        }

        // Run the kernel
        error = clSetKernelArg( item->k[j], 0, sizeof( cl_mem), &item->m[0] );
        if( error )
        {
            if( err )
            {
                log_error( "FAILURE to set arg 0 for kernel # %ld :  %d\n", j, error );
                *err = error;
            }
            return error;
        }

        error = clSetKernelArg( item->k[j], 1, sizeof( cl_mem), &item->m[1] );
        if( error )
        {
            if( err )
            {
                log_error( "FAILURE: Unable to set arg 1 for kernel # %ld :  %d\n", j, error );
                *err = error;
            }
            return error;
        }

        size_t work_size = TEST_SIZE;
        size_t global_offset = 0;
        error = clEnqueueNDRangeKernel( item->q, item->k[j], 1, &global_offset, &work_size, NULL, 0, NULL, NULL );
        if( CL_SUCCESS != error )
        {
            if( err )
            {
                log_error( "FAILURE: Unable to enqueue kernel %ld: %d\n", j, error );
                *err = error;
            }
            return error;
        }

        // Get the results back
        mapped = (cl_int*) clEnqueueMapBuffer( item->q, item->m[1], CL_TRUE, CL_MAP_READ, 0, TEST_SIZE * sizeof( cl_int ), 0, NULL, NULL, &error );
        if( NULL == mapped || CL_SUCCESS != error )
        {
            if( err )
            {
                log_error( "Failed to map buffer 1 for reading: %d\n", error );
                *err = error;
            }
            return error;
        }

        // Get our input data so we can check against it
        cl_int *inputData = (cl_int*) clEnqueueMapBuffer( item->q, item->m[0], CL_TRUE, CL_MAP_READ, 0, TEST_SIZE * sizeof( cl_int ), 0, NULL, NULL, &error );
        if( NULL == mapped || CL_SUCCESS != error )
        {
            if( err )
            {
                log_error( "Failed to map buffer 0 for reading: %d\n", error );
                *err = error;
            }
            return error;
        }


        //Verify the results
        for( i = 0; i < TEST_SIZE; i++ )
        {
            int expected = sampleActions[j]( inputData[i] );
            int result = mapped[i];
            if( expected != result )
            {
                log_error( "FAILURE:  Sample data at position %ld does not match expected result: *0x%8.8x vs. 0x%8.8x\n", i, expected, result );
                if( err )
                    *err = -1;
                return -1;
            }
        }

        //Clean up
        error = clEnqueueUnmapMemObject( item->q, item->m[0], inputData, 0, NULL, NULL );
        if( CL_SUCCESS != error )
        {
            if( err )
            {
                log_error( "Failed to unmap buffer 0 for reading: %d\n", error );
                *err = error;
            }
            return error;
        }

        error = clEnqueueUnmapMemObject( item->q, item->m[1], mapped, 0, NULL, NULL );
        if( CL_SUCCESS != error )
        {
            if( err )
            {
                log_error( "Failed to unmap buffer 1 for reading: %d\n", error );
                *err = error;
            }
            return error;
        }

    }

    // Make sure that the last set of unmap calls get run
    error = clFinish( item->q );
    if( CL_SUCCESS != error )
    {
        if( err )
        {
            log_error( "Failed to clFinish: %d\n", error );
            *err = error;
        }
        return error;
    }

    return CL_SUCCESS;
}



int test_context_multiple_contexts_same_device(cl_device_id deviceID, size_t maxCount, size_t minCount )
{
    size_t i, j;
    cl_int err = CL_SUCCESS;

    //Figure out how many of these we can make before the first failure
    TestItem *list = NULL;

    for( i = 0; i < maxCount; i++ )
    {
        // create a context and accompanying objects
        TestItem *current = CreateTestItem( deviceID, NULL /*no error reporting*/ );
        if( NULL == current )
            break;

        // Attempt to use it
        cl_int failed = UseTestItem( current, NULL );

        if( failed )
        {
            DestroyTestItem( current );
            break;
        }

        // Add the successful test item to the list
        current->next = list;
        list = current;
    }

    // Check to make sure we made the minimum amount
    if( i < minCount )
    {
        log_error( "FAILURE: only could make %ld of %ld contexts!\n", i, minCount );
        err = -1;
        goto exit;
    }

    // Report how many contexts we made
    if( i == maxCount )
        log_info( "Successfully created all %lu contexts.\n", i );
    else
        log_info( "Successfully created %lu contexts out of %lu\n", i, maxCount );

    // Set the count to be the number we succesfully made
    maxCount = i;

    // Make sure we can do it again a few times
    log_info( "Tring to do it 5 more times" );
    fflush( stdout);
    for( j = 0; j < 5; j++ )
    {
        //free all the contexts we already made
        while( list )
        {
            TestItem *current = list;
            list = list->next;
            current->next = NULL;
            DestroyTestItem( current );
        }

        // Attempt to make them again
        for( i = 0; i < maxCount; i++ )
        {
            // create a context and accompanying objects
            TestItem *current = CreateTestItem( deviceID, &err );
            if( err )
            {
                log_error( "\nTest Failed with error at CreateTestItem: %d\n", err );
                goto exit;
            }

            // Attempt to use it
            cl_int failed = UseTestItem( current, &err );

            if( failed || err )
            {
                DestroyTestItem( current );
                log_error( "\nTest Failed with error at UseTestItem: %d\n", err );
                goto exit;
            }

            // Add the successful test item to the list
            current->next = list;
            list = current;
        }
        log_info( "." );
        fflush( stdout );
    }

    log_info( "Done.\n" );

exit:
    //free all the contexts we already made
    while( list )
    {
        TestItem *current = list;
        list = list->next;
        current->next = NULL;

        DestroyTestItem( current );
    }

    return err;
}

//  This test tests to make sure that your implementation isn't super leaky.  We make a bunch of contexts (up to some
//  sane limit, currently 200), attempting to use each along the way. We keep track of how many we could make before
//  a failure occurred.   We then free everything and attempt to go do it again a few times.  If you are able to make
//  that many contexts 5 times over, then you pass.
int test_context_multiple_contexts_same_device(cl_device_id deviceID, cl_context context, cl_command_queue queue, int num_elements)
{
    return test_context_multiple_contexts_same_device( deviceID, 200, 1 );
}

int test_context_two_contexts_same_device(cl_device_id deviceID, cl_context context, cl_command_queue queue, int num_elements)
{
    return test_context_multiple_contexts_same_device( deviceID, 2, 2 );
}

int test_context_three_contexts_same_device(cl_device_id deviceID, cl_context context, cl_command_queue queue, int num_elements)
{
    return test_context_multiple_contexts_same_device( deviceID, 3, 3 );
}

int test_context_four_contexts_same_device(cl_device_id deviceID, cl_context context, cl_command_queue queue, int num_elements)
{
    return test_context_multiple_contexts_same_device( deviceID, 4, 4 );
}

