package sherpats

import (
	"bufio"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/mjl-/sherpadoc"
)

// Keywords in Typescript, from https://github.com/microsoft/TypeScript/blob/master/doc/spec.md.
var keywords = map[string]struct{}{
	"break":       {},
	"case":        {},
	"catch":       {},
	"class":       {},
	"const":       {},
	"continue":    {},
	"debugger":    {},
	"default":     {},
	"delete":      {},
	"do":          {},
	"else":        {},
	"enum":        {},
	"export":      {},
	"extends":     {},
	"false":       {},
	"finally":     {},
	"for":         {},
	"function":    {},
	"if":          {},
	"import":      {},
	"in":          {},
	"instanceof":  {},
	"new":         {},
	"null":        {},
	"return":      {},
	"super":       {},
	"switch":      {},
	"this":        {},
	"throw":       {},
	"true":        {},
	"try":         {},
	"typeof":      {},
	"var":         {},
	"void":        {},
	"while":       {},
	"with":        {},
	"implements":  {},
	"interface":   {},
	"let":         {},
	"package":     {},
	"private":     {},
	"protected":   {},
	"public":      {},
	"static":      {},
	"yield":       {},
	"any":         {},
	"boolean":     {},
	"number":      {},
	"string":      {},
	"symbol":      {},
	"abstract":    {},
	"as":          {},
	"async":       {},
	"await":       {},
	"constructor": {},
	"declare":     {},
	"from":        {},
	"get":         {},
	"is":          {},
	"module":      {},
	"namespace":   {},
	"of":          {},
	"require":     {},
	"set":         {},
	"type":        {},
}

type sherpaType interface {
	TypescriptType() string
}

// baseType can be one of: "any", "int16", etc
type baseType struct {
	Name string
}

// nullableType is: "nullable" <type>.
type nullableType struct {
	Type sherpaType
}

// arrayType is: "[]" <type>
type arrayType struct {
	Type sherpaType
}

// objectType is: "{}" <type>
type objectType struct {
	Value sherpaType
}

// identType is: [a-zA-Z][a-zA-Z0-9]*
type identType struct {
	Name string
}

func (t baseType) TypescriptType() string {
	switch t.Name {
	case "bool":
		return "boolean"
	case "timestamp":
		return "Date"
	case "int8", "uint8", "int16", "uint16", "int32", "uint32", "int64", "uint64", "float32", "float64":
		return "number"
	case "int64s", "uint64s":
		return "string"
	default:
		return t.Name
	}
}

func isBaseOrIdent(t sherpaType) bool {
	if _, ok := t.(baseType); ok {
		return true
	}
	if _, ok := t.(identType); ok {
		return true
	}
	return false
}

func (t nullableType) TypescriptType() string {
	if isBaseOrIdent(t.Type) {
		return t.Type.TypescriptType() + " | null"
	}
	return "(" + t.Type.TypescriptType() + ") | null"
}

func (t arrayType) TypescriptType() string {
	if isBaseOrIdent(t.Type) {
		return t.Type.TypescriptType() + "[] | null"
	}
	return "(" + t.Type.TypescriptType() + ")[] | null"
}

func (t objectType) TypescriptType() string {
	return fmt.Sprintf("{ [key: string]: %s }", t.Value.TypescriptType())
}

func (t identType) TypescriptType() string {
	return t.Name
}

type genError struct{ error }

type Options struct {
	// If not empty, the generated typescript is wrapped in a namespace. This allows
	// easy compilation, with "tsc --module none" that uses the generated typescript
	// api, while keeping all types/functions isolated.
	Namespace string

	// With SlicesNullable and MapsNullable, generated typescript types are made
	// nullable, with "| null". Go's JSON package marshals a nil slice/map to null, so
	// it can be wise to make TypeScript consumers check that. Go code typically
	// handles incoming nil and empty slices/maps in the same way.
	SlicesNullable bool
	MapsNullable   bool

	// If nullables are optional, the generated typescript types allow the "undefined"
	// value where nullable values are expected. This includes slices/maps when
	// SlicesNullable/MapsNullable is set. When JavaScript marshals JSON, a field with the
	// "undefined" value is treated as if the field doesn't exist, and isn't
	// marshalled. The "undefined" value in an array is marshalled as null. It is
	// common (though not always the case!) in Go server code to not make a difference
	// between a missing field and a null value
	NullableOptional bool

	// If set, "[]uint8" is changed into "string" before before interpreting the
	// sherpadoc definitions. Go's JSON marshaller turns []byte (which is []uint8) into
	// base64 strings. Having the same types in TypeScript is convenient.
	// If SlicesNullable is set, the strings are made nullable.
	BytesToString bool
}

// Generate reads sherpadoc from in and writes a typescript file containing a
// client package to out.  apiNameBaseURL is either an API name or sherpa
// baseURL, depending on whether it contains a slash. If it is a package name, the
// baseURL is created at runtime by adding the packageName to the current location.
func Generate(in io.Reader, out io.Writer, apiNameBaseURL string, opts Options) (retErr error) {
	defer func() {
		e := recover()
		if e == nil {
			return
		}
		g, ok := e.(genError)
		if !ok {
			panic(e)
		}
		retErr = error(g)
	}()

	var doc sherpadoc.Section
	err := json.NewDecoder(os.Stdin).Decode(&doc)
	if err != nil {
		panic(genError{fmt.Errorf("parsing sherpadoc json: %s", err)})
	}

	const sherpadocVersion = 1
	if doc.SherpadocVersion != sherpadocVersion {
		panic(genError{fmt.Errorf("unexpected sherpadoc version %d, expected %d", doc.SherpadocVersion, sherpadocVersion)})
	}

	if opts.BytesToString {
		toString := func(tw []string) []string {
			n := len(tw) - 1
			for i := 0; i < n; i++ {
				if tw[i] == "[]" && tw[i+1] == "uint8" {
					if opts.SlicesNullable && (i == 0 || tw[i-1] != "nullable") {
						tw[i] = "nullable"
						tw[i+1] = "string"
						i++
					} else {
						tw[i] = "string"
						copy(tw[i+1:], tw[i+2:])
						tw = tw[:len(tw)-1]
						n--
					}
				}
			}
			return tw
		}

		var bytesToString func(sec *sherpadoc.Section)
		bytesToString = func(sec *sherpadoc.Section) {
			for i := range sec.Functions {
				for j := range sec.Functions[i].Params {
					sec.Functions[i].Params[j].Typewords = toString(sec.Functions[i].Params[j].Typewords)
				}
				for j := range sec.Functions[i].Returns {
					sec.Functions[i].Returns[j].Typewords = toString(sec.Functions[i].Returns[j].Typewords)
				}
			}
			for i := range sec.Structs {
				for j := range sec.Structs[i].Fields {
					sec.Structs[i].Fields[j].Typewords = toString(sec.Structs[i].Fields[j].Typewords)
				}
			}
			for _, s := range sec.Sections {
				bytesToString(s)
			}
		}
		bytesToString(&doc)
	}

	// Validate the sherpadoc.
	err = sherpadoc.Check(&doc)
	if err != nil {
		panic(genError{err})
	}

	// Make a copy, the ugly way. We'll strip the documentation out before including
	// the types. We need types for runtime type checking, but the docs just bloat the
	// size.
	var typesdoc sherpadoc.Section
	if typesbuf, err := json.Marshal(doc); err != nil {
		panic(genError{fmt.Errorf("marshal sherpadoc for types: %s", err)})
	} else if err := json.Unmarshal(typesbuf, &typesdoc); err != nil {
		panic(genError{fmt.Errorf("unmarshal sherpadoc for types: %s", err)})
	}
	for i := range typesdoc.Structs {
		typesdoc.Structs[i].Docs = ""
		for j := range typesdoc.Structs[i].Fields {
			typesdoc.Structs[i].Fields[j].Docs = ""
		}
	}
	for i := range typesdoc.Ints {
		typesdoc.Ints[i].Docs = ""
		for j := range typesdoc.Ints[i].Values {
			typesdoc.Ints[i].Values[j].Docs = ""
		}
	}
	for i := range typesdoc.Strings {
		typesdoc.Strings[i].Docs = ""
		for j := range typesdoc.Strings[i].Values {
			typesdoc.Strings[i].Values[j].Docs = ""
		}
	}

	bout := bufio.NewWriter(out)
	xprintf := func(format string, args ...interface{}) {
		_, err := fmt.Fprintf(out, format, args...)
		if err != nil {
			panic(genError{err})
		}
	}

	xprintMultiline := func(indent, docs string, always bool) []string {
		lines := docLines(docs)
		if len(lines) == 1 && !always {
			return lines
		}
		for _, line := range lines {
			xprintf("%s// %s\n", indent, line)
		}
		return lines
	}

	xprintSingleline := func(lines []string) {
		if len(lines) != 1 {
			return
		}
		xprintf("  // %s", lines[0])
	}

	// Type and function names could be typescript keywords. If they are, give them a different name.
	typescriptNames := map[string]string{}
	typescriptName := func(name string, names map[string]string) string {
		if _, ok := keywords[name]; !ok {
			return name
		}
		n := names[name]
		if n != "" {
			return n
		}
		for i := 0; ; i++ {
			n = fmt.Sprintf("%s%d", name, i)
			if _, ok := names[n]; ok {
				continue
			}
			names[name] = n
			return n
		}
	}

	structTypes := map[string]bool{}
	stringsTypes := map[string]bool{}
	intsTypes := map[string]bool{}

	var generateTypes func(sec *sherpadoc.Section)
	generateTypes = func(sec *sherpadoc.Section) {
		for _, t := range sec.Structs {
			structTypes[t.Name] = true
			xprintMultiline("", t.Docs, true)
			name := typescriptName(t.Name, typescriptNames)
			xprintf("export interface %s {\n", name)
			names := map[string]string{}
			for _, f := range t.Fields {
				lines := xprintMultiline("", f.Docs, false)
				what := fmt.Sprintf("field %s for type %s", f.Name, t.Name)
				optional := ""
				if opts.NullableOptional && f.Typewords[0] == "nullable" || opts.NullableOptional && (opts.SlicesNullable && f.Typewords[0] == "[]" || opts.MapsNullable && f.Typewords[0] == "{}") {
					optional = "?"
				}
				xprintf("\t%s%s: %s", typescriptName(f.Name, names), optional, typescriptType(what, f.Typewords))
				xprintSingleline(lines)
				xprintf("\n")
			}
			xprintf("}\n\n")
		}

		for _, t := range sec.Ints {
			intsTypes[t.Name] = true
			xprintMultiline("", t.Docs, true)
			name := typescriptName(t.Name, typescriptNames)
			if len(t.Values) == 0 {
				xprintf("export type %s = number\n\n", name)
				continue
			}
			xprintf("export enum %s {\n", name)
			names := map[string]string{}
			for _, v := range t.Values {
				lines := xprintMultiline("\t", v.Docs, false)
				xprintf("\t%s = %d,", typescriptName(v.Name, names), v.Value)
				xprintSingleline(lines)
				xprintf("\n")
			}
			xprintf("}\n\n")
		}

		for _, t := range sec.Strings {
			stringsTypes[t.Name] = true
			xprintMultiline("", t.Docs, true)
			name := typescriptName(t.Name, typescriptNames)
			if len(t.Values) == 0 {
				xprintf("export type %s = string\n\n", name)
				continue
			}
			xprintf("export enum %s {\n", name)
			names := map[string]string{}
			for _, v := range t.Values {
				lines := xprintMultiline("\t", v.Docs, false)
				s := mustMarshalJSON(v.Value)
				xprintf("\t%s = %s,", typescriptName(v.Name, names), s)
				xprintSingleline(lines)
				xprintf("\n")
			}
			xprintf("}\n\n")
		}

		for _, subsec := range sec.Sections {
			generateTypes(subsec)
		}
	}

	var generateFunctionTypes func(sec *sherpadoc.Section)
	generateFunctionTypes = func(sec *sherpadoc.Section) {
		for _, typ := range sec.Structs {
			xprintf("	%s: %s,\n", mustMarshalJSON(typ.Name), mustMarshalJSON(typ))
		}
		for _, typ := range sec.Ints {
			xprintf("	%s: %s,\n", mustMarshalJSON(typ.Name), mustMarshalJSON(typ))
		}
		for _, typ := range sec.Strings {
			xprintf("	%s: %s,\n", mustMarshalJSON(typ.Name), mustMarshalJSON(typ))
		}

		for _, subsec := range sec.Sections {
			generateFunctionTypes(subsec)
		}
	}

	var generateParser func(sec *sherpadoc.Section)
	generateParser = func(sec *sherpadoc.Section) {
		for _, typ := range sec.Structs {
			xprintf("	%s: (v: any) => parse(%s, v) as %s,\n", typ.Name, mustMarshalJSON(typ.Name), typ.Name)
		}
		for _, typ := range sec.Ints {
			xprintf("	%s: (v: any) => parse(%s, v) as %s,\n", typ.Name, mustMarshalJSON(typ.Name), typ.Name)
		}
		for _, typ := range sec.Strings {
			xprintf("	%s: (v: any) => parse(%s, v) as %s,\n", typ.Name, mustMarshalJSON(typ.Name), typ.Name)
		}

		for _, subsec := range sec.Sections {
			generateParser(subsec)
		}
	}

	var generateSectionDocs func(sec *sherpadoc.Section)
	generateSectionDocs = func(sec *sherpadoc.Section) {
		xprintMultiline("", sec.Docs, true)
		for _, subsec := range sec.Sections {
			xprintf("//\n")
			xprintf("// # %s\n", subsec.Name)
			generateSectionDocs(subsec)
		}
	}

	var generateFunctions func(sec *sherpadoc.Section)
	generateFunctions = func(sec *sherpadoc.Section) {
		for i, fn := range sec.Functions {
			whatParam := "pararameter for " + fn.Name
			paramNameTypes := []string{}
			paramNames := []string{}
			sherpaParamTypes := [][]string{}
			names := map[string]string{}
			for _, p := range fn.Params {
				name := typescriptName(p.Name, names)
				v := fmt.Sprintf("%s: %s", name, typescriptType(whatParam, p.Typewords))
				paramNameTypes = append(paramNameTypes, v)
				paramNames = append(paramNames, name)
				sherpaParamTypes = append(sherpaParamTypes, p.Typewords)
			}

			var returnType string
			switch len(fn.Returns) {
			case 0:
				returnType = "void"
			case 1:
				what := "return type for " + fn.Name
				returnType = typescriptType(what, fn.Returns[0].Typewords)
			default:
				var types []string
				what := "return type for " + fn.Name
				for _, t := range fn.Returns {
					types = append(types, typescriptType(what, t.Typewords))
				}
				returnType = fmt.Sprintf("[%s]", strings.Join(types, ", "))
			}
			sherpaReturnTypes := [][]string{}
			for _, a := range fn.Returns {
				sherpaReturnTypes = append(sherpaReturnTypes, a.Typewords)
			}

			name := typescriptName(fn.Name, typescriptNames)
			xprintMultiline("\t", fn.Docs, true)
			xprintf("\tasync %s(%s): Promise<%s> {\n", name, strings.Join(paramNameTypes, ", "), returnType)
			xprintf("\t\tconst fn: string = %s\n", mustMarshalJSON(fn.Name))
			xprintf("\t\tconst paramTypes: string[][] = %s\n", mustMarshalJSON(sherpaParamTypes))
			xprintf("\t\tconst returnTypes: string[][] = %s\n", mustMarshalJSON(sherpaReturnTypes))
			xprintf("\t\tconst params: any[] = [%s]\n", strings.Join(paramNames, ", "))
			xprintf("\t\treturn await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as %s\n", returnType)
			xprintf("\t}\n")
			if i < len(sec.Functions)-1 {
				xprintf("\n")
			}
		}

		for _, s := range sec.Sections {
			generateFunctions(s)
		}
	}

	xprintf("// NOTE: GENERATED by github.com/mjl-/sherpats, DO NOT MODIFY\n\n")
	if opts.Namespace != "" {
		xprintf("namespace %s {\n\n", opts.Namespace)
	}
	generateTypes(&doc)
	xprintf("export const structTypes: {[typename: string]: boolean} = %s\n", mustMarshalJSON(structTypes))
	xprintf("export const stringsTypes: {[typename: string]: boolean} = %s\n", mustMarshalJSON(stringsTypes))
	xprintf("export const intsTypes: {[typename: string]: boolean} = %s\n", mustMarshalJSON(intsTypes))
	xprintf("export const types: TypenameMap = {\n")
	generateFunctionTypes(&typesdoc)
	xprintf("}\n\n")
	xprintf("export const parser = {\n")
	generateParser(&doc)
	xprintf("}\n\n")
	generateSectionDocs(&doc)
	xprintf(`let defaultOptions: ClientOptions = {slicesNullable: %v, mapsNullable: %v, nullableOptional: %v}

export class Client {
	private baseURL: string
	public authState: AuthState
	public options: ClientOptions

	constructor() {
		this.authState = {}
		this.options = {...defaultOptions}
		this.baseURL = this.options.baseURL || defaultBaseURL
	}

	withAuthToken(token: string): Client {
		const c = new Client()
		c.authState.token = token
		c.options = this.options
		return c
	}

	withOptions(options: ClientOptions): Client {
		const c = new Client()
		c.authState = this.authState
		c.options = { ...this.options, ...options }
		return c
	}

`, opts.SlicesNullable, opts.MapsNullable, opts.NullableOptional)
	generateFunctions(&doc)
	xprintf("}\n\n")

	const findBaseURL = `(function() {
	let p = location.pathname
	if (p && p[p.length - 1] !== '/') {
		let l = location.pathname.split('/')
		l = l.slice(0, l.length - 1)
		p = '/' + l.join('/') + '/'
	}
	return location.protocol + '//' + location.host + p + 'API_NAME/'
})()`

	var apiJS string
	if strings.Contains(apiNameBaseURL, "/") {
		apiJS = mustMarshalJSON(apiNameBaseURL)
	} else {
		apiJS = strings.Replace(findBaseURL, "API_NAME", apiNameBaseURL, -1)
	}
	xprintf("%s\n", strings.Replace(libTS, "BASEURL", apiJS, -1))
	if opts.Namespace != "" {
		xprintf("}\n")
	}

	err = bout.Flush()
	if err != nil {
		panic(genError{err})
	}
	return nil
}

func typescriptType(what string, typeTokens []string) string {
	t := parseType(what, typeTokens)
	return t.TypescriptType()
}

func parseType(what string, tokens []string) sherpaType {
	checkOK := func(ok bool, v interface{}, msg string) {
		if !ok {
			panic(genError{fmt.Errorf("invalid type for %s: %s, saw %q", what, msg, v)})
		}
	}
	checkOK(len(tokens) > 0, tokens, "need at least one element")
	s := tokens[0]
	tokens = tokens[1:]
	switch s {
	case "any", "bool", "int8", "uint8", "int16", "uint16", "int32", "uint32", "int64", "uint64", "int64s", "uint64s", "float32", "float64", "string", "timestamp":
		if len(tokens) != 0 {
			checkOK(false, tokens, "leftover tokens after base type")
		}
		return baseType{s}
	case "nullable":
		return nullableType{parseType(what, tokens)}
	case "[]":
		return arrayType{parseType(what, tokens)}
	case "{}":
		return objectType{parseType(what, tokens)}
	default:
		if len(tokens) != 0 {
			checkOK(false, tokens, "leftover tokens after identifier type")
		}
		return identType{s}
	}
}

func docLines(s string) []string {
	s = strings.TrimSpace(s)
	if s == "" {
		return nil
	}
	return strings.Split(s, "\n")
}

func mustMarshalJSON(v interface{}) string {
	buf, err := json.Marshal(v)
	if err != nil {
		panic(genError{fmt.Errorf("marshalling json: %s", err)})
	}
	return string(buf)
}
