package tygoja

import (
	"fmt"
	"log"
	"regexp"
	"strings"

	"go/ast"
	"go/token"
)

// Options for the writeType() method that can be used for extra context
// to determine the format of the return type.
const (
	optionExtends        = "extends"
	optionParenthesis    = "parenthesis"
	optionFunctionReturn = "func_return"
)

func (g *PackageGenerator) writeIndent(s *strings.Builder, depth int) {
	for i := 0; i < depth; i++ {
		s.WriteString(g.conf.Indent)
	}
}

func (g *PackageGenerator) writeStartModifier(s *strings.Builder, depth int) {
	g.writeIndent(s, depth)

	if g.conf.StartModifier != "" {
		s.WriteString(g.conf.StartModifier)
		s.WriteString(" ")
	}
}

func (g *PackageGenerator) writeType(s *strings.Builder, t ast.Expr, depth int, options ...string) {
	switch t := t.(type) {
	case *ast.StarExpr:
		if hasOption(optionParenthesis, options) {
			s.WriteByte('(')
		}

		g.writeType(s, t.X, depth)

		// allow undefined union only when not used in an "extends" expression or as return type
		if !hasOption(optionExtends, options) && !hasOption(optionFunctionReturn, options) {
			s.WriteString(" | undefined")
		}

		if hasOption(optionParenthesis, options) {
			s.WriteByte(')')
		}
	case *ast.Ellipsis:
		if v, ok := t.Elt.(*ast.Ident); ok && v.String() == "byte" {
			s.WriteString("string")
			break
		}

		// wrap variadic args with parenthesis to support function declarations
		// (eg. "...callbacks: (() => number)[]")
		_, isFunc := t.Elt.(*ast.FuncType)
		if isFunc {
			s.WriteString("(")
		}

		g.writeType(s, t.Elt, depth, optionParenthesis)

		if isFunc {
			s.WriteString(")")
		}

		s.WriteString("[]")
	case *ast.ArrayType:
		if v, ok := t.Elt.(*ast.Ident); ok && v.String() == "byte" && !hasOption(optionExtends, options) {
			// union type with string since depending where it is used
			// goja auto converts string to []byte if the field expect that
			s.WriteString("string|")
		}

		s.WriteString("Array<")
		g.writeType(s, t.Elt, depth, optionParenthesis)
		s.WriteString(">")
	case *ast.StructType:
		s.WriteString("{\n")
		g.writeStructFields(s, t.Fields.List, depth+1)
		g.writeIndent(s, depth+1)
		s.WriteByte('}')
	case *ast.Ident:
		v := t.String()

		mappedType, ok := g.conf.TypeMappings[v]
		if ok {
			// use the mapped type
			v = mappedType
		} else {
			// try to find a matching js equivalent
			switch v {
			case "string":
				v = "string"
			case "bool":
				v = "boolean"
			case "int", "int8", "int16", "int32", "int64",
				"uint", "uint8", "uint16", "uint32", "uint64",
				"float32", "float64",
				"complex64", "complex128",
				"uintptr", "byte", "rune":
				v = "number"
			case "error":
				v = "Error"
			default:
				g.unknownTypes[v] = struct{}{}
			}
		}

		s.WriteString(v)
	case *ast.SelectorExpr:
		// e.g. `unsafe.Pointer` or `unsafe.*`
		fullType := fmt.Sprintf("%s.%s", t.X, t.Sel)
		fullTypeWildcard := fmt.Sprintf("%s.*", t.X)

		if v, ok := g.conf.TypeMappings[fullType]; ok {
			s.WriteString(v)
		} else if v, ok := g.conf.TypeMappings[fullTypeWildcard]; ok {
			s.WriteString(v)
		} else {
			g.unknownTypes[fullType] = struct{}{}
			s.WriteString(fullType)
		}
	case *ast.MapType:
		s.WriteString("_TygojaDict")
	case *ast.BasicLit:
		s.WriteString(t.Value)
	case *ast.ParenExpr:
		s.WriteByte('(')
		g.writeType(s, t.X, depth)
		s.WriteByte(')')
	case *ast.BinaryExpr:
		g.writeType(s, t.X, depth)
		s.WriteByte(' ')
		s.WriteString(t.Op.String())
		s.WriteByte(' ')
		g.writeType(s, t.Y, depth)
	case *ast.InterfaceType:
		s.WriteString("{\n")
		g.writeInterfaceFields(s, t.Methods.List, depth)
		g.writeIndent(s, depth+1)
		s.WriteByte('}')
	case *ast.FuncType:
		g.writeFuncType(s, t, depth, hasOption(optionParenthesis, options))
	case *ast.UnaryExpr:
		if t.Op == token.TILDE {
			// we just ignore the tilde token, in Typescript extended types are
			// put into the generic typing itself, which we can't support yet.
			g.writeType(s, t.X, depth)
		} else {
			// only log for now
			log.Printf("unhandled unary expr: %v\n %T\n", t, t)
		}
	case *ast.IndexListExpr:
		g.writeType(s, t.X, depth)
		s.WriteByte('<')
		for i, index := range t.Indices {
			g.writeType(s, index, depth)
			if i != len(t.Indices)-1 {
				s.WriteString(", ")
			}
		}
		s.WriteByte('>')
	case *ast.IndexExpr:
		g.writeType(s, t.X, depth)
		s.WriteByte('<')
		g.writeType(s, t.Index, depth)
		s.WriteByte('>')
	case *ast.CallExpr, *ast.ChanType, *ast.CompositeLit:
		s.WriteString("undefined")
	default:
		s.WriteString("any")
	}
}

func (g *PackageGenerator) writeTypeParamsFields(s *strings.Builder, fields []*ast.Field) {
	// extract params
	names := []string{}
	for _, f := range fields {
		for _, ident := range f.Names {
			names = append(names, ident.Name)

			// disable extends for now as it complicates the interfaces merge
			//
			// s.WriteString(" extends ")
			// g.writeType(s, f.Type, 0, true)
			// if i != len(fields)-1 || j != len(f.Names)-1 {
			// 	s.WriteString(", ")
			// }
		}
	}

	if len(names) == 0 {
		return
	}

	s.WriteByte('<')

	for i, name := range names {
		if i > 0 {
			s.WriteString(",")
		}
		s.WriteString(name)
	}

	s.WriteByte('>')
}

func (g *PackageGenerator) writeInterfaceFields(s *strings.Builder, fields []*ast.Field, depth int) {
	for _, f := range fields {
		g.writeCommentGroup(s, f.Doc, depth+1)

		var methodName string
		if len(f.Names) != 0 && f.Names[0] != nil && len(f.Names[0].Name) != 0 {
			methodName = f.Names[0].Name
		}
		if len(methodName) == 0 || 'A' > methodName[0] || methodName[0] > 'Z' {
			continue
		}

		if g.conf.MethodNameFormatter != nil {
			methodName = g.conf.MethodNameFormatter(methodName)
		}

		g.writeIndent(s, depth+1)
		s.WriteString(methodName)
		g.writeType(s, f.Type, depth)

		if f.Comment != nil {
			s.WriteString(" // ")
			s.WriteString(f.Comment.Text())
		} else {
			s.WriteByte('\n')
		}
	}
}

func (g *PackageGenerator) writeStructFields(s *strings.Builder, fields []*ast.Field, depth int) {
	for _, f := range fields {
		var fieldName string
		if len(f.Names) != 0 && f.Names[0] != nil && len(f.Names[0].Name) != 0 {
			fieldName = f.Names[0].Name
		}
		if len(fieldName) == 0 || 'A' > fieldName[0] || fieldName[0] > 'Z' {
			continue
		}

		if g.conf.FieldNameFormatter != nil {
			fieldName = g.conf.FieldNameFormatter(fieldName)
		}

		g.writeCommentGroup(s, f.Doc, depth+1)

		g.writeIndent(s, depth+1)
		quoted := !isValidJSName(fieldName)
		if quoted {
			s.WriteByte('\'')
		}
		s.WriteString(fieldName)
		if quoted {
			s.WriteByte('\'')
		}

		// check if it is nil-able, aka. optional
		switch t := f.Type.(type) {
		case *ast.StarExpr:
			f.Type = t.X
			s.WriteByte('?')
		}

		s.WriteString(": ")
		g.writeType(s, f.Type, depth, optionParenthesis)

		if f.Comment != nil {
			// Line comment is present, that means a comment after the field.
			s.WriteString(" // ")
			s.WriteString(f.Comment.Text())
		} else {
			s.WriteByte('\n')
		}
	}
}

func (g *PackageGenerator) writeFuncType(s *strings.Builder, t *ast.FuncType, depth int, returnAsProp bool) {
	s.WriteString("(")

	if t.Params != nil {
		g.writeFuncParams(s, t.Params.List, depth)
	}

	if returnAsProp {
		s.WriteString(") => ")
	} else {
		s.WriteString("): ")
	}

	// (from https://pkg.go.dev/github.com/dop251/goja)
	// Functions with multiple return values return an Array.
	// If the last return value is an `error` it is not returned but converted into a JS exception.
	// If the error is *Exception, it is thrown as is, otherwise it's wrapped in a GoEerror.
	// Note that if there are exactly two return values and the last is an `error`,
	// the function returns the first value as is, not an Array.
	if t.Results == nil || len(t.Results.List) == 0 {
		s.WriteString("void")
	} else {
		// remove the last return error type
		lastReturn, ok := t.Results.List[len(t.Results.List)-1].Type.(*ast.Ident)
		if ok && lastReturn.Name == "error" {
			t.Results.List = t.Results.List[:len(t.Results.List)-1]
		}

		if len(t.Results.List) == 0 {
			s.WriteString("void")
		} else {
			// multiple and shortened return type values must be wrapped in []
			// (combined/shortened return values from the same type are part of a single ast.Field but with different names)
			hasMultipleReturnValues := len(t.Results.List) > 1 || len(t.Results.List[0].Names) > 1
			if hasMultipleReturnValues {
				s.WriteRune('[')
			}

			for i, f := range t.Results.List {
				totalNames := max(len(f.Names), 1)
				for j := range totalNames {
					if i > 0 || j > 0 {
						s.WriteString(", ")
					}

					g.writeType(s, f.Type, 0, optionParenthesis, optionFunctionReturn)
				}
			}

			if hasMultipleReturnValues {
				s.WriteRune(']')
			}
		}
	}
}

func (g *PackageGenerator) writeFuncParams(s *strings.Builder, params []*ast.Field, depth int) {
	for i, f := range params {
		// normalize params iteration
		// (params with omitted types will be part of a single ast.Field but with different names)
		names := make([]string, 0, len(f.Names))
		for j, ident := range f.Names {
			name := ident.Name
			if name == "" || isReservedIdentifier(name) {
				name = fmt.Sprintf("_arg%d%d", i, j)
			}
			names = append(names, name)
		}
		if len(names) == 0 {
			// ommitted param name (eg. func(string))
			names = append(names, fmt.Sprintf("_arg%d", i))
		}

		for j, fieldName := range names {
			if i > 0 || j > 0 {
				s.WriteString(", ")
			}

			var isVariadic bool

			switch t := f.Type.(type) {
			case *ast.StarExpr:
				f.Type = t.X
			case *ast.Ellipsis:
				isVariadic = true
			}

			g.writeCommentGroup(s, f.Doc, depth+2)
			if isVariadic {
				s.WriteString("...")
			}
			s.WriteString(fieldName)

			s.WriteString(": ")

			g.writeType(s, f.Type, depth, optionParenthesis)

			if f.Comment != nil {
				// Line comment is present, that means a comment after the field.
				s.WriteString(" /* ")
				s.WriteString(f.Comment.Text())
				s.WriteString(" */ ")
			}
		}
	}
}

// see https://es5.github.io/#x7.6.1.1
var reservedIdentifiers = map[string]struct{}{
	"break":      {},
	"do":         {},
	"instanceof": {},
	"typeof":     {},
	"case":       {},
	"else":       {},
	"new":        {},
	"var":        {},
	"catch":      {},
	"finally":    {},
	"return":     {},
	"void":       {},
	"continue":   {},
	"for":        {},
	"switch":     {},
	"while":      {},
	"debugger":   {},
	"function":   {},
	"this":       {},
	"with":       {},
	"default":    {},
	"if":         {},
	"throw":      {},
	"delete":     {},
	"in":         {},
	"try":        {},
	"class":      {},
	"enum":       {},
	"extends":    {},
	"super":      {},
	"const":      {},
	"export":     {},
	"import":     {},
	"implements": {},
	"let":        {},
	"private":    {},
	"public":     {},
	"yield":      {},
	"interface":  {},
	"package":    {},
	"protected":  {},
	"static":     {},
}

func isReservedIdentifier(name string) bool {
	_, ok := reservedIdentifiers[name]
	return ok
}

var isValidJSNameRegexp = regexp.MustCompile(`(?m)^[\pL_][\pL\pN_]*$`)

func isValidJSName(name string) bool {
	return isReservedIdentifier(name) || isValidJSNameRegexp.MatchString(name)
}

func hasOption(opt string, options []string) bool {
	for _, o := range options {
		if o == opt {
			return true
		}
	}

	return false
}
