package pixelgl

import (
	"fmt"
	"runtime"

	"github.com/faiface/mainthread"
	"github.com/go-gl/gl/v3.3-core/gl"
	"github.com/pkg/errors"
)

// VertexSlice points to a portion of (or possibly whole) vertex array. It is used as a pointer,
// contrary to Go's builtin slices. This is, so that append can be 'in-place'. That's for the good,
// because Begin/End-ing a VertexSlice would become super confusing, if append returned a new
// VertexSlice.
//
// It also implements all basic slice-like operations: appending, sub-slicing, etc.
//
// Note that you need to Begin a VertexSlice before getting or updating it's elements or drawing it.
// After you're done with it, you need to End it.
type VertexSlice struct {
	va   *vertexArray
	i, j int
}

// MakeVertexSlice allocates a new vertex array with specified capacity and returns a VertexSlice
// that points to it's first len elements.
//
// Note, that a vertex array is specialized for a specific shader and can't be used with another
// shader.
func MakeVertexSlice(shader *Shader, len, cap int) *VertexSlice {
	if len > cap {
		panic("failed to make vertex slice: len > cap")
	}
	return &VertexSlice{
		va: newVertexArray(shader, cap),
		i:  0,
		j:  len,
	}
}

// VertexFormat returns the format of vertex attributes inside the underlying vertex array of this
// VertexSlice.
func (vs *VertexSlice) VertexFormat() AttrFormat {
	return vs.va.format
}

// Stride returns the number of float32 elements occupied by one vertex.
func (vs *VertexSlice) Stride() int {
	return vs.va.stride / 4
}

// Len returns the length of the VertexSlice.
func (vs *VertexSlice) Len() int {
	return vs.j - vs.i
}

// Cap returns the capacity of an underlying vertex array.
func (vs *VertexSlice) Cap() int {
	return vs.va.cap - vs.i
}

// Slice returns a sub-slice of this VertexSlice covering the range [i, j) (relative to this
// VertexSlice).
//
// Note, that the returned VertexSlice shares an underlying vertex array with the original
// VertexSlice. Modifying the contents of one modifies corresponding contents of the other.
func (vs *VertexSlice) Slice(i, j int) *VertexSlice {
	if i < 0 || j < i || j > vs.va.cap {
		panic("failed to slice vertex slice: index out of range")
	}
	return &VertexSlice{
		va: vs.va,
		i:  vs.i + i,
		j:  vs.i + j,
	}
}

// grow returns supplied vs with length changed to len. Allocates new underlying vertex array if
// necessary. The original content is preserved.
func (vs VertexSlice) grow(len int) VertexSlice {
	if len <= vs.Cap() {
		// capacity sufficient
		return VertexSlice{
			va: vs.va,
			i:  vs.i,
			j:  vs.i + len,
		}
	}

	// grow the capacity
	newCap := vs.Cap()
	if newCap < 1024 {
		newCap += newCap
	} else {
		newCap += newCap / 4
	}
	if newCap < len {
		newCap = len
	}
	newVs := VertexSlice{
		va: newVertexArray(vs.va.shader, newCap),
		i:  0,
		j:  len,
	}
	// preserve the original content
	newVs.Begin()
	newVs.Slice(0, vs.Len()).SetVertexData(vs.VertexData())
	newVs.End()
	return newVs
}

// Append adds supplied vertices to the end of the VertexSlice. If the capacity of the VertexSlice
// is not sufficient, a new, larger underlying vertex array will be allocated. The content of the
// original VertexSlice will be copied to the new underlying vertex array.
//
// The data is in the same format as with SetVertexData.
//
// The VertexSlice is appended 'in-place', contrary Go's builtin slices.
func (vs *VertexSlice) Append(data []float32) {
	vs.End() // vs must have been Begin-ed before calling this method
	*vs = vs.grow(vs.Len() + len(data)/vs.Stride())
	vs.Begin()
	vs.Slice(vs.Len()-len(data)/vs.Stride(), vs.Len()).SetVertexData(data)
}

// SetVertexData sets the contents of the VertexSlice.
//
// The data is a slice of float32's, where each vertex attribute occupies a certain number of
// elements. Namely, Float occupies 1, Vec2 occupies 2, Vec3 occupies 3 and Vec4 occupies 4. The
// attribues in the data slice must be in the same order as in the vertex format of this Vertex
// Slice.
//
// If the length of vertices does not match the length of the VertexSlice, this methdo panics.
func (vs *VertexSlice) SetVertexData(data []float32) {
	if len(data)/vs.Stride() != vs.Len() {
		fmt.Println(len(data)/vs.Stride(), vs.Len())
		panic("set vertex data: wrong length of vertices")
	}
	vs.va.setVertexData(vs.i, vs.j, data)
}

// VertexData returns the contents of the VertexSlice.
//
// The data is in the same format as with SetVertexData.
func (vs *VertexSlice) VertexData() []float32 {
	return vs.va.vertexData(vs.i, vs.j)
}

// Draw draws the content of the VertexSlice.
func (vs *VertexSlice) Draw() {
	vs.va.draw(vs.i, vs.j)
}

// Begin binds the underlying vertex array. Calling this method is necessary before using the VertexSlice.
func (vs *VertexSlice) Begin() {
	vs.va.begin()
}

// End unbinds the underlying vertex array. Call this method when you're done with VertexSlice.
func (vs *VertexSlice) End() {
	vs.va.end()
}

type vertexArray struct {
	vao, vbo binder
	cap      int
	format   AttrFormat
	stride   int
	offset   []int
	shader   *Shader
}

const vertexArrayMinCap = 4

func newVertexArray(shader *Shader, cap int) *vertexArray {
	if cap < vertexArrayMinCap {
		cap = vertexArrayMinCap
	}

	va := &vertexArray{
		vao: binder{
			restoreLoc: gl.VERTEX_ARRAY_BINDING,
			bindFunc: func(obj uint32) {
				gl.BindVertexArray(obj)
			},
		},
		vbo: binder{
			restoreLoc: gl.ARRAY_BUFFER_BINDING,
			bindFunc: func(obj uint32) {
				gl.BindBuffer(gl.ARRAY_BUFFER, obj)
			},
		},
		cap:    cap,
		format: shader.VertexFormat(),
		stride: shader.VertexFormat().Size(),
		offset: make([]int, len(shader.VertexFormat())),
		shader: shader,
	}

	offset := 0
	for i, attr := range va.format {
		switch attr.Type {
		case Float, Vec2, Vec3, Vec4:
		default:
			panic(errors.New("failed to create vertex array: invalid attribute type"))
		}
		va.offset[i] = offset
		offset += attr.Type.Size()
	}

	gl.GenVertexArrays(1, &va.vao.obj)

	va.vao.bind()

	gl.GenBuffers(1, &va.vbo.obj)
	defer va.vbo.bind().restore()

	emptyData := make([]byte, cap*va.stride)
	gl.BufferData(gl.ARRAY_BUFFER, len(emptyData), gl.Ptr(emptyData), gl.DYNAMIC_DRAW)

	for i, attr := range va.format {
		loc := gl.GetAttribLocation(shader.program.obj, gl.Str(attr.Name+"\x00"))

		var size int32
		switch attr.Type {
		case Float:
			size = 1
		case Vec2:
			size = 2
		case Vec3:
			size = 3
		case Vec4:
			size = 4
		}

		gl.VertexAttribPointer(
			uint32(loc),
			size,
			gl.FLOAT,
			false,
			int32(va.stride),
			gl.PtrOffset(va.offset[i]),
		)
		gl.EnableVertexAttribArray(uint32(loc))
	}

	va.vao.restore()

	runtime.SetFinalizer(va, (*vertexArray).delete)

	return va
}

func (va *vertexArray) delete() {
	mainthread.CallNonBlock(func() {
		gl.DeleteVertexArrays(1, &va.vao.obj)
		gl.DeleteBuffers(1, &va.vbo.obj)
	})
}

func (va *vertexArray) begin() {
	va.vao.bind()
	va.vbo.bind()
}

func (va *vertexArray) end() {
	va.vbo.restore()
	va.vao.restore()
}

func (va *vertexArray) draw(i, j int) {
	gl.DrawArrays(gl.TRIANGLES, int32(i), int32(i+j))
}

func (va *vertexArray) setVertexData(i, j int, data []float32) {
	if j-i == 0 {
		// avoid setting 0 bytes of buffer data
		return
	}
	gl.BufferSubData(gl.ARRAY_BUFFER, i*va.stride, len(data)*4, gl.Ptr(data))
}

func (va *vertexArray) vertexData(i, j int) []float32 {
	if j-i == 0 {
		// avoid getting 0 bytes of buffer data
		return nil
	}
	data := make([]float32, (j-i)*va.stride/4)
	gl.GetBufferSubData(gl.ARRAY_BUFFER, i*va.stride, len(data)*4, gl.Ptr(data))
	return data
}