// Copyright 2020 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 export

import (
	"bytes"
	"cmp"
	"fmt"
	"slices"
	"strings"

	"cuelang.org/go/cue/ast"
	"cuelang.org/go/cue/ast/astutil"
	"cuelang.org/go/cue/literal"
	"cuelang.org/go/cue/token"
	"cuelang.org/go/internal"
	"cuelang.org/go/internal/core/adt"
)

func (e *exporter) ident(x adt.Feature) *ast.Ident {
	s := x.IdentString(e.ctx)
	if !ast.IsValidIdent(s) {
		panic(s + " is not a valid identifier")
	}
	return ast.NewIdent(s)
}

func (e *exporter) adt(env *adt.Environment, expr adt.Elem) ast.Expr {
	switch x := expr.(type) {
	case adt.Value:
		return e.expr(env, x)

	case *adt.ListLit:
		env := &adt.Environment{Up: env, Vertex: e.node()}
		a := []ast.Expr{}
		for _, x := range x.Elems {
			a = append(a, e.elem(env, x))
		}
		return ast.NewList(a...)

	case *adt.StructLit:
		// TODO: should we use pushFrame here?
		// _, saved := e.pushFrame([]adt.Conjunct{adt.MakeConjunct(nil, x)})
		// defer e.popFrame(saved)
		// s := e.frame(0).scope

		s := &ast.StructLit{}
		// TODO: ensure e.node() is set in more cases. Right now it is not
		// always set in mergeValues, even in cases where it could be. Better
		// to be conservative for now, though.
		env := &adt.Environment{Up: env, Vertex: e.node()}

		for _, d := range x.Decls {
			var a *ast.Alias
			if orig, ok := d.Source().(*ast.Field); ok {
				if alias, ok := orig.Value.(*ast.Alias); ok {
					if e.valueAlias == nil {
						e.valueAlias = map[*ast.Alias]*ast.Alias{}
					}
					a = &ast.Alias{Ident: ast.NewIdent(alias.Ident.Name)}
					e.valueAlias[alias] = a
				}
			}
			decl := e.decl(env, d)

			// decl may be nil if it represents a let. Lets are added later, and
			// only when they are still used.
			if decl == nil {
				continue
			}

			if e.cfg.ShowDocs {
				ast.SetComments(decl, filterDocs(ast.Comments(d.Source())))
			}
			// TODO: use e.copyMeta for positions, but only when the original
			// source is available.

			if a != nil {
				if f, ok := decl.(*ast.Field); ok {
					a.Expr = f.Value
					f.Value = a
				}
			}

			s.Elts = append(s.Elts, decl)
		}

		return s

	// TODO: why does LabelReference not implement resolve?
	case *adt.LabelReference:
		// get potential label from Source. Otherwise use X.
		v, ok := e.ctx.Evaluate(env, x)
		f := e.frame(x.UpCount)
		if ok && (adt.IsConcrete(v) || f.field == nil) {
			return e.value(v)
		}
		if f.field == nil {
			// This can happen when the LabelReference is evaluated outside of
			// normal evaluation, that is, if a pattern constraint or
			// additional constraint is evaluated by itself.
			return ast.NewIdent("string")
		}
		list, ok := f.field.Label.(*ast.ListLit)
		if !ok || len(list.Elts) != 1 {
			panic("label reference to non-pattern constraint field or invalid list")
		}
		name := ""
		if a, ok := list.Elts[0].(*ast.Alias); ok {
			name = a.Ident.Name
		} else {
			if x.Src != nil {
				name = x.Src.Name
			}
			name = e.uniqueAlias(name)
			list.Elts[0] = &ast.Alias{
				Ident: ast.NewIdent(name),
				Expr:  list.Elts[0],
			}
		}
		ident := ast.NewIdent(name)
		ident.Scope = f.field
		ident.Node = f.labelExpr
		return ident

	case adt.Resolver:
		return e.resolve(env, x)

	case *adt.SliceExpr:
		var lo, hi ast.Expr
		if x.Lo != nil {
			lo = e.innerExpr(env, x.Lo)
		}
		if x.Hi != nil {
			hi = e.innerExpr(env, x.Hi)
		}
		// TODO: Stride not yet? implemented.
		// if x.Stride != nil {
		// 	stride = e.innerExpr(env, x.Stride)
		// }
		return &ast.SliceExpr{X: e.innerExpr(env, x.X), Low: lo, High: hi}

	case *adt.Interpolation:
		var (
			tripple    = `"""`
			openQuote  = `"`
			closeQuote = `"`
			f          = literal.String
		)
		if x.K&adt.BytesKind != 0 {
			tripple = `'''`
			openQuote = `'`
			closeQuote = `'`
			f = literal.Bytes
		}
		toString := func(v adt.Expr) string {
			str := ""
			switch x := v.(type) {
			case *adt.String:
				str = x.Str
			case *adt.Bytes:
				str = string(x.B)
			}
			return str
		}
		t := &ast.Interpolation{}
		f = f.WithGraphicOnly()
		indent := ""
		// TODO: mark formatting in interpolation itself.
		for i := 0; i < len(x.Parts); i += 2 {
			if strings.IndexByte(toString(x.Parts[i]), '\n') >= 0 {
				f = f.WithTabIndent(len(e.stack))
				indent = strings.Repeat("\t", len(e.stack))
				openQuote = tripple + "\n" + indent
				closeQuote = tripple
				break
			}
		}
		prefix := openQuote
		suffix := `\(`
		for i, elem := range x.Parts {
			if i%2 == 1 {
				t.Elts = append(t.Elts, e.innerExpr(env, elem))
			} else {
				// b := strings.Builder{}
				buf := []byte(prefix)
				str := toString(elem)
				buf = f.AppendEscaped(buf, str)
				if i == len(x.Parts)-1 {
					if len(closeQuote) > 1 {
						buf = append(buf, '\n')
						buf = append(buf, indent...)
					}
					buf = append(buf, closeQuote...)
				} else {
					if bytes.HasSuffix(buf, []byte("\n")) {
						buf = append(buf, indent...)
					}
					buf = append(buf, suffix...)
				}
				t.Elts = append(t.Elts, &ast.BasicLit{
					Kind:  token.STRING,
					Value: string(buf),
				})
			}
			prefix = ")"
		}
		return t

	case *adt.BoundExpr:
		return &ast.UnaryExpr{
			Op: x.Op.Token(),
			X:  e.innerExpr(env, x.Expr),
		}

	case *adt.UnaryExpr:
		return &ast.UnaryExpr{
			Op: x.Op.Token(),
			X:  e.innerExpr(env, x.X),
		}

	case *adt.BinaryExpr:
		if x.Op == adt.AndOp || x.Op == adt.OrOp {
			return e.sortBinaryTree(env, x)
		}
		return &ast.BinaryExpr{
			Op: x.Op.Token(),
			X:  e.innerExpr(env, x.X),
			Y:  e.innerExpr(env, x.Y),
		}

	case *adt.CallExpr:
		a := []ast.Expr{}
		for _, arg := range x.Args {
			v := e.innerExpr(env, arg)
			if v == nil {
				e.innerExpr(env, arg)
				panic("")
			}
			a = append(a, v)
		}
		fun := e.innerExpr(env, x.Fun)
		return &ast.CallExpr{Fun: fun, Args: a}

	case *adt.DisjunctionExpr:
		a := []ast.Expr{}
		for _, d := range x.Values {
			v := e.expr(env, d.Val)
			if d.Default {
				v = &ast.UnaryExpr{Op: token.MUL, X: v}
			}
			a = append(a, v)
		}
		return ast.NewBinExpr(token.OR, a...)

	case *adt.ConjunctGroup:
		a := []ast.Expr{}
		for _, c := range *x {
			v := e.expr(c.EnvExpr())
			a = append(a, v)
		}
		return ast.NewBinExpr(token.AND, a...)

	case *adt.Comprehension:
		if !x.DidResolve() {
			return dummyTop
		}
		for _, c := range x.Clauses {
			switch c.(type) {
			case *adt.ForClause:
				env = &adt.Environment{Up: env, Vertex: empty}
			case *adt.IfClause:
			case *adt.LetClause:
				env = &adt.Environment{Up: env, Vertex: empty}
			case *adt.ValueClause:
				// Can occur in nested comprehenions.
				env = &adt.Environment{Up: env, Vertex: empty}
			default:
				panic("unreachable")
			}
		}

		// If this is an "unwrapped" comprehension, we need to also
		// account for the curly braces of the original comprehension.
		if x.Nest() > 0 {
			env = &adt.Environment{Up: env, Vertex: empty}
		}

		// TODO: consider using adt.EnvExpr.
		return e.adt(env, adt.ToExpr(x.Value))

	default:
		panic(fmt.Sprintf("unknown field %T", x))
	}
}

// sortBinaryTree converte x to a binary tree and sorts it's elements
// using sortLeafAdt.
func (e *exporter) sortBinaryTree(env *adt.Environment, x *adt.BinaryExpr) (b ast.Expr) {
	var exprs []adt.Node

	var flatten func(expr adt.Expr)
	flatten = func(expr adt.Expr) {
		if y, ok := expr.(*adt.BinaryExpr); ok && x.Op == y.Op {
			flatten(y.X)
			flatten(y.Y)
		} else {
			exprs = append(exprs, expr)
		}
	}
	flatten(x)

	// Sort the expressions
	slices.SortStableFunc(exprs, cmpLeafNodes)

	nodes := make([]ast.Expr, 0, len(exprs))
	for _, x := range exprs {
		switch y := x.(type) {
		case *adt.Top:
		case *adt.BasicType:
			if y.K != adt.TopKind {
				nodes = append(nodes, e.expr(env, y))
			}
		default:
			nodes = append(nodes, e.innerExpr(env, y.(adt.Expr)))
		}
	}

	if len(nodes) == 0 {
		return e.adt(env, &adt.Top{})
	}

	return ast.NewBinExpr(x.Op.Token(), nodes...)
}

// cmpConjuncts compares two Conjunct based on their element using cmpLeafNodes.
func cmpConjuncts(a, b adt.Conjunct) int {
	return cmpLeafNodes(a.Expr(), b.Expr())
}

// cmpLeafNodes compares two adt.Expr values. The values may not be a binary
// expressions. It returns true if a is less than b.
func cmpLeafNodes[T adt.Node](a, b T) int {
	if c := cmp.Compare(typeOrder(a), typeOrder(b)); c != 0 {
		return c
	}

	srcA := a.Source()
	srcB := b.Source()

	if srcA == nil || srcB == nil {
		// TODO: some tie breaker
		return 0
	}

	return srcA.Pos().Compare(srcB.Pos())
}

func typeOrder(x adt.Node) int {
	switch x.(type) {
	case *adt.Top:
		return 0
	case *adt.BasicType:
		return 1
	case *adt.FieldReference:
		return 2 // sometimes basic types are represented as field references.
	case *adt.Bool, *adt.Null, *adt.Num, *adt.String, *adt.Bytes:
		return 10
	case *adt.BoundValue:
		return 20
	case *adt.StructLit, *adt.ListLit:
		return 500
	case adt.Expr:
		return 25
	default:
		return 100
	}
}

var dummyTop = &ast.Ident{Name: "_"}

func (e *exporter) resolve(env *adt.Environment, r adt.Resolver) ast.Expr {
	if c := e.pivotter; c != nil {
		if alt := c.refExpr(r); alt != nil {
			return alt
		}
	}

	switch x := r.(type) {
	case *adt.FieldReference:
		// Special case when the original CUE already had an alias.
		if x.Src != nil {
			if f, ok := x.Src.Node.(*ast.Field); ok {
				if entry, ok := e.fieldAlias[f]; ok {
					ident := ast.NewIdent(aliasFromLabel(f))
					ident.Node = entry.field
					ident.Scope = entry.scope
					return ident
				}
			}
		}

		ident, _ := e.newIdentForField(x.Src, x.Label, x.UpCount)

		// Use low-level lookup to bypass structural cycle detection. This is
		// fine as we do not recurse on the result and it is necessary to detect
		// shadowing even when a configuration has a structural cycle.
		for i := 0; i < int(x.UpCount); i++ {
			env = env.Up
		}

		// Exclude comprehensions and other temporary/ inlined Vertices, which
		// cannot be properly resolved, throwing off the sanitize. Also,
		// comprehensions originate from a single source and do not need to be
		// handled.
		if env != nil { // for generated stuff
			// TODO: note that env.Vertex should never be nil; investigate and replace the nil check below.
			if v := env.Vertex; v != nil && !v.IsDynamic {
				if v = v.Lookup(x.Label); v != nil {
					e.linkIdentifier(v, ident)
				}
			}
		}

		return ident

	case *adt.ValueReference:
		name := x.Label.IdentString(e.ctx)
		if a, ok := x.Src.Node.(*ast.Alias); ok { // Should always pass
			if b, ok := e.valueAlias[a]; ok {
				name = b.Ident.Name
			}
		}
		ident := ast.NewIdent(name)
		return ident

	case *adt.DynamicReference:
		// TODO(unshadow): ensure we correctly unshadow newly visible fields.
		//   before uncommenting this.
		// if v := x.EvaluateLabel(e.ctx, env); v != 0 {
		// 	str := v.StringValue(e.ctx)
		// 	if ast.IsValidIdent(str) {
		// 		label := e.ctx.StringLabel(str)
		// 		ident, ok := e.newIdentForField(x.Src, label, x.UpCount)
		// 		if ok {
		// 			return ident
		// 		}
		// 	}
		// }

		name := "X"
		if x.Src != nil {
			name = x.Src.Name
		}
		var f *ast.Field
		for i := len(e.stack) - 1; i >= 0; i-- {
			for _, entry := range e.stack[i].dynamicFields {
				if entry.alias == name {
					f = entry.field
				}
			}
		}

		if f != nil {
			name = e.getFieldAlias(f, name)
		}

		ident := ast.NewIdent(name)
		ident.Scope = f
		ident.Node = f
		return ident

	case *adt.ImportReference:
		importPath := x.ImportPath.StringValue(e.index)
		spec := ast.NewImport(nil, importPath)

		info, _ := astutil.ParseImportSpec(spec)
		name := info.PkgName
		if x.Label != 0 {
			name = x.Label.StringValue(e.index)
			if name != info.PkgName {
				spec.Name = ast.NewIdent(name)
			}
		}
		ident := ast.NewIdent(name)
		ident.Node = spec
		return ident

	case *adt.LetReference:
		return e.resolveLet(env, x)

	case *adt.SelectorExpr:
		return &ast.SelectorExpr{
			X:   e.innerExpr(env, x.X),
			Sel: e.stringLabel(x.Sel),
		}

	case *adt.IndexExpr:
		return &ast.IndexExpr{
			X:     e.innerExpr(env, x.X),
			Index: e.innerExpr(env, x.Index),
		}
	}
	panic("unreachable")
}

func (e *exporter) newIdentForField(
	orig *ast.Ident,
	label adt.Feature,
	upCount int32) (ident *ast.Ident, ok bool) {
	f := e.frame(upCount)
	entry := f.fields[label]

	name := e.identString(label)
	switch {
	case entry.alias != "":
		name = entry.alias

	case !ast.IsValidIdent(name):
		name = "X"
		if orig != nil {
			name = orig.Name
		}
		name = e.uniqueAlias(name)
		entry.alias = name
	}

	ident = ast.NewIdent(name)
	entry.references = append(entry.references, ident)

	if f.fields != nil {
		f.fields[label] = entry
		ok = true
	}

	return ident, ok
}

func (e *exporter) decl(env *adt.Environment, d adt.Decl) ast.Decl {
	switch x := d.(type) {
	case adt.Elem:
		return e.elem(env, x)

	case *adt.Field:
		e.setDocs(x)
		f := e.getFixedField(x)

		internal.SetConstraint(f, x.ArcType.Token())
		e.setField(x.Label, f)

		f.Attrs = extractFieldAttrs(nil, x)

		st, ok := x.Value.(*adt.StructLit)
		if !ok {
			f.Value = e.expr(env, x.Value)
			return f

		}

		top := e.frame(0)
		var src *adt.Vertex
		if top.node != nil {
			src = top.node.Lookup(x.Label)
		}

		// Instead of calling e.expr directly, we inline the case for
		// *adt.StructLit, so that we can pass src.
		c := adt.MakeRootConjunct(env, st)
		f.Value = e.mergeValues(adt.InvalidLabel, src, []conjunct{{c: c, up: 0}}, c)

		if top.node != nil {
			if v := top.node.Lookup(x.Label); v != nil {
				e.linkField(v, f)
			}
		}

		return f

	case *adt.LetField:
		// Handled elsewhere
		return nil

	case *adt.BulkOptionalField:
		e.setDocs(x)
		// set bulk in frame.
		frame := e.frame(0)

		expr := e.innerExpr(env, x.Filter)
		frame.labelExpr = expr // see astutil.Resolve.

		if x.Label != 0 {
			expr = &ast.Alias{Ident: e.ident(x.Label), Expr: expr}
		}
		f := &ast.Field{
			Label: ast.NewList(expr),
		}

		frame.field = f

		if alias := aliasFromLabel(x.Src); alias != "" {
			frame.dynamicFields = append(frame.dynamicFields, &entry{
				alias: alias,
				field: f,
			})
		}

		f.Value = e.expr(env, x.Value)
		f.Attrs = extractFieldAttrs(nil, x)

		return f

	case *adt.DynamicField:
		e.setDocs(x)
		srcKey := x.Key

		f := &ast.Field{}
		internal.SetConstraint(f, x.ArcType.Token())

		v, _ := e.ctx.Evaluate(env, x.Key)

		switch s, ok := v.(*adt.String); {
		// TODO(unshadow): allow once unshadowing algorithm is fixed.
		// case ok && ast.IsValidIdent(s.Str):
		// 	label := e.ctx.StringLabel(s.Str)
		// 	f.Label = ast.NewIdent(s.Str)
		// 	e.setField(label, f)

		case ok:
			srcKey = s

			fallthrough

		default:
			key := e.innerExpr(env, srcKey)
			switch key.(type) {
			case *ast.Interpolation, *ast.BasicLit:
			default:
				key = &ast.ParenExpr{X: key}
			}
			f.Label = key.(ast.Label)
		}

		alias := aliasFromLabel(x.Src)

		frame := e.frame(0)
		frame.dynamicFields = append(frame.dynamicFields, &entry{
			alias: alias,
			field: f,
		})

		f.Value = e.expr(env, x.Value)
		f.Attrs = extractFieldAttrs(nil, x)

		return f

	default:
		panic(fmt.Sprintf("unknown field %T", x))
	}
}

func (e *exporter) copyMeta(dst, src ast.Node) {
	if e.cfg.ShowDocs {
		ast.SetComments(dst, filterDocs(ast.Comments(src)))
	}
	astutil.CopyPosition(dst, src)
}

func filterDocs(a []*ast.CommentGroup) (out []*ast.CommentGroup) {
	out = append(out, a...)
	k := 0
	for _, c := range a {
		if !c.Doc {
			continue
		}
		out[k] = c
		k++
	}
	out = out[:k]
	return out
}

func (e *exporter) setField(label adt.Feature, f *ast.Field) {
	frame := e.frame(0)
	entry := frame.fields[label]
	entry.field = f
	entry.node = f.Value
	// This can happen when evaluation is "pivoted".
	if frame.fields != nil {
		frame.fields[label] = entry
	}
}

func aliasFromLabel(src *ast.Field) string {
	if src != nil {
		if a, ok := src.Label.(*ast.Alias); ok {
			return a.Ident.Name
		}
	}
	return ""
}

func (e *exporter) elem(env *adt.Environment, d adt.Elem) ast.Expr {

	switch x := d.(type) {
	case adt.Expr:
		return e.expr(env, x)

	case *adt.Ellipsis:
		t := &ast.Ellipsis{}
		if x.Value != nil {
			t.Type = e.expr(env, x.Value)
		}
		return t

	case *adt.Comprehension:
		return e.comprehension(env, x)

	default:
		panic(fmt.Sprintf("unknown field %T", x))
	}
}

func (e *exporter) comprehension(env *adt.Environment, comp *adt.Comprehension) *ast.Comprehension {
	c := &ast.Comprehension{}

	for _, y := range comp.Clauses {
		switch x := y.(type) {
		case *adt.ForClause:
			env = &adt.Environment{Up: env, Vertex: empty}
			value := e.ident(x.Value)
			src := e.innerExpr(env, x.Src)
			clause := &ast.ForClause{Value: value, Source: src}
			e.copyMeta(clause, x.Syntax)
			c.Clauses = append(c.Clauses, clause)

			_, saved := e.pushFrame(empty, nil)
			defer e.popFrame(saved)

			if x.Key != adt.InvalidLabel ||
				(x.Syntax != nil && x.Syntax.Key != nil) {
				key := e.ident(x.Key)
				clause.Key = key
				e.addField(x.Key, nil, clause)
			}
			e.addField(x.Value, nil, clause)

		case *adt.IfClause:
			cond := e.innerExpr(env, x.Condition)
			clause := &ast.IfClause{Condition: cond}
			e.copyMeta(clause, x.Src)
			c.Clauses = append(c.Clauses, clause)

		case *adt.LetClause:
			env = &adt.Environment{Up: env, Vertex: empty}
			expr := e.innerExpr(env, x.Expr)
			clause := &ast.LetClause{
				Ident: e.ident(x.Label),
				Expr:  expr,
			}
			e.copyMeta(clause, x.Src)
			c.Clauses = append(c.Clauses, clause)

			_, saved := e.pushFrame(empty, nil)
			defer e.popFrame(saved)

			e.addField(x.Label, nil, clause)

		case *adt.ValueClause:
			// Can occur in nested comprehenions.
			env = &adt.Environment{Up: env, Vertex: empty}

		default:
			panic(fmt.Sprintf("unknown field %T", x))
		}
	}
	e.copyMeta(c, comp.Syntax)

	// If this is an "unwrapped" comprehension, we need to also
	// account for the curly braces of the original comprehension.
	if comp.Nest() > 0 {
		env = &adt.Environment{Up: env, Vertex: empty}
	}

	// TODO: consider using adt.EnvExpr.
	v := e.expr(env, adt.ToExpr(comp.Value))
	if _, ok := v.(*ast.StructLit); !ok {
		v = ast.NewStruct(ast.Embed(v))
	}
	c.Value = v
	return c
}
