package mcmas.core;

import org.jocl.CL;
import org.jocl.CLException;
import org.jocl.Pointer;
import org.jocl.Sizeof;
import org.jocl.cl_command_queue;
import org.jocl.cl_context;
import org.jocl.cl_context_properties;
import org.jocl.cl_device_id;
import org.jocl.cl_event;
import org.jocl.cl_kernel;
import org.jocl.cl_mem;
import org.jocl.cl_platform_id;
import org.jocl.cl_program;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Package-specific helpers used to allocate native objets.
 */
class MCMHelpers {
	
	private final static Logger logger = LoggerFactory.getLogger(MCMHelpers.class);
	
	static {
		CL.setExceptionsEnabled(true);
	}
	
	/**
	 * Create a new cl_context object trying each of the specified devices types.
	 * @param properties properties for the returned context
	 * @param types types to try when searching for supported devices
	 * @return a new cl_context
	 */
	public static cl_context createContextFromType(cl_context_properties properties, long... types) {
		cl_context context = null;
		
		for (long type : types) {
			try {
				context = CL.clCreateContextFromType(properties, type, null, null, null);
			} catch (CLException e) {
				logger.debug("No device of type " + type + " found.");
			}
			
			if (context != null) {
				logger.debug("Device of type " + type + " found.");
				break;
			}
		}
		
		return context;
	}
	
	/**
	 * Create a new cl_context from a specified platform and one or more device types.
	 * @param platform platform to use as support
	 * @param types types to try when searching for supported devices.
	 * @return a new cl_context
	 */
	public static cl_context createContextFromType(cl_platform_id platform, long... types) {
		cl_context_properties properties = new cl_context_properties();
		properties.addProperty(CL.CL_CONTEXT_PLATFORM, platform);
		return createContextFromType(properties, types);
	}
	
	/**
	 * Create a new context from one or more device types.
	 * @param types types to try when searching for supported devices.
	 * @return a new cl_context
	 */
	public static cl_context createContextFromType(long... types) {
		return createContextFromType((cl_context_properties) null, types);
	}
	
	/**
	 * Create a new cl_event
	 * @return a new cl_event
	 */
	public static cl_event createEvent() {
		return new cl_event();
	}
	
	/**
	 * Create a new cl_mem object
	 * @param context context to use for the allocation
	 * @param flags memory flags to use
	 * @param size size to allocate
	 * @param data CPU data to use for initialization (can be null).
	 * @return a new cl_mem object
	 */
	public static cl_mem createMem(cl_context context, long flags, long size, Pointer data) {
		// Use host accessible PCI-E memory
		/*flags |= CL.CL_MEM_ALLOC_HOST_PTR;
		
		// If there is already data (to PCI-E memory)
		if (data != null) {
			flags |= CL.CL_MEM_COPY_HOST_PTR;
		}*/
		
		return CL.clCreateBuffer(context, flags, size, data, null);
	}
	
	/**
	 * Create a new cl_command_queue
	 * @param context context to use
	 * @param properties properties for the new queue
	 * @return a new cl_command_queue
	 */
	public static cl_command_queue createCommandQueue(cl_context context, long properties) {
		return createCommandQueue(context, getDevices(context)[0], properties);
	}
	
	/**
	 * Create a new cl_command_queue for a specific device
	 * @param context context to use
	 * @param device device to use
	 * @param properties properties for the new queue
	 * @return a new cl_command_queue
	 */
	public static cl_command_queue createCommandQueue(cl_context context, cl_device_id device, long properties) {
		return CL.clCreateCommandQueue(context, device, properties, null);
	}
	
	/**
	 * Create a new cl_program
	 * @param context context to use
	 * @param source source code for the program
	 * @param options compilation option to use to generate the program
	 * @return
	 */
	public static cl_program createProgram(cl_context context, String source, String options) {
		//String buildOptions = options + " -cl-nv-verbose --ptxas-options=-v";
		String buildOptions = options;
		
		cl_program program = CL.clCreateProgramWithSource(context, 1, new String[] {source}, null, null);
		CL.clBuildProgram(program, 0, null, buildOptions, null, null);
		
		return program;
	}

	/**
	 * Create a new cl_kernel
	 * @param program program containing the kernel definition
	 * @param functionName name of the function to use as kernel
	 * @return
	 */
	public static cl_kernel createKernel(cl_program program, String functionName) {
		return CL.clCreateKernel(program, functionName, null);
	}
	
	/**
	 * Create a new user event
	 * @param context context to use
	 * @return a new cl_event
	 */
	public static cl_event createUserEvent(cl_context context) {
        int [] error = new int[1];
		cl_event e = CL.clCreateUserEvent(context, error);

        if (error[0] != CL.CL_SUCCESS) {
            throw new RuntimeException("Error creating a new event: " + error[0]);
        }

        return e;
	}
	
	/**
	 * Get the version of OpenCL supported by the platform.
	 * @param id platform
	 * @return an int array of two elements : the major and minor version digit
	 */
	public static int[] getSupportedOpenCLVersion(cl_platform_id id) {
		int [] version = new int[2];
		
		long [] bufferSize = new long[1];
		
		CL.clGetPlatformInfo(id, CL.CL_PLATFORM_VERSION, 0, null, bufferSize);
		
		byte[] buffer = new byte[(int) bufferSize[0]];
		
		CL.clGetPlatformInfo(id, CL.CL_PLATFORM_VERSION, bufferSize[0], Pointer.to(buffer), null);
		
		String verString = new String(buffer, 0, (int) bufferSize[0]);
		
		String[] token = verString.split(" ")[1].split("[.]");
		
		version[0] = Integer.parseInt(token[0]);
		version[1] = Integer.parseInt(token[1]);
		
		return version;
	}
	
	/**
	 * Get the version of OpenCL supported by the platform.
	 * @param id
	 * @return an int where the first digit indicates the major version,
	 * and the second digit the minor version.
	 */
	public static int getSupportedOpenCLVersionAsInt(cl_platform_id id) {
		int [] tokens = getSupportedOpenCLVersion(id);
		return tokens[0] * 100 + tokens[1] * 10;
	}
	
	/**
	 * Get all available local platforms.
	 * @return an array of platforms.
	 */
	public static cl_platform_id[] getPlatforms() {
		int [] num = new int[1];
		CL.clGetPlatformIDs(0, null, num);
		
		cl_platform_id[] platforms = new cl_platform_id[num[0]];
		CL.clGetPlatformIDs(num[0], platforms, null);
		
		return platforms;
	}
	
	/**
	 * Get all available devices for a specified platform.
	 * @param platform platform to use
	 * @param deviceType device type to search for
	 * @return an array of devices
	 */
	public static cl_device_id[] getDevices(cl_platform_id platform, long deviceType) {
		int [] num = new int[1];
		CL.clGetDeviceIDs(platform, deviceType, 0, null, num);
		
		cl_device_id[] devices = new cl_device_id[num[0]];
		CL.clGetDeviceIDs(platform, deviceType, num[0], devices, null);
		
		return devices;
	}
	
	/**
	 * Get all available devices for a specified context
	 * @param context context to use
	 * @return an array of devices
	 */
	public static cl_device_id[] getDevices(cl_context context) {
		
		// Get the total number of devices associated to this context
		long[] size = new long[1];
		CL.clGetContextInfo(context, CL.CL_CONTEXT_DEVICES, 0, null, size);
		
		// Retrieve them
		int numDevices = (int) size[0] / Sizeof.cl_device_id;
		cl_device_id devices[] = new cl_device_id[numDevices];
		CL.clGetContextInfo(context, CL.CL_CONTEXT_DEVICES, size[0], Pointer.to(devices), null);
		
		return devices;
	}
	
	/**
	 * Combine multiple MCMFlag objects into one long bitmask
	 * @param flags flags to combine
	 * @return a bitmask
	 */
	public static long binaryFlag(MCMFlag... flags) {
		long value = 0;
		
		for (MCMFlag flag : flags) {
			value |= flag.getValue();
		}
		
		return value;
	}
	
	/**
	 * Set a particular flag in a existing bitmask
	 * @param value existing bitmask
	 * @param flag flag to set
	 * @return the new bitmask
	 */
	public static long flagSet(long value, MCMFlag flag) {
		return value | flag.getValue();
	}
	
	/**
	 * Check if a particular flag is set in an existing bitmask
	 * @param value existing bitmask
	 * @param flag flag to check
	 * @return whether the flag is set
	 */
	public static boolean isFlagSet(long value, MCMFlag flag) {
		return (value & flag.getValue()) != 0;
	}

}
