//+build ignore

package main

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/build"
	"go/parser"
	"go/token"
	"io/ioutil"
	"log"
	"regexp"
	"strings"
	"unicode"
)

func main() {
	// find the go files in the current directory
	pkg, err := build.Default.ImportDir(".", 0)
	if err != nil {
		log.Fatalf("Could not process directory: %s", err)
	}

	vsMap := make(map[string]string)
	fsMap := make(map[string]string)

	// go through each file and find const raw string literals with
	fset := token.NewFileSet()
	for _, goFile := range pkg.GoFiles {
		parsedFile, err := parser.ParseFile(fset, goFile, nil, 0)
		if err != nil {
			log.Fatalf("Failed to parse file %s: %s", goFile, err)
		}

		for _, obj := range parsedFile.Scope.Objects {
			isVS := strings.HasSuffix(obj.Name, "VS")
			isFS := strings.HasSuffix(obj.Name, "FS")
			var shaderCode string
			if isVS || isFS {
				if value, ok := obj.Decl.(*ast.ValueSpec); ok && len(value.Values) == 1 {
					if lit, ok := value.Values[0].(*ast.BasicLit); ok {
						shaderCode = lit.Value
					}
				}
			}
			if shaderCode != "" {
				baseName := obj.Name[:len(obj.Name)-2]
				if isVS {
					vsMap[baseName] = shaderCode
				} else if isFS {
					fsMap[baseName] = shaderCode
				}
			}
		}
	}

	for name := range vsMap {
		if _, ok := fsMap[name]; !ok {
			log.Println("Warning: Vertex shader with no corresponding fragment shader (" + name + ")")
			delete(vsMap, name)
			continue
		}
	}
	for name := range fsMap {
		if _, ok := vsMap[name]; !ok {
			log.Println("Warning: Fragment shader with no corresponding vertex shader (" + name + ")")
			delete(fsMap, name)
			continue
		}
	}

	goCode := &bytes.Buffer{}
	buildCodeHeader(goCode)

	for name, vs := range vsMap {
		fs := fsMap[name]

		vs = vs[1 : len(vs)-1]
		fs = fs[1 : len(fs)-1]

		inputs := shaderFindInputVariables(vs + "\n" + fs)

		buildCode(goCode, name, inputs)
	}

	err = ioutil.WriteFile("made_shaders.go", goCode.Bytes(), 0777)
	if err != nil {
		log.Fatalf("Could not write made_shaders.go: %s", err)
	}
}

type ShaderInput struct {
	Name      string
	IsAttrib  bool
	IsUniform bool
}

func shaderFindInputVariables(source string) []ShaderInput {
	inputs := make([]ShaderInput, 0, 10)

	varDefSplitter := regexp.MustCompile("[ \t\r\n,]+")
	lines := shaderGetTopLevelLines(source)
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if len(line) == 0 || line[0] == '#' {
			continue
		}
		if strings.Contains(line, "{") {
			break
		}
		parts := varDefSplitter.Split(line, -1)
		isAttrib := parts[0] == "attribute"
		isUniform := parts[0] == "uniform"
		if !isAttrib && !isUniform {
			continue
		}
		for i := 2; i < len(parts); i++ {
			inputs = append(inputs, ShaderInput{
				Name:      parts[i],
				IsAttrib:  isAttrib,
				IsUniform: isUniform})
		}
	}
	return inputs
}

func shaderGetTopLevelLines(source string) []string {
	sourceBytes := []byte(source)
	l := len(sourceBytes)
	if l == 0 {
		return make([]string, 0)
	}

	var inPrecompiledStatement, inLineComment, inBlockComment, inString, stringEscape, lastWasWhitespace bool
	curlyBraceDepth := 0

	topLevelLines := make([]string, 0, 100)
	currentLine := make([]byte, 0, 1000)

	var c0, c1 byte = ' ', ' '
	for i := 0; i < l; i++ {
		c1 = sourceBytes[i]
		isWhitespace := unicode.IsSpace(rune(c1))
		if !inBlockComment && !inString && c0 == '/' && c1 == '/' {
			inLineComment = true
			if len(currentLine) > 0 {
				currentLine = currentLine[:len(currentLine)-1]
			}
		} else if !inBlockComment && !inString && c1 == '#' {
			inPrecompiledStatement = true
		} else if !inBlockComment && !inLineComment && !inPrecompiledStatement && !inString && c0 == '/' && c1 == '*' {
			inBlockComment = true
			if len(currentLine) > 0 {
				currentLine = currentLine[:len(currentLine)-1]
			}
		} else if !inBlockComment && !inString && !inLineComment && !inPrecompiledStatement && c1 == '"' {
			inString = true
		} else if inString && !stringEscape && c1 == '\\' {
			stringEscape = true
		} else if inString && stringEscape {
			stringEscape = false
		} else if inString && !stringEscape && c1 == '"' {
			inString = false
		} else if !inBlockComment && !inLineComment && !inPrecompiledStatement && !inString && c1 == '{' {
			if curlyBraceDepth == 0 {
				topLevelLines = append(topLevelLines, string(currentLine))
				currentLine = currentLine[:0]
			}
			curlyBraceDepth++
		}
		if !inBlockComment && !inLineComment && !inPrecompiledStatement && !inString && curlyBraceDepth == 0 {
			if c1 == ';' {
				topLevelLines = append(topLevelLines, string(currentLine))
				currentLine = currentLine[:0]
			} else if !isWhitespace {
				currentLine = append(currentLine, c1)
			} else if !lastWasWhitespace {
				currentLine = append(currentLine, ' ')
			}
		}
		if !inBlockComment && !inLineComment && !inPrecompiledStatement && !inString && c1 == '}' {
			curlyBraceDepth--
		} else if (inLineComment || inPrecompiledStatement) && (c1 == '\r' || c1 == '\n') {
			inLineComment = false
			inPrecompiledStatement = false
		} else if inBlockComment && c0 == '*' && c1 == '/' {
			inBlockComment = false
		}

		lastWasWhitespace = isWhitespace
		c0 = c1
	}

	topLevelLines = append(topLevelLines, string(currentLine))

	return topLevelLines
}

const compilePart = `
	{
		SHADER_VAR = gli.CreateShader(gl_SHADER_TYPE)
		gli.ShaderSource(SHADER_VAR, SHADER_SRC)
		gli.CompileShader(SHADER_VAR)

		shLog := gli.GetShaderInfoLog(SHADER_VAR)
		if len(shLog) > 0 {
			fmt.Printf("SHADER_TYPE compilation log for SHADER_SRC:\n\n%s\n", shLog)
		}

		var status int32
		gli.GetShaderiv(SHADER_VAR, gl_COMPILE_STATUS, &status)
		if status != gl_TRUE {
			gli.DeleteShader(SHADER_VAR)
			return nil, errors.New("Error compiling GL_SHADER_TYPE shader part for SHADER_SRC")
		}
		if glErr := gli.GetError(); glErr != gl_NO_ERROR {
			return nil, errors.New("error compiling shader part for SHADER_SRC, glError: " + fmt.Sprint(glErr))
		}
	}
`
const linkPart = `
	{
		program = gli.CreateProgram()
		gli.AttachShader(program, vs)
		gli.AttachShader(program, fs)
		gli.LinkProgram(program)

		shLog := gli.GetProgramInfoLog(program)
		if len(shLog) > 0 {
			fmt.Printf("Shader link log for SHADER_SRC:\n\n%s\n", shLog)
		}

		var status int32
		gli.GetProgramiv(program, gl_LINK_STATUS, &status)
		if status != gl_TRUE {
			gli.DeleteShader(vs)
			gli.DeleteShader(fs)
			return nil, errors.New("error linking shader for SHADER_SRC")
		}
		if glErr := gli.GetError(); glErr != gl_NO_ERROR {
			return nil, errors.New("error linking shader for SHADER_SRC, glError: " + fmt.Sprint(glErr))
		}
	}
`

func capitalize(s string) string {
	return strings.ToUpper(s[:1]) + s[1:]
}

func buildCodeHeader(buf *bytes.Buffer) {
	fmt.Fprint(buf, "package canvas\n\n")
	fmt.Fprint(buf, "import (\n")
	fmt.Fprint(buf, "\t\"errors\"\n")
	fmt.Fprint(buf, "\t\"fmt\"\n")
	fmt.Fprint(buf, ")\n\n")
}

func buildCode(buf *bytes.Buffer, baseName string, inputs []ShaderInput) {
	shaderName := baseName + "Shader"
	vsName := baseName + "VS"
	fsName := baseName + "FS"

	fmt.Fprintf(buf, "type %s struct {\n", shaderName)
	fmt.Fprint(buf, "\tid uint32\n")
	for _, input := range inputs {
		if input.IsAttrib {
			fmt.Fprintf(buf, "\t%s uint32\n", input.Name)
		} else if input.IsUniform {
			fmt.Fprintf(buf, "\t%s int32\n", input.Name)
		}
	}
	fmt.Fprint(buf, "}\n\n")
	fmt.Fprintf(buf, "func load%s() (*%s, error) {\n", capitalize(shaderName), shaderName)
	fmt.Fprint(buf, "\tvar vs, fs, program uint32\n")

	var part string

	part = strings.Replace(compilePart, "SHADER_SRC", vsName, -1)
	part = strings.Replace(part, "SHADER_TYPE", "VERTEX_SHADER", -1)
	part = strings.Replace(part, "SHADER_VAR", "vs", -1)
	fmt.Fprint(buf, part)

	part = strings.Replace(compilePart, "SHADER_SRC", fsName, -1)
	part = strings.Replace(part, "SHADER_TYPE", "FRAGMENT_SHADER", -1)
	part = strings.Replace(part, "SHADER_VAR", "fs", -1)
	fmt.Fprint(buf, part)

	part = strings.Replace(linkPart, "SHADER_SRC", fsName, -1)
	fmt.Fprint(buf, part)

	fmt.Fprint(buf, "\n")
	fmt.Fprintf(buf, "\tresult := &%s{}\n", shaderName)
	fmt.Fprint(buf, "\tresult.id = program\n")

	for _, input := range inputs {
		if input.IsAttrib {
			fmt.Fprintf(buf, "\tresult.%s = uint32(gli.GetAttribLocation(program, \"%s\"))\n", input.Name, input.Name)
		} else if input.IsUniform {
			fmt.Fprintf(buf, "\tresult.%s = gli.GetUniformLocation(program, \"%s\")\n", input.Name, input.Name)
		}
	}

	fmt.Fprint(buf, "\n")

	fmt.Fprint(buf, "\treturn result, nil\n")
	fmt.Fprint(buf, "}\n")
}
