// Copyright 2019 CUE 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 gocode

import (
	"bytes"
	"cmp"
	"fmt"
	"go/ast"
	"go/format"
	"go/types"
	"text/template"

	"golang.org/x/tools/go/packages"

	"cuelang.org/go/cue"
	"cuelang.org/go/cue/errors"
)

// Config defines options for generation Go code.
type Config struct {
	// Prefix is used as a prefix to all generated variables. It defaults to
	// cuegen.
	Prefix string

	// ValidateName defines the default name for validation methods or prefix
	// for validation functions. The default is "Validate". Set to "-" to
	// disable generating validators.
	ValidateName string

	// CompleteName defines the default name for complete methods or prefix
	// for complete functions. The default is "-" (disabled).
	CompleteName string

	// The cue.Runtime variable name to use for initializing Codecs.
	// A new Runtime is created by default.
	RuntimeVar string
}

const defaultPrefix = "cuegen"

// Generate generates Go code for the given instance in the directory of the
// given package.
//
// Generate converts top-level declarations to corresponding Go code. By default,
// it will only generate validation functions of methods for exported top-level
// declarations. The behavior can be altered with the @go attribute.
//
// The go attribute has the following form @go(<name>{,<option>}), where option
// is either a key-value pair or a flag. The name maps the CUE name to an
// alternative Go name. The special value '-' is used to indicate the field
// should be ignored for any Go generation.
//
// The following options are supported:
//
//	type=<gotype>    The Go type as which this value should be interpreted.
//	                 This defaults to the type with the (possibly overridden)
//	                 name of the field.
//	validate=<name>  Alternative name for the validation function or method
//	                 Setting this to the empty string disables generation.
//	complete=<name>  Alternative name for the validation function or method.
//	                 Setting this to the empty string disables generation.
//	func             Generate as a function instead of a method.
//
// # Selection and Naming
//
// Generate will not generate any code for fields that have no go attribute
// and that are not exported or for which there is no namesake Go type.
// If the go attribute has the special value '-' as its name it will be dropped
// as well. In all other cases Generate will generate Go code, even if the
// resulting code will not compile. For instance, Generate will generate Go
// code even if the user defines a Go type in the attribute that does not
// exist.
//
// If a field selected for generation and the go name matches that the name of
// the Go type, the corresponding validate and complete code are generated as
// methods by default. If not, it will be generated as a function. The default
// function name is the default operation name with the Go name as a suffix.
//
// Caveats
// Currently not supported:
//   - option to generate Go structs (or automatically generate if undefined)
//   - for type option to refer to types outside the package.
func Generate(pkgPath string, inst cue.InstanceOrValue, c *Config) (b []byte, err error) {
	// TODO: if inst is nil, the instance is loaded from CUE files in the same
	// package directory with the same package name.
	if c == nil {
		c = &Config{}
	}

	g := &generator{
		Config:  *c,
		typeMap: map[string]types.Type{},
	}

	val := inst.Value()
	pkgName := inst.Value().BuildInstance().PkgName
	if pkgPath != "" {
		loadCfg := &packages.Config{
			Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo,
		}
		pkgs, err := packages.Load(loadCfg, pkgPath)
		if err != nil {
			return nil, fmt.Errorf("generating failed: %v", err)
		}

		if len(pkgs) != 1 {
			return nil, fmt.Errorf(
				"generate only allowed for one package at a time, found %d",
				len(pkgs))
		}

		g.pkg = pkgs[0]
		if len(g.pkg.Errors) > 0 {
			for _, err := range g.pkg.Errors {
				g.addErr(err)
			}
			return nil, g.err
		}

		pkgName = g.pkg.Name

		for _, obj := range g.pkg.TypesInfo.Defs {
			if obj == nil || obj.Pkg() != g.pkg.Types || obj.Parent() == nil {
				continue
			}
			g.typeMap[obj.Name()] = obj.Type()
		}
	}

	// TODO: add package doc if there is no existing Go package or if it doesn't
	// have package documentation already.
	g.exec(headerCode, map[string]string{
		"pkgName": pkgName,
	})

	iter, err := val.Fields(cue.Definitions(true))
	g.addErr(err)

	for iter.Next() {
		// TODO(mvdan): using cue.Definitions above means that we iterate over definitions,
		// whose selector will not be of string type. Revisit this, because using Unquoted
		// for definitions is likely not right.
		g.decl(iter.Selector().Unquoted(), iter.Value())
	}

	r := (*cue.Runtime)(val.Context())
	b, err = r.Marshal(&val)
	g.addErr(err)

	g.exec(loadCode, map[string]string{
		"runtime": g.RuntimeVar,
		"prefix":  cmp.Or(g.Prefix, defaultPrefix),
		"data":    string(b),
	})

	if g.err != nil {
		return nil, g.err
	}

	b, err = format.Source(g.w.Bytes())
	if err != nil {
		// Return bytes as well to allow analysis of the failed Go code.
		return g.w.Bytes(), err
	}

	return b, err
}

type generator struct {
	Config
	pkg     *packages.Package
	typeMap map[string]types.Type

	w   bytes.Buffer
	err errors.Error
}

func (g *generator) addErr(err error) {
	if err != nil {
		g.err = errors.Append(g.err, errors.Promote(err, "generate failed"))
	}
}

func (g *generator) exec(t *template.Template, data interface{}) {
	g.addErr(t.Execute(&g.w, data))
}

func (g *generator) decl(name string, v cue.Value) {
	attr := v.Attribute("go")

	if !ast.IsExported(name) && attr.Err() != nil {
		return
	}

	goName := name
	switch s, _ := attr.String(0); s {
	case "":
	case "-":
		return
	default:
		goName = s
	}

	goTypeName := goName
	goType := ""
	if str, ok, _ := attr.Lookup(1, "type"); ok {
		goType = str
		goTypeName = str
	}

	isFunc, _ := attr.Flag(1, "func")
	if goTypeName != goName {
		isFunc = true
	}

	zero := "nil"

	typ, ok := g.typeMap[goTypeName]
	if !ok && !mappedGoTypes(goTypeName) {
		return
	}
	if goType == "" {
		goType = goTypeName
		if typ != nil {
			switch typ.Underlying().(type) {
			case *types.Struct, *types.Array:
				goType = "*" + goTypeName
				zero = fmt.Sprintf("&%s{}", goTypeName)
			case *types.Pointer:
				zero = fmt.Sprintf("%s(nil)", goTypeName)
				isFunc = true
			}
		}
	}

	g.exec(stubCode, map[string]interface{}{
		"prefix":  cmp.Or(g.Prefix, defaultPrefix),
		"cueName": name,   // the field name of the CUE type
		"goType":  goType, // the receiver or argument type
		"zero":    zero,   // the zero value of the underlying type

		// @go attribute options
		"func":     isFunc,
		"validate": lookupName(attr, "validate", cmp.Or(g.ValidateName, "Validate")),
		"complete": lookupName(attr, "complete", g.CompleteName),
	})
}

func lookupName(attr cue.Attribute, option, config string) string {
	name, ok, _ := attr.Lookup(1, option)
	if !ok {
		name = config
	}
	if name == "-" {
		return ""
	}
	return name
}

func mappedGoTypes(s string) bool {
	switch s {
	case "bool", "float32", "float64",
		"int", "int8", "int16", "int32", "int64", "string",
		"uint", "uint8", "uint16", "uint32", "uint64":
		return true
	}
	return false
}
