package render

import (
	"fmt"
	"log"
	"strings"

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

// returns max texture unit count
func getMaxTextureUnits() int32 {
	var cnt int32
	gl.GetIntegerv(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS, &cnt)
	return cnt
}

// Shader contains a shader program, with a vertex and a fragment (pixel) shader.
type Shader struct {
	prog     uint32
	uniforms map[string]int32
	textures map[int32]*Texture // maps uniform location to *Texture
}

// helper construct to get uniforms and restore previous glUseProgram
func (s *Shader) uniformBlender(name string) (location int32, restore func()) {
	if s.prog != 0 {
		var saved int32
		// Use program object
		gl.GetIntegerv(gl.CURRENT_PROGRAM, &saved)
		if uint32(saved) != s.prog {
			gl.UseProgram(s.prog)
		}

		return s.UniformLocation(name), func() {
			if uint32(saved) != s.prog {
				gl.UseProgram(uint32(saved))
			}
		}
	}
	return 0, func() {}
}

func compileShader(src string, stype uint32) (prog uint32, err error) {
	prog = gl.CreateShader(stype)

	strs, free := gl.Strs(src, "\x00")
	gl.ShaderSource(prog, 1, strs, nil)
	free()
	gl.CompileShader(prog)

	var status int32
	gl.GetShaderiv(prog, gl.COMPILE_STATUS, &status)
	if status == gl.FALSE {
		var len int32
		gl.GetShaderiv(prog, gl.INFO_LOG_LENGTH, &len)

		log := strings.Repeat("\x00", int(len+1))
		gl.GetShaderInfoLog(prog, len, nil, gl.Str(log))

		gl.DeleteShader(prog)

		switch stype {
		case gl.VERTEX_SHADER:
			return 0, fmt.Errorf("failed to compile Vertex Shader: %s", log)
		case gl.FRAGMENT_SHADER:
			return 0, fmt.Errorf("failed to compile Fragment Shader: %s", log)
		default:
			return 0, fmt.Errorf("failed to compile Unknown(%d) Shader: %s", stype, log)
		}
	}

	return
}

// NewShader compiles and links a Vertex and a Fragment (Pixel) shader into one program.
// The source code does not need to be terminated with \x00.
func NewShader(vert, frag string) (s *Shader, err error) {

	vertid, err := compileShader(vert, gl.VERTEX_SHADER)
	if err != nil {
		return nil, err
	}

	fragid, err := compileShader(frag, gl.FRAGMENT_SHADER)
	if err != nil {
		gl.DeleteShader(vertid)
		return nil, err
	}

	s = &Shader{}
	s.uniforms = make(map[string]int32)
	s.textures = make(map[int32]*Texture)
	s.prog = gl.CreateProgram()

	gl.AttachShader(s.prog, vertid)
	gl.AttachShader(s.prog, fragid)
	gl.LinkProgram(s.prog)

	var status int32
	gl.GetProgramiv(s.prog, gl.LINK_STATUS, &status)
	if status == gl.FALSE {
		var len int32
		gl.GetProgramiv(s.prog, gl.INFO_LOG_LENGTH, &len)

		log := strings.Repeat("\x00", int(len+1))
		gl.GetProgramInfoLog(s.prog, len, nil, gl.Str(log))

		gl.DeleteProgram(s.prog)
		gl.DeleteShader(vertid)
		gl.DeleteShader(fragid)

		return nil, fmt.Errorf("failed to link Program: %s", log)
	}

	gl.DeleteShader(vertid)
	gl.DeleteShader(fragid)

	return
}

// UniformLocation returns the location id of the given uniform.
// it returns -1 if the uniform is not found.
func (s *Shader) UniformLocation(name string) int32 {
	if id, ok := s.uniforms[name]; ok {
		return id
	} else {

		location := gl.GetUniformLocation(s.prog, gl.Str(name+"\x00"))
		s.uniforms[name] = location

		if location == -1 {
			log.Printf("Shader: uniform \"%s\" not found", name)
		}

		return location
	}
}

// UseProgram calls glUseProgram.
func (s *Shader) UseProgram() {
	gl.UseProgram(s.prog)
}

// BindTextures calls glActiveTexture and glBindTexture, updating the texture unit slots.
func (s *Shader) BindTextures() {
	var i int
	for loc, tex := range s.textures {

		index := int32(i + 1)

		gl.Uniform1i(loc, index)
		gl.ActiveTexture(uint32(gl.TEXTURE0 + index))
		gl.BindTexture(gl.TEXTURE_2D, tex.tex)
		i++
	}

	gl.ActiveTexture(gl.TEXTURE0)
}

// Handle returns the OpenGL handle of the program.
func (s *Shader) Handle() uint32 {
	return s.prog
}

func (s *Shader) SetUniformTexture(name string, tex *Texture) {
	if s.prog == 0 {
		return
	}

	loc := s.UniformLocation(name)
	if loc == -1 {
		return
	}

	// Store the location to texture map
	_, ok := s.textures[loc]
	if !ok {
		// new texture, make sure there are enough texture units
		if len(s.textures)+1 >= int(getMaxTextureUnits()) {
			log.Printf("Shader: Warning: Impossible to use texture \"%s\" for shader: all available texture units are used", name)
			return
		}
	}

	s.textures[loc] = tex
}

// SetUniformTextureHandle sets a uniform as a sampler2D from an external OpenGL texture.
// tex is the OpenGL texture handle (the one you get with glGenTextures())
func (s *Shader) SetUniformTextureHandle(name string, tex uint32) {
	s.SetUniformTexture(name, &Texture{tex: tex})
}

func (s *Shader) SetUniformMat4(name string, value mgl32.Mat4) {
	loc, restore := s.uniformBlender(name)
	defer restore()

	gl.UniformMatrix4fv(loc, 1, false, &value[0])
}

func (s *Shader) SetUniformFloat(name string, value float32) {
	loc, restore := s.uniformBlender(name)
	defer restore()

	gl.Uniform1f(loc, value)
}
func (s *Shader) SetUniformVec2f(name string, value itype.Vec2f) {
	loc, restore := s.uniformBlender(name)
	defer restore()

	gl.Uniform2f(loc, value[0], value[1])
}
func (s *Shader) SetUniformVec3f(name string, value itype.Vec3f) {
	loc, restore := s.uniformBlender(name)
	defer restore()

	gl.Uniform3f(loc, value[0], value[1], value[2])
}
func (s *Shader) SetUniformVec4f(name string, value itype.Vec4f) {
	loc, restore := s.uniformBlender(name)
	defer restore()

	gl.Uniform4f(loc, value[0], value[1], value[2], value[3])
}

func (s *Shader) SetUniformInt(name string, value int32) {
	loc, restore := s.uniformBlender(name)
	defer restore()

	gl.Uniform1i(loc, value)
}
func (s *Shader) SetUniformVec2i(name string, value itype.Vec2i) {
	loc, restore := s.uniformBlender(name)
	defer restore()

	gl.Uniform2i(loc, int32(value[0]), int32(value[1]))
}
func (s *Shader) SetUniformVec3i(name string, value itype.Vec3i) {
	loc, restore := s.uniformBlender(name)
	defer restore()

	gl.Uniform3i(loc, int32(value[0]), int32(value[1]), int32(value[2]))
}
func (s *Shader) SetUniformVec4i(name string, value itype.Vec4i) {
	loc, restore := s.uniformBlender(name)
	defer restore()

	gl.Uniform4i(loc, int32(value[0]), int32(value[1]), int32(value[2]), int32(value[3]))
}

func (s *Shader) GetAttribLocation(name string) uint32 {
	name = name + "\x00"
	return uint32(gl.GetAttribLocation(s.prog, gl.Str(name)))
}
