// Package main stl.go - GOOS=js GOARCH=wasm go build -ldflags="-X main.stlFileName=STLFILE.stl" stl.go
package main

import (
	"bytes"
	"encoding/base64"
	"errors"
	"fmt"
	"math"
	"math/rand"
	"reflect"
	"runtime"
	"strings"
	"syscall/js"
	"time"
	"unsafe"

	"github.com/go-gl/mathgl/mgl32"
	"gitlab.com/russoj88/stl/stl"
)

var gl js.Value

var verticesNative = []float32{
	-1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1,
	-1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1,
	-1, -1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1,
	1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1,
	-1, -1, -1, -1, -1, 1, 1, -1, 1, 1, -1, -1,
	-1, 1, -1, -1, 1, 1, 1, 1, 1, 1, 1, -1,
}
var colorsNative = []float32{
	5, 3, 7, 5, 3, 7, 5, 3, 7, 5, 3, 7,
	1, 1, 3, 1, 1, 3, 1, 1, 3, 1, 1, 3,
	0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1,
	1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,
	1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0,
	0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,
}
var indicesNative = []uint32{
	0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7,
	8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15,
	16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23,
}

const vertShaderCode = `
attribute vec3 position;
uniform mat4 Pmatrix;
uniform mat4 Vmatrix;
uniform mat4 Mmatrix;
attribute vec3 color;
varying vec3 vColor;

void main(void) {
	gl_Position = Pmatrix*Vmatrix*Mmatrix*vec4(position, 1.);
	vColor = color;
}
`
const fragShaderCode = `
precision mediump float;
varying vec3 vColor;
void main(void) {
	gl_FragColor = vec4(vColor, 1.);
}
`

var (
	done                 chan struct{}
	stlFileName          string
	originalHTML         string
	render               Renderer
	existingFooter       js.Value
	body                 js.Value
	footer               js.Value
	speedSliderXValue    js.Value
	speedSliderYValue    js.Value
	speedSliderZValue    js.Value
	speedSliderZoomValue js.Value
	canvasElement        js.Value
	currentZoom          float32 = 3
)

func parseBase64File(input string) (output []byte, err error) {
	searchString := "base64,"
	index := strings.Index(input, searchString)
	if index < 0 {
		err = errors.New("Error opening file")
		return
	}
	sBuffer := input[index+len(searchString):]
	return base64.StdEncoding.DecodeString(sBuffer)
}

func uploaded(_ js.Value, args []js.Value) interface{} { // nolint
	fmt.Println("Finished uploading")
	result := args[0].Get("target").Get("result").String()
	func() {
		defer func() {
			if r := recover(); r != nil {
				fmt.Println("Recovered in upload", r)
				js.Global().Call("alert", "Failed to parse file")
			}
		}()
		uploadedFile, err := parseBase64File(result)
		if err != nil {
			panic(err)
		}
		stlSolid, err := NewSTL(uploadedFile)
		if err != nil {
			js.Global().Call("alert", "Could not parse file")
		}
		vert, colors, indices := stlSolid.GetModel()
		modelSize := getMaxScalar(vert)
		currentZoom = modelSize * 3
		render.SetZoom(currentZoom)
		render.SetModel(colors, vert, indices)
	}()
	return nil
}

func getMaxScalar(vertices []float32) float32 {
	var max float32
	for baseIndex := 0; baseIndex < len(vertices); baseIndex += 3 {
		testScale := scalar(vertices[baseIndex], vertices[baseIndex], vertices[baseIndex])
		if testScale > max {
			max = testScale
		}
	}
	return max
}

func scalar(x float32, y float32, z float32) float32 {
	xy := math.Sqrt(float64(x*x + y*y))
	return float32(math.Sqrt(xy*xy + float64(z*z)))
}

func main() {

	time.Sleep(time.Second)
	if strings.Contains(strings.ToLower(js.Global().Get("navigator").Get("userAgent").String()), "mobile") {
		return
	}
	rawHTML := `
<datalist id="speeds">
<option>-1</option>
<option>0</option>
<option>1</option>
</datalist>
<table>
<tr>
<td>
<h2>3D Model</h2>
</td>
<td>
<p>Rotation X
<input id="speedSliderX" type="range" min="-1" max="1" step="0.01" list="speeds">
<text id="speedSliderXValue">0.0</text>
</p>
</td>
<td>
<p>Rotation Y
<input id="speedSliderY" type="range" min="-1" max="1" step="0.01" list="speeds">
<text id="speedSliderYValue">0.0</text>
</p>
</td>
<td>
<p>Rotation Z
<input id="speedSliderZ" type="range" min="-1" max="1" step="0.01" list="speeds">
<text id="speedSliderZValue">0.0</text>
</p>
</td>
<td>
<p>Zoom
<input id="speedSliderZoom" type="range" min="0" max="1000" step="1" list="speeds">
<text id="speedSliderZoomValue">0.0</text>
</p>
</td>
<td>
<p><button type="button" id="stop">Stop Rendering</button>
</p>
</td>
</tr>
</table>
`
	//<canvas id='gocanvas'></canvas>
	doc := js.Global().Get("document")
	body = doc.Get("body")
	//	body.Set("innerHTML", rawHTML)
	existingFooter = doc.Call("getElementsByTagName", "footer").Index(0)
	if existingFooter.Truthy() {
		originalHTML = existingFooter.Get("innerHTML").String()
		footer = doc.Call("createElement", "footer")
		footer.Set("innerHTML", originalHTML+rawHTML)
		body.Call("replaceChild", footer, existingFooter)
	} else {

		footer = doc.Call("createElement", "footer")
		footer.Set("innerHTML", rawHTML)
		body.Call("appendChild", footer)
	}
	doc = js.Global().Get("document")
	//	canvasResizeCallback := js.FuncOf(canvasResize)
	canvasElement = doc.Call("getElementById", "gocanvas")
	//	js.Global().Get("window").Call("addEventListener", "resize", canvasResizeCallback)

	width := doc.Get("body").Get("clientWidth").Int()
	height := doc.Get("body").Get("clientHeight").Int()
	canvasElement.Set("width", width)
	canvasElement.Set("height", height)
	sliderSpeedXCallback := js.FuncOf(sliderChangeX)
	speedSliderX := doc.Call("getElementById", "speedSliderX")
	speedSliderX.Call("addEventListener", "input", sliderSpeedXCallback)
	speedSliderXValue = doc.Call("getElementById", "speedSliderXValue")

	sliderSpeedYCallback := js.FuncOf(sliderChangeY)
	speedSliderY := doc.Call("getElementById", "speedSliderY")
	speedSliderY.Call("addEventListener", "input", sliderSpeedYCallback)
	speedSliderYValue = doc.Call("getElementById", "speedSliderYValue")

	sliderSpeedZCallback := js.FuncOf(sliderChangeZ)
	speedSliderZ := doc.Call("getElementById", "speedSliderZ")
	speedSliderZ.Call("addEventListener", "input", sliderSpeedZCallback)
	speedSliderZValue = doc.Call("getElementById", "speedSliderZValue")

	sliderSpeedZoomCallback := js.FuncOf(sliderChangeZoom)
	speedSliderZoom := doc.Call("getElementById", "speedSliderZoom")
	speedSliderZoom.Call("addEventListener", "input", sliderSpeedZoomCallback)
	speedSliderZoomValue = doc.Call("getElementById", "speedSliderZoomValue")
	//zoomChangeCallback := js.FuncOf(zoomChange)
	//js.Global().Get("window").Call("addEventListener", "wheel", zoomChangeCallback)

	stopButtonCallback := js.FuncOf(stopApplication)
	stopButton := doc.Call("getElementById", "stop")
	stopButton.Call("addEventListener", "click", stopButtonCallback)
	defer stopButtonCallback.Release()

	response := js.Global().Call("fetch", "/stl/base64/"+stlFileName)
	promise := response.Call("then", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
		if p[0].Get("ok").Bool() {
			return p[0].Call("text")
		}
		return "Error fetching stereolithograph"
	}))
	promise.Call("then", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
		result := p[0].String()

		uploadedFile, _ := parseBase64File(result) // nolint
		//		uploadedFile, err := parseBase64File(result)
		//		if err != nil {
		//			return nil
		//			js.Global().Call("alert", "Could not parse fetched file")
		//		}

		stlSolid, _ := NewSTL(uploadedFile) // nolint
		//		stlSolid, err := NewSTL(uploadedFile)
		//		if err != nil {
		//			return nil
		//			js.Global().Call("alert", "Could not parse fetched file")
		//		}

		vert, colors, indices := stlSolid.GetModel()
		modelSize := getMaxScalar(vert)
		currentZoom := modelSize * 3
		render.SetZoom(currentZoom)
		render.SetModel(colors, vert, indices)
		return nil
	}))
	gl = canvasElement.Call("getContext", "webgl")
	if gl.IsUndefined() {
		gl = canvasElement.Call("getContext", "experimental-webgl")
	}
	if gl.IsUndefined() {
		js.Global().Call("alert", "browser might not support webgl")
		return
	}

	config := InitialConfig{
		Width:              width,
		Height:             height,
		SpeedX:             0,
		SpeedY:             0,
		SpeedZ:             0,
		Colors:             colorsNative,
		Vertices:           verticesNative,
		Indices:            indicesNative,
		FragmentShaderCode: fragShaderCode,
		VertexShaderCode:   vertShaderCode,
	}
	var err error
	render, err = NewRenderer(gl, config)
	if err != nil {
		js.Global().Call("alert", fmt.Sprintf("Cannot load webgl %v", err))
		return
	}
	render.SetZoom(currentZoom)
	defer render.Release()

	x, y, z := render.GetSpeed()
	speedSliderX.Set("value", fmt.Sprint(x))
	speedSliderXValue.Set("innerHTML", fmt.Sprint(x))
	speedSliderY.Set("value", fmt.Sprint(y))
	speedSliderYValue.Set("innerHTML", fmt.Sprint(y))
	speedSliderZ.Set("value", fmt.Sprint(z))
	speedSliderZValue.Set("innerHTML", fmt.Sprint(z))

	var renderFrame js.Func
	renderFrame = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		render.Render(this, args)
		js.Global().Call("requestAnimationFrame", renderFrame)
		return nil
	})
	js.Global().Call("requestAnimationFrame", renderFrame)

	done := make(chan struct{})
	<-done
}

func stopApplication(_ js.Value, _ []js.Value) interface{} {

	speedSliderZoomValue.Set("innerHTML", float32(10000))
	currentZoom = float32(10000)
	render.SetZoom(float32(10000))
	footer.Set("innerHTML", originalHTML)
	js.Global().Call("setTimeout", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
		close(done)
		done <- struct{}{}
		return nil
	}), time.Duration(5*time.Second).Milliseconds())
	return nil
}

func canvasResize(_ js.Value, _ []js.Value) interface{} { // nolint
	width := canvasElement.Get("clientWidth").Int()
	height := canvasElement.Get("clientHeight").Int()
	canvasElement.Set("width", width)
	canvasElement.Set("height", height)
	render.SetSize(height*2, width)
	return nil
}

func sliderChangeX(this js.Value, _ []js.Value) interface{} {
	var speed float32
	sSpeed := this.Get("value").String()
	fmt.Sscan(sSpeed, &speed) // nolint
	render.SetSpeedX(speed)
	speedSliderXValue.Set("innerHTML", sSpeed)
	return nil
}

func sliderChangeY(this js.Value, _ []js.Value) interface{} {
	var speed float32
	sSpeed := this.Get("value").String()
	fmt.Sscan(sSpeed, &speed) // nolint
	render.SetSpeedY(speed)
	speedSliderYValue.Set("innerHTML", sSpeed)
	return nil
}

func sliderChangeZ(this js.Value, _ []js.Value) interface{} {
	var speed float32
	sSpeed := this.Get("value").String()
	fmt.Sscan(sSpeed, &speed) // nolint
	render.SetSpeedZ(speed)
	speedSliderZValue.Set("innerHTML", sSpeed)
	return nil
}

func sliderChangeZoom(this js.Value, _ []js.Value) interface{} {
	var speed float32
	sSpeed := this.Get("value").String()
	fmt.Sscan(sSpeed, &speed) // nolint
	speedSliderZoomValue.Set("innerHTML", sSpeed)
	//	deltaScale := 1 - (speed * 0.0001)
	//	currentZoom *= deltaScale
	//    zoomValue := this.Get("value").Float()
	currentZoom = speed
	render.SetZoom(currentZoom)
	//    speedSliderZoomValue.Set("innerHTML", fmt.Sprintf("%.2f", currentZoom))
	return nil
}

func zoomChange(_ js.Value, args []js.Value) interface{} {
	deltaY := args[0].Get("deltaY").Float()
	deltaScale := 1 - (float32(deltaY) * 0.001)
	currentZoom *= deltaScale
	render.SetZoom(currentZoom)
	speedSliderZoomValue.Set("innerHTML", fmt.Sprintf("%.2f", currentZoom))
	return nil
}

// Model is an interface for a model
type Model interface {
	GetModel() ([]float32, []float32, []uint16)
}

// NewSTL returns a new STL & errror
func NewSTL(buffer []byte) (output STL, err error) {
	bufferReader := bytes.NewReader(buffer)
	solid, err := stl.From(bufferReader)
	if err != nil {
		return
	}
	fmt.Printf("Parsed in %d Triangles\n", solid.TriangleCount)
	numColors := (rand.Int() % 5) + 2 // nolint
	colors := GenerateGradient(numColors, int(solid.TriangleCount))
	var index uint32
	for i, triangle := range solid.Triangles {

		colorR := colors[i].Red
		colorG := colors[i].Green
		colorB := colors[i].Blue
		output.vertices = append(output.vertices, triangle.Vertices[0].X)
		output.vertices = append(output.vertices, triangle.Vertices[0].Y)
		output.vertices = append(output.vertices, triangle.Vertices[0].Z)
		output.indices = append(output.indices, index)
		output.colors = append(output.colors, colorR)
		output.colors = append(output.colors, colorG)
		output.colors = append(output.colors, colorB)
		index++
		output.vertices = append(output.vertices, triangle.Vertices[1].X)
		output.vertices = append(output.vertices, triangle.Vertices[1].Y)
		output.vertices = append(output.vertices, triangle.Vertices[1].Z)
		output.indices = append(output.indices, index)
		output.colors = append(output.colors, colorR)
		output.colors = append(output.colors, colorG)
		output.colors = append(output.colors, colorB)
		index++
		output.vertices = append(output.vertices, triangle.Vertices[2].X)
		output.vertices = append(output.vertices, triangle.Vertices[2].Y)
		output.vertices = append(output.vertices, triangle.Vertices[2].Z)
		output.indices = append(output.indices, index)
		output.colors = append(output.colors, colorR)
		output.colors = append(output.colors, colorG)
		output.colors = append(output.colors, colorB)
		index++
	}
	return output, err
}

// STL is a stereolithograph
type STL struct {
	vertices []float32
	colors   []float32
	indices  []uint32
}

// GetModel gets the model
func (s STL) GetModel() ([]float32, []float32, []uint32) {
	return s.vertices, s.colors, s.indices
}

// InitialConfig is the initial config
type InitialConfig struct {
	Width              int
	Height             int
	SpeedX             float32
	SpeedY             float32
	SpeedZ             float32
	Colors             []float32
	Vertices           []float32
	Indices            []uint32
	FragmentShaderCode string
	VertexShaderCode   string
}

// Renderer is the renderer
type Renderer struct {
	glContext      js.Value
	glTypes        GLTypes
	colors         js.Value
	vertices       js.Value
	indices        js.Value
	colorBuffer    js.Value
	vertexBuffer   js.Value
	indexBuffer    js.Value
	numIndices     int
	fragShader     js.Value
	vertShader     js.Value
	shaderProgram  js.Value
	tmark          float32
	rotationX      float32
	rotationY      float32
	rotationZ      float32
	movMatrix      mgl32.Mat4
	PositionMatrix js.Value
	ViewMatrix     js.Value
	ModelMatrix    js.Value
	height         int
	width          int
	speedX         float32
	speedY         float32
	speedZ         float32
}

// NewRenderer returns a new renderer & error
func NewRenderer(gl js.Value, config InitialConfig) (r Renderer, err error) {
	// Get some WebGL bindings
	r.glContext = gl
	err = r.glTypes.New(r.glContext)
	r.numIndices = len(config.Indices)
	r.movMatrix = mgl32.Ident4()
	r.width = config.Width
	r.height = config.Height

	r.speedX = config.SpeedX
	r.speedY = config.SpeedY
	r.speedZ = config.SpeedZ

	// Convert buffers to JS TypedArrays
	r.UpdateColorBuffer(config.Colors)
	r.UpdateVerticesBuffer(config.Vertices)
	r.UpdateIndicesBuffer(config.Indices)

	r.UpdateFragmentShader(config.FragmentShaderCode)
	r.UpdateVertexShader(config.VertexShaderCode)
	r.updateShaderProgram()
	r.attachShaderProgram()

	r.setContextFlags()

	r.createMatrixes()
	r.EnableObject()
	return
}

// SetModel sets a new model
func (r *Renderer) SetModel(Colors []float32, Vertices []float32, Indices []uint32) {
	fmt.Println("Renderer.SetModel")
	r.numIndices = len(Indices)
	fmt.Println("Number of Indices:", len(Indices))
	r.UpdateColorBuffer(Colors)
	fmt.Println("Number of Colors:", len(Colors))
	r.UpdateVerticesBuffer(Vertices)
	fmt.Println("Number of Vertices:", len(Vertices))
	r.UpdateIndicesBuffer(Indices)
	r.EnableObject()
}

// Release releases the renderer
func (r *Renderer) Release() {
	fmt.Println("Renderer.Release")
}

// EnableObject enables the object
func (r *Renderer) EnableObject() {
	fmt.Println("Renderer.EnableObject")
	r.glContext.Call("bindBuffer", r.glTypes.ElementArrayBuffer, r.indexBuffer)
}

// SetSpeedX set rotation x axis speed
func (r *Renderer) SetSpeedX(x float32) {
	r.speedX = x
}

// SetSpeedY set rotation y axis speed
func (r *Renderer) SetSpeedY(y float32) {
	r.speedY = y
}

// SetSpeedZ set rotation z axis speed
func (r *Renderer) SetSpeedZ(z float32) {
	r.speedZ = z
}

// GetSpeed returns the rotation speeds
func (r *Renderer) GetSpeed() (x, y, z float32) {
	return r.speedX, r.speedY, r.speedZ
}

// SetSize sets the size of the rendering
func (r *Renderer) SetSize(height, width int) {
	r.height = height
	r.width = width
	fmt.Println("Size", r.width, r.height)
}

func (r *Renderer) createMatrixes() {
	ratio := float32(r.width) / float32(r.height)
	fmt.Println("Renderer.createMatrixes")
	projMatrix := mgl32.Perspective(mgl32.DegToRad(45.0), ratio, 1, 100000.0)
	projMatrixBuffer := (*[16]float32)(unsafe.Pointer(&projMatrix)) // nolint
	typedProjMatrixBuffer := SliceToTypedArray([]float32((*projMatrixBuffer)[:]))
	r.glContext.Call("uniformMatrix4fv", r.PositionMatrix, false, typedProjMatrixBuffer)

	viewMatrix := mgl32.LookAtV(mgl32.Vec3{3.0, 3.0, 3.0}, mgl32.Vec3{0.0, 0.0, 0.0}, mgl32.Vec3{0.0, 1.0, 0.0})
	viewMatrixBuffer := (*[16]float32)(unsafe.Pointer(&viewMatrix)) // nolint
	typedViewMatrixBuffer := SliceToTypedArray([]float32((*viewMatrixBuffer)[:]))
	r.glContext.Call("uniformMatrix4fv", r.ViewMatrix, false, typedViewMatrixBuffer)
}

func (r *Renderer) setContextFlags() {
	fmt.Println("Renderer.setContextFlags")
	r.glContext.Call("clearColor", 0.0, 0.0, 0.0, 0.0) // Color the screen is cleared to
	//	r.glContext.Call("clearDepth", 1.0)                   // Z value that is set to the Depth buffer every frame
	r.glContext.Call("viewport", 0, 0, r.width, r.height) // Viewport size
	r.glContext.Call("depthFunc", r.glTypes.LEqual)
}

// UpdateFragmentShader Updates the Fragment Shader
func (r *Renderer) UpdateFragmentShader(shaderCode string) {
	fmt.Println("Renderer.UpdateFragmentShader")
	r.fragShader = r.glContext.Call("createShader", r.glTypes.FragmentShader)
	r.glContext.Call("shaderSource", r.fragShader, shaderCode)
	r.glContext.Call("compileShader", r.fragShader)
}

// UpdateVertexShader updates the vertex shader
func (r *Renderer) UpdateVertexShader(shaderCode string) {
	fmt.Println("Renderer.UpdateVertexShader")
	r.vertShader = r.glContext.Call("createShader", r.glTypes.VertexShader)
	r.glContext.Call("shaderSource", r.vertShader, shaderCode)
	r.glContext.Call("compileShader", r.vertShader)
}

func (r *Renderer) updateShaderProgram() {
	fmt.Println("Renderer.updateShaderProgram")
	if r.fragShader.IsUndefined() || r.vertShader.IsUndefined() {
		return
	}
	r.shaderProgram = r.glContext.Call("createProgram")
	r.glContext.Call("attachShader", r.shaderProgram, r.vertShader)
	r.glContext.Call("attachShader", r.shaderProgram, r.fragShader)
	r.glContext.Call("linkProgram", r.shaderProgram)
}

func (r *Renderer) attachShaderProgram() {
	fmt.Println("Renderer.attachShaderProgram")
	r.PositionMatrix = r.glContext.Call("getUniformLocation", r.shaderProgram, "Pmatrix")
	r.ViewMatrix = r.glContext.Call("getUniformLocation", r.shaderProgram, "Vmatrix")
	r.ModelMatrix = r.glContext.Call("getUniformLocation", r.shaderProgram, "Mmatrix")

	r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.vertexBuffer)
	position := r.glContext.Call("getAttribLocation", r.shaderProgram, "position")
	r.glContext.Call("vertexAttribPointer", position, 3, r.glTypes.Float, false, 0, 0)
	r.glContext.Call("enableVertexAttribArray", position)

	r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.colorBuffer)
	color := r.glContext.Call("getAttribLocation", r.shaderProgram, "color")
	r.glContext.Call("vertexAttribPointer", color, 3, r.glTypes.Float, false, 0, 0)
	r.glContext.Call("enableVertexAttribArray", color)

	r.glContext.Call("useProgram", r.shaderProgram)
}

// UpdateColorBuffer Updates the ColorBuffer
func (r *Renderer) UpdateColorBuffer(buffer []float32) {
	fmt.Println("Renderer.UpdateColorBuffer")
	r.colors = SliceToTypedArray(buffer)
	if r.colorBuffer.IsUndefined() {
		r.colorBuffer = r.glContext.Call("createBuffer")
	}
	r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.colorBuffer)
	r.glContext.Call("bufferData", r.glTypes.ArrayBuffer, r.colors, r.glTypes.StaticDraw)
}

// UpdateVerticesBuffer Updates the VerticesBuffer
func (r *Renderer) UpdateVerticesBuffer(buffer []float32) {
	fmt.Println("Renderer.UpdateVerticesBuffer")
	r.vertices = SliceToTypedArray(buffer)
	if r.vertexBuffer.IsUndefined() {
		r.vertexBuffer = r.glContext.Call("createBuffer")
	}
	r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.vertexBuffer)
	r.glContext.Call("bufferData", r.glTypes.ArrayBuffer, r.vertices, r.glTypes.StaticDraw)
}

// UpdateIndicesBuffer Updates the IndicesBuffer
func (r *Renderer) UpdateIndicesBuffer(buffer []uint32) {
	fmt.Println("Renderer.UpdateIndicesBuffer")
	r.indices = SliceToTypedArray(buffer)
	if r.indexBuffer.IsUndefined() {
		r.indexBuffer = r.glContext.Call("createBuffer")
	}
	r.glContext.Call("bindBuffer", r.glTypes.ElementArrayBuffer, r.indexBuffer)
	r.glContext.Call("bufferData", r.glTypes.ElementArrayBuffer, r.indices, r.glTypes.StaticDraw)
}

// Render renders
func (r *Renderer) Render(_ js.Value, args []js.Value) interface{} { // nolint
	now := float32(args[0].Float())
	tdiff := now - r.tmark
	r.tmark = now
	r.rotationX = r.rotationX + r.speedX*float32(tdiff)/500
	r.rotationY = r.rotationY + r.speedY*float32(tdiff)/500
	r.rotationZ = r.rotationZ + r.speedZ*float32(tdiff)/500

	r.movMatrix = mgl32.HomogRotate3DX(r.rotationX)
	r.movMatrix = r.movMatrix.Mul4(mgl32.HomogRotate3DY(r.rotationY))
	r.movMatrix = r.movMatrix.Mul4(mgl32.HomogRotate3DZ(r.rotationZ))

	modelMatrixBuffer := (*[16]float32)(unsafe.Pointer(&r.movMatrix)) // nolint
	typedModelMatrixBuffer := SliceToTypedArray([]float32((*modelMatrixBuffer)[:]))

	r.glContext.Call("uniformMatrix4fv", r.ModelMatrix, false, typedModelMatrixBuffer)

	r.glContext.Call("enable", r.glTypes.DepthTest)
	r.glContext.Call("clear", r.glTypes.ColorBufferBit)
	r.glContext.Call("clear", r.glTypes.DepthBufferBit)

	r.glContext.Call("drawElements", r.glTypes.Triangles, r.numIndices, r.glTypes.UnsignedInt, 0)

	return nil
}

// SetZoom Sets the Zoom
func (r *Renderer) SetZoom(currentZoom float32) {
	fmt.Println("Renderer.SetZoom")
	viewMatrix := mgl32.LookAtV(mgl32.Vec3{currentZoom, currentZoom, currentZoom}, mgl32.Vec3{0.0, 0.0, 0.0}, mgl32.Vec3{0.0, 1.0, 0.0})
	viewMatrixBuffer := (*[16]float32)(unsafe.Pointer(&viewMatrix)) // nolint
	typedViewMatrixBuffer := SliceToTypedArray([]float32((*viewMatrixBuffer)[:]))
	r.glContext.Call("uniformMatrix4fv", r.ViewMatrix, false, typedViewMatrixBuffer)
}

// NewColorInterpolation generates color interpolation
func NewColorInterpolation(a Color, b Color) ColorInterpolation {
	return ColorInterpolation{
		a,
		b,
		a.Subtract(b),
	}
}

// ColorInterpolation is interpolated color
type ColorInterpolation struct {
	startColor Color
	endColor   Color
	deltaColor Color
}

// Interpolate interpolates
func (c ColorInterpolation) Interpolate(percent float32) Color {
	scaled := c.deltaColor.MultiplyFloat(percent)
	return c.startColor.Add(scaled)
}

// Color represents a color
type Color struct {
	Red   float32
	Green float32
	Blue  float32
}

// NewRandomColor returns a New RandomColor
func NewRandomColor() Color {
	return Color{rand.Float32(), rand.Float32(), rand.Float32()} // nolint
}

// Subtract Subtracts color
func (c Color) Subtract(d Color) Color {
	return Color{
		c.Red - d.Red,
		c.Green - d.Green,
		c.Blue - d.Blue,
	}
}

// Add Adds color
func (c Color) Add(d Color) Color {
	return Color{
		c.Red + d.Red,
		c.Green + d.Green,
		c.Blue + d.Blue,
	}
}

// MultiplyFloat Multiplies Float
func (c Color) MultiplyFloat(x float32) Color {
	return Color{
		c.Red * x,
		c.Green * x,
		c.Blue * x,
	}
}

// GenerateGradient Generates Gradient
func GenerateGradient(numColors int, steps int) []Color {
	distribution := distributeColors(numColors, steps)
	colors := make([]Color, numColors)
	for i := 0; i < numColors; i++ {
		colors[i] = NewRandomColor()
	}
	outputBuffer := make([]Color, 0, steps)
	for index := 0; index < numColors; index++ {
		if index >= numColors-1 {
			size := steps - distribution[index]
			interpolation := NewColorInterpolation(colors[index-1], colors[index])
			buffer := generateSingleGradient(interpolation, size)
			outputBuffer = append(outputBuffer, buffer...)
			break
		}
		currentStep := distribution[index]
		nextStep := distribution[index+1]
		size := nextStep - currentStep
		interpolation := NewColorInterpolation(colors[index], colors[index+1])
		buffer := generateSingleGradient(interpolation, size)
		outputBuffer = append(outputBuffer, buffer...)
	}
	return outputBuffer
}

func distributeColors(numColors int, steps int) []int {
	diff := int(math.Ceil(float64(steps) / float64(numColors)))
	output := make([]int, numColors)
	for i := 0; i < numColors; i++ {
		output[i] = diff * i
	}
	return output
}

func generateSingleGradient(c ColorInterpolation, numSteps int) []Color {
	output := make([]Color, numSteps)
	for i := 0; i < numSteps; i++ {
		percent := float32(i) / float32(numSteps)
		output[i] = c.Interpolate(percent)
	}
	return output
}

// GLTypes provides WebGL bindings.
type GLTypes struct {
	StaticDraw         js.Value
	ArrayBuffer        js.Value
	ElementArrayBuffer js.Value
	VertexShader       js.Value
	FragmentShader     js.Value
	Float              js.Value
	DepthTest          js.Value
	ColorBufferBit     js.Value
	DepthBufferBit     js.Value
	Triangles          js.Value
	UnsignedShort      js.Value
	UnsignedInt        js.Value
	LEqual             js.Value
	LineLoop           js.Value
}

// New grabs the WebGL bindings from a GL context.
func (types *GLTypes) New(gl js.Value) error {
	types.StaticDraw = gl.Get("STATIC_DRAW")
	types.ArrayBuffer = gl.Get("ARRAY_BUFFER")
	types.ElementArrayBuffer = gl.Get("ELEMENT_ARRAY_BUFFER")
	types.VertexShader = gl.Get("VERTEX_SHADER")
	types.FragmentShader = gl.Get("FRAGMENT_SHADER")
	types.Float = gl.Get("FLOAT")
	types.DepthTest = gl.Get("DEPTH_TEST")
	types.ColorBufferBit = gl.Get("COLOR_BUFFER_BIT")
	types.Triangles = gl.Get("TRIANGLES")
	types.UnsignedShort = gl.Get("UNSIGNED_SHORT")
	types.LEqual = gl.Get("LEQUAL")
	types.DepthBufferBit = gl.Get("DEPTH_BUFFER_BIT")
	types.LineLoop = gl.Get("LINE_LOOP")
	enabled := gl.Call("getExtension", "OES_element_index_uint")
	if !enabled.Truthy() {
		return errors.New("missing extension: OES_element_index_uint")
	}
	types.UnsignedInt = gl.Get("UNSIGNED_INT")
	return nil
}

func sliceToByteSlice(s interface{}) []byte {
	switch s := s.(type) {
	case []int8:
		h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
		return *(*[]byte)(unsafe.Pointer(h))            // nolint
	case []int16:
		h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
		h.Len *= 2
		h.Cap *= 2
		return *(*[]byte)(unsafe.Pointer(h)) // nolint
	case []int32:
		h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
		h.Len *= 4
		h.Cap *= 4
		return *(*[]byte)(unsafe.Pointer(h)) // nolint
	case []int64:
		h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
		h.Len *= 8
		h.Cap *= 8
		return *(*[]byte)(unsafe.Pointer(h)) // nolint
	case []uint8:
		return s
	case []uint16:
		h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
		h.Len *= 2
		h.Cap *= 2
		return *(*[]byte)(unsafe.Pointer(h)) // nolint
	case []uint32:
		h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
		h.Len *= 4
		h.Cap *= 4
		return *(*[]byte)(unsafe.Pointer(h)) // nolint
	case []uint64:
		h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
		h.Len *= 8
		h.Cap *= 8
		return *(*[]byte)(unsafe.Pointer(h)) // nolint
	case []float32:
		h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
		h.Len *= 4
		h.Cap *= 4
		return *(*[]byte)(unsafe.Pointer(h)) // nolint
	case []float64:
		h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) // nolint
		h.Len *= 8
		h.Cap *= 8
		return *(*[]byte)(unsafe.Pointer(h)) // nolint
	default:
		panic(fmt.Sprintf("jsutil: unexpected value at sliceToBytesSlice: %T", s))
	}
}

// SliceToTypedArray converts Slice To TypedArray
func SliceToTypedArray(s interface{}) js.Value {
	switch s := s.(type) {
	case []int8:
		a := js.Global().Get("Uint8Array").New(len(s))
		js.CopyBytesToJS(a, sliceToByteSlice(s))
		runtime.KeepAlive(s)
		buf := a.Get("buffer")
		return js.Global().Get("Int8Array").New(buf, a.Get("byteOffset"), a.Get("byteLength"))
	case []int16:
		a := js.Global().Get("Uint8Array").New(len(s) * 2)
		js.CopyBytesToJS(a, sliceToByteSlice(s))
		runtime.KeepAlive(s)
		buf := a.Get("buffer")
		return js.Global().Get("Int16Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/2)
	case []int32:
		a := js.Global().Get("Uint8Array").New(len(s) * 4)
		js.CopyBytesToJS(a, sliceToByteSlice(s))
		runtime.KeepAlive(s)
		buf := a.Get("buffer")
		return js.Global().Get("Int32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4)
	case []uint8:
		a := js.Global().Get("Uint8Array").New(len(s))
		js.CopyBytesToJS(a, s)
		runtime.KeepAlive(s)
		return a
	case []uint16:
		a := js.Global().Get("Uint8Array").New(len(s) * 2)
		js.CopyBytesToJS(a, sliceToByteSlice(s))
		runtime.KeepAlive(s)
		buf := a.Get("buffer")
		return js.Global().Get("Uint16Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/2)
	case []uint32:
		a := js.Global().Get("Uint8Array").New(len(s) * 4)
		js.CopyBytesToJS(a, sliceToByteSlice(s))
		runtime.KeepAlive(s)
		buf := a.Get("buffer")
		return js.Global().Get("Uint32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4)
	case []float32:
		a := js.Global().Get("Uint8Array").New(len(s) * 4)
		js.CopyBytesToJS(a, sliceToByteSlice(s))
		runtime.KeepAlive(s)
		buf := a.Get("buffer")
		return js.Global().Get("Float32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4)
	case []float64:
		a := js.Global().Get("Uint8Array").New(len(s) * 8)
		js.CopyBytesToJS(a, sliceToByteSlice(s))
		runtime.KeepAlive(s)
		buf := a.Get("buffer")
		return js.Global().Get("Float64Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/8)
	default:
		panic(fmt.Sprintf("jsutil: unexpected value at SliceToTypedArray: %T", s))
	}
}