package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"text/template"

	"github.com/Masterminds/semver/v3"
	"github.com/fatih/camelcase"
	"github.com/getkin/kin-openapi/openapi3"
	"github.com/stoewer/go-strcase"
)

type service struct {
	Spec *openapi3.Swagger
	Name string
	// overwrite import name of service when it's a keyword ie function in javascript
	ImportName string
}

type example struct {
	Title       string `json:"title"`
	Description string `json:"description"`
	Request     map[string]interface{}
	Response    map[string]interface{}
}

func main() {
	files, err := ioutil.ReadDir(os.Args[1])
	if err != nil {
		log.Fatal(err)
	}
	workDir, _ := os.Getwd()
	tsPath := filepath.Join(workDir, "clients", "ts")
	err = os.MkdirAll(tsPath, 0777)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	goPath := filepath.Join(workDir, "clients", "go")
	err = os.MkdirAll(goPath, 0777)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	examplesPath := filepath.Join(workDir, "examples")
	err = os.MkdirAll(goPath, 0777)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	funcs := map[string]interface{}{
		"recursiveTypeDefinition": func(language, serviceName, typeName string, schemas map[string]*openapi3.SchemaRef) string {
			return schemaToType(language, serviceName, typeName, schemas)
		},
		"requestTypeToEndpointName": func(requestType string) string {
			parts := camelcase.Split(requestType)
			return strings.Join(parts[1:len(parts)-1], "")
		},
		// strips service name from the request type
		"requestType": func(requestType string) string {
			parts := camelcase.Split(requestType)
			return strings.Join(parts[1:], "")
		},
		"requestTypeToResponseType": func(requestType string) string {
			parts := camelcase.Split(requestType)
			return strings.Join(parts[1:len(parts)-1], "") + "Response"
		},
		"endpointComment": func(endpoint string, schemas map[string]*openapi3.SchemaRef) string {
			v := schemas[strings.Title(endpoint)+"Request"]
			if v == nil {
				panic("can't find " + strings.Title(endpoint) + "Request")
			}
			if v.Value == nil {
				return ""
			}
			comm := v.Value.Description
			ret := ""
			for _, line := range strings.Split(comm, "\n") {
				ret += "// " + strings.TrimSpace(line) + "\n"
			}
			return ret
		},
		"requestTypeToEndpointPath": func(requestType string) string {
			parts := camelcase.Split(requestType)
			return strings.Title(strings.Join(parts[1:len(parts)-1], ""))
		},
		"title": strings.Title,
		"untitle": func(t string) string {
			return strcase.LowerCamelCase(t)
		},
		"goExampleRequest": func(serviceName, endpoint string, schemas map[string]*openapi3.SchemaRef, exampleJSON map[string]interface{}) string {
			return schemaToGoExample(serviceName, strings.Title(endpoint)+"Request", schemas, exampleJSON)
		},
		"tsExampleRequest": func(serviceName, endpoint string, schemas map[string]*openapi3.SchemaRef, exampleJSON map[string]interface{}) string {
			bs, _ := json.MarshalIndent(exampleJSON, "", "  ")
			return string(bs)
		},
	}
	services := []service{}
	tsFileList := []string{"esm", "index.js", "index.d.ts"}
	for _, f := range files {
		if strings.Contains(f.Name(), "clients") || strings.Contains(f.Name(), "examples") {
			continue
		}
		if f.IsDir() && !strings.HasPrefix(f.Name(), ".") {
			serviceName := f.Name()
			tsFileList = append(tsFileList, serviceName)
			serviceDir := filepath.Join(workDir, f.Name())
			cmd := exec.Command("make", "api")
			cmd.Dir = serviceDir
			outp, err := cmd.CombinedOutput()
			if err != nil {
				fmt.Println(string(outp))
			}

			serviceFiles, err := ioutil.ReadDir(serviceDir)
			if err != nil {
				fmt.Println("Failed to read service dir", err)
				os.Exit(1)
			}
			skip := false

			// detect openapi json file
			apiJSON := ""
			for _, serviceFile := range serviceFiles {
				if strings.Contains(serviceFile.Name(), "api") && strings.Contains(serviceFile.Name(), "-") && strings.HasSuffix(serviceFile.Name(), ".json") {
					apiJSON = filepath.Join(serviceDir, serviceFile.Name())
				}
				if serviceFile.Name() == "skip" {
					skip = true
				}
			}
			if skip {
				continue
			}

			fmt.Println("Processing folder", serviceDir, "api json", apiJSON)

			js, err := ioutil.ReadFile(apiJSON)

			if err != nil {
				fmt.Println("Failed to read json spec", err)
				os.Exit(1)
			}
			spec := &openapi3.Swagger{}
			err = json.Unmarshal(js, &spec)
			if err != nil {
				fmt.Println("Failed to unmarshal", err)
				os.Exit(1)
			}
			service := service{
				Name:       serviceName,
				ImportName: serviceName,
				Spec:       spec,
			}
			if service.Name == "function" {
				service.ImportName = "fx"
			}
			services = append(services, service)

			templ, err := template.New("ts" + serviceName).Funcs(funcs).Parse(tsServiceTemplate)
			if err != nil {
				fmt.Println("Failed to unmarshal", err)
				os.Exit(1)
			}
			var b bytes.Buffer
			buf := bufio.NewWriter(&b)
			err = templ.Execute(buf, map[string]interface{}{
				"service": service,
			})
			if err != nil {
				fmt.Println("Failed to unmarshal", err)
				os.Exit(1)
			}

			err = os.MkdirAll(filepath.Join(tsPath, "src", serviceName), 0777)
			if err != nil {
				fmt.Println(err)
				os.Exit(1)
			}
			f, err := os.OpenFile(filepath.Join(tsPath, "src", serviceName, "index.ts"), os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0744)
			if err != nil {
				fmt.Println("Failed to open schema file", err)
				os.Exit(1)
			}
			buf.Flush()
			_, err = f.Write(b.Bytes())
			if err != nil {
				fmt.Println("Failed to append to schema file", err)
				os.Exit(1)
			}
			cmd = exec.Command("prettier", "-w", "index.ts")
			cmd.Dir = filepath.Join(tsPath, "src", serviceName)
			outp, err = cmd.CombinedOutput()
			if err != nil {
				fmt.Println(fmt.Sprintf("Problem formatting '%v' client: %v %s", serviceName, string(outp), err.Error()))
				os.Exit(1)
			}

			templ, err = template.New("go" + serviceName).Funcs(funcs).Parse(goServiceTemplate)
			if err != nil {
				fmt.Println("Failed to unmarshal", err)
				os.Exit(1)
			}
			b = bytes.Buffer{}
			buf = bufio.NewWriter(&b)
			err = templ.Execute(buf, map[string]interface{}{
				"service": service,
			})
			if err != nil {
				fmt.Println("Failed to unmarshal", err)
				os.Exit(1)
			}
			err = os.MkdirAll(filepath.Join(goPath, serviceName), 0777)
			if err != nil {
				fmt.Println(err)
				os.Exit(1)
			}

			goClientFile := filepath.Join(goPath, serviceName, serviceName+".go")
			f, err = os.OpenFile(goClientFile, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0744)
			if err != nil {
				fmt.Println("Failed to open schema file", err)
				os.Exit(1)
			}
			buf.Flush()
			_, err = f.Write(b.Bytes())
			if err != nil {
				fmt.Println("Failed to append to schema file", err)
				os.Exit(1)
			}
			cmd = exec.Command("gofmt", "-w", serviceName+".go")
			cmd.Dir = filepath.Join(goPath, serviceName)
			outp, err = cmd.CombinedOutput()
			if err != nil {
				fmt.Println(fmt.Sprintf("Problem formatting '%v' client: %v", serviceName, string(outp)))
				os.Exit(1)
			}
			cmd = exec.Command("go", "build", "-o", "/tmp/bin/outputfile")
			cmd.Dir = filepath.Join(goPath, serviceName)
			outp, err = cmd.CombinedOutput()
			if err != nil {
				fmt.Println(fmt.Sprintf("Problem building '%v' example: %v", serviceName, string(outp)))
				os.Exit(1)
			}

			exam, err := ioutil.ReadFile(filepath.Join(workDir, serviceName, "examples.json"))
			if err != nil {
				fmt.Println(err)
				os.Exit(1)
			}
			if err == nil {
				m := map[string][]example{}
				err = json.Unmarshal(exam, &m)
				if err != nil {
					fmt.Println(string(exam), err)
					os.Exit(1)
				}
				if len(service.Spec.Paths) != len(m) {
					fmt.Printf("Service has %v endpoints, but only %v examples\n", len(service.Spec.Paths), len(m))
				}
				for endpoint, examples := range m {
					for _, example := range examples {
						title := regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString(strcase.LowerCamelCase(strings.Replace(example.Title, " ", "_", -1)), "")
						templ, err = template.New("go" + serviceName + endpoint).Funcs(funcs).Parse(goExampleTemplate)
						if err != nil {
							fmt.Println("Failed to unmarshal", err)
							os.Exit(1)
						}
						b = bytes.Buffer{}
						buf = bufio.NewWriter(&b)
						err = templ.Execute(buf, map[string]interface{}{
							"service":  service,
							"example":  example,
							"endpoint": endpoint,
							"funcName": strcase.UpperCamelCase(title),
						})
						if err != nil {
							fmt.Println(err)
							os.Exit(1)
						}

						// create go examples directory
						err = os.MkdirAll(filepath.Join(examplesPath, serviceName, endpoint, "go"), 0777)
						if err != nil {
							fmt.Println(err)
							os.Exit(1)
						}
						goExampleFile := filepath.Join(examplesPath, serviceName, endpoint, "go", title+".go")
						f, err = os.OpenFile(goExampleFile, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0744)
						if err != nil {
							fmt.Println("Failed to open schema file", err)
							os.Exit(1)
						}

						buf.Flush()
						_, err = f.Write(b.Bytes())
						if err != nil {
							fmt.Println("Failed to append to schema file", err)
							os.Exit(1)
						}

						cmd := exec.Command("gofmt", "-w", title+".go")
						cmd.Dir = filepath.Join(examplesPath, serviceName, endpoint, "go")
						outp, err = cmd.CombinedOutput()
						if err != nil {
							fmt.Println(fmt.Sprintf("Problem with '%v' example '%v': %v", serviceName, endpoint, string(outp)))
							os.Exit(1)
						}

						// node example
						templ, err = template.New("ts" + serviceName + endpoint).Funcs(funcs).Parse(tsExampleTemplate)
						if err != nil {
							fmt.Println("Failed to unmarshal", err)
							os.Exit(1)
						}
						b = bytes.Buffer{}
						buf = bufio.NewWriter(&b)
						err = templ.Execute(buf, map[string]interface{}{
							"service":  service,
							"example":  example,
							"endpoint": endpoint,
							"funcName": strcase.UpperCamelCase(title),
						})

						err = os.MkdirAll(filepath.Join(examplesPath, serviceName, endpoint, "node"), 0777)
						if err != nil {
							fmt.Println(err)
							os.Exit(1)
						}
						tsExampleFile := filepath.Join(examplesPath, serviceName, endpoint, "node", title+".js")
						f, err = os.OpenFile(tsExampleFile, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0744)
						if err != nil {
							fmt.Println("Failed to open schema file", err)
							os.Exit(1)
						}

						buf.Flush()
						_, err = f.Write(b.Bytes())
						if err != nil {
							fmt.Println("Failed to append to schema file", err)
							os.Exit(1)
						}

						cmd = exec.Command("prettier", "-w", title+".js")
						cmd.Dir = filepath.Join(examplesPath, serviceName, endpoint, "node")
						outp, err = cmd.CombinedOutput()
						if err != nil {
							fmt.Println(fmt.Sprintf("Problem with '%v' example '%v': %v", serviceName, endpoint, string(outp)))
							os.Exit(1)
						}

						// curl example
						templ, err = template.New("curl" + serviceName + endpoint).Funcs(funcs).Parse(curlExampleTemplate)
						if err != nil {
							fmt.Println("Failed to unmarshal", err)
							os.Exit(1)
						}
						b = bytes.Buffer{}
						buf = bufio.NewWriter(&b)
						err = templ.Execute(buf, map[string]interface{}{
							"service":  service,
							"example":  example,
							"endpoint": endpoint,
							"funcName": strcase.UpperCamelCase(title),
						})

						err = os.MkdirAll(filepath.Join(examplesPath, serviceName, endpoint, "curl"), 0777)
						if err != nil {
							fmt.Println(err)
							os.Exit(1)
						}

						curlExampleFile := filepath.Join(examplesPath, serviceName, endpoint, "curl", title+".sh")
						f, err = os.OpenFile(curlExampleFile, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0744)
						if err != nil {
							fmt.Println("Failed to open schema file", err)
							os.Exit(1)
						}

						buf.Flush()
						_, err = f.Write(b.Bytes())
						if err != nil {
							fmt.Println("Failed to append to schema file", err)
							os.Exit(1)
						}
					}
					// only build after each example is generated as old files from
					// previous generation might not compile
					cmd = exec.Command("go", "build", "-o", "/tmp/bin/outputfile")
					cmd.Dir = filepath.Join(examplesPath, serviceName, endpoint, "go")
					outp, err = cmd.CombinedOutput()
					if err != nil {
						fmt.Println(fmt.Sprintf("Problem with '%v' example '%v': %v", serviceName, endpoint, string(outp)))
						os.Exit(1)
					}
				}
			} else {
				fmt.Println(err)
			}
		}
	}

	templ, err := template.New("tsclient").Funcs(funcs).Parse(tsIndexTemplate)
	if err != nil {
		fmt.Println("Failed to unmarshal", err)
		os.Exit(1)
	}
	var b bytes.Buffer
	buf := bufio.NewWriter(&b)
	err = templ.Execute(buf, map[string]interface{}{
		"services": services,
	})
	if err != nil {
		fmt.Println("Failed to unmarshal", err)
		os.Exit(1)
	}

	f, err := os.OpenFile(filepath.Join(tsPath, "index.ts"), os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0744)
	if err != nil {
		fmt.Println("Failed to open schema file", err)
		os.Exit(1)
	}
	buf.Flush()
	_, err = f.Write(b.Bytes())
	if err != nil {
		fmt.Println("Failed to append to schema file", err)
		os.Exit(1)
	}
	cmd := exec.Command("prettier", "-w", "index.ts")
	cmd.Dir = filepath.Join(tsPath)
	outp, err := cmd.CombinedOutput()
	if err != nil {
		fmt.Println(fmt.Sprintf("Problem with prettifying clients index.ts '%v", string(outp)))
		os.Exit(1)
	}
	tsFiles := filepath.Join(workDir, "cmd", "clients", "ts")
	cmd = exec.Command("cp", filepath.Join(tsFiles, "package.json"), filepath.Join(tsFiles, ".gitignore"), filepath.Join(tsFiles, "package-lock.json"), filepath.Join(tsFiles, "package-lock.json"), filepath.Join(tsFiles, "build.js"), filepath.Join(tsFiles, "tsconfig.es.json"), filepath.Join(tsFiles, "package-lock.json"), filepath.Join(tsFiles, "tsconfig.json"), filepath.Join(workDir, "clients", "ts"))
	cmd.Dir = filepath.Join(tsPath)
	outp, err = cmd.CombinedOutput()
	if err != nil {
		fmt.Println(fmt.Sprintf("Problem with prettifying clients index.ts '%v", string(outp)))
		os.Exit(1)
	}

	templ, err = template.New("goclient").Funcs(funcs).Parse(goIndexTemplate)
	if err != nil {
		fmt.Println("Failed to unmarshal", err)
		os.Exit(1)
	}
	b = bytes.Buffer{}
	buf = bufio.NewWriter(&b)
	err = templ.Execute(buf, map[string]interface{}{
		"services": services,
	})
	if err != nil {
		fmt.Println("Failed to unmarshal", err)
		os.Exit(1)
	}
	f, err = os.OpenFile(filepath.Join(goPath, "m3o.go"), os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0744)
	if err != nil {
		fmt.Println("Failed to open schema file", err)
		os.Exit(1)
	}
	buf.Flush()
	_, err = f.Write(b.Bytes())
	if err != nil {
		fmt.Println("Failed to append to schema file", err)
		os.Exit(1)
	}
	cmd = exec.Command("gofmt", "-w", "m3o.go")
	cmd.Dir = filepath.Join(goPath)
	outp, err = cmd.CombinedOutput()
	if err != nil {
		fmt.Println(fmt.Sprintf("Problem with formatting m3o.go '%v", string(outp)))
		os.Exit(1)
	}
	cmd = exec.Command("go", "build", "-o", "/tmp/bin/outputfile")
	cmd.Dir = filepath.Join(goPath)
	outp, err = cmd.CombinedOutput()
	if err != nil {
		fmt.Println(fmt.Sprintf("Problem building m3o.go '%v'", string(outp)))
		os.Exit(1)
	}

	// login to NPM
	f, err = os.OpenFile(filepath.Join(tsPath, ".npmrc"), os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0600)
	if err != nil {
		fmt.Println("Failed to open npmrc", err)
		os.Exit(1)
	}

	defer f.Close()
	if len(os.Getenv("NPM_TOKEN")) == 0 {
		fmt.Println("No NPM_TOKEN env found")
		os.Exit(1)
	}
	if _, err = f.WriteString("//registry.npmjs.org/:_authToken=" + os.Getenv("NPM_TOKEN")); err != nil {
		fmt.Println("Failed to open npmrc", err)
		os.Exit(1)
	}

	// get latest version from github
	getVersions := exec.Command("npm", "show", "m3o", "--time", "--json")
	getVersions.Dir = tsPath

	outp, err = getVersions.CombinedOutput()
	if err != nil {
		fmt.Println("Failed to get versions of NPM package", string(outp))
		os.Exit(1)
	}
	type npmVers struct {
		Versions []string `json:"versions"`
	}

	beta := os.Getenv("IS_BETA") != ""
	if beta {
		fmt.Println("creating beta version")
	} else {
		fmt.Println("creating live version")
	}

	npmOutput := &npmVers{}
	var latest *semver.Version
	if len(outp) > 0 {
		err = json.Unmarshal(outp, npmOutput)
		if err != nil {
			fmt.Println("Failed to unmarshal versions", string(outp))
			os.Exit(1)
		}
	}
	fmt.Println("npm output version: ", npmOutput.Versions)

	for _, version := range npmOutput.Versions {
		v, err := semver.NewVersion(version)
		if err != nil {
			fmt.Println("Failed to parse semver", err)
			os.Exit(1)
		}
		if latest == nil {
			latest = v
		}
		if v.GreaterThan(latest) {
			latest = v
		}

	}

	if latest == nil {
		fmt.Println("found no semver version")
		os.Exit(1)
	}

	var newV semver.Version
	if beta {
		// bump a beta version
		if strings.Contains(latest.String(), "beta") {
			newV = incBeta(*latest)
		} else {
			// make beta out of latest non beta version
			v, _ := semver.NewVersion(latest.IncPatch().String() + "-beta1")
			newV = *v
		}
	} else {
		newV = latest.IncPatch()
	}

	// add file list to gitignore
	f, err = os.OpenFile(filepath.Join(tsPath, ".gitignore"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0744)
	for _, sname := range tsFileList {
		_, err := f.Write([]byte(sname + "\n"))
		if err != nil {
			fmt.Println("failed to append service to gitignore", err)
			os.Exit(1)
		}
	}

	// bump package to latest version
	fmt.Println("Bumping to ", newV.String())
	repl := exec.Command("sed", "-i", "-e", "s/1.0.1/"+newV.String()+"/g", "package.json")
	repl.Dir = tsPath
	outp, err = repl.CombinedOutput()
	if err != nil {
		fmt.Println("Failed to make docs", string(outp))
		os.Exit(1)
	}

	// apppend exports to to package.json
	pak, err := ioutil.ReadFile(filepath.Join(tsPath, "package.json"))
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	m := map[string]interface{}{}
	err = json.Unmarshal(pak, &m)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	m["files"] = tsFileList
	pakJS, err := json.MarshalIndent(m, "", " ")
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	f, err = os.OpenFile(filepath.Join(tsPath, "package.json"), os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0744)
	if err != nil {
		fmt.Println("Failed to open package.json", err)
		os.Exit(1)
	}
	_, err = f.Write(pakJS)
	if err != nil {
		fmt.Println("Failed to write to package.json", err)
		os.Exit(1)
	}
}

func incBeta(ver semver.Version) semver.Version {
	s := ver.String()
	parts := strings.Split(s, "beta")
	if len(parts) < 2 {
		panic("not a beta version " + s)
	}
	i, err := strconv.ParseInt(parts[1], 10, 64)
	if err != nil {
		panic(err)
	}
	i++
	v, err := semver.NewVersion(parts[0] + "beta" + fmt.Sprintf("%v", i))
	if err != nil {
		panic(err)
	}
	return *v
}

func schemaToType(language, serviceName, typeName string, schemas map[string]*openapi3.SchemaRef) string {
	var recurse func(props map[string]*openapi3.SchemaRef, level int) string

	var spec *openapi3.SchemaRef = schemas[typeName]
	detectType := func(currentType string, properties map[string]*openapi3.SchemaRef) (string, bool) {
		index := map[string]bool{}
		for key, prop := range properties {
			index[key+prop.Value.Title+prop.Value.Description] = true
		}

		for k, schema := range schemas {
			// we don't want to return the type matching itself
			if strings.ToLower(k) == currentType {
				continue
			}
			if strings.HasSuffix(k, "Request") || strings.HasSuffix(k, "Response") {
				continue
			}
			if len(schema.Value.Properties) != len(properties) {
				continue
			}
			found := false
			for key, prop := range schema.Value.Properties {
				_, ok := index[key+prop.Value.Title+prop.Value.Description]
				found = ok
				if !ok {
					break
				}
			}
			if found {
				return schema.Value.Title, true
			}
		}
		return "", false
	}
	var fieldSeparator, arrayPrefix, arrayPostfix, fieldDelimiter, stringType, numberType, boolType string
	var int32Type, int64Type, floatType, doubleType, mapType, anyType, typePrefix string
	var fieldUpperCase bool
	switch language {
	case "typescript":
		fieldUpperCase = false
		fieldSeparator = "?: "
		arrayPrefix = ""
		arrayPostfix = "[]"
		//objectOpen = "{\n"
		//objectClose = "}"
		fieldDelimiter = ";"
		stringType = "string"
		numberType = "number"
		boolType = "boolean"
		int32Type = "number"
		int64Type = "number"
		floatType = "number"
		doubleType = "number"
		anyType = "any"
		mapType = "{ [key: string]: %v }"
		typePrefix = ""
	case "go":
		fieldUpperCase = true
		fieldSeparator = " "
		arrayPrefix = "[]"
		arrayPostfix = ""
		//objectOpen = "{"
		//	objectClose = "}"
		fieldDelimiter = ""
		stringType = "string"
		numberType = "int64"
		boolType = "bool"
		int32Type = "int32"
		int64Type = "int64"
		floatType = "float32"
		doubleType = "float64"
		mapType = "map[string]%v"
		anyType = "interface{}"
		typePrefix = "*"
	}

	valueToType := func(v *openapi3.SchemaRef) string {
		switch v.Value.Type {
		case "string":
			return stringType
		case "boolean":
			return boolType
		case "number":
			switch v.Value.Format {
			case "int32":
				return int32Type
			case "int64":
				return int64Type
			case "float":
				return floatType
			case "double":
				return doubleType
			}
		default:
			return "unrecognized: " + v.Value.Type
		}
		return ""
	}

	recurse = func(props map[string]*openapi3.SchemaRef, level int) string {
		ret := ""

		i := 0
		var keys []string
		for k := range props {
			keys = append(keys, k)
		}
		sort.Strings(keys)
		for _, k := range keys {
			v := props[k]
			ret += strings.Repeat("  ", level)
			if v.Value.Description != "" {
				for _, commentLine := range strings.Split(v.Value.Description, "\n") {
					ret += "// " + strings.TrimSpace(commentLine) + "\n" + strings.Repeat("  ", level)
				}

			}

			if fieldUpperCase {
				k = strcase.UpperCamelCase(k)
			}
			var typ string
			// @todo clean up this piece of code by
			// separating out type string marshaling and not
			// repeating code
			switch v.Value.Type {
			case "object":
				typ, found := detectType(k, v.Value.Properties)
				if found {
					ret += k + fieldSeparator + typePrefix + strings.Title(typ) + fieldDelimiter
				} else {
					// type is a dynamic map
					// if additional properties is not present, it's an any type,
					// like the proto struct type
					if v.Value.AdditionalProperties != nil {
						ret += k + fieldSeparator + fmt.Sprintf(mapType, valueToType(v.Value.AdditionalProperties)) + fieldDelimiter
					} else {
						ret += k + fieldSeparator + fmt.Sprintf(mapType, anyType) + fieldDelimiter
					}
				}
			case "array":
				typ, found := detectType(k, v.Value.Items.Value.Properties)
				if found {
					ret += k + fieldSeparator + arrayPrefix + strings.Title(typ) + arrayPostfix + fieldDelimiter
				} else {
					switch v.Value.Items.Value.Type {
					case "string":
						ret += k + fieldSeparator + arrayPrefix + stringType + arrayPostfix + fieldDelimiter
					case "number":
						typ := numberType
						switch v.Value.Format {
						case "int32":
							typ = int32Type
						case "int64":
							typ = int64Type
						case "float":
							typ = floatType
						case "double":
							typ = doubleType
						}
						ret += k + fieldSeparator + arrayPrefix + typ + arrayPostfix + fieldDelimiter
					case "boolean":
						ret += k + fieldSeparator + arrayPrefix + boolType + arrayPostfix + fieldDelimiter
					case "object":
						// type is a dynamic map
						// if additional properties is not present, it's an any type,
						// like the proto struct type
						if v.Value.AdditionalProperties != nil {
							ret += k + fieldSeparator + arrayPrefix + fmt.Sprintf(mapType, valueToType(v.Value.AdditionalProperties)) + arrayPostfix + fieldDelimiter
						} else {
							ret += k + fieldSeparator + arrayPrefix + fmt.Sprintf(mapType, anyType) + arrayPostfix + fieldDelimiter
						}
					}
				}
			case "string":
				ret += k + fieldSeparator + stringType + fieldDelimiter
			case "number":
				typ = numberType
				switch v.Value.Format {
				case "int32":
					typ = int32Type
				case "int64":
					typ = int64Type
				case "float":
					typ = floatType
				case "double":
					typ = doubleType
				}
				ret += k + fieldSeparator + typ + fieldDelimiter
			case "boolean":
				ret += k + fieldSeparator + boolType + fieldDelimiter
			}
			// go specific hack for lowercase json
			if language == "go" {
				ret += " " + "`json:\"" + strcase.LowerCamelCase(k)
				if typ == int64Type {
					ret += ",string"
				}
				ret += "\"`"
			}

			if i < len(props) {
				ret += "\n"
			}
			i++

		}
		return ret
	}
	return recurse(spec.Value.Properties, 1)
}

func schemaToMethods(title string, spec *openapi3.RequestBodyRef) string {
	return ""
}

// CopyFile copies a file from src to dst. If src and dst files exist, and are
// the same, then return success. Otherise, attempt to create a hard link
// between the two files. If that fail, copy the file contents from src to dst.
// from https://stackoverflow.com/questions/21060945/simple-way-to-copy-a-file-in-golang
func CopyFile(src, dst string) (err error) {
	sfi, err := os.Stat(src)
	if err != nil {
		return
	}
	if !sfi.Mode().IsRegular() {
		// cannot copy non-regular files (e.g., directories,
		// symlinks, devices, etc.)
		return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
	}
	dfi, err := os.Stat(dst)
	if err != nil {
		if !os.IsNotExist(err) {
			return
		}
	} else {
		if !(dfi.Mode().IsRegular()) {
			return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
		}
		if os.SameFile(sfi, dfi) {
			return
		}
	}
	if err = os.Link(src, dst); err == nil {
		return
	}
	err = copyFileContents(src, dst)
	return
}

// copyFileContents copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file.
func copyFileContents(src, dst string) (err error) {
	in, err := os.Open(src)
	if err != nil {
		return
	}
	defer in.Close()
	out, err := os.Create(dst)
	if err != nil {
		return
	}
	defer func() {
		cerr := out.Close()
		if err == nil {
			err = cerr
		}
	}()
	if _, err = io.Copy(out, in); err != nil {
		return
	}
	err = out.Sync()
	return
}
