package render

import (
	"image/color"
	"unsafe"

	"edgaru089.ink/go/gl01/internal/asset"
	"edgaru089.ink/go/gl01/internal/util/itype"
	"github.com/go-gl/gl/all-core/gl"
	"github.com/go-gl/mathgl/mgl32"
)

// framewire vertex
type frvertex struct {
	pos   itype.Vec3f
	color itype.Vec4f
}

// FramewireRenderer is a renderer drawing framewires.
//
// It is mainly for debugging uses.
type FramewireRenderer struct {
	shader *Shader

	vao, vbo uint32
	vertex   []frvertex
}

// Framewire is a global FramewireRenderer managed by game logic, for debugging.
var Framewire = &FramewireRenderer{}

func (f *FramewireRenderer) Init() (err error) {

	f.shader, err = NewShader(asset.FramewireShaderVert, asset.FramewireShaderFrag)
	if err != nil {
		return err
	}

	f.shader.SetUniformMat4("model", mgl32.Ident4())
	gl.BindFragDataLocation(f.shader.Handle(), 0, gl.Str("outputColor\x00"))

	gl.GenVertexArrays(1, &f.vao)
	gl.BindVertexArray(f.vao)
	gl.GenBuffers(1, &f.vbo)
	gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo)

	vertAttrib := uint32(gl.GetAttribLocation(f.shader.Handle(), gl.Str("vert\x00")))
	gl.VertexAttribPointer(vertAttrib, 3, gl.FLOAT, false, int32(unsafe.Sizeof(frvertex{})), gl.PtrOffset(int(unsafe.Offsetof(frvertex{}.pos))))

	colorAttrib := uint32(gl.GetAttribLocation(f.shader.Handle(), gl.Str("vertColor\x00")))
	gl.VertexAttribPointer(colorAttrib, 4, gl.FLOAT, false, int32(unsafe.Sizeof(frvertex{})), gl.PtrOffset(int(unsafe.Offsetof(frvertex{}.color))))

	gl.EnableVertexAttribArray(vertAttrib)
	gl.EnableVertexAttribArray(colorAttrib)

	return nil
}

func colorToVec4f(color color.Color) itype.Vec4f {
	r, g, b, a := color.RGBA()
	return itype.Vec4f{float32(r) / 0xffff, float32(g) / 0xffff, float32(b) / 0xffff, float32(a) / 0xffff}
}

// PushLine pushes a line from p0 to p1 into the vertex array.
func (f *FramewireRenderer) PushLine(p0, p1 itype.Vec3f, color0, color1 color.Color) {
	f.vertex = append(f.vertex,
		frvertex{
			pos:   p0,
			color: colorToVec4f(color0),
		},
		frvertex{
			pos:   p1,
			color: colorToVec4f(color1),
		},
	)
}

const FramewireSizeShrink = 1e-4

// PushBox pushes a 3D box into the vertex array.
//
// The size of the box is shrunk by a little bit so that it is not obstructed by the world.
func (f *FramewireRenderer) PushBox(box itype.Boxf, color color.Color) {
	box = itype.Boxf{
		OffX:  box.OffX + FramewireSizeShrink,
		OffY:  box.OffY + 5*FramewireSizeShrink,
		OffZ:  box.OffZ + FramewireSizeShrink,
		SizeX: box.SizeX - 2*FramewireSizeShrink,
		SizeY: box.SizeY - 10*FramewireSizeShrink,
		SizeZ: box.SizeZ - 2*FramewireSizeShrink,
	}
	base := box.MinPoint()

	f.PushLine(base, base.Addv(box.SizeX, 0, 0), color, color)
	f.PushLine(base, base.Addv(0, 0, box.SizeZ), color, color)
	f.PushLine(base.Addv(box.SizeX, 0, box.SizeZ), base.Addv(box.SizeX, 0, 0), color, color)
	f.PushLine(base.Addv(box.SizeX, 0, box.SizeZ), base.Addv(0, 0, box.SizeZ), color, color)

	f.PushLine(base, base.Addv(0, box.SizeY, 0), color, color)
	f.PushLine(base.Addv(box.SizeX, 0, 0), base.Addv(box.SizeX, box.SizeY, 0), color, color)
	f.PushLine(base.Addv(0, 0, box.SizeZ), base.Addv(0, box.SizeY, box.SizeZ), color, color)
	f.PushLine(base.Addv(box.SizeX, 0, box.SizeZ), base.Addv(box.SizeX, box.SizeY, box.SizeZ), color, color)

	f.PushLine(base.Addv(0, box.SizeY, 0), base.Addv(box.SizeX, box.SizeY, 0), color, color)
	f.PushLine(base.Addv(0, box.SizeY, 0), base.Addv(0, box.SizeY, box.SizeZ), color, color)
	f.PushLine(base.Addv(box.SizeX, box.SizeY, box.SizeZ), base.Addv(box.SizeX, box.SizeY, 0), color, color)
	f.PushLine(base.Addv(box.SizeX, box.SizeY, box.SizeZ), base.Addv(0, box.SizeY, box.SizeZ), color, color)
}

// Render renders the framewire into the current OpenGL context and clears it.
func (f *FramewireRenderer) Render(view *View) {
	if len(f.vertex) == 0 {
		return
	}

	f.shader.UseProgram()
	f.shader.SetUniformMat4("view", view.View())
	f.shader.SetUniformMat4("projection", view.Perspective())

	gl.BindVertexArray(f.vao)
	gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo)
	gl.BufferData(gl.ARRAY_BUFFER, len(f.vertex)*int(unsafe.Sizeof(frvertex{})), gl.Ptr(f.vertex), gl.STREAM_DRAW)

	gl.DrawArrays(gl.LINES, 0, int32(len(f.vertex)))

	f.vertex = f.vertex[0:0]
}
