// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package geometry allows the generation of geometries...
package geometry

import (
	"github.com/g3n/engine/gls"
	"github.com/g3n/engine/math32"
)

// Interface for all geometries
type IGeometry interface {
	GetGeometry() *Geometry
	RenderSetup(gs *gls.GLS)
	Dispose()
}

type Geometry struct {
	refcount            int             // Current number of references
	vbos                []*gls.VBO      // Array of VBOs
	groups              []Group         // Array geometry groups
	indices             math32.ArrayU32 // Buffer with indices
	gs                  *gls.GLS        // Pointer to gl context. Valid after first render setup
	handleVAO           uint32          // Handle to OpenGL VAO
	handleIndices       uint32          // Handle to OpenGL buffer for indices
	updateIndices       bool            // Flag to indicate that indices must be transferred
	boundingBox         math32.Box3     // Last calculated bounding box
	boundingBoxValid    bool            // Indicates if last calculated bounding box is valid
	boundingSphere      math32.Sphere   // Last calculated bounding sphere
	boundingSphereValid bool            // Indicates if last calculated bounding sphere is valid
}

// Geometry group object
type Group struct {
	Start    int    // Index of first element of the group
	Count    int    // Number of elements in the group
	Matindex int    // Material index for this group
	Matid    string // Material id used when loading external models
}

func NewGeometry() *Geometry {

	g := new(Geometry)
	g.Init()
	return g
}

// Init initializes the geometry
func (g *Geometry) Init() {

	g.refcount = 1
	g.vbos = make([]*gls.VBO, 0)
	g.groups = make([]Group, 0)
	g.gs = nil
	g.handleVAO = 0
	g.handleIndices = 0
	g.updateIndices = true
}

// Incref increments the reference count for this geometry
// and returns a pointer to the geometry.
// It should be used when this geometry is shared by another
// Graphic object.
func (g *Geometry) Incref() *Geometry {

	g.refcount++
	return g
}

// Dispose decrements this geometry reference count and
// if necessary releases OpenGL resources, C memory
// and VBOs associated with this geometry.
func (g *Geometry) Dispose() {

	if g.refcount > 1 {
		g.refcount--
		return
	}

	if g.gs != nil {
		g.gs.DeleteVertexArrays(g.handleVAO)
		g.gs.DeleteBuffers(g.handleIndices)
	}
	g.Init()
}

func (g *Geometry) GetGeometry() *Geometry {

	return g
}

// AddGroup adds a geometry group (for multimaterial)
func (g *Geometry) AddGroup(start, count, matIndex int) *Group {

	g.groups = append(g.groups, Group{start, count, matIndex, ""})
	return &g.groups[len(g.groups)-1]
}

// AddGroupList adds the specified list of groups to this geometry
func (g *Geometry) AddGroupList(groups []Group) {

	for _, group := range groups {
		g.groups = append(g.groups, group)
	}
}

// GroupCount returns the number of geometry groups (for multimaterial)
func (g *Geometry) GroupCount() int {

	return len(g.groups)
}

// GroupAt returns pointer to geometry group at the specified index
func (g *Geometry) GroupAt(idx int) *Group {

	return &g.groups[idx]
}

// SetIndices sets the indices array for this geometry
func (g *Geometry) SetIndices(indices math32.ArrayU32) {

	g.indices = indices
	g.boundingBoxValid = false
	g.boundingSphereValid = false
}

// Indices returns this geometry indices array
func (g *Geometry) Indices() math32.ArrayU32 {

	return g.indices
}

// AddVBO adds a Vertex Buffer Object for this geometry
func (g *Geometry) AddVBO(vbo *gls.VBO) {

	g.vbos = append(g.vbos, vbo)
}

// VBO returns a pointer to this geometry VBO for the specified attribute.
// Returns nil if the VBO is not found.
func (g *Geometry) VBO(attrib string) *gls.VBO {

	for _, vbo := range g.vbos {
		if vbo.Attrib(attrib) != nil {
			return vbo
		}
	}
	return nil
}

// Returns the number of items in the first VBO
// (The number of items should be same for all VBOs)
// An item is a complete vertex position (3 floats) for example
func (g *Geometry) Items() int {

	if len(g.vbos) == 0 {
		return 0
	}
	vbo := g.vbos[0]
	if vbo.AttribCount() == 0 {
		return 0
	}
	attrib := vbo.AttribAt(0)
	return vbo.Buffer().Size() / int(attrib.ItemSize)
}

// BoundingBox computes the bounding box of the geometry if necessary
// and returns is value
func (g *Geometry) BoundingBox() math32.Box3 {

	// If valid, returns its value
	if g.boundingBoxValid {
		return g.boundingBox
	}

	// Get buffer with position vertices
	vbPos := g.VBO("VertexPosition")
	if vbPos == nil {
		return g.boundingBox
	}
	positions := vbPos.Buffer()

	// Calculates bounding box
	var vertex math32.Vector3
	g.boundingBox.Min.Set(0, 0, 0)
	g.boundingBox.Max.Set(0, 0, 0)
	for i := 0; i < positions.Size(); i += 3 {
		positions.GetVector3(i, &vertex)
		g.boundingBox.ExpandByPoint(&vertex)
	}
	g.boundingBoxValid = true
	return g.boundingBox
}

// BoundingSphere computes the bounding sphere of this geometry
// if necessary and returns its value.
func (g *Geometry) BoundingSphere() math32.Sphere {

	// if valid, returns its value
	if g.boundingSphereValid {
		return g.boundingSphere
	}

	// Get buffer with position vertices
	vbPos := g.VBO("VertexPosition")
	if vbPos == nil {
		return g.boundingSphere
	}
	positions := vbPos.Buffer()

	// Get/calculates the bounding box
	box := g.BoundingBox()

	// Sets the center of the bounding sphere the same as the center of the bounding box.
	box.Center(&g.boundingSphere.Center)
	center := g.boundingSphere.Center

	// Find the radius of the bounding sphere
	maxRadiusSq := float32(0.0)
	for i := 0; i < positions.Size(); i += 3 {
		var vertex math32.Vector3
		positions.GetVector3(i, &vertex)
		maxRadiusSq = math32.Max(maxRadiusSq, center.DistanceToSquared(&vertex))
	}
	radius := math32.Sqrt(maxRadiusSq)
	if math32.IsNaN(radius) {
		panic("geometry.BoundingSphere: computed radius is NaN")
	}
	g.boundingSphere.Radius = float32(radius)
	g.boundingSphereValid = true
	return g.boundingSphere
}

// ApplyMatrix multiplies each of the geometry position vertices
// by the specified matrix and apply the correspondent normal
// transform matrix to the geometry normal vectors.
// The geometry's bounding box and sphere are recomputed if needed.
func (g *Geometry) ApplyMatrix(m *math32.Matrix4) {

	// Get positions buffer
	vboPos := g.VBO("VertexPosition")
	if vboPos == nil {
		return
	}
	positions := vboPos.Buffer()
	// Apply matrix to all position vertices
	for i := 0; i < positions.Size(); i += 3 {
		var vertex math32.Vector3
		positions.GetVector3(i, &vertex)
		vertex.ApplyMatrix4(m)
		positions.SetVector3(i, &vertex)
	}
	vboPos.Update()

	// Get normals buffer
	vboNormals := g.VBO("VertexNormal")
	if vboNormals == nil {
		return
	}
	normals := vboNormals.Buffer()
	// Apply normal matrix to all normal vectors
	var normalMatrix math32.Matrix3
	normalMatrix.GetNormalMatrix(m)
	for i := 0; i < normals.Size(); i += 3 {
		var vertex math32.Vector3
		normals.GetVector3(i, &vertex)
		vertex.ApplyMatrix3(&normalMatrix).Normalize()
		normals.SetVector3(i, &vertex)
	}
	vboNormals.Update()
}

// RenderSetup is called by the renderer before drawing the geometry
func (g *Geometry) RenderSetup(gs *gls.GLS) {

	// First time initialization
	if g.gs == nil {
		// Generates VAO and binds it
		g.handleVAO = gs.GenVertexArray()
		gs.BindVertexArray(g.handleVAO)
		// Generates VBO for indices
		g.handleIndices = gs.GenBuffer()
		// Saves pointer to gl indicating initialization was done.
		g.gs = gs
	}

	// Update VBOs
	gs.BindVertexArray(g.handleVAO)
	for _, vbo := range g.vbos {
		vbo.Transfer(gs)
	}

	// Updates Indices buffer if necessary
	if g.indices.Size() > 0 && g.updateIndices {
		gs.BindBuffer(gls.ELEMENT_ARRAY_BUFFER, g.handleIndices)
		gs.BufferData(gls.ELEMENT_ARRAY_BUFFER, g.indices.Bytes(), g.indices, gls.STATIC_DRAW)
		g.updateIndices = false
	}
}
