package speiger.src.coreengine.rendering.models.buffers;

import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.List;

import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL45;
import org.lwjgl.system.MemoryUtil;

import speiger.src.collections.ints.misc.pairs.IntObjectPair;
import speiger.src.coreengine.rendering.utils.AllocationTracker;
import speiger.src.coreengine.rendering.utils.values.BufferState;
import speiger.src.coreengine.rendering.utils.values.IGLValue.IGLBuffer;
import speiger.src.coreengine.utils.io.GameLog;

public class VertexBuffer {
	protected final int id;
	protected final IGLBuffer type;
	protected BufferState state = BufferState.STATIC_DRAW;
	protected int size;
	
	public VertexBuffer(IGLBuffer type) {
		this(type, GL45.glCreateBuffers());
		AllocationTracker.INSTANCE.track(this);
	}
	
	protected VertexBuffer(IGLBuffer type, int id) {
		this.type = type;
		this.id = id;
	}
	
	public static VertexBuffer wrap(IGLBuffer type, int id) {
		return new VertexBuffer(type, id);
	}
	
	public VertexBuffer withState(BufferState state) {
		this.state = state;
		return this;
	}
	
	public int id() { return id; }
	public IGLBuffer type() { return type; }
	public BufferState state() { return state; }
	public int size() { return size; }
	protected void size(int newSize) { this.size = newSize; }
	
	public VertexBuffer delete() {
		AllocationTracker.INSTANCE.untrack(this);
		GL15.glDeleteBuffers(id);
		return this;
	}
	
	public VertexBuffer bind() {
		GL15.glBindBuffer(type.glValue(), id);
		return this;
	}
	
	public VertexBuffer unbind() {
		GL15.glBindBuffer(type.glValue(), 0);
		return this;
	}
	
	public VertexBuffer allocate(int totalBytes) {
		this.size = totalBytes;
		GL45.glNamedBufferData(id, totalBytes, state.glValue());
		AllocationTracker.INSTANCE.addGPUBytes(totalBytes);
		return this;
	}
	
	public VertexBuffer set(long pointer, int totalBytes) {
		this.size = totalBytes;
		GL45.nglNamedBufferData(id, totalBytes, pointer, state.glValue());
		AllocationTracker.INSTANCE.addGPUBytes(totalBytes);
		return this;
	}
	
	public VertexBuffer fill(long pointer, int totalBytes, int offset) {
		GL45.nglNamedBufferSubData(id, offset, totalBytes, pointer);
		AllocationTracker.INSTANCE.addGPUBytes(totalBytes);
		return this;
	}
	
	public VertexBuffer read(long pointer, int totalBytes, int offset) {
		GL45.nglGetNamedBufferSubData(id, offset, totalBytes, pointer);
		AllocationTracker.INSTANCE.addGPUBytes(totalBytes);
		return this;
	}
	
	private VertexBuffer read(Buffer buffer, int totalBytes, int offset) {
		read(MemoryUtil.memAddress(buffer), totalBytes, offset);
		buffer.flip();
		return this;
	}
	
	public VertexBuffer set(ByteBuffer buffer) { return set(MemoryUtil.memAddress(buffer), buffer.remaining()); }
	public VertexBuffer set(ShortBuffer buffer) { return set(MemoryUtil.memAddress(buffer), buffer.remaining() << 1); }
	public VertexBuffer set(IntBuffer buffer) { return set(MemoryUtil.memAddress(buffer), buffer.remaining() << 2); }
	public VertexBuffer set(FloatBuffer buffer) { return set(MemoryUtil.memAddress(buffer), buffer.remaining() << 2); }
	public VertexBuffer fill(int offset, ByteBuffer buffer) { return fill(MemoryUtil.memAddress(buffer), buffer.remaining(), offset); }
	public VertexBuffer fill(int offset, ShortBuffer buffer) { return fill(MemoryUtil.memAddress(buffer), buffer.remaining() << 1, offset); }
	public VertexBuffer fill(int offset, IntBuffer buffer) { return fill(MemoryUtil.memAddress(buffer), buffer.remaining() << 2, offset); }
	public VertexBuffer fill(int offset, FloatBuffer buffer) { return fill(MemoryUtil.memAddress(buffer), buffer.remaining() << 2, offset); }
	
	public VertexBuffer read(int offset, ByteBuffer buffer) { return read(buffer, buffer.remaining(), offset); }
	public VertexBuffer read(int offset, ShortBuffer buffer) { return read(buffer, buffer.remaining() << 1, offset); }
	public VertexBuffer read(int offset, IntBuffer buffer) { return read(buffer, buffer.remaining() << 2, offset); }
	public VertexBuffer read(int offset, FloatBuffer buffer) { return read(buffer, buffer.remaining() << 2, offset); }
	
	public VertexBuffer set(byte[] data) {
		ByteBuffer buffer = MemoryUtil.memAlloc(data.length).put(data).flip();
		set(buffer);
		MemoryUtil.memFree(buffer);
		return this;
	}
	
	public VertexBuffer set(short[] data) {
		ShortBuffer buffer = MemoryUtil.memAllocShort(data.length).put(data).flip();
		set(buffer);
		MemoryUtil.memFree(buffer);
		return this;
	}
	
	public VertexBuffer set(int[] data) {
		IntBuffer buffer = MemoryUtil.memAllocInt(data.length).put(data).flip();
		set(buffer);
		MemoryUtil.memFree(buffer);
		return this;
	}
	
	public VertexBuffer set(float[] data) {
		FloatBuffer buffer = MemoryUtil.memAllocFloat(data.length).put(data).flip();
		set(buffer);
		MemoryUtil.memFree(buffer);
		return this;
	}
	
	public VertexBuffer fill(int offset, byte[] data) {
		GL45.glMapNamedBuffer(id, GL15.GL_WRITE_ONLY).position(offset).put(data);
		GL45.glUnmapNamedBuffer(id);
		AllocationTracker.INSTANCE.addGPUBytes(data.length);
		return this;
	}
	
	public VertexBuffer fill(int offset, short[] data) {
		GL45.glMapNamedBuffer(id, GL15.GL_WRITE_ONLY).position(offset).asShortBuffer().put(data);
		GL45.glUnmapNamedBuffer(id);
		AllocationTracker.INSTANCE.addGPUBytes(data.length * 2L);
		return this;
	}
	
	public VertexBuffer fill(int offset, int[] data) {
		GL45.glMapNamedBuffer(id, GL15.GL_WRITE_ONLY).position(offset).asIntBuffer().put(data);
		GL45.glUnmapNamedBuffer(id);
		AllocationTracker.INSTANCE.addGPUBytes(data.length * 4L);
		return this;
	}
	
	public VertexBuffer fill(int offset, float[] data) {
		GL45.glMapNamedBuffer(id, GL15.GL_WRITE_ONLY).position(offset).asFloatBuffer().put(data);
		GL45.glUnmapNamedBuffer(id);
		AllocationTracker.INSTANCE.addGPUBytes(data.length * 4L);
		return this;
	}
	
	public VertexBuffer fill(List<IntObjectPair<byte[]>> data) {
		return fill(0, data);
	}
	
	public VertexBuffer fill(int offset, List<IntObjectPair<byte[]>> data) {
		if(data.size() > 0) {
			long totalInjected = 0;
			ByteBuffer buffer = GL45.glMapNamedBuffer(id, GL15.GL_WRITE_ONLY);
			for(int i = 0,m = data.size();i < m;i++) {
				IntObjectPair<byte[]> entry = data.get(i);
				buffer.position(entry.getIntKey() + offset);
				buffer.put(entry.getValue());
				totalInjected += entry.getValue().length;
			}
			buffer.flip();
			if(!GL45.glUnmapNamedBuffer(id)) GameLog.info("Memory Corruption?");
			AllocationTracker.INSTANCE.addGPUBytes(totalInjected);
		}
		return this;
	}
	
	public VertexBuffer grow(int newSize) {
		if(size >= newSize) throw new IllegalArgumentException("New Size ["+newSize+"] is smaller then the buffer ["+size+"]");
		ByteBuffer buffer = GL45.glMapNamedBuffer(id, GL15.GL_READ_ONLY);
		if(size != buffer.remaining()) throw new IllegalStateException("Grow Function found inconsisten data: Found=["+buffer.remaining()+"], Expected=["+size+"]");
		ByteBuffer newBuff = MemoryUtil.memAlloc(newSize);
		MemoryUtil.memCopy(MemoryUtil.memAddress(buffer), MemoryUtil.memAddress(newBuff), size);
		newBuff.position(newSize).flip();
		GL45.glUnmapNamedBuffer(id);
		AllocationTracker.INSTANCE.addGPUBytes(size);
		set(MemoryUtil.memAddress(newBuff), newSize);
		MemoryUtil.memFree(newBuff);
		return this;
	}
	
	public VertexBuffer shrink(int newSize) {
		if(size <= newSize) throw new IllegalArgumentException("New Size ["+newSize+"] is bigger then the buffer ["+size+"]");
		ByteBuffer buffer = GL45.glMapNamedBuffer(id, GL15.GL_READ_WRITE);
		if(size != buffer.remaining()) throw new IllegalStateException("Grow Function found inconsisten data: Found=["+buffer.remaining()+"], Expected=["+size+"]");
		ByteBuffer newBuff = MemoryUtil.memAlloc(newSize);
		MemoryUtil.memCopy(MemoryUtil.memAddress(buffer), MemoryUtil.memAddress(newBuff), newSize);
		newBuff.position(newSize).flip();
		GL45.glUnmapNamedBuffer(id);
		AllocationTracker.INSTANCE.addGPUBytes(size);
		set(MemoryUtil.memAddress(newBuff), newSize);
		MemoryUtil.memFree(newBuff);
		return this;
	}
	
	public VertexBuffer shift(int source, int dest, int length) {
		if(source < 0 || source + length >= size) throw new ArrayIndexOutOfBoundsException();
		if(dest < 0 || dest + length >= size) throw new ArrayIndexOutOfBoundsException();
		GL45.glCopyNamedBufferSubData(id, id, source, dest, length);
		AllocationTracker.INSTANCE.addGPUBytes(length*2);
		return this;
	}
}
