package buffers

import (
	"math"
	"reflect"

	"github.com/bloeys/gglm/gglm"
	"github.com/bloeys/nmage/assert"
	"github.com/bloeys/nmage/logging"
	"github.com/go-gl/gl/v4.1-core/gl"
)

type UniformBufferFieldInput struct {
	Id   uint16
	Type ElementType
	// Count should be set in case this field is an array of type `[Count]Type`.
	// Count=0 is valid and is equivalent to Count=1, which means the type is NOT an array, but a single field.
	Count uint16

	// Subfields is used when type is a struct, in which case it holds the fields of the struct.
	// Ids do not have to be unique across structs.
	Subfields []UniformBufferFieldInput
}

type UniformBufferField struct {
	Id            uint16
	AlignedOffset uint16
	// Count should be set in case this field is an array of type `[Count]Type`.
	// Count=0 is valid and is equivalent to Count=1, which means the type is NOT an array, but a single field.
	Count uint16
	Type  ElementType

	// Subfields is used when type is a struct, in which case it holds the fields of the struct.
	// Ids do not have to be unique across structs.
	Subfields []UniformBufferField
}

type UniformBuffer struct {
	Id uint32
	// Size is the allocated memory in bytes on the GPU for this uniform buffer
	Size   uint32
	Fields []UniformBufferField
}

func (ub *UniformBuffer) Bind() {
	gl.BindBuffer(gl.UNIFORM_BUFFER, ub.Id)
}

func (ub *UniformBuffer) UnBind() {
	gl.BindBuffer(gl.UNIFORM_BUFFER, 0)
}

func (ub *UniformBuffer) SetBindPoint(bindPointIndex uint32) {
	gl.BindBufferBase(gl.UNIFORM_BUFFER, bindPointIndex, ub.Id)
}

func addUniformBufferFieldsToArray(startAlignedOffset uint16, arrayToAddTo *[]UniformBufferField, fieldsToAdd []UniformBufferFieldInput) (totalSize uint32) {

	if len(fieldsToAdd) == 0 {
		return 0
	}

	// This function is recursive so only size the array once
	if cap(*arrayToAddTo) == 0 {
		*arrayToAddTo = make([]UniformBufferField, 0, len(fieldsToAdd))
	}

	var alignedOffset uint16 = 0
	fieldIdToTypeMap := make(map[uint16]ElementType, len(fieldsToAdd))

	for i := 0; i < len(fieldsToAdd); i++ {

		f := fieldsToAdd[i]
		if f.Count == 0 {
			f.Count = 1
		}

		existingFieldType, ok := fieldIdToTypeMap[f.Id]
		assert.T(!ok, "Uniform buffer field id is reused within the same uniform buffer. FieldId=%d was first used on a field with type=%s and then used on a different field with type=%s\n", f.Id, existingFieldType.String(), f.Type.String())

		// To understand this take an example. Say we have a total offset of 100 and we are adding a vec4.
		// Vec4s must be aligned to a 16 byte boundary but 100 is not (100 % 16 != 0).
		//
		// To fix this, we take the alignment error which is alignErr=100 % 16=4, but this is error to the nearest
		// boundary, which is below the offset.
		//
		// To get the nearest boundary larger than the offset we can:
		// offset + (boundary - alignErr) == 100 + (16 - 4) == 112; 112 % 16 == 0, meaning its a boundary
		//
		// Note that arrays of scalars/vectors are always aligned to 16 bytes, like a vec4
		//
		// Official spec and full details in subsection 'Standard Uniform Block Layout' at http://www.opengl.org/registry/specs/ARB/uniform_buffer_object.txt
		var alignmentBoundary uint16 = 16
		if f.Count == 1 {
			alignmentBoundary = f.Type.GlStd140AlignmentBoundary()
		}

		alignmentError := alignedOffset % alignmentBoundary
		if alignmentError != 0 {
			alignedOffset += alignmentBoundary - alignmentError
		}

		newField := UniformBufferField{Id: f.Id, Type: f.Type, AlignedOffset: startAlignedOffset + alignedOffset, Count: f.Count}
		*arrayToAddTo = append(*arrayToAddTo, newField)

		// Prepare aligned offset for the next field.
		//
		// Matrices are treated as an array of column vectors, where each column is a vec4,
		// that's why we have a multiplier depending on how many columns we have when calculating
		// the offset
		multiplier := uint16(1)
		if f.Type == DataTypeMat2 {
			multiplier = 2
		} else if f.Type == DataTypeMat3 {
			multiplier = 3
		} else if f.Type == DataTypeMat4 {
			multiplier = 4
		}

		if f.Type == DataTypeStruct {

			subfieldsAlignedOffset := uint16(addUniformBufferFieldsToArray(startAlignedOffset+alignedOffset, arrayToAddTo, f.Subfields))

			// Pad structs to 16 byte boundary
			padTo16Boundary(&subfieldsAlignedOffset)
			alignedOffset += subfieldsAlignedOffset * f.Count

		} else {

			// Elements advance the alignedOffset by their actual byte size.
			// Aligned offset is padded if the place its at is not aligned to the boundary required by the next element.
			//
			// The exception is structs, because fields after a struct field are always aligned at a 16 byte boundary.
			//
			// For example, a vec3 starting at offset 80, taking 12 bytes, would put the aligned offset at 92.
			// If the next element is a float32 (alignment boundary = 4) then no padding is required and
			// the float will start at 92 and end at 96.
			// However, if the element after the vec3 is a vec3 (alignment boundary = 16), then it would require
			// a padding of 4 bytes so that it can start at 96, which is aligned to 16. In this case the second vec3
			// would start at 96 and end at 96+12=108.
			alignedOffset = newField.AlignedOffset + uint16(f.Type.GlStd140SizeBytes())*f.Count*multiplier - startAlignedOffset
		}
	}

	return uint32(alignedOffset)
}

func padTo16Boundary[T uint16 | int | int32](val *T) {
	alignmentError := *val % 16
	if alignmentError != 0 {
		*val += 16 - alignmentError
	}
}

func (ub *UniformBuffer) getField(fieldId uint16, fieldType ElementType) UniformBufferField {

	for i := 0; i < len(ub.Fields); i++ {

		f := ub.Fields[i]

		if f.Id != fieldId {
			continue
		}

		assert.T(f.Type == fieldType, "Uniform buffer field id is reused within the same uniform buffer. FieldId=%d was first used on a field with type=%v, but is now being used on a field with type=%v\n", fieldId, f.Type.String(), fieldType.String())

		return f
	}

	logging.ErrLog.Panicf("couldn't find uniform buffer field of id=%d and type=%s\n", fieldId, fieldType.String())
	return UniformBufferField{}
}

func (ub *UniformBuffer) SetInt32(fieldId uint16, val int32) {

	f := ub.getField(fieldId, DataTypeInt32)
	gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4, gl.Ptr(&val))
}

func (ub *UniformBuffer) SetUint32(fieldId uint16, val uint32) {

	f := ub.getField(fieldId, DataTypeUint32)
	gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4, gl.Ptr(&val))
}

func (ub *UniformBuffer) SetFloat32(fieldId uint16, val float32) {

	f := ub.getField(fieldId, DataTypeFloat32)
	gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4, gl.Ptr(&val))
}

func (ub *UniformBuffer) SetVec2(fieldId uint16, val *gglm.Vec2) {
	f := ub.getField(fieldId, DataTypeVec2)
	gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4*2, gl.Ptr(&val.Data[0]))
}

func (ub *UniformBuffer) SetVec3(fieldId uint16, val *gglm.Vec3) {
	f := ub.getField(fieldId, DataTypeVec3)
	gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4*3, gl.Ptr(&val.Data[0]))
}

func (ub *UniformBuffer) SetVec4(fieldId uint16, val *gglm.Vec4) {
	f := ub.getField(fieldId, DataTypeVec4)
	gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4*4, gl.Ptr(&val.Data[0]))
}

func (ub *UniformBuffer) SetMat2(fieldId uint16, val *gglm.Mat2) {
	f := ub.getField(fieldId, DataTypeMat2)
	gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4*4, gl.Ptr(&val.Data[0][0]))
}

func (ub *UniformBuffer) SetMat3(fieldId uint16, val *gglm.Mat3) {
	f := ub.getField(fieldId, DataTypeMat3)
	gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4*9, gl.Ptr(&val.Data[0][0]))
}

func (ub *UniformBuffer) SetMat4(fieldId uint16, val *gglm.Mat4) {
	f := ub.getField(fieldId, DataTypeMat4)
	gl.BufferSubData(gl.UNIFORM_BUFFER, int(f.AlignedOffset), 4*16, gl.Ptr(&val.Data[0][0]))
}

func (ub *UniformBuffer) SetStruct(inputStruct any) {
	setStruct(ub.Fields, make([]byte, ub.Size), inputStruct, 1000_000, false, 0)
}

func setStruct(fields []UniformBufferField, buf []byte, inputStruct any, maxFieldsToConsume int, onlyBufWrite bool, writeOffset int) (bytesWritten, fieldsConsumed int) {

	if len(fields) == 0 {
		return
	}

	if inputStruct == nil {
		logging.ErrLog.Panicf("UniformBuffer.SetStruct called with a value that is nil")
	}

	structVal := reflect.ValueOf(inputStruct)
	if structVal.Kind() != reflect.Struct {
		logging.ErrLog.Panicf("UniformBuffer.SetStruct called with a value that is not a struct. Val=%v\n", inputStruct)
	}

	// Needed because fieldIndex can move faster than struct fields in case of struct fields
	structFieldIndex := 0
	for fieldIndex := 0; fieldIndex < len(fields) && fieldIndex < maxFieldsToConsume; fieldIndex++ {

		ubField := &fields[fieldIndex]
		valField := structVal.Field(structFieldIndex)

		fieldsConsumed++
		structFieldIndex++

		kind := valField.Kind()
		if kind == reflect.Pointer {
			valField = valField.Elem()
		}

		var elementType reflect.Type
		isArray := kind == reflect.Slice || kind == reflect.Array
		if isArray {
			elementType = valField.Type().Elem()
			kind = elementType.Kind()
		} else {
			elementType = valField.Type()
		}

		if isArray {
			assert.T(valField.Len() == int(ubField.Count), "ubo field of id=%d is an array/slice field of length=%d but got input of length=%d\n", ubField.Id, ubField.Count, valField.Len())
		}

		typeMatches := false
		bytesWritten = int(ubField.AlignedOffset) + writeOffset

		switch ubField.Type {

		case DataTypeUint32:

			typeMatches = elementType.Name() == "uint32"
			if typeMatches {

				if isArray {
					Write32BitIntegerSliceToByteBufWithAlignment(buf, &bytesWritten, 16, valField.Slice(0, valField.Len()).Interface().([]uint32))
				} else {
					Write32BitIntegerToByteBuf(buf, &bytesWritten, uint32(valField.Uint()))
				}
			}

		case DataTypeFloat32:

			typeMatches = elementType.Name() == "float32"
			if typeMatches {

				if isArray {
					WriteF32SliceToByteBufWithAlignment(buf, &bytesWritten, 16, valField.Slice(0, valField.Len()).Interface().([]float32))
				} else {
					WriteF32ToByteBuf(buf, &bytesWritten, float32(valField.Float()))
				}
			}

		case DataTypeInt32:

			typeMatches = elementType.Name() == "int32"
			if typeMatches {

				if isArray {
					Write32BitIntegerSliceToByteBufWithAlignment(buf, &bytesWritten, 16, valField.Slice(0, valField.Len()).Interface().([]int32))
				} else {
					Write32BitIntegerToByteBuf(buf, &bytesWritten, uint32(valField.Int()))
				}
			}

		case DataTypeVec2:

			typeMatches = elementType.Name() == "Vec2"

			if typeMatches {

				if isArray {
					WriteVec2SliceToByteBufWithAlignment(buf, &bytesWritten, 16, valField.Slice(0, valField.Len()).Interface().([]gglm.Vec2))
				} else {
					v2 := valField.Interface().(gglm.Vec2)
					WriteF32SliceToByteBuf(buf, &bytesWritten, v2.Data[:])
				}
			}

		case DataTypeVec3:

			typeMatches = elementType.Name() == "Vec3"

			if typeMatches {

				if isArray {
					WriteVec3SliceToByteBufWithAlignment(buf, &bytesWritten, 16, valField.Slice(0, valField.Len()).Interface().([]gglm.Vec3))
				} else {
					v3 := valField.Interface().(gglm.Vec3)
					WriteF32SliceToByteBuf(buf, &bytesWritten, v3.Data[:])
				}
			}

		case DataTypeVec4:

			typeMatches = elementType.Name() == "Vec4"

			if typeMatches {

				if isArray {
					WriteVec4SliceToByteBufWithAlignment(buf, &bytesWritten, 16, valField.Slice(0, valField.Len()).Interface().([]gglm.Vec4))
				} else {
					v3 := valField.Interface().(gglm.Vec4)
					WriteF32SliceToByteBuf(buf, &bytesWritten, v3.Data[:])
				}
			}

		case DataTypeMat2:

			typeMatches = elementType.Name() == "Mat2"

			if typeMatches {

				if isArray {
					m2Arr := valField.Interface().([]gglm.Mat2)
					WriteMat2SliceToByteBufWithAlignment(buf, &bytesWritten, 16*2, m2Arr)
				} else {
					m := valField.Interface().(gglm.Mat2)
					WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[0][:])
					WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[1][:])
				}
			}

		case DataTypeMat3:

			typeMatches = elementType.Name() == "Mat3"

			if typeMatches {

				if isArray {
					m3Arr := valField.Interface().([]gglm.Mat3)
					WriteMat3SliceToByteBufWithAlignment(buf, &bytesWritten, 16*3, m3Arr)
				} else {
					m := valField.Interface().(gglm.Mat3)
					WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[0][:])
					WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[1][:])
					WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[2][:])
				}
			}

		case DataTypeMat4:

			typeMatches = elementType.Name() == "Mat4"

			if typeMatches {

				if isArray {
					m4Arr := valField.Interface().([]gglm.Mat4)
					WriteMat4SliceToByteBufWithAlignment(buf, &bytesWritten, 16*4, m4Arr)
				} else {
					m := valField.Interface().(gglm.Mat4)
					WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[0][:])
					WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[1][:])
					WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[2][:])
					WriteF32SliceToByteBuf(buf, &bytesWritten, m.Data[3][:])
				}
			}

		case DataTypeStruct:

			typeMatches = kind == reflect.Struct

			if typeMatches {

				if isArray {

					offset := 0
					arrSize := valField.Len()
					fieldsToUse := fields[fieldIndex+1:]
					for i := 0; i < arrSize; i++ {

						setStructBytesWritten, setStructFieldsConsumed := setStruct(fieldsToUse, buf, valField.Index(i).Interface(), elementType.NumField(), true, offset*i)

						if offset == 0 {
							offset = setStructBytesWritten
							padTo16Boundary(&offset)

							bytesWritten += offset * arrSize

							// Tracking consumed fields is needed because if we have a struct inside another struct
							// elementType.NumField() will only give us the fields consumed by the first struct,
							// but we need to count all fields of all nested structs inside this one
							fieldIndex += setStructFieldsConsumed
							fieldsConsumed += setStructFieldsConsumed
						}
					}

				} else {

					setStructBytesWritten, setStructFieldsConsumed := setStruct(fields[fieldIndex+1:], buf, valField.Interface(), valField.NumField(), true, writeOffset)

					bytesWritten += setStructBytesWritten
					fieldIndex += setStructFieldsConsumed
					fieldsConsumed += setStructFieldsConsumed
				}
			}

		default:
			assert.T(false, "Unknown uniform buffer data type passed. DataType '%d'", ubField.Type)
		}

		if !typeMatches {
			logging.ErrLog.Panicf("Struct field ordering and types must match uniform buffer fields, but at field index %d got UniformBufferField=%v but a struct field of type %s\n", fieldIndex, ubField, valField.String())
		}
	}

	if bytesWritten == 0 {
		return 0, fieldsConsumed
	}

	if !onlyBufWrite {
		gl.BufferSubData(gl.UNIFORM_BUFFER, 0, bytesWritten, gl.Ptr(&buf[0]))
	}

	return bytesWritten - int(fields[0].AlignedOffset) - writeOffset, fieldsConsumed
}

func Write32BitIntegerToByteBuf[T uint32 | int32](buf []byte, startIndex *int, val T) {

	assert.T(*startIndex+4 <= len(buf), "failed to write uint32/int32 to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d", *startIndex, len(buf))

	buf[*startIndex] = byte(val)
	buf[*startIndex+1] = byte(val >> 8)
	buf[*startIndex+2] = byte(val >> 16)
	buf[*startIndex+3] = byte(val >> 24)

	*startIndex += 4
}

func Write32BitIntegerSliceToByteBufWithAlignment[T uint32 | int32](buf []byte, startIndex *int, alignmentPerField int, vals []T) {

	assert.T(*startIndex+len(vals)*alignmentPerField <= len(buf), "failed to write uint32/int32 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerField, *startIndex, len(buf), len(vals)*alignmentPerField)

	for i := 0; i < len(vals); i++ {

		val := vals[i]

		buf[*startIndex] = byte(val)
		buf[*startIndex+1] = byte(val >> 8)
		buf[*startIndex+2] = byte(val >> 16)
		buf[*startIndex+3] = byte(val >> 24)

		*startIndex += alignmentPerField
	}
}

func WriteF32ToByteBuf(buf []byte, startIndex *int, val float32) {

	assert.T(*startIndex+4 <= len(buf), "failed to write float32 to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d", *startIndex, len(buf))

	bits := math.Float32bits(val)

	buf[*startIndex] = byte(bits)
	buf[*startIndex+1] = byte(bits >> 8)
	buf[*startIndex+2] = byte(bits >> 16)
	buf[*startIndex+3] = byte(bits >> 24)

	*startIndex += 4
}

func WriteF32SliceToByteBuf(buf []byte, startIndex *int, vals []float32) {

	assert.T(*startIndex+len(vals)*4 <= len(buf), "failed to write slice of float32 to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", *startIndex, len(buf), len(vals)*4)

	for i := 0; i < len(vals); i++ {

		bits := math.Float32bits(vals[i])

		buf[*startIndex] = byte(bits)
		buf[*startIndex+1] = byte(bits >> 8)
		buf[*startIndex+2] = byte(bits >> 16)
		buf[*startIndex+3] = byte(bits >> 24)

		*startIndex += 4
	}
}

func WriteF32SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerField int, vals []float32) {

	assert.T(*startIndex+len(vals)*alignmentPerField <= len(buf), "failed to write slice of float32 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerField, *startIndex, len(buf), len(vals)*alignmentPerField)

	for i := 0; i < len(vals); i++ {

		bits := math.Float32bits(vals[i])

		buf[*startIndex] = byte(bits)
		buf[*startIndex+1] = byte(bits >> 8)
		buf[*startIndex+2] = byte(bits >> 16)
		buf[*startIndex+3] = byte(bits >> 24)

		*startIndex += alignmentPerField
	}
}

func WriteVec2SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerVector int, vals []gglm.Vec2) {

	assert.T(*startIndex+len(vals)*alignmentPerVector <= len(buf), "failed to write slice of gglm.Vec2 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerVector, *startIndex, len(buf), len(vals)*alignmentPerVector)

	for i := 0; i < len(vals); i++ {

		bitsX := math.Float32bits(vals[i].X())
		bitsY := math.Float32bits(vals[i].Y())

		buf[*startIndex] = byte(bitsX)
		buf[*startIndex+1] = byte(bitsX >> 8)
		buf[*startIndex+2] = byte(bitsX >> 16)
		buf[*startIndex+3] = byte(bitsX >> 24)

		buf[*startIndex+4] = byte(bitsY)
		buf[*startIndex+5] = byte(bitsY >> 8)
		buf[*startIndex+6] = byte(bitsY >> 16)
		buf[*startIndex+7] = byte(bitsY >> 24)

		*startIndex += alignmentPerVector
	}
}

func WriteVec3SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerVector int, vals []gglm.Vec3) {

	assert.T(*startIndex+len(vals)*alignmentPerVector <= len(buf), "failed to write slice of gglm.Vec3 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerVector, *startIndex, len(buf), len(vals)*alignmentPerVector)

	for i := 0; i < len(vals); i++ {

		bitsX := math.Float32bits(vals[i].X())
		bitsY := math.Float32bits(vals[i].Y())
		bitsZ := math.Float32bits(vals[i].Z())

		buf[*startIndex] = byte(bitsX)
		buf[*startIndex+1] = byte(bitsX >> 8)
		buf[*startIndex+2] = byte(bitsX >> 16)
		buf[*startIndex+3] = byte(bitsX >> 24)

		buf[*startIndex+4] = byte(bitsY)
		buf[*startIndex+5] = byte(bitsY >> 8)
		buf[*startIndex+6] = byte(bitsY >> 16)
		buf[*startIndex+7] = byte(bitsY >> 24)

		buf[*startIndex+8] = byte(bitsZ)
		buf[*startIndex+9] = byte(bitsZ >> 8)
		buf[*startIndex+10] = byte(bitsZ >> 16)
		buf[*startIndex+11] = byte(bitsZ >> 24)

		*startIndex += alignmentPerVector
	}
}

func WriteVec4SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerVector int, vals []gglm.Vec4) {

	assert.T(*startIndex+len(vals)*alignmentPerVector <= len(buf), "failed to write slice of gglm.Vec4 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerVector, *startIndex, len(buf), len(vals)*alignmentPerVector)

	for i := 0; i < len(vals); i++ {

		bitsX := math.Float32bits(vals[i].X())
		bitsY := math.Float32bits(vals[i].Y())
		bitsZ := math.Float32bits(vals[i].Z())
		bitsW := math.Float32bits(vals[i].W())

		buf[*startIndex] = byte(bitsX)
		buf[*startIndex+1] = byte(bitsX >> 8)
		buf[*startIndex+2] = byte(bitsX >> 16)
		buf[*startIndex+3] = byte(bitsX >> 24)

		buf[*startIndex+4] = byte(bitsY)
		buf[*startIndex+5] = byte(bitsY >> 8)
		buf[*startIndex+6] = byte(bitsY >> 16)
		buf[*startIndex+7] = byte(bitsY >> 24)

		buf[*startIndex+8] = byte(bitsZ)
		buf[*startIndex+9] = byte(bitsZ >> 8)
		buf[*startIndex+10] = byte(bitsZ >> 16)
		buf[*startIndex+11] = byte(bitsZ >> 24)

		buf[*startIndex+12] = byte(bitsW)
		buf[*startIndex+13] = byte(bitsW >> 8)
		buf[*startIndex+14] = byte(bitsW >> 16)
		buf[*startIndex+15] = byte(bitsW >> 24)

		*startIndex += alignmentPerVector
	}
}

func WriteMat2SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerMatrix int, vals []gglm.Mat2) {

	assert.T(*startIndex+len(vals)*alignmentPerMatrix <= len(buf), "failed to write slice of gglm.Mat2 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerMatrix, *startIndex, len(buf), len(vals)*alignmentPerMatrix)

	for i := 0; i < len(vals); i++ {

		m := &vals[i]

		WriteVec2SliceToByteBufWithAlignment(
			buf,
			startIndex,
			16,
			[]gglm.Vec2{
				{Data: m.Data[0]},
				{Data: m.Data[1]},
			},
		)
	}
}

func WriteMat3SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerMatrix int, vals []gglm.Mat3) {

	assert.T(*startIndex+len(vals)*alignmentPerMatrix <= len(buf), "failed to write slice of gglm.Mat3 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerMatrix, *startIndex, len(buf), len(vals)*alignmentPerMatrix)

	for i := 0; i < len(vals); i++ {

		m := &vals[i]

		WriteVec3SliceToByteBufWithAlignment(
			buf,
			startIndex,
			16,
			[]gglm.Vec3{
				{Data: m.Data[0]},
				{Data: m.Data[1]},
				{Data: m.Data[2]},
			},
		)
	}
}

func WriteMat4SliceToByteBufWithAlignment(buf []byte, startIndex *int, alignmentPerMatrix int, vals []gglm.Mat4) {

	assert.T(*startIndex+len(vals)*alignmentPerMatrix <= len(buf), "failed to write slice of gglm.Mat2 with custom alignment=%d to buffer because the buffer doesn't have enough space. Start index=%d, Buffer length=%d, but needs %d bytes free", alignmentPerMatrix, *startIndex, len(buf), len(vals)*alignmentPerMatrix)

	for i := 0; i < len(vals); i++ {

		m := &vals[i]

		WriteVec4SliceToByteBufWithAlignment(
			buf,
			startIndex,
			16,
			[]gglm.Vec4{
				{Data: m.Data[0]},
				{Data: m.Data[1]},
				{Data: m.Data[2]},
				{Data: m.Data[3]},
			},
		)
	}
}

func ReflectValueMatchesUniformBufferField(v reflect.Value, ubField *UniformBufferField) bool {

	if v.Kind() == reflect.Pointer {
		v = v.Elem()
	}

	switch ubField.Type {

	case DataTypeUint32:
		t := v.Type()
		return t.Name() == "uint32"
	case DataTypeFloat32:
		t := v.Type()
		return t.Name() == "float32"
	case DataTypeInt32:
		t := v.Type()
		return t.Name() == "int32"
	case DataTypeVec2:
		_, ok := v.Interface().(gglm.Vec2)
		return ok
	case DataTypeVec3:
		_, ok := v.Interface().(gglm.Vec3)
		return ok
	case DataTypeVec4:
		_, ok := v.Interface().(gglm.Vec4)
		return ok
	case DataTypeMat2:
		_, ok := v.Interface().(gglm.Mat2)
		return ok
	case DataTypeMat3:
		_, ok := v.Interface().(gglm.Mat3)
		return ok
	case DataTypeMat4:
		_, ok := v.Interface().(gglm.Mat4)
		return ok

	default:
		assert.T(false, "Unknown uniform buffer data type passed. DataType '%d'", ubField.Type)
		return false
	}
}

func NewUniformBuffer(fields []UniformBufferFieldInput, usage BufUsage) UniformBuffer {

	ub := UniformBuffer{}

	ub.Size = addUniformBufferFieldsToArray(0, &ub.Fields, fields)

	gl.GenBuffers(1, &ub.Id)
	if ub.Id == 0 {
		logging.ErrLog.Panicln("Failed to create OpenGL buffer for a uniform buffer")
	}

	ub.Bind()
	gl.BufferData(gl.UNIFORM_BUFFER, int(ub.Size), gl.Ptr(nil), usage.ToGL())
	ub.UnBind()

	return ub
}
