//
// 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 "allocation_functions.h"
#include "allocation_fill.h"


static cl_image_format    image_format = { CL_RGBA, CL_UNSIGNED_INT32 };

int allocate_buffer(cl_context context, cl_command_queue *queue, cl_device_id device_id, cl_mem *mem, size_t size_to_allocate, cl_bool blocking_write) {
  int error;
  // log_info("\t\tAttempting to allocate a %gMB array and fill with %s writes.\n", (size_to_allocate/(1024.0*1024.0)), (blocking_write ? "blocking" : "non-blocking"));
  *mem = clCreateBuffer(context, CL_MEM_READ_WRITE, size_to_allocate, NULL, &error);
  return check_allocation_error(context, device_id, error, queue);
}


int find_good_image_size(cl_device_id device_id, size_t size_to_allocate, size_t *width, size_t *height, size_t* max_size) {
  size_t max_width, max_height, num_pixels, found_width, found_height;
  int error;

  if (checkForImageSupport(device_id)) {
    log_info("Can not allocate an image on this device because it does not support images.");
    return FAILED_ABORT;
  }

  if (size_to_allocate == 0) {
    log_error("Trying to allcoate a zero sized image.\n");
    return FAILED_ABORT;
  }

  error = clGetDeviceInfo( device_id, CL_DEVICE_IMAGE2D_MAX_WIDTH, sizeof( max_width ), &max_width, NULL );
  test_error_abort(error, "clGetDeviceInfo failed.");
  error = clGetDeviceInfo( device_id, CL_DEVICE_IMAGE2D_MAX_HEIGHT, sizeof( max_height ), &max_height, NULL );
  test_error_abort(error, "clGetDeviceInfo failed.");

  num_pixels = size_to_allocate / (sizeof(cl_uint)*4);

  if (num_pixels > (max_width*max_height)) {
    if(NULL != max_size) {
      *max_size = max_width * max_height * sizeof(cl_uint) * 4;
    }
    return FAILED_TOO_BIG;
  }

  // We want a close-to-square aspect ratio.
  // Note that this implicitly assumes that  max width >= max height
  found_width = (int)sqrt( (double) num_pixels );
  if( found_width > max_width ) {
    found_width = max_width;
  }
  if (found_width == 0)
    found_width = 1;

  found_height = (size_t)num_pixels/found_width;
  if (found_height > max_height) {
    found_height = max_height;
  }
  if (found_height == 0)
    found_height = 1;

  *width = found_width;
  *height = found_height;

  if(NULL != max_size) {
    *max_size = found_width * found_height * sizeof(cl_uint) * 4;
  }

  return SUCCEEDED;
}


int allocate_image2d_read(cl_context context, cl_command_queue *queue, cl_device_id device_id, cl_mem *mem, size_t size_to_allocate, cl_bool blocking_write) {
  size_t width, height;
  int error;

  error = find_good_image_size(device_id, size_to_allocate, &width, &height, NULL);
  if (error != SUCCEEDED)
    return error;

  log_info("\t\tAttempting to allocate a %gMB read-only image (%d x %d) and fill with %s writes.\n",
          (size_to_allocate/(1024.0*1024.0)), (int)width, (int)height, (blocking_write ? "blocking" : "non-blocking"));
  *mem = create_image_2d(context, CL_MEM_READ_ONLY, &image_format, width, height, 0, NULL, &error);

  return check_allocation_error(context, device_id, error, queue);
}


int allocate_image2d_write(cl_context context, cl_command_queue *queue, cl_device_id device_id, cl_mem *mem, size_t size_to_allocate, cl_bool blocking_write) {
  size_t width, height;
  int error;

  error = find_good_image_size(device_id, size_to_allocate, &width, &height, NULL);
  if (error != SUCCEEDED)
    return error;

  //log_info("\t\tAttempting to allocate a %gMB write-only image (%d x %d) and fill with %s writes.\n",
           //(size_to_allocate/(1024.0*1024.0)), (int)width, (int)height, (blocking_write ? "blocking" : "non-blocking"));
  *mem = create_image_2d(context, CL_MEM_WRITE_ONLY, &image_format, width, height, 0, NULL, &error);

  return check_allocation_error(context, device_id, error, queue);
}

int do_allocation(cl_context context, cl_command_queue *queue, cl_device_id device_id, size_t size_to_allocate, int type, cl_mem *mem) {
  if (type == BUFFER) return allocate_buffer(context, queue, device_id, mem, size_to_allocate, true);
  if (type == IMAGE_READ) return allocate_image2d_read(context, queue, device_id, mem, size_to_allocate, true);
  if (type == IMAGE_WRITE) return allocate_image2d_write(context, queue, device_id, mem, size_to_allocate, true);
  if (type == BUFFER_NON_BLOCKING) return allocate_buffer(context, queue, device_id, mem, size_to_allocate, false);
  if (type == IMAGE_READ_NON_BLOCKING) return allocate_image2d_read(context, queue, device_id, mem, size_to_allocate, false);
  if (type == IMAGE_WRITE_NON_BLOCKING) return allocate_image2d_write(context, queue, device_id, mem, size_to_allocate, false);
    log_error("Invalid allocation type: %d\n", type);
  return FAILED_ABORT;
}


int allocate_size(cl_context context, cl_command_queue *queue, cl_device_id device_id, int multiple_allocations, size_t size_to_allocate,
                  int type, cl_mem mems[], int *number_of_mems, size_t *final_size, int force_fill, MTdata d) {

    cl_ulong max_individual_allocation_size, global_mem_size;
  int error, result;
  size_t amount_allocated;
  size_t reduction_amount;
  int current_allocation;
  size_t allocation_this_time, actual_allocation;

  // Set the number of mems used to 0 so if we fail to create even a single one we don't end up returning a garbage value
  *number_of_mems = 0;

  error = clGetDeviceInfo(device_id, CL_DEVICE_MAX_MEM_ALLOC_SIZE, sizeof(max_individual_allocation_size), &max_individual_allocation_size, NULL);
  test_error_abort( error, "clGetDeviceInfo failed for CL_DEVICE_MAX_MEM_ALLOC_SIZE");
  error = clGetDeviceInfo(device_id, CL_DEVICE_GLOBAL_MEM_SIZE, sizeof(global_mem_size), &global_mem_size, NULL);
  test_error_abort( error, "clGetDeviceInfo failed for CL_DEVICE_GLOBAL_MEM_SIZE");

  if (global_mem_size > (cl_ulong)SIZE_MAX) {
    global_mem_size = (cl_ulong)SIZE_MAX;
  }

//  log_info("Device reports CL_DEVICE_MAX_MEM_ALLOC_SIZE=%llu bytes (%gMB), CL_DEVICE_GLOBAL_MEM_SIZE=%llu bytes (%gMB).\n",
//           max_individual_allocation_size, toMB(max_individual_allocation_size),
//           global_mem_size, toMB(global_mem_size));

  if (size_to_allocate > global_mem_size) {
    log_error("Can not allocate more than the global memory size.\n");
    return FAILED_ABORT;
  }

  amount_allocated = 0;
  current_allocation = 0;

  // If allocating for images, reduce the maximum allocation size to the maximum image size.
  // If we don't do this, then the value of CL_DEVICE_MAX_MEM_ALLOC_SIZE / 4 can be higher
  // than the maximum image size on systems with 16GB or RAM or more. In this case, we
  // succeed in allocating an image but its size is less than CL_DEVICE_MAX_MEM_ALLOC_SIZE / 4
  // (min_allocation_allowed) and thus we fail the allocation below.
  if(type == IMAGE_READ || type == IMAGE_READ_NON_BLOCKING || type == IMAGE_WRITE || type == IMAGE_WRITE_NON_BLOCKING) {
    size_t width;
    size_t height;
    size_t max_size;
    error = find_good_image_size(device_id, size_to_allocate, &width, &height, &max_size);
    if (!(error == SUCCEEDED || error == FAILED_TOO_BIG))
      return error;
    if(max_size < max_individual_allocation_size)
      max_individual_allocation_size = max_size;
  }

  reduction_amount = (size_t)max_individual_allocation_size/16;

  if (type == BUFFER || type == BUFFER_NON_BLOCKING) log_info("\tAttempting to allocate a buffer of size %gMB.\n", toMB(size_to_allocate));
  else if (type == IMAGE_READ || type == IMAGE_READ_NON_BLOCKING) log_info("\tAttempting to allocate a read-only image of size %gMB.\n", toMB(size_to_allocate));
  else if (type == IMAGE_WRITE || type == IMAGE_WRITE_NON_BLOCKING) log_info("\tAttempting to allocate a write-only image of size %gMB.\n", toMB(size_to_allocate));

//  log_info("\t\t(Reduction size is %gMB per iteration, minimum allowable individual allocation size is %gMB.)\n",
//           toMB(reduction_amount), toMB(min_allocation_allowed));
//  if (force_fill && type != IMAGE_WRITE && type != IMAGE_WRITE_NON_BLOCKING) log_info("\t\t(Allocations will be filled with random data for checksum calculation.)\n");

  // If we are only doing a single allocation, only allow 1
  int max_to_allocate = multiple_allocations ? MAX_NUMBER_TO_ALLOCATE : 1;

  // Make sure that the maximum number of images allocated is constrained by the
  // maximum that may be passed to a kernel
  if (type != BUFFER && type != BUFFER_NON_BLOCKING) {
    cl_device_info param_name = (type == IMAGE_READ || type == IMAGE_READ_NON_BLOCKING) ?
      CL_DEVICE_MAX_READ_IMAGE_ARGS : CL_DEVICE_MAX_WRITE_IMAGE_ARGS;

    cl_uint max_image_args;
    error = clGetDeviceInfo(device_id, param_name, sizeof(max_image_args), &max_image_args, NULL);
    test_error( error, "clGetDeviceInfo failed for CL_DEVICE_MAX IMAGE_ARGS");

    if ((int)max_image_args < max_to_allocate) {
      log_info("\t\tMaximum number of images per kernel limited to %d\n",(int)max_image_args);
      max_to_allocate =  max_image_args;
    }
  }


  // Try to allocate the requested amount.
  while (amount_allocated != size_to_allocate && current_allocation < max_to_allocate) {

    // Determine how much more is needed
    allocation_this_time = size_to_allocate - amount_allocated;

    // Bound by the individual allocation size
    if (allocation_this_time > max_individual_allocation_size)
        allocation_this_time = (size_t)max_individual_allocation_size;

    // Allocate the largest object possible
    result = FAILED_TOO_BIG;
    //log_info("\t\tTrying sub-allocation %d at size %gMB.\n", current_allocation, toMB(allocation_this_time));
    while (result == FAILED_TOO_BIG && allocation_this_time != 0) {

      // Create the object
        result = do_allocation(context, queue, device_id, allocation_this_time, type, &mems[current_allocation]);
      if (result == SUCCEEDED) {
        // Allocation succeeded, another memory object was added to the array
        *number_of_mems = (current_allocation+1);

          // Verify the size is correct to within 1MB.
        actual_allocation = get_actual_allocation_size(mems[current_allocation]);
        if (fabs((double)allocation_this_time - (double)actual_allocation) > 1024.0*1024.0) {
             log_error("Allocation not of expected size. Expected %gMB, got %gMB.\n", toMB(allocation_this_time), toMB( actual_allocation));
          return FAILED_ABORT;
        }

        // If we are filling the allocation for verification do so
        if (force_fill) {
          //log_info("\t\t\tWriting random values to object and calculating checksum.\n");
          cl_bool blocking_write = true;
          if (type == BUFFER_NON_BLOCKING || type == IMAGE_READ_NON_BLOCKING || type == IMAGE_WRITE_NON_BLOCKING) {
            blocking_write = false;
          }
          result = fill_mem_with_data(context, device_id, queue, mems[current_allocation], d, blocking_write);
        }
      }

      // If creation failed, try to create a smaller object
      if (result == FAILED_TOO_BIG) {
        //log_info("\t\t\tAllocation %d failed at size %gMB. Trying smaller.\n", current_allocation, toMB(allocation_this_time));
        if (allocation_this_time > reduction_amount)
            allocation_this_time -= reduction_amount;
        else if (reduction_amount > 1) {
          reduction_amount /= 2;
        }
        else {
          allocation_this_time = 0;
        }

      }
    }

    if (result == FAILED_ABORT) {
      log_error("\t\tAllocation failed.\n");
      return FAILED_ABORT;
    }

    if (!allocation_this_time) {
      log_info("\t\tFailed to allocate %gMB across several objects.\n", toMB(size_to_allocate));
      return FAILED_TOO_BIG;
    }

    // Otherwise we succeeded
    if (result != SUCCEEDED) {
      log_error("Test logic error.");
      test_finish();
      exit(-1);
    }
    amount_allocated += allocation_this_time;

    *final_size = amount_allocated;

    current_allocation++;
  }

  log_info("\t\tSucceeded in allocating %gMB using %d memory objects.\n", toMB(amount_allocated), current_allocation);
  return SUCCEEDED;
}
