package speiger.src.coreengine.rendering.models;

import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.List;
import java.util.function.Consumer;

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

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

public class VertexBuffer
{
	public final int bufferID;
	public final int bufferType;
	public int bufferState = GL15.GL_STATIC_DRAW;
	
	public VertexBuffer(int type)
	{
		this(type, GL15.glGenBuffers());
	}
	
	public VertexBuffer(int type, int id)
	{
		bufferType = type;
		bufferID = id;
	}
	
	public VertexBuffer setDataType(int bufferState)
	{
		this.bufferState = bufferState;
		return this;
	}
	
	public int getBufferID()
	{
		return bufferID;
	}
	
	public int getBufferType()
	{
		return bufferType;
	}
	
	public VertexBuffer delete()
	{
		GL15.glDeleteBuffers(bufferID);
		return this;
	}
	
	public VertexBuffer bind()
	{
		GL15.glBindBuffer(bufferType, bufferID);
		return this;
	}
	
	public VertexBuffer unbind()
	{
		GL15.glBindBuffer(bufferType, 0);
		return this;
	}
	
	public VertexBuffer allocateMemory(int totalBytes)
	{
		GL15.glBufferData(bufferType, totalBytes, bufferState);
		AllocationTracker.INSTANCE.addGPUBytes(totalBytes);
		return this;
	}
	
	public VertexBuffer setBufferData(long pointer, int totalBytes)
	{
		GL15.nglBufferData(bufferType, totalBytes, pointer, bufferState);
		AllocationTracker.INSTANCE.addGPUBytes(totalBytes);
		return this;
	}
	
	public VertexBuffer setBufferData(float[] data)
	{
		FloatBuffer buffer = MemoryUtil.memAllocFloat(data.length);
		buffer.put(data).flip();
		AllocationTracker.INSTANCE.addGPUBytes(data.length * 4L);
		GL15.glBufferData(bufferType, buffer, bufferState);
		MemoryUtil.memFree(buffer);
		return this;
	}
	
	public VertexBuffer setBufferData(int[] data)
	{
		IntBuffer buffer = MemoryUtil.memAllocInt(data.length);
		buffer.put(data).flip();
		AllocationTracker.INSTANCE.addGPUBytes(data.length * 4L);
		GL15.glBufferData(bufferType, buffer, bufferState);
		MemoryUtil.memFree(buffer);
		return this;
	}
	
	public VertexBuffer setBufferData(byte[] data)
	{
		ByteBuffer buffer = MemoryUtil.memAlloc(data.length);
		buffer.put(data).flip();
		GL15.glBufferData(bufferType, buffer, bufferState);
		MemoryUtil.memFree(buffer);
		AllocationTracker.INSTANCE.addGPUBytes(data.length);
		return this;
	}
	
	public VertexBuffer setBufferData(FloatBuffer data)
	{
		GL15.glBufferData(bufferType, data, bufferState);
		AllocationTracker.INSTANCE.addGPUBytes(data.remaining() * 4L);
		return this;
	}
	
	public VertexBuffer setBufferData(IntBuffer data)
	{
		GL15.glBufferData(bufferType, data, bufferState);
		AllocationTracker.INSTANCE.addGPUBytes(data.remaining() * 4L);
		return this;
	}
	
	public VertexBuffer setBufferData(ByteBuffer data)
	{
		GL15.glBufferData(bufferType, data, bufferState);
		AllocationTracker.INSTANCE.addGPUBytes(data.remaining());
		return this;
	}
	
	public VertexBuffer setBufferDataFloat(int neededFloats, Consumer<FloatBuffer> supplier)
	{
		FloatBuffer buffer = MemoryUtil.memAllocFloat(neededFloats);
		supplier.accept(buffer);
		buffer.flip();
		AllocationTracker.INSTANCE.addGPUBytes(buffer.remaining() * 4L);
		GL15.glBufferData(bufferType, buffer, bufferState);
		MemoryUtil.memFree(buffer);
		return this;
	}
	
	public VertexBuffer setBufferDataInt(int neededInts, Consumer<IntBuffer> supplier)
	{
		IntBuffer buffer = MemoryUtil.memAllocInt(neededInts);
		supplier.accept(buffer);
		buffer.flip();
		AllocationTracker.INSTANCE.addGPUBytes(buffer.remaining() * 4L);
		GL15.glBufferData(bufferType, buffer, bufferState);
		MemoryUtil.memFree(buffer);
		return this;
	}
	
	public VertexBuffer setBufferDataByte(int neededBytes, Consumer<ByteBuffer> supplier)
	{
		ByteBuffer buffer = MemoryUtil.memAlloc(neededBytes);
		supplier.accept(buffer);
		buffer.flip();
		AllocationTracker.INSTANCE.addGPUBytes(buffer.remaining());
		GL15.glBufferData(bufferType, buffer, bufferState);
		MemoryUtil.memFree(buffer);
		return this;
	}
	
	public VertexBuffer fillExternalBuffer(FloatBuffer buffer, int floatOffset)
	{
		GL15.glBufferSubData(bufferType, floatOffset, buffer);
		AllocationTracker.INSTANCE.addGPUBytes(buffer.remaining() * 4);
		return this;
	}
	
	public VertexBuffer fillExternalBuffer(IntBuffer buffer, int intOffset)
	{
		GL15.glBufferSubData(bufferType, intOffset, buffer);
		AllocationTracker.INSTANCE.addGPUBytes(buffer.remaining() * 4);
		return this;
	}
	
	public VertexBuffer fillExternalBuffer(ByteBuffer buffer, int byteOffset)
	{
		GL15.glBufferSubData(bufferType, byteOffset, buffer);
		AllocationTracker.INSTANCE.addGPUBytes(buffer.remaining());
		return this;
	}
	
	public VertexBuffer fillBuffer(int byteOffset, float[] data)
	{
		ByteBuffer buffer = GL15.glMapBuffer(bufferType, GL15.GL_WRITE_ONLY);
		buffer.position(byteOffset);
		buffer.asFloatBuffer().put(data);
		GL15.glUnmapBuffer(bufferType);
		AllocationTracker.INSTANCE.addGPUBytes(data.length * 4L);
		return this;
	}
	
	public VertexBuffer fillBuffer(int byteOffset, int[] data)
	{
		ByteBuffer buffer = GL15.glMapBuffer(bufferType, GL15.GL_WRITE_ONLY);
		buffer.position(byteOffset);
		buffer.asIntBuffer().put(data);
		GL15.glUnmapBuffer(bufferType);
		AllocationTracker.INSTANCE.addGPUBytes(data.length * 4L);
		return this;
	}
	
	public VertexBuffer fillBuffer(int byteOffset, byte[] data)
	{
		ByteBuffer buffer = GL15.glMapBuffer(bufferType, GL15.GL_WRITE_ONLY);
		buffer.position(byteOffset);
		buffer.put(data);
		GL15.glUnmapBuffer(bufferType);
		AllocationTracker.INSTANCE.addGPUBytes(data.length);
		return this;
	}
	
	public VertexBuffer fillBuffer(List<IntObjectPair<byte[]>> data)
	{
		return fillBuffer(0, data);
	}
	
	public VertexBuffer fillBuffer(int byteOffset, List<IntObjectPair<byte[]>> data)
	{
		if(data.isEmpty())
		{
			return this;
		}
		long totalData = 0;
		ByteBuffer buffer = GL15.glMapBuffer(bufferType, GL15.GL_WRITE_ONLY);
		for(int i = 0,m=data.size();i<m;i++)
		{
			IntObjectPair<byte[]> entry = data.get(i);
			buffer.position(entry.getIntKey()+byteOffset);
			buffer.put(entry.getValue());
			totalData += entry.getValue().length;
		}
		buffer.flip();
		if(!GL15.glUnmapBuffer(bufferType))
		{
			GameLog.info("Memory Corruption?");
		}
		AllocationTracker.INSTANCE.addGPUBytes(totalData);
		return this;
	}
	
	public VertexBuffer fillBufferFloat(int byteOffset, int neededFloats, Consumer<FloatBuffer> supplier)
	{
		FloatBuffer buffer = MemoryUtil.memAllocFloat(neededFloats);
		supplier.accept(buffer);
		buffer.flip();
		AllocationTracker.INSTANCE.addGPUBytes(buffer.remaining() * 4L);
		GL15.glBufferSubData(bufferType, byteOffset, buffer);
		MemoryUtil.memFree(buffer);
		return this;
	}
	
	public VertexBuffer fillBufferInt(int byteOffset, int neededInts, Consumer<IntBuffer> supplier)
	{
		IntBuffer buffer = MemoryUtil.memAllocInt(neededInts);
		supplier.accept(buffer);
		buffer.flip();
		AllocationTracker.INSTANCE.addGPUBytes(buffer.remaining() * 4L);
		GL15.glBufferSubData(bufferType, byteOffset, buffer);
		MemoryUtil.memFree(buffer);
		return this;
	}
	
	public VertexBuffer fillBufferByte(int byteOffset, int neededBytes, Consumer<ByteBuffer> supplier)
	{
		ByteBuffer buffer = MemoryUtil.memAlloc(neededBytes);
		supplier.accept(buffer);
		buffer.flip();
		AllocationTracker.INSTANCE.addGPUBytes(buffer.remaining());
		GL15.glBufferSubData(bufferType, byteOffset, buffer);
		MemoryUtil.memFree(buffer);
		return this;
	}
	
	public VertexBuffer readBufferData(FloatBuffer buffer, int floatsToRead)
	{
		GL15.nglGetBufferSubData(bufferType, 0, floatsToRead << 2, MemoryUtil.memAddress(buffer));
		buffer.flip();
		AllocationTracker.INSTANCE.addGPUBytes(floatsToRead * 4L);
		return this;
	}
	
	public VertexBuffer readBufferData(IntBuffer buffer, int intsToRead)
	{
		GL15.nglGetBufferSubData(bufferType, 0, intsToRead << 2, MemoryUtil.memAddress(buffer));
		buffer.flip();
		AllocationTracker.INSTANCE.addGPUBytes(intsToRead * 4L);
		return this;
	}
	
	public VertexBuffer readBufferData(ByteBuffer buffer, int bytesToRead)
	{
		GL15.nglGetBufferSubData(bufferType, 0, bytesToRead, MemoryUtil.memAddress(buffer));
		buffer.flip();
		AllocationTracker.INSTANCE.addGPUBytes(bytesToRead);
		return this;
	}
	
	public VertexBuffer grow(int oldByteSize, int newByteSize, String source)
	{
		ByteBuffer buffer = GL15.glMapBuffer(bufferType, GL15.GL_READ_ONLY);
		if(oldByteSize != buffer.remaining())
		{
			GameLog.info("Grow: [Expected="+oldByteSize+", Present="+buffer.remaining()+", source="+source+"]");
		}
		ByteBuffer newCap = MemoryUtil.memAlloc(newByteSize);
		MemoryUtil.memCopy(MemoryUtil.memAddress(buffer), MemoryUtil.memAddress(newCap), oldByteSize);
		GL15.glUnmapBuffer(bufferType);
		newCap.position(newByteSize);
		newCap.flip();
		GL15.glBufferData(bufferType, newCap, bufferState);
		MemoryUtil.memFree(newCap);
		AllocationTracker.INSTANCE.addGPUBytes(oldByteSize + newByteSize);
		return this;
	}
	
	public VertexBuffer shrink(int oldByteSize, int newByteSize, String source)
	{
		ByteBuffer buffer = GL15.glMapBuffer(bufferType, GL15.GL_READ_WRITE);
		if(oldByteSize != buffer.remaining())
		{
			GameLog.info("Shrink: [Expected="+oldByteSize+", Present="+buffer.remaining()+", source="+source+"]");	
		}
		ByteBuffer newBuffer = MemoryUtil.memAlloc(newByteSize);
		MemoryUtil.memCopy(MemoryUtil.memAddress(buffer), MemoryUtil.memAddress(newBuffer), newByteSize);	
		newBuffer.position(newByteSize);
		newBuffer.flip();
		GL15.glUnmapBuffer(bufferType);
		GL15.glBufferData(bufferType, newBuffer, bufferState);
		MemoryUtil.memFree(newBuffer);
		AllocationTracker.INSTANCE.addGPUBytes(oldByteSize + newByteSize);
		return this;
	}
	
	public VertexBuffer shiftData(int startBytes, int bytesLength, int byteShift, String source)
	{
		ByteBuffer buffer = GL15.glMapBuffer(bufferType, GL15.GL_READ_WRITE);
		int min = Math.min(startBytes, startBytes + byteShift);
		int max = Math.max(startBytes + bytesLength, startBytes + bytesLength + byteShift);
		if(min < 0 || max > buffer.remaining())
		{
			GameLog.info("Shift Aborted due to out of range [Expected Min="+min+", Max="+max+", Source="+source+"]");
			return this;
		}
		MemoryUtil.memCopy(MemoryUtil.memAddress(buffer, startBytes), MemoryUtil.memAddress(buffer, startBytes+byteShift), bytesLength);
		GL15.glUnmapBuffer(bufferType);
		AllocationTracker.INSTANCE.addGPUBytes(bytesLength + byteShift);
		return this;
	}
}
