// Package main b.go - GOOS=js GOARCH=wasm go build b.go
package main

import (
	"fmt"
	"math"
	"math/rand"
	"reflect"
	"runtime"
	"strings"
	"sync"
	"syscall/js"
	"time"
	"unsafe"

	"github.com/go-gl/mathgl/mgl32"
)

var glTypes GLTypes
var mobileversion string
var linuxversion string
var wg sync.WaitGroup

func generateSphereVertices(radius float32, stacks, slices int) ([]float32, []uint16) {
	var vertices []float32
	var indices []uint16
	for i := 0; i <= stacks; i++ {
		phi := float32(i) * float32(math.Pi) / float32(stacks)
		for j := 0; j <= slices; j++ {
			theta := float32(j) * 2.0 * float32(math.Pi) / float32(slices)
			x := radius * float32(math.Sin(float64(phi))) * float32(math.Cos(float64(theta)))
			y := radius * float32(math.Sin(float64(phi))) * float32(math.Sin(float64(theta)))
			z := radius * float32(math.Cos(float64(phi)))
			vertices = append(vertices, x, y, z)
		}
	}
	for i := 0; i < stacks; i++ {
		for j := 0; j <= slices; j++ {
			indices = append(indices, uint16(i*(slices+1)+j), uint16((i+1)*(slices+1)+j))
		}
	}

	return vertices, indices
}

/*
	func randomRotationMatrix() mgl32.Mat4 {
		rand.Seed(int64(rand.Uint32())) // Initialize the random number generator
		return mgl32.HomogRotate3D(
			mgl32.DegToRad(rand.Float32() * 360),
			mgl32.Vec3{rand.Float32(), rand.Float32(), rand.Float32()}.Normalize(),
		)
	}
*/
func randomRotationMatrix() mgl32.Mat4 {
	return mgl32.HomogRotate3D(mgl32.DegToRad(rand.Float32()*360.0), mgl32.Vec3{rand.Float32(), rand.Float32(), rand.Float32()}.Normalize()) //nolint
}

func prependChild(newElement, parent js.Value) {
	firstChild := parent.Get("firstChild")
	if firstChild.IsNull() {
		parent.Call("appendChild", newElement)
	} else {
		parent.Call("insertBefore", newElement, firstChild)
	}
}
func stopApplication(_ js.Value, _ []js.Value) interface{} {
	doc := js.Global().Get("document")
	bh := doc.Call("getElementById", "middletd")
	if bh.Truthy() {
		bh.Set("innerHTML", "&nbsp;&nbsp;")
	}
/*
	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 main() {
	time.Sleep(time.Second)
	time.Sleep(time.Second)
	time.Sleep(time.Second)
	time.Sleep(time.Second)
	time.Sleep(time.Second)
	doc := js.Global().Get("document")
/*
	bh := doc.Call("getElementById", "middletd")
	if bh.Truthy() {
		bh.Set("innerHTML", "<button type='button' id='stop'>Stop Rendering</button>")
	}
	stopButtonCallback := js.FuncOf(stopApplication)
	stopButton := doc.Call("getElementById", "stop")
	stopButton.Call("addEventListener", "click", stopButtonCallback)
	defer stopButtonCallback.Release()
*/

	canvasEl := doc.Call("getElementById", "gocanvas")
	width := doc.Get("body").Get("clientWidth").Int()
	height := doc.Get("body").Get("clientHeight").Int()
	canvasEl.Set("width", width)
	canvasEl.Set("height", height)
	ismobile := strings.Contains(strings.ToLower(js.Global().Get("navigator").Get("userAgent").String()), "mobile")
	islinux := strings.Contains(strings.ToLower(js.Global().Get("navigator").Get("userAgent").String()), "linux")
	if !ismobile && islinux && !strings.Contains(strings.ToLower(js.Global().Get("navigator").Get("userAgent").String()), "firefox") {

		wg.Add(1)

		logoimg := js.Global().Get("document").Call("getElementById", "logo")
		if !logoimg.IsNull() {
			logoimg.Call("remove")
		}
		var htmllogo string
		htmllogo = "logolarge.html"
		if ismobile {
			htmllogo = "mobilelogo.html"
		}
		response := js.Global().Call("fetch", htmllogo)
		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 ASCII art"
		}))

		promise.Call("then", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
			asciiArt := p[0].String()

			doc := js.Global().Get("document")
			//		body := doc.Get("body")
			htmlanimationdiv := doc.Call("getElementById", "htmlanimation")

			//if ismobile {
			//	htmlanimationdiv.Set("innerHTML", "<pre style='margin: 0; padding: 0; font-size: 1px;'>"+asciiArt+"</pre>")
			//	return nil
			//}
			var lines []string
			if !ismobile && islinux {
				lines = strings.Split(asciiArt, "\n")
			}
			div1 := doc.Call("createElement", "div")
			div1.Get("style").Set("margin", "0")
			div1.Get("style").Set("padding", "0")
			div2 := doc.Call("createElement", "div")
			div2.Get("style").Set("margin", "0")
			div2.Get("style").Set("padding", "0")
			htmlanimationdiv.Call("appendChild", div1)
			htmlanimationdiv.Call("appendChild", div2)

			if !ismobile && islinux {
				index := 0
				index1 := len(lines) - 1
				//		index := (len(lines) - 1)/2 // Start from the last line
				//		index1 := index - 1
				// Define a recursive function to append lines with delay
				var appendLineWithDelay func()
				appendLineWithDelay = func() {
					if index1 >= 0 && index < len(lines) && index < index1 {
						//				if index1 >= 0 && index < len(lines) {

						pre1 := doc.Call("createElement", "pre")
						//if ismobile {
						//	pre1.Get("style").Set("font-size", "3px")
						//} else {
						pre1.Get("style").Set("font-size", "1px")
						//}
						pre1.Get("style").Set("text-align", "center")
						pre1.Get("style").Set("margin", "0")
						pre1.Get("style").Set("padding", "0")
						//				pre1.Set("innerHTML", lines[index1]+"\n")
						pre1.Set("innerHTML", lines[index]+"\n")

						pre2 := doc.Call("createElement", "pre")
						//if ismobile {
						//	pre2.Get("style").Set("font-size", "3px")
						//} else {
						pre2.Get("style").Set("font-size", "1px")
						//}
						pre2.Get("style").Set("text-align", "center")
						pre2.Get("style").Set("margin", "0")
						pre2.Get("style").Set("padding", "0")
						//				pre2.Set("innerHTML", lines[index]+"\n")
						pre2.Set("innerHTML", lines[index1]+"\n")

						//				div2.Call("appendChild", pre2)
						//				prependChild(pre1, div1)
						div1.Call("appendChild", pre1)
						prependChild(pre2, div2)

						index++
						index1--
						js.Global().Call("setTimeout", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
							appendLineWithDelay()
							return nil
						}), 0)
					} else {
						wg.Done()
					}
				}
				appendLineWithDelay()
			} else {
				htmlanimationdiv.Set("innerHTML", "<pre style='margin: 0; padding: 0; font-size: 1px;'>"+asciiArt+"</pre>")
			}

			return nil
		}))

		wg.Wait()
		htmlanimationdiv := doc.Call("getElementById", "htmlanimation")
		htmlanimationdiv.Get("style").Set("margin", "0")
		htmlanimationdiv.Get("style").Set("position", "absolute")
		htmlanimationdiv.Get("style").Set("top", "50%")
		htmlanimationdiv.Get("style").Set("-ms-transform", "translateY(-50%)")
		htmlanimationdiv.Get("style").Set("transform", "translate(50%, -50%)")
		htmlanimationdiv.Get("style").Set("z-index", "-1")
	}

	time.Sleep(time.Second)

	gl := canvasEl.Call("getContext", "webgl")
	if gl.IsUndefined() {
		gl = canvasEl.Call("getContext", "experimental-webgl")
	}
	if gl.IsUndefined() {
		js.Global().Call("alert", "browser might not support webgl")
		return
	}

	glTypes.New(gl)

	fragShaderCode := `
	precision mediump float;
	uniform vec3 uBaseColor; // Color value at the base
	uniform vec3 uTopColor;  // Color value at the top
	varying vec3 vPosition;  // Interpolated vertex position
	void main(void) {
		float t = (vPosition.y + 1.0) * 0.5; // Normalize the y-coordinate to [0, 1]
		vec3 rainbowColor = mix(uBaseColor, uTopColor, t);
		gl_FragColor = vec4(rainbowColor, 1.0);
	}
	`

	vertShaderCode := `
	attribute vec3 position;
	uniform mat4 Pmatrix;
	uniform mat4 Vmatrix;
	uniform mat4 Mmatrix;
	varying vec3 vPosition;  // Pass vertex position to fragment shader
	void main(void) {
		gl_Position = Pmatrix * Vmatrix * Mmatrix * vec4(position, 1.0);
		vPosition = position;  // Pass vertex position to fragment shader
	}
	`

	radius := float32(1.0)
	stacks := 30
	slices := 30
	vertices, _ := generateSphereVertices(radius, stacks, slices)

	var verticesTyped = SliceToTypedArray(vertices)

	vertexBuffer := gl.Call("createBuffer")
	gl.Call("bindBuffer", glTypes.ArrayBuffer, vertexBuffer)
	gl.Call("bufferData", glTypes.ArrayBuffer, verticesTyped, glTypes.StaticDraw)

	vertShader := gl.Call("createShader", glTypes.VertexShader)
	gl.Call("shaderSource", vertShader, vertShaderCode)
	gl.Call("compileShader", vertShader)

	fragShader := gl.Call("createShader", glTypes.FragmentShader)
	gl.Call("shaderSource", fragShader, fragShaderCode)
	gl.Call("compileShader", fragShader)

	shaderProgram := gl.Call("createProgram")
	gl.Call("attachShader", shaderProgram, vertShader)
	gl.Call("attachShader", shaderProgram, fragShader)
	gl.Call("linkProgram", shaderProgram)

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

	gl.Call("useProgram", shaderProgram)

	uBaseColor := gl.Call("getUniformLocation", shaderProgram, "uBaseColor")
	uTopColor := gl.Call("getUniformLocation", shaderProgram, "uTopColor")
	uColor := gl.Call("getUniformLocation", shaderProgram, "uColor")
	gl.Call("uniform3f", uBaseColor, 1.0, 0.0, 0.0)
	gl.Call("uniform3f", uTopColor, 0.0, 0.0, 1.0)
	gl.Call("uniform3f", uColor, 1.0, 1.0, 1.0)

	gl.Call("clearColor", 0, 0, 0, 0)
	gl.Call("clearDepth", 1.0)
	gl.Call("viewport", 0, 0, width, height)
	gl.Call("depthFunc", glTypes.LEqual)

	projMatrix := mgl32.Perspective(mgl32.DegToRad(45.0), float32(width)/float32(height), 1, 100.0)
	projMatrixBuffer := (*[16]float32)(unsafe.Pointer(&projMatrix)) // nolint
	typedProjMatrixBuffer := SliceToTypedArray([]float32((*projMatrixBuffer)[:]))
	gl.Call("uniformMatrix4fv", gl.Call("getUniformLocation", shaderProgram, "Pmatrix"), 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)[:]))
	gl.Call("uniformMatrix4fv", gl.Call("getUniformLocation", shaderProgram, "Vmatrix"), false, typedViewMatrixBuffer)

	movMatrix := randomRotationMatrix()
	var renderFrame js.Func
	var tmark float32
	var rotation = float32(0)

	renderFrame = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		if tmark == 0 {
			tmark = float32(args[0].Float())
			rotation = rand.Float32() * 360.0 // nolint
		}
		now := float32(args[0].Float())
		tdiff := now - tmark
		tmark = now
		rotation = rotation + float32(tdiff)/5000

		movMatrix = mgl32.HomogRotate3DX(0.5 * rotation)
		movMatrix = movMatrix.Mul4(mgl32.HomogRotate3DY(0.3 * rotation))
		movMatrix = movMatrix.Mul4(mgl32.HomogRotate3DZ(0.2 * rotation))

		modelMatrixBuffer := (*[16]float32)(unsafe.Pointer(&movMatrix)) // nolint
		typedModelMatrixBuffer := SliceToTypedArray([]float32((*modelMatrixBuffer)[:]))
		gl.Call("uniformMatrix4fv", gl.Call("getUniformLocation", shaderProgram, "Mmatrix"), false, typedModelMatrixBuffer)

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

		indices := []uint16{}
		for i := 0; i < stacks; i++ {
			for j := 0; j <= slices; j++ {
				indices = append(indices, uint16(i*(slices+1)+j), uint16((i+1)*(slices+1)+j))
			}
		}

		indicesTyped := SliceToTypedArray(indices)

		indexBuffer := gl.Call("createBuffer")
		gl.Call("bindBuffer", glTypes.ElementArrayBuffer, indexBuffer)
		gl.Call("bufferData", glTypes.ElementArrayBuffer, indicesTyped, glTypes.StaticDraw)
		gl.Call("drawElements", glTypes.Line, len(indices), glTypes.UnsignedShort, 0)
		gl.Call("drawArrays", glTypes.LineLoop, 0, len(vertices)/3)

		js.Global().Call("requestAnimationFrame", renderFrame)

		return nil
	})

	defer renderFrame.Release()

	//	<div id='anchor'></div>"
	//	// Calculate the position of the anchor element
	//	anchorEl := doc.Call("getElementById", "anchor")
	//	anchorRect := anchorEl.Call("getBoundingClientRect")
	//	anchorX := anchorRect.Get("left").Float() + anchorRect.Get("width").Float()/2
	//	anchorY := anchorRect.Get("top").Float() + anchorRect.Get("height").Float()/2

	//	// Adjust the position of the animation container based on the anchor position
	//	gocanvasContainer := doc.Call("getElementById", "gocanvas-container")
	//	gocanvasContainer.Get("style").Set("left", fmt.Sprintf("%fpx", anchorX))
	//	gocanvasContainer.Get("style").Set("top", fmt.Sprintf("%fpx", anchorY))

	js.Global().Call("requestAnimationFrame", renderFrame)

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

// 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
	LEqual             js.Value
	LineLoop           js.Value
	Line               js.Value
}

// New returns new webgl bindings
func (types *GLTypes) New(gl js.Value) {
	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")
	types.Line = gl.Get("LINES")
}

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 convert Slice To Typed Array
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))
	}
}