/*
Copyright 2018 Caicloud Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package golang

import (
	"fmt"
	"go/token"
	"path"
	"reflect"
	"sort"
	"strconv"
	"strings"

	"github.com/caicloud/nirvana/definition"
	"github.com/caicloud/nirvana/service"
	"github.com/caicloud/nirvana/utils/api"
	"github.com/caicloud/nirvana/utils/generators/utils"
)

type parameterExtension struct {
	Source string
	Name   string
	Key    string
}

type functionParameter struct {
	Source string
	// Name is the name in the Parameter of the Definition, used in the generated function body.
	Name string
	// ProposedName is the parameter name of the generated function.
	ProposedName string
	Typ          string
	Extensions   []parameterExtension
}

type functionResult struct {
	Destination  string
	ProposedName string
	Typ          string
	Creator      string
}

type function struct {
	Path       string
	Method     string
	Code       int
	Name       string
	Comments   string
	Parameters []functionParameter
	Results    []functionResult
}

// helper provides methods to help to generate codes.
type helper struct {
	definitions *api.Definitions
	namer       *typeNamer
	rootPkg     string
}

// newHelper creates a generator helper.
func newHelper(rootPkg string, definitions *api.Definitions) (*helper, error) {
	namer, err := newTypeNamer(rootPkg, definitions.Types)
	if err != nil {
		return nil, err
	}
	return &helper{definitions, namer, rootPkg}, nil
}

// packages generates a list of imported packages with aliases.
func (h *helper) packages(types []*api.Type, extended bool) []string {
	pkgMap := map[string]bool{}
	for _, typ := range types {
		pkgs := h.pkgs(typ, extended)
		for _, pkg := range pkgs {
			pkgMap[pkg] = true
		}
	}
	results := make([]string, 0, len(pkgMap))
	for pkg := range pkgMap {
		alias := h.namer.Alias(pkg)
		results = append(results, fmt.Sprintf(`%s "%s"`, alias, pkg))
	}
	return results
}

// pkgs generates a list of imported packages without aliases.
func (h *helper) pkgs(typ *api.Type, extended bool) []string {
	// handle third-party types and cases where the referenced type is an array/map alias
	// eg:
	//   import (
	//	     "go.mongodb.org/mongo-driver/bson/primitive"
	//   )
	//
	//   type XXX struct {
	//	     ID primitive.ObjectID `json:"_id"`
	//   }
	// definition of ObjectID:
	//   type ObjectID [12]byte
	// for this type, you don't need to import the package of its child, just the package itself
	if typ.PkgPath != "" {
		index := strings.LastIndex(typ.PkgPath, "/vendor/")
		pkg := typ.PkgPath
		if index > 0 {
			pkg = pkg[index+len("/vendor/"):]
		}
		pkgs := []string{pkg}

		if extended && typ.Kind == reflect.Struct {
			localType := fmt.Sprintf("%s.%s", typ.PkgPath, typ.Name)
			for _, field := range typ.Fields {
				// The name may be composed of pkgPath and name, when the strcut
				// contains itself recursive, such as slice ptr map, the functions pkgs will be invoked infinitely,
				// ignore it.
				// TODO the format of the name is error, should correct it.
				childType, ok := h.definitions.Types[field.Type]
				if !ok {
					continue
				}
				if strings.HasSuffix(childType.Name, localType) {
					continue
				}
				pkgs = append(pkgs, h.pkgs(childType, extended)...)
			}
		}
		return pkgs
	}

	// handle normal array/map definitions, eg:
	//   Metas   []*v1.ObjectMeta      `json:"metas"`
	//   Objects map[string]*v1.Object `json:"objects"`
	switch typ.Kind {
	case reflect.Array, reflect.Slice, reflect.Ptr:
		return h.pkgs(h.definitions.Types[typ.Elem], extended)
	case reflect.Map:
		return append(h.pkgs(h.definitions.Types[typ.Key], extended), h.pkgs(h.definitions.Types[typ.Elem], extended)...)
	}
	return nil
}

// Functions returns functions which is required to generate.
func (h *helper) Functions() ([]function, []string) {
	functionNames := map[string]int{}
	functions := make([]function, 0, len(h.definitions.Definitions))
	types := make([]*api.Type, 0, len(h.definitions.Definitions))
	for path, defs := range h.definitions.Definitions {
		for _, def := range defs {
			fn := function{
				Path:   path,
				Method: def.HTTPMethod,
				Code:   def.HTTPCode,
			}
			// The priority of summary is higher than original function name.
			if def.Summary != "" {
				// Remove invalid chars and regard as function name.
				fn.Name = utils.NameReplacer.ReplaceAllString(def.Summary, "")
			}

			if fn.Name == "" {
				// If original function is public and there is no summary,
				// original function name is selected.
				name := strings.TrimSpace(h.namer.Name(def.Function))
				if name != "" && name[0] >= 'A' && name[0] <= 'Z' {
					fn.Name = name
				}
			}

			if fn.Name == "" {
				// Anonymous function.
				fn.Name = "AnonymousAPI"
			}

			count := functionNames[fn.Name]
			functionNames[fn.Name]++
			if count > 0 {
				fn.Name += strconv.Itoa(count)
			}

			comments := fmt.Sprintf("%s ", fn.Name)
			if def.Description != "" {
				comments += fmt.Sprintf("description:\n%s", def.Description)
			} else {
				comments += "does not have any description."
			}
			fn.Comments = api.ParseComments(comments).LineComments()
			sigNames := h.namer.nameContainer()

			// If there is no specified consumer, defaults to application/json.
			firstNonEmptyConsume := definition.MIMEJSON
			for _, consume := range def.Consumes {
				if consume != "" {
					firstNonEmptyConsume = consume
					break
				}
			}

			for _, param := range def.Parameters {
				if param.Source == definition.Prefab {
					// Ignore prefabs.
					continue
				}
				p := functionParameter{
					Source:       string(param.Source),
					Name:         param.Name,
					ProposedName: sigNames.proposeName(param.Name, param.Type),
					Typ:          h.namer.Name(param.Type),
				}
				types = append(types, h.definitions.Types[param.Type])
				if param.Source == definition.Body {
					// Use first consumer as the name of body parameter.
					p.Name = firstNonEmptyConsume
				}
				if param.Source == definition.Auto {
					// Generate field extensions for auto struct.
					h.enumFields(param.Type, "",
						func(key string, tag string, field api.StructField) {
							source, name, _, err := service.ParseAutoParameterTag(tag)
							if err != nil {
								// Ignore invalid source tag.
								return
							}
							extension := parameterExtension{
								Source: string(source),
								Name:   name,
								Key:    key,
							}
							if source == definition.Body {
								// Use first consumer as the name of body parameter.
								extension.Name = firstNonEmptyConsume
							}
							p.Extensions = append(p.Extensions, extension)
						})
				}
				fn.Parameters = append(fn.Parameters, p)
			}
			for _, result := range def.Results {
				if strings.Contains(string(result.Destination), string(definition.Error)) {
					// Ignore errors
					continue
				}
				r := functionResult{
					Destination: string(result.Destination),
					Typ:         h.namer.Name(result.Type),
				}
				r.ProposedName = sigNames.proposeName("", result.Type, r.Typ)

				typ := h.definitions.Types[result.Type]
				types = append(types, typ)
				if typ.Kind == reflect.Ptr {
					r.Creator = fmt.Sprintf("new(%s)", h.namer.Name(typ.Elem))
				}
				fn.Results = append(fn.Results, r)
			}
			functions = append(functions, fn)
		}
	}
	sort.Sort(functionSorter(functions))
	return functions, h.packages(types, false)
}

func (h *helper) enumFields(name api.TypeName, key string, fn func(key string, source string, field api.StructField)) {
	typ := h.definitions.Types[name]
	if typ.Kind == reflect.Ptr {
		typ = h.definitions.Types[typ.Elem]
	}

	if typ.Kind == reflect.Struct {
		if key != "" {
			key += "."
		}
		for _, field := range typ.Fields {
			source := field.Tag.Get("source")
			fieldKey := key + field.Name
			if source != "" {
				fn(fieldKey, source, field)
			} else {
				fieldType := h.definitions.Types[field.Type]
				if fieldType.Kind == reflect.Struct {
					h.enumFields(field.Type, key, fn)
				}
			}
		}
	}
}

type nameContainer struct {
	names map[string]int
	namer *typeNamer
}

var builtInTypes = map[string]bool{
	"bool":        true,
	"true":        true,
	"false":       true,
	"uint8 ":      true,
	"uint16 ":     true,
	"uint32 ":     true,
	"uint64":      true,
	"int8":        true,
	"int16":       true,
	"int32":       true,
	"int64":       true,
	"float32":     true,
	"float64":     true,
	"complex64":   true,
	"complex128":  true,
	"string":      true,
	"int":         true,
	"uint":        true,
	"uintptr":     true,
	"byte":        true,
	"rune":        true,
	"iota":        true,
	"nil":         true,
	"Type":        true,
	"Type1":       true,
	"IntegerType": true,
	"FloatType":   true,
	"ComplexType": true,
	"append":      true,
	"copy":        true,
	"delete":      true,
	"len":         true,
	"cap":         true,
	"make":        true,
	"new":         true,
	"complex":     true,
	"real":        true,
	"imag":        true,
	"close":       true,
	"panic":       true,
	"recover":     true,
	"print":       true,
	"println":     true,
	"error":       true,
}

func (n *nameContainer) proposeName(name string, typ api.TypeName, targetTypes ...string) string {
	if name == "" {
		name = n.deconstruct(typ)
	}
	name = utils.NameReplacer.ReplaceAllString(name, "")
	if name == "" {
		name = "temp"
	}

	// to lower case the first letter if it is uppercase.
	if name[0] >= 'A' && name[0] <= 'Z' {
		name = string(name[0]|0x20) + name[1:]
	}
	// name may be `type` `int` `string` etc.
	if token.Lookup(name).IsKeyword() || builtInTypes[name] {
		name += "_"
	}

	// add '_' suffix if the name is conflict with the type name
	for _, target := range targetTypes {
		if name == target {
			name += "_"
			break
		}
	}

	index := n.names[name]
	// add index suffix if the name is duplicate.
	n.names[name]++
	if index > 0 {
		name += strconv.Itoa(index)
	}
	return name
}

func (n *nameContainer) deconstruct(name api.TypeName) string {
	typ := n.namer.types[name]
	switch typ.Kind {
	case reflect.Ptr:
		return n.deconstruct(typ.Elem)
	case reflect.Array, reflect.Slice, reflect.Map:
		result := n.deconstruct(typ.Elem)
		// Unsafe to convert result to plural form.
		switch {
		case strings.HasSuffix(result, "es"):
		case strings.HasSuffix(result, "y"):
			result = result[:len(result)-1] + "ies"
		case strings.HasSuffix(result, "s"):
			result = result[:len(result)-1] + "es"
		default:
			result += "s"
		}
		return result
	default:
		return n.namer.Name(name)
	}
}

type typeNamer struct {
	types    map[api.TypeName]*api.Type
	names    map[api.TypeName]string
	aliases  map[string]int
	packages map[string]string
	rootPkg  string
}

func newTypeNamer(rootPkg string, types map[api.TypeName]*api.Type) (*typeNamer, error) {
	n := &typeNamer{
		types:    types,
		names:    make(map[api.TypeName]string),
		aliases:  make(map[string]int),
		packages: make(map[string]string),
		rootPkg:  rootPkg,
	}

	typeNames := make([]string, 0, len(types))
	for name := range types {
		typeNames = append(typeNames, string(name))
	}
	sort.Strings(typeNames)

	for _, tn := range typeNames {
		if _, err := n.parse(api.TypeName(tn)); err != nil {
			return nil, err
		}
	}
	return n, nil
}

func (n *typeNamer) nameContainer() *nameContainer {
	return &nameContainer{
		names: map[string]int{},
		namer: n,
	}
}

func (n *typeNamer) parseName(typ *api.Type) string {
	alias := n.packages[typ.PkgPath]
	if alias == "" && typ.PkgPath != "" {
		alias = path.Base(typ.PkgPath)
		index := n.aliases[alias]
		n.aliases[alias]++
		if index > 0 {
			alias += strconv.Itoa(index)
		}
		n.packages[typ.PkgPath] = alias
	}
	name := typ.Name
	if alias != "" {
		name = fmt.Sprintf("%s.%s", alias, name)
	}
	return name
}

func (n *typeNamer) parse(tn api.TypeName) (string, error) {
	name, ok := n.names[tn]
	if ok {
		return name, nil
	}
	typ, ok := n.types[tn]
	if !ok {
		return "", fmt.Errorf("no type with name %s", tn)
	}

	switch typ.Kind {
	case reflect.Array, reflect.Slice:
		elemName, err := n.parse(typ.Elem)
		if err != nil {
			return "", err
		}
		name = fmt.Sprintf("[]%s", elemName)
	case reflect.Ptr:
		elemName, err := n.parse(typ.Elem)
		if err != nil {
			return "", err
		}
		name = fmt.Sprintf("*%s", elemName)
	case reflect.Map:
		keyName, err := n.parse(typ.Key)
		if err != nil {
			return "", err
		}
		elemName, err := n.parse(typ.Elem)
		if err != nil {
			return "", err
		}
		name = fmt.Sprintf("map[%s]%s", keyName, elemName)
	default:
		name = n.parseName(typ)
	}

	n.names[tn] = name
	return name, nil
}

func (n *typeNamer) Name(tn api.TypeName) string {
	name, ok := n.names[tn]
	if !ok {
		panic(fmt.Errorf("can't find type %s", tn))
	}
	return name
}

func (n *typeNamer) Alias(pkg string) string {
	return n.packages[pkg]
}

type functionSorter []function

// Len is the number of elements in the collection.
func (s functionSorter) Len() int {
	return len(s)
}

// Less reports whether the element with
// index i should sort before the element with index j.
func (s functionSorter) Less(i, j int) bool {
	return s[i].Name < s[j].Name
}

// Swap swaps the elements with indexes i and j.
func (s functionSorter) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}
