package main

import (
	"bufio"
	"flag"
	"fmt"
	"log"
	"os"
	"regexp"
	"strings"
)

func main() {

	var experiments string
	flag.StringVar(&experiments, "experiments", "", "experiments to enable")
	flag.Parse()
	loadedExperiments := make(map[string]bool)
	for _, exp := range strings.Split(experiments, ",") {
		loadedExperiments[exp] = true
	}

	generatedBindingsPrefix := ``
	generatedBindings := ``
	experimentRegistry := ``
	experimentUpdateSettings := ``

	file, err := os.Open("spec")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	// optionally, resize scanner's capacity for lines over 64K, see next example
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, "#") || len(line) == 0 {
			// ignore
			continue
		}

		parts := strings.Split(line, " ")
		if len(parts) < 2 {
			fmt.Printf("all spec lines must start with a type prefix and a function name: %v\n", parts)
			os.Exit(1)
		}

		fType := parts[0]
		fName := parts[1]
		args := parts[2:]

		experiment := ""
		if strings.HasPrefix(fType, "!") {
			experiment = strings.ReplaceAll(fType, "!", "")
			if _, gen := loadedExperiments[experiment]; gen {
				fType = parts[1]
				fName = parts[2]
				args = parts[3:]
			} else {
				continue // skip experiment
			}
		}

		if strings.HasPrefix(fType, "import") {
			generatedBindingsPrefix += fName + "\n"
			continue
		}

		if strings.HasPrefix(fType, "global") {
			generatedBindings += fmt.Sprintf("var %s %s\n", parts[2], parts[3])

			experimentRegistry += fmt.Sprintf(`
	%s = %s.Init(&globalACN, appDir)
	eventHandler.AddModule(%s)
	%s.Enable(application, &globalACN)
`, parts[2], parts[4], parts[2], parts[2])

			experimentUpdateSettings += fmt.Sprintf(`
	%s.UpdateSettings(application, &globalACN)
`, parts[2])

			continue
		}

		fmt.Printf("generating %v function for %v\n", fType, fName)

		switch fType {

		case "app":
			generatedBindings = generateAppFunction(generatedBindings, fName, args)
		case "exp":
			generatedBindings = generateExpFunction(generatedBindings, fName, experiment, args)
		case "(string)app":
			fallthrough
		case "(json)app":
			generatedBindings = generateJsonAppFunction(generatedBindings, fName, args)
		case "profile":
			generatedBindings = generateProfileFunction(generatedBindings, fName, args)
		case "(json)profile":
			generatedBindings = generateJsonProfileFunction(generatedBindings, fName, args, false)
		case "(json-err)profile":
			generatedBindings = generateJsonProfileFunction(generatedBindings, fName, args, true)
		case "@profile-experiment":
			experiment := args[0]
			generatedBindings = generateExperimentalProfileFunction(generatedBindings, experiment, fName, args[1:])
		case "@(json)profile-experiment":
			experiment := args[0]
			generatedBindings = generateExperimentalJsonProfileFunction(generatedBindings, experiment, fName, args[1:])
		default:

			fmt.Printf("unknown function type %v\n", parts)
			os.Exit(1)
		}
	}

	if err := scanner.Err(); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%v\n", generatedBindings)
	// NOTE: You can see individual generations by uncommenting the below lines..
	// os.WriteFile("templates/bindings.go", []byte(generatedBindings), 0644)
	// os.WriteFile("templates/imports.go", []byte(generatedBindingsPrefix), 0644)

	template, _ := os.ReadFile("templates/lib_template.go")
	templateString := string(template)
	templateString = strings.ReplaceAll(templateString, "{{BINDINGS}}", generatedBindings)
	templateString = strings.ReplaceAll(templateString, "{{EXPERIMENT_REGISTER}}", experimentRegistry)
	templateString = strings.ReplaceAll(templateString, "{{EXPERIMENT_UPDATESETTINGS}}", experimentUpdateSettings)
	templateString = strings.ReplaceAll(templateString, "{{IMPORTS}}", generatedBindingsPrefix)
	os.WriteFile("lib.go", []byte(templateString), 0644)
}

var uniqueVarCounter = 0

func profileHandleArgPrototype() (string, string, string, string) {
	return `onion_ptr *C.char, onion_len C.int`, `C.GoStringN(onion_ptr, onion_len)`, `profile string`, "profile"
}

func nameArgPrototype() (string, string, string, string) {
	return `name_ptr *C.char, name_len C.int`, `C.GoStringN(name_ptr, name_len)`, `name string`, "name"
}

func passwordArgPrototype() (string, string, string, string) {
	return `password_ptr *C.char, password_len C.int`, `C.GoStringN(password_ptr, password_len)`, `password string`, "password"
}

func intArgPrototype(varName string) (string, string, string, string) {
	return fmt.Sprintf(`%s C.int`, ToSnakeCase(varName)), fmt.Sprintf(`int(%v)`, ToSnakeCase(varName)), fmt.Sprintf(`%v int`, varName), varName
}

func uintArgPrototype(varName string) (string, string, string, string) {
	return fmt.Sprintf(`%s C.uint`, ToSnakeCase(varName)), fmt.Sprintf(`int(%v)`, ToSnakeCase(varName)), fmt.Sprintf(`%v int`, varName), varName
}

func conversationArgPrototype(varName string) (string, string, string, string) {
	return intArgPrototype(varName)
}

func channelArgPrototype() (string, string, string, string) {
	return intArgPrototype("channel_id")
}

func messageArgPrototype() (string, string, string, string) {
	return intArgPrototype("message_id")
}

func boolArgPrototype(name string) (string, string, string, string) {
	uniqueVarCounter += 1
	varName := fmt.Sprintf("%s%d", name, uniqueVarCounter)
	return fmt.Sprintf(`%s C.char`, ToSnakeCase(varName)), fmt.Sprintf(`%v == 1`, ToSnakeCase(varName)), fmt.Sprintf(`%v bool`, varName), varName
}

func stringArgPrototype(name string) (string, string, string, string) {
	uniqueVarCounter += 1
	varName := fmt.Sprintf("%s%d", name, uniqueVarCounter)
	return fmt.Sprintf(`%s_ptr *C.char, %s_len C.int`, varName, varName), fmt.Sprintf(`C.GoStringN(%s_ptr, %s_len)`, varName, varName), fmt.Sprintf(`%v string`, varName), varName
}

func mapArgs(argsTypes []string) (string, string, string, string) {
	var cArgs []string
	var c2GoArgs []string
	var goSpec []string
	var gUse []string
	for _, argSpec := range argsTypes {
		argTypeParts := strings.Split(argSpec, ":")
		argType := argTypeParts[0]
		switch argType {
		case "application":
			gUse = append(gUse, "application")
		case "acn":
			gUse = append(gUse, "&globalACN")
		case "profile":
			c1, c2, c3, c4 := profileHandleArgPrototype()
			cArgs = append(cArgs, c1)
			c2GoArgs = append(c2GoArgs, c2)
			goSpec = append(goSpec, c3)
			gUse = append(gUse, c4)
		case "name":
			c1, c2, c3, c4 := nameArgPrototype()
			cArgs = append(cArgs, c1)
			c2GoArgs = append(c2GoArgs, c2)
			goSpec = append(goSpec, c3)
			gUse = append(gUse, c4)
		case "password":
			c1, c2, c3, c4 := passwordArgPrototype()
			cArgs = append(cArgs, c1)
			c2GoArgs = append(c2GoArgs, c2)
			goSpec = append(goSpec, c3)
			gUse = append(gUse, c4)
		case "conversation":
			name := "conversation"
			if len(argTypeParts) == 2 {
				name = argTypeParts[1]
			}
			c1, c2, c3, c4 := conversationArgPrototype(name)
			cArgs = append(cArgs, c1)
			c2GoArgs = append(c2GoArgs, c2)
			goSpec = append(goSpec, c3)
			gUse = append(gUse, c4)
		case "channel":
			c1, c2, c3, c4 := channelArgPrototype()
			cArgs = append(cArgs, c1)
			c2GoArgs = append(c2GoArgs, c2)
			goSpec = append(goSpec, c3)
			gUse = append(gUse, c4)
		case "message":
			c1, c2, c3, c4 := messageArgPrototype()
			cArgs = append(cArgs, c1)
			c2GoArgs = append(c2GoArgs, c2)
			goSpec = append(goSpec, c3)
			gUse = append(gUse, c4)
		case "bool":
			if len(argTypeParts) != 2 {
				fmt.Printf("generic bool arg must have have e.g. bool:<name>\n")
				os.Exit(1)
			}
			c1, c2, c3, c4 := boolArgPrototype(argTypeParts[1])
			cArgs = append(cArgs, c1)
			c2GoArgs = append(c2GoArgs, c2)
			goSpec = append(goSpec, c3)
			gUse = append(gUse, c4)
		case "int":
			if len(argTypeParts) != 2 {
				fmt.Printf("generic int arg must have have e.g. int:<name>\n")
				os.Exit(1)
			}
			c1, c2, c3, c4 := intArgPrototype(argTypeParts[1])
			cArgs = append(cArgs, c1)
			c2GoArgs = append(c2GoArgs, c2)
			goSpec = append(goSpec, c3)
			gUse = append(gUse, c4)
		case "uint":
			if len(argTypeParts) != 2 {
				fmt.Printf("generic uint arg must have have e.g. uint:<name>\n")
				os.Exit(1)
			}
			c1, c2, c3, c4 := uintArgPrototype(argTypeParts[1])
			cArgs = append(cArgs, c1)
			c2GoArgs = append(c2GoArgs, c2)
			goSpec = append(goSpec, c3)
			// because of java/kotlin/android/gomobile inability to recognize unsigned integers
			// we need to pretent this is a signed interface...so do the final cast here...
			// this will cause bad behavior if a negative number is passed through the java
			// interface...so...don't do that...
			gUse = append(gUse, fmt.Sprintf("uint(%s)", c4))
		case "string":
			if len(argTypeParts) != 2 {
				fmt.Printf("generic string arg must have have e.g. string:<name>\n")
				os.Exit(1)
			}
			c1, c2, c3, c4 := stringArgPrototype(argTypeParts[1])
			cArgs = append(cArgs, c1)
			c2GoArgs = append(c2GoArgs, c2)
			goSpec = append(goSpec, c3)
			gUse = append(gUse, c4)
		default:
			fmt.Printf("unknown arg type [%v]\n", argType)
			os.Exit(1)
		}
	}
	return strings.Join(cArgs, ","), strings.Join(c2GoArgs, ","), strings.Join(goSpec, ","), strings.Join(gUse, ",")
}

func generateAppFunction(bindings string, name string, argsTypes []string) string {
	appPrototype := `
//export c_{{FNAME}}
func c_{{FNAME}}({{C_ARGS}}) {
	{{FNAME}}({{C2GO_ARGS}})
}

func {{FNAME}}({{GO_ARGS_SPEC}}) {
	application.{{LIBNAME}}({{GO_ARG}})
}
`

	cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes)
	appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced"))
	appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name)
	appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", cArgs)
	appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", c2GoArgs)
	appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", goSpec)
	appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse)

	bindings += appPrototype
	return bindings
}

func generateExpFunction(bindings string, name string, exp string, argsTypes []string) string {
	appPrototype := `
//export c_{{FNAME}}
func c_{{FNAME}}({{C_ARGS}}) {
	{{FNAME}}({{C2GO_ARGS}})
}

func {{FNAME}}({{GO_ARGS_SPEC}}) {
	{{EXPERIMENT}}.{{LIBNAME}}({{GO_ARG}})
}
`

	cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes)
	appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced"))
	appPrototype = strings.ReplaceAll(appPrototype, "{{EXPERIMENT}}", exp)
	appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name)
	appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", cArgs)
	appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", c2GoArgs)
	appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", goSpec)
	appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse)

	bindings += appPrototype
	return bindings
}

func generateJsonAppFunction(bindings string, name string, argsTypes []string) string {
	appPrototype := `
//export c_{{FNAME}}
func c_{{FNAME}}({{C_ARGS}}) *C.char  {
	return C.CString({{FNAME}}({{C2GO_ARGS}}))
}

func {{FNAME}}({{GO_ARGS_SPEC}}) string {
	return application.{{LIBNAME}}({{GO_ARG}})
}
`

	cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes)
	appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced"))
	appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name)
	appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", cArgs)
	appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", c2GoArgs)
	appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", goSpec)
	appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse)

	bindings += appPrototype
	return bindings
}

func generateProfileFunction(bindings string, name string, argsTypes []string) string {
	appPrototype := `
//export c_{{FNAME}}
func c_{{FNAME}}({{C_ARGS}}) {
	{{FNAME}}({{C2GO_ARGS}})
}

func {{FNAME}}({{GO_ARGS_SPEC}}) {
	cwtchProfile := application.GetPeer(profile)
	if cwtchProfile != nil {
		cwtchProfile.{{LIBNAME}}({{GO_ARG}})
	}
}
`

	cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes)

	appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced"))
	appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name)
	// We need to prepend a set of profile handle arguments...
	pArgs, c2GoPArg, goSpecP, _ := profileHandleArgPrototype()
	appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", strings.Join(append([]string{pArgs}, cArgs), ","))
	appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", strings.Join(append([]string{c2GoPArg}, c2GoArgs), ","))
	appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", strings.Join(append([]string{goSpecP}, goSpec), ","))
	appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse)

	bindings += appPrototype
	return bindings
}

func generateJsonProfileFunction(bindings string, name string, argsTypes []string, handleErr bool) string {
	appPrototype := `
//export c_{{FNAME}}
func c_{{FNAME}}({{C_ARGS}})  *C.char {
	return C.CString({{FNAME}}({{C2GO_ARGS}}))
}

func {{FNAME}}({{GO_ARGS_SPEC}}) string {
	cwtchProfile := application.GetPeer(profile)
	if cwtchProfile != nil {
				{{HANDLE_FUNC}}
	}
	return ""
}
`

	noErrorPrototype := `return cwtchProfile.{{LIBNAME}}({{GO_ARG}})`
	withErrorPrototype := `res,err :=  cwtchProfile.{{LIBNAME}}({{GO_ARG}})
													if err != nil {
															log.Errorf("could not {{FNAME}} %v", err)
													}
												 return res`
	functionPrototype := noErrorPrototype
	if handleErr {
		functionPrototype = withErrorPrototype
	}

	cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes)

	appPrototype = strings.ReplaceAll(appPrototype, "{{HANDLE_FUNC}}", functionPrototype)
	appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced"))
	appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name)
	// We need to prepend a set of profile handle arguments...
	pArgs, c2GoPArg, goSpecP, _ := profileHandleArgPrototype()
	appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", strings.Join(append([]string{pArgs}, cArgs), ","))
	appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", strings.Join(append([]string{c2GoPArg}, c2GoArgs), ","))
	appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", strings.Join(append([]string{goSpecP}, goSpec), ","))
	appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse)

	bindings += appPrototype
	return bindings
}

func generateExperimentalProfileFunction(bindings string, experiment string, name string, argsTypes []string) string {
	appPrototype := `
//export c_{{FNAME}}
func c_{{FNAME}}({{C_ARGS}}) {
	{{FNAME}}({{C2GO_ARGS}})
}

func {{FNAME}}({{GO_ARGS_SPEC}}) {
	cwtchProfile := application.GetPeer(profile)
	if cwtchProfile != nil {
		functionality := {{EXPERIMENT}}.FunctionalityGate()
		if functionality != nil {
			err := functionality.{{LIBNAME}}(cwtchProfile, {{GO_ARG}})
			if err != nil {
				log.Errorf("error calling experimental feature {{FNAME}}: %v", err)
			}
		}
	}
}
`

	cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes)

	appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced"))
	appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name)
	appPrototype = strings.ReplaceAll(appPrototype, "{{EXPERIMENT}}", experiment)
	// We need to prepend a set of profile handle arguments...
	pArgs, c2GoPArg, goSpecP, _ := profileHandleArgPrototype()
	appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", strings.Join(append([]string{pArgs}, cArgs), ","))
	appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", strings.Join(append([]string{c2GoPArg}, c2GoArgs), ","))
	appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", strings.Join(append([]string{goSpecP}, goSpec), ","))
	appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse)

	bindings += appPrototype
	return bindings
}

func generateExperimentalJsonProfileFunction(bindings string, experiment string, name string, argsTypes []string) string {
	appPrototype := `
//export c_{{FNAME}}
func c_{{FNAME}}({{C_ARGS}})  *C.char  {
	return C.CString({{FNAME}}({{C2GO_ARGS}}))
}

func {{FNAME}}({{GO_ARGS_SPEC}}) string {
	cwtchProfile := application.GetPeer(profile)
	if cwtchProfile != nil {
		functionality := {{EXPERIMENT}}.FunctionalityGate()
		if functionality != nil {
			return functionality.{{LIBNAME}}(cwtchProfile, {{GO_ARG}})
		}
	}
	return ""
}
`

	cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes)

	appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced"))
	appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name)
	appPrototype = strings.ReplaceAll(appPrototype, "{{EXPERIMENT}}", experiment)
	// We need to prepend a set of profile handle arguments...
	pArgs, c2GoPArg, goSpecP, _ := profileHandleArgPrototype()
	appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", strings.Join(append([]string{pArgs}, cArgs), ","))
	appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", strings.Join(append([]string{c2GoPArg}, c2GoArgs), ","))
	appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", strings.Join(append([]string{goSpecP}, goSpec), ","))
	appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse)

	bindings += appPrototype
	return bindings
}

var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")

func ToSnakeCase(str string) string {
	snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
	snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
	return strings.ToLower(snake)
}
