package mcmas.core;

import java.nio.ByteBuffer;

import mcmas.utils.Objects7;

import org.jocl.CL;
import org.jocl.Pointer;
import org.jocl.cl_command_queue;
import org.jocl.struct.Buffers;
import org.jocl.struct.Struct;

import static mcmas.core.MCMEvent.events_arr;

public class MCMCommandQueue extends MCMObject {
	
	private final MCMContext context;
	private final cl_command_queue queue;
	
	MCMCommandQueue(MCMContext context, MCMCommandQueueProperty... properties) {
		this(context, context.getDevices()[0], properties);
	}
	
	MCMCommandQueue(MCMContext context, MCMDevice device, MCMCommandQueueProperty... properties) {
		this(context, device, MCMUtils.binaryOr(properties));
	}
	
	MCMCommandQueue(MCMContext context, long... properties) {
		this(context, context.getDevices()[0], properties);
	}
	
	MCMCommandQueue(MCMContext context, MCMDevice device, long... properties) {
		context = Objects7.requireNonNull(context, "Can't create a command queue using a null context");
		device = Objects7.requireNonNull(device, "Can't create a command queue using a null device");
		
		this.queue = MCMHelpers.createCommandQueue(context.getContext(), device.getId(), MCMUtils.binaryOr(properties));
		this.context = context;
	}
	
	public cl_command_queue getQueue() {
		return queue;
	}
	
	public MCMContext getContext() {
		return context;
	}
	
	// READ BUFFER (BLOCKING)
	
	public void blockingReadBuffer(MCMMem buffer, Pointer data, long offset, long size, MCMEvent... events) {
		internalReadBuffer(buffer, data, offset, size, true, events);
	}
	
	public void blockingReadBuffer(MCMMem buffer, Struct[] data, long offset, long size, MCMEvent... events) {
		ByteBuffer temp = Buffers.allocateBuffer(data);
		internalReadBuffer(buffer, Pointer.to(temp), offset, size, true, events);
		Buffers.readFromBuffer(temp, data);
	}
	
	// READ BUFFER (ASYNC)
	
	public MCMEvent enqueueReadBuffer(MCMMem buffer, Pointer data, long offset, long size, MCMEvent... events) {
		return internalReadBuffer(buffer, data, offset, size, false, events);
	}
	
	public MCMEvent enqueueReadBuffer(MCMMem buffer, Struct[] data, long offset, long size, MCMEvent... events) {
		MCMEvent newEvent;
		
		ByteBuffer temp = Buffers.allocateBuffer(data);
		newEvent = internalReadBuffer(buffer, Pointer.to(temp), offset, size, false, events);
		this.register(newEvent);
		
		Buffers.readFromBuffer(temp, data);
		
		return newEvent;
	}
	
	// READ BUFFER (IMPLEM)
	
	private MCMEvent internalReadBuffer(MCMMem buffer, Pointer data, long offset, long size, boolean blocking, MCMEvent... events) {
		Objects7.requireNonNull(buffer, "Can't read from a null buffer");
		
		if (blocking) {
			CL.clEnqueueReadBuffer(queue, buffer.getMem(), blocking, offset, size, data, events.length, events_arr(events), null);
			return null;
		} else {
			MCMEvent newEvent = new MCMEvent();
			this.register(newEvent);
			
			CL.clEnqueueReadBuffer(queue, buffer.getMem(), blocking, offset, size, data, events.length, events_arr(events), newEvent.getEvent());
			return newEvent;
		}
	}
	
	// WRITE BUFFER (BLOCKING)
	
	public void blockingWriteBuffer(MCMMem buffer, Pointer data, long offset, long size, MCMEvent... events) {
		internalWriteBuffer(buffer, data, offset, size, true, events);
	}
	
	public void blockingWriteBuffer(MCMMem buffer, Struct[] data, long offset, long size, MCMEvent... events) {
		ByteBuffer temp = Buffers.allocateBuffer(data);
		Buffers.writeToBuffer(temp, data);
		
		internalWriteBuffer(buffer, Pointer.to(temp), offset, size, true, events);
	}
	
	/*
	public void blockingWriteBuffer(OCLMem buffer, OCLPointer data, long offset, long size, OCLEvent... events) {
		internalWriteBuffer(buffer, data, offset, size, true, events);
	}
	
	public void blockingWriteBuffer(OCLMem buffer, OCLPointer data, OCLEvent... events) {
		internalWriteBuffer(buffer, data, 0, buffer.getSize(), true, events);
	}*/
	
	// WRITE BUFFER (ASYNC)
	
	public MCMEvent enqueueWriteBuffer(MCMMem buffer, Pointer data, long offset, long size, MCMEvent... events) {
		return internalWriteBuffer(buffer, data, offset, size, false, events);
	}
	
	public MCMEvent enqueueWriteBuffer(MCMMem buffer, Struct[] data, long offset, long size, MCMEvent... events) {
		ByteBuffer temp = Buffers.allocateBuffer(data);
		Buffers.writeToBuffer(temp, data);
		
		return internalWriteBuffer(buffer, Pointer.to(temp), offset, size, false, events);
	}
	
	// WRITE BUFFER (IMPLEM)
	
	private MCMEvent internalWriteBuffer(MCMMem buffer, Pointer data, long offset, long size, boolean blocking, MCMEvent... events) {
		Objects7.requireNonNull(buffer, "Can't write to a null buffer");
		
		if (blocking) {
			CL.clEnqueueWriteBuffer(queue, buffer.getMem(), CL.CL_FALSE, offset, size, data, events.length, events_arr(events), null);
			return null;
		} else {
			MCMEvent newEvent = new MCMEvent();
			this.register(newEvent);
			
			CL.clEnqueueWriteBuffer(queue, buffer.getMem(), CL.CL_FALSE, offset, size, data, events.length, events_arr(events), newEvent.getEvent());
			return newEvent;
		}
	}
	
	// COPY BUFFERS
	
	public MCMEvent enqueueCopyBuffer(MCMMem src, MCMMem dest, long src_offset, long dest_offset, long size, MCMEvent... events) {
		Objects7.requireNonNull(src, "Can't copy a null source buffer");
		Objects7.requireNonNull(dest, "Can't copy a null dest buffer");
		
		MCMEvent newEvent = new MCMEvent();
		this.register(newEvent);
		
		CL.clEnqueueCopyBuffer(queue, src.getMem(), dest.getMem(), src_offset, dest_offset, size, events.length, events_arr(events), newEvent.getEvent());
		return newEvent;
	}
	
	// SUBMIT KERNELS
	
	public MCMEvent enqueueKernel(MCMKernel kernel, int dim, long[] global, MCMEvent... events) {
		return enqueueKernel(kernel, dim, null, global, null, events);
	}
	
	public MCMEvent enqueueKernel(MCMKernel kernel, int dim, long[] global, long[] local, MCMEvent... events) {
		return enqueueKernel(kernel, dim, null, global, local, events);
	}
	
	public MCMEvent enqueueKernel(MCMKernel kernel, int dim, long[] global_offset, long[] global, long[] local, MCMEvent... events) {
		Objects7.requireNonNull(kernel, "Can't enqueue a null kernel");
		Objects7.requireNonNull(global, "Global dimensions must be a non-null array");
		
		if (global_offset != null && context.getPlatforms()[0].getSupportedVersionInt() < MCM.CL_VERSION_1_1) {
			throw new IllegalArgumentException("Non-null global offset is not supported in OpenCL < 1.1");
		}
		
        MCMEvent newEvent = new MCMEvent();
        this.register(newEvent);
        
		CL.clEnqueueNDRangeKernel(queue, kernel.getKernel(), dim, global_offset, global, local, events.length, events_arr(events), newEvent.getEvent());
		return newEvent;
	}
	
	public MCMEvent enqueue1DKernel(MCMKernel kernel, long global, MCMEvent... events) {
		return enqueueKernel(kernel, 1, null, new long[] { global }, null, events);
	}
	
	public MCMEvent enqueue1DKernel(MCMKernel kernel, long global, long local, MCMEvent... events) {
		return enqueueKernel(kernel, 1, null, new long[] { global }, new long[] { local }, events);
	}
	
	public MCMEvent enqueue1DKernel(MCMKernel kernel, long offset, long global, long local, MCMEvent... events) {
		return enqueueKernel(kernel, 1, new long[] { offset }, new long[] { global }, new long[] { local }, events);
	}
	
	public MCMEvent enqueueTask(MCMKernel kernel, MCMEvent... events) {
		Objects7.requireNonNull(kernel, "Can't enqueue a null task");
		
		MCMEvent newEvent = new MCMEvent();
		this.register(newEvent);
		
		CL.clEnqueueTask(queue, kernel.getKernel(), events.length, events_arr(events), newEvent.getEvent());
		return newEvent;
	}
	
	public void flush() {
		CL.clFlush(queue);
	}
	
	public void finish() {
		CL.clFinish(queue);
	}
	
	@Override
	protected void releaseImpl() {
		CL.clReleaseCommandQueue(queue);
	}

}
