package nodejs

import (
	"bytes"
	"fmt"
	"io"
	"math/big"
	"strings"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2"
	"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/convert"
)

type nameInfo int

func (nameInfo) Format(name string) string {
	return makeValidIdentifier(name)
}

func (g *generator) lowerExpression(expr model.Expression) model.Expression {
	// TODO(pdg): diagnostics
	if g.asyncMain {
		expr = g.awaitInvokes(expr)
	}
	expr = hcl2.RewritePropertyReferences(expr)
	expr, _ = hcl2.RewriteApplies(expr, nameInfo(0), !g.asyncMain)
	expr, _ = g.lowerProxyApplies(expr)
	return expr
}

func (g *generator) GetPrecedence(expr model.Expression) int {
	// Precedence is derived from
	// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence.
	switch expr := expr.(type) {
	case *model.ConditionalExpression:
		return 4
	case *model.BinaryOpExpression:
		switch expr.Operation {
		case hclsyntax.OpLogicalOr:
			return 5
		case hclsyntax.OpLogicalAnd:
			return 6
		case hclsyntax.OpEqual, hclsyntax.OpNotEqual:
			return 11
		case hclsyntax.OpGreaterThan, hclsyntax.OpGreaterThanOrEqual, hclsyntax.OpLessThan,
			hclsyntax.OpLessThanOrEqual:
			return 12
		case hclsyntax.OpAdd, hclsyntax.OpSubtract:
			return 14
		case hclsyntax.OpMultiply, hclsyntax.OpDivide, hclsyntax.OpModulo:
			return 15
		default:
			contract.Failf("unexpected binary expression %v", expr)
		}
	case *model.UnaryOpExpression:
		return 17
	case *model.FunctionCallExpression:
		switch expr.Name {
		case intrinsicAwait:
			return 17
		case intrinsicInterpolate:
			return 22
		default:
			return 20
		}
	case *model.ForExpression, *model.IndexExpression, *model.RelativeTraversalExpression, *model.SplatExpression,
		*model.TemplateJoinExpression:
		return 20
	case *model.AnonymousFunctionExpression, *model.LiteralValueExpression, *model.ObjectConsExpression,
		*model.ScopeTraversalExpression, *model.TemplateExpression, *model.TupleConsExpression:
		return 22
	default:
		contract.Failf("unexpected expression %v of type %T", expr, expr)
	}
	return 0
}

func (g *generator) GenAnonymousFunctionExpression(w io.Writer, expr *model.AnonymousFunctionExpression) {
	switch len(expr.Signature.Parameters) {
	case 0:
		g.Fgen(w, "()")
	case 1:
		g.Fgenf(w, "%s", expr.Signature.Parameters[0].Name)
	default:
		g.Fgen(w, "([")
		for i, p := range expr.Signature.Parameters {
			if i > 0 {
				g.Fgen(w, ", ")
			}
			g.Fgenf(w, "%s", p.Name)
		}
		g.Fgen(w, "])")
	}

	g.Fgenf(w, " => %.v", expr.Body)
}

func (g *generator) GenBinaryOpExpression(w io.Writer, expr *model.BinaryOpExpression) {
	opstr, precedence := "", g.GetPrecedence(expr)
	switch expr.Operation {
	case hclsyntax.OpAdd:
		opstr = "+"
	case hclsyntax.OpDivide:
		opstr = "/"
	case hclsyntax.OpEqual:
		opstr = "=="
	case hclsyntax.OpGreaterThan:
		opstr = ">"
	case hclsyntax.OpGreaterThanOrEqual:
		opstr = ">="
	case hclsyntax.OpLessThan:
		opstr = "<"
	case hclsyntax.OpLessThanOrEqual:
		opstr = "<="
	case hclsyntax.OpLogicalAnd:
		opstr = "&&"
	case hclsyntax.OpLogicalOr:
		opstr = "||"
	case hclsyntax.OpModulo:
		opstr = "%"
	case hclsyntax.OpMultiply:
		opstr = "*"
	case hclsyntax.OpNotEqual:
		opstr = "!="
	case hclsyntax.OpSubtract:
		opstr = "-"
	default:
		opstr, precedence = ",", 1
	}

	g.Fgenf(w, "%.[1]*[2]v %[3]v %.[1]*[4]o", precedence, expr.LeftOperand, opstr, expr.RightOperand)
}

func (g *generator) GenConditionalExpression(w io.Writer, expr *model.ConditionalExpression) {
	g.Fgenf(w, "%.4v ? %.4v : %.4v", expr.Condition, expr.TrueResult, expr.FalseResult)
}

func (g *generator) GenForExpression(w io.Writer, expr *model.ForExpression) {
	switch expr.Collection.Type().(type) {
	case *model.ListType, *model.TupleType:
		if expr.KeyVariable == nil {
			g.Fgenf(w, "%.20v", expr.Collection)
		} else {
			g.Fgenf(w, "%.20v.map((v, k) => [k, v])", expr.Collection)
		}
	case *model.MapType, *model.ObjectType:
		if expr.KeyVariable == nil {
			g.Fgenf(w, "Object.values(%.v)", expr.Collection)
		} else {
			g.Fgenf(w, "Object.entries(%.v)", expr.Collection)
		}
	}

	fnParams, reduceParams := expr.ValueVariable.Name, expr.ValueVariable.Name
	if expr.KeyVariable != nil {
		reduceParams = fmt.Sprintf("[%.v, %.v]", expr.KeyVariable.Name, expr.ValueVariable.Name)
		fnParams = fmt.Sprintf("(%v)", reduceParams)
	}

	if expr.Condition != nil {
		g.Fgenf(w, ".filter(%s => %.v)", fnParams, expr.Condition)
	}

	if expr.Key != nil {
		// TODO(pdg): grouping
		g.Fgenf(w, ".reduce((__obj, %s) => { ...__obj, [%.v]: %.v })", reduceParams, expr.Key, expr.Value)
	} else {
		g.Fgenf(w, ".map(%s => %.v)", fnParams, expr.Value)
	}
}

func (g *generator) genApply(w io.Writer, expr *model.FunctionCallExpression) {
	// Extract the list of outputs and the continuation expression from the `__apply` arguments.
	applyArgs, then := hcl2.ParseApplyCall(expr)

	// If all of the arguments are promises, use promise methods. If any argument is an output, convert all other args
	// to outputs and use output methods.
	anyOutputs := false
	for _, arg := range applyArgs {
		if isOutputType(arg.Type()) {
			anyOutputs = true
		}
	}

	apply, all := "then", "Promise.all"
	if anyOutputs {
		apply, all = "apply", "pulumi.all"
	}

	if len(applyArgs) == 1 {
		// If we only have a single output, just generate a normal `.apply` or `.then`.
		g.Fgenf(w, "%.20v.%v(%.v)", applyArgs[0], apply, then)
	} else {
		// Otherwise, generate a call to `pulumi.all([]).apply()`.
		g.Fgenf(w, "%v([", all)
		for i, o := range applyArgs {
			if i > 0 {
				g.Fgen(w, ", ")
			}
			g.Fgenf(w, "%v", o)
		}
		g.Fgenf(w, "]).%v(%.v)", apply, then)
	}
}

// functionName computes the NodeJS package, module, and name for the given function token.
func functionName(tokenArg model.Expression) (string, string, string, hcl.Diagnostics) {
	token := tokenArg.(*model.TemplateExpression).Parts[0].(*model.LiteralValueExpression).Value.AsString()
	tokenRange := tokenArg.SyntaxNode().Range()

	// Compute the resource type from the Pulumi type token.
	pkg, module, member, diagnostics := hcl2.DecomposeToken(token, tokenRange)
	return pkg, strings.Replace(module, "/", ".", -1), member, diagnostics
}

func (g *generator) genRange(w io.Writer, call *model.FunctionCallExpression, entries bool) {
	var from, to model.Expression
	switch len(call.Args) {
	case 1:
		from, to = &model.LiteralValueExpression{Value: cty.NumberIntVal(0)}, call.Args[0]
	case 2:
		from, to = call.Args[0], call.Args[1]
	default:
		contract.Failf("expected range() to have exactly 1 or 2 args; got %v", len(call.Args))
	}

	genPrefix := func() { g.Fprint(w, "((from, to) => (new Array(to - from))") }
	mapValue := "from + i"
	genSuffix := func() { g.Fgenf(w, ")(%.v, %.v)", from, to) }

	if litFrom, ok := from.(*model.LiteralValueExpression); ok {
		fromV, err := convert.Convert(litFrom.Value, cty.Number)
		contract.Assert(err == nil)

		from, _ := fromV.AsBigFloat().Int64()
		if litTo, ok := to.(*model.LiteralValueExpression); ok {
			toV, err := convert.Convert(litTo.Value, cty.Number)
			contract.Assert(err == nil)

			to, _ := toV.AsBigFloat().Int64()
			if from == 0 {
				mapValue = "i"
			} else {
				mapValue = fmt.Sprintf("%d + i", from)
			}
			genPrefix = func() { g.Fprintf(w, "(new Array(%d))", to-from) }
			genSuffix = func() {}
		} else if from == 0 {
			genPrefix = func() { g.Fgenf(w, "(new Array(%.v))", to) }
			mapValue = "i"
			genSuffix = func() {}
		}
	}

	if entries {
		mapValue = fmt.Sprintf("{key: %[1]s, value: %[1]s}", mapValue)
	}

	genPrefix()
	g.Fprintf(w, ".map((_, i) => %v)", mapValue)
	genSuffix()
}

var functionImports = map[string]string{
	intrinsicInterpolate: "@pulumi/pulumi",
	"fileArchive":        "@pulumi/pulumi",
	"fileAsset":          "@pulumi/pulumi",
	"filebase64":         "fs",
	"readFile":           "fs",
	"readDir":            "fs",
	"sha1":               "crypto",
}

func (g *generator) getFunctionImports(x *model.FunctionCallExpression) string {
	if x.Name != hcl2.Invoke {
		return functionImports[x.Name]
	}

	pkg, _, _, diags := functionName(x.Args[0])
	contract.Assert(len(diags) == 0)
	return "@pulumi/" + pkg
}

func (g *generator) GenFunctionCallExpression(w io.Writer, expr *model.FunctionCallExpression) {
	switch expr.Name {
	case hcl2.IntrinsicApply:
		g.genApply(w, expr)
	case intrinsicAwait:
		g.Fgenf(w, "await %.17v", expr.Args[0])
	case intrinsicInterpolate:
		g.Fgen(w, "pulumi.interpolate`")
		for _, part := range expr.Args {
			if lit, ok := part.(*model.LiteralValueExpression); ok && model.StringType.AssignableFrom(lit.Type()) {
				g.Fgen(w, lit.Value.AsString())
			} else {
				g.Fgenf(w, "${%.v}", part)
			}
		}
		g.Fgen(w, "`")
	case "element":
		g.Fgenf(w, "%.20v[%.v]", expr.Args[0], expr.Args[1])
	case "entries":
		switch model.ResolveOutputs(expr.Args[0].Type()).(type) {
		case *model.ListType, *model.TupleType:
			if call, ok := expr.Args[0].(*model.FunctionCallExpression); ok && call.Name == "range" {
				g.genRange(w, call, true)
				return
			}
			g.Fgenf(w, "%.20v.map((k, v)", expr.Args[0])
		case *model.MapType, *model.ObjectType:
			g.Fgenf(w, "Object.entries(%.v).map(([k, v])", expr.Args[0])
		}
		g.Fgenf(w, " => {key: k, value: v})")
	case "fileArchive":
		g.Fgenf(w, "new pulumi.asset.FileArchive(%.v)", expr.Args[0])
	case "fileAsset":
		g.Fgenf(w, "new pulumi.asset.FileAsset(%.v)", expr.Args[0])
	case "filebase64":
		g.Fgenf(w, "Buffer.from(fs.readFileSync(%v), 'binary').toString('base64')", expr.Args[0])
	case hcl2.Invoke:
		pkg, module, fn, diags := functionName(expr.Args[0])
		contract.Assert(len(diags) == 0)
		if module != "" {
			module = "." + module
		}
		name := fmt.Sprintf("%s%s.%s", makeValidIdentifier(pkg), module, fn)

		g.Fprintf(w, "%s(", name)
		if len(expr.Args) >= 2 {
			g.Fgenf(w, "%.v", expr.Args[1])
		}
		if len(expr.Args) == 3 {
			g.Fgenf(w, ", %.v", expr.Args[2])
		}
		g.Fprint(w, ")")
	case "join":
		g.Fgenf(w, "%.20v.join(%v)", expr.Args[1], expr.Args[0])
	case "length":
		g.Fgenf(w, "%.20v.length", expr.Args[0])
	case "lookup":
		g.Fgenf(w, "%v[%v]", expr.Args[0], expr.Args[1])
		if len(expr.Args) == 3 {
			g.Fgenf(w, " || %v", expr.Args[2])
		}
	case "range":
		g.genRange(w, expr, false)
	case "readFile":
		g.Fgenf(w, "fs.readFileSync(%v)", expr.Args[0])
	case "readDir":
		g.Fgenf(w, "fs.readDirSync(%v)", expr.Args[0])
	case "secret":
		g.Fgenf(w, "pulumi.secret(%v)", expr.Args[0])
	case "split":
		g.Fgenf(w, "%.20v.split(%v)", expr.Args[1], expr.Args[0])
	case "toBase64":
		g.Fgenf(w, "Buffer.from(%v).toString(\"base64\")", expr.Args[0])
	case "toJSON":
		g.Fgenf(w, "JSON.stringify(%v)", expr.Args[0])
	case "sha1":
		g.Fgenf(w, "crypto.createHash('sha1').update(%v).digest('hex')", expr.Args[0])

	default:
		var rng hcl.Range
		if expr.Syntax != nil {
			rng = expr.Syntax.Range()
		}
		g.genNYI(w, "FunctionCallExpression: %v (%v)", expr.Name, rng)
	}
}

func (g *generator) GenIndexExpression(w io.Writer, expr *model.IndexExpression) {
	g.Fgenf(w, "%.20v[%.v]", expr.Collection, expr.Key)
}

func (g *generator) genStringLiteral(w io.Writer, v string) {
	builder := strings.Builder{}
	newlines := strings.Count(v, "\n")
	if newlines == 0 || newlines == 1 && (v[0] == '\n' || v[len(v)-1] == '\n') {
		// This string either does not contain newlines or contains a single leading or trailing newline, so we'll
		// Generate a normal string literal. Quotes, backslashes, and newlines will be escaped in conformance with
		// ECMA-262 11.8.4 ("String Literals").
		builder.WriteRune('"')
		for _, c := range v {
			if c == '\n' {
				builder.WriteString(`\n`)
			} else {
				if c == '"' || c == '\\' {
					builder.WriteRune('\\')
				}
				builder.WriteRune(c)
			}
		}
		builder.WriteRune('"')
	} else {
		// This string does contain newlines, so we'll Generate a template string literal. "${", backquotes, and
		// backslashes will be escaped in conformance with ECMA-262 11.8.6 ("Template Literal Lexical Components").
		runes := []rune(v)
		builder.WriteRune('`')
		for i, c := range runes {
			switch c {
			case '$':
				if i < len(runes)-1 && runes[i+1] == '{' {
					builder.WriteRune('\\')
				}
			case '`', '\\':
				builder.WriteRune('\\')
			}
			builder.WriteRune(c)
		}
		builder.WriteRune('`')
	}

	g.Fgenf(w, "%s", builder.String())
}

func (g *generator) GenLiteralValueExpression(w io.Writer, expr *model.LiteralValueExpression) {
	typ := expr.Type()
	if cns, ok := typ.(*model.ConstType); ok {
		typ = cns.Type
	}

	switch typ {
	case model.BoolType:
		g.Fgenf(w, "%v", expr.Value.True())
	case model.NoneType:
		g.Fgen(w, "undefined")
	case model.NumberType:
		bf := expr.Value.AsBigFloat()
		if i, acc := bf.Int64(); acc == big.Exact {
			g.Fgenf(w, "%d", i)
		} else {
			f, _ := bf.Float64()
			g.Fgenf(w, "%g", f)
		}
	case model.StringType:
		g.genStringLiteral(w, expr.Value.AsString())
	default:
		contract.Failf("unexpected literal type in GenLiteralValueExpression: %v (%v)", expr.Type(),
			expr.SyntaxNode().Range())
	}
}

func (g *generator) literalKey(x model.Expression) (string, bool) {
	strKey := ""
	switch x := x.(type) {
	case *model.LiteralValueExpression:
		if model.StringType.AssignableFrom(x.Type()) {
			strKey = x.Value.AsString()
			break
		}
		var buf bytes.Buffer
		g.GenLiteralValueExpression(&buf, x)
		return buf.String(), true
	case *model.TemplateExpression:
		if len(x.Parts) == 1 {
			if lit, ok := x.Parts[0].(*model.LiteralValueExpression); ok && model.StringType.AssignableFrom(lit.Type()) {
				strKey = lit.Value.AsString()
				break
			}
		}
		var buf bytes.Buffer
		g.GenTemplateExpression(&buf, x)
		return buf.String(), true
	default:
		return "", false
	}

	if isLegalIdentifier(strKey) {
		return strKey, true
	}
	return fmt.Sprintf("%q", strKey), true

}

func (g *generator) GenObjectConsExpression(w io.Writer, expr *model.ObjectConsExpression) {
	if len(expr.Items) == 0 {
		g.Fgen(w, "{}")
	} else {
		g.Fgen(w, "{")
		g.Indented(func() {
			for _, item := range expr.Items {
				g.Fgenf(w, "\n%s", g.Indent)
				if lit, ok := g.literalKey(item.Key); ok {
					g.Fgenf(w, "%s", lit)
				} else {
					g.Fgenf(w, "[%.v]", item.Key)
				}
				g.Fgenf(w, ": %.v,", item.Value)
			}
		})
		g.Fgenf(w, "\n%s}", g.Indent)
	}
}

func (g *generator) genRelativeTraversal(w io.Writer, traversal hcl.Traversal, parts []model.Traversable) {
	for i, part := range traversal {
		var key cty.Value
		switch part := part.(type) {
		case hcl.TraverseAttr:
			key = cty.StringVal(part.Name)
		case hcl.TraverseIndex:
			key = part.Key
		default:
			contract.Failf("unexpected traversal part of type %T (%v)", part, part.SourceRange())
		}

		if model.IsOptionalType(model.GetTraversableType(parts[i])) {
			g.Fgen(w, "?")
		}

		switch key.Type() {
		case cty.String:
			keyVal := key.AsString()
			if isLegalIdentifier(keyVal) {
				g.Fgenf(w, ".%s", keyVal)
			} else {
				g.Fgenf(w, "[%q]", keyVal)
			}
		case cty.Number:
			idx, _ := key.AsBigFloat().Int64()
			g.Fgenf(w, "[%d]", idx)
		default:
			g.Fgenf(w, "[%q]", key.AsString())
		}
	}
}

func (g *generator) GenRelativeTraversalExpression(w io.Writer, expr *model.RelativeTraversalExpression) {
	g.Fgenf(w, "%.20v", expr.Source)
	g.genRelativeTraversal(w, expr.Traversal, expr.Parts)
}

func (g *generator) GenScopeTraversalExpression(w io.Writer, expr *model.ScopeTraversalExpression) {
	rootName := makeValidIdentifier(expr.RootName)
	if _, ok := expr.Parts[0].(*model.SplatVariable); ok {
		rootName = "__item"
	}

	g.Fgen(w, rootName)
	g.genRelativeTraversal(w, expr.Traversal.SimpleSplit().Rel, expr.Parts)
}

func (g *generator) GenSplatExpression(w io.Writer, expr *model.SplatExpression) {
	g.Fgenf(w, "%.20v.map(__item => %.v)", expr.Source, expr.Each)
}

func (g *generator) GenTemplateExpression(w io.Writer, expr *model.TemplateExpression) {
	if len(expr.Parts) == 1 {
		if lit, ok := expr.Parts[0].(*model.LiteralValueExpression); ok && model.StringType.AssignableFrom(lit.Type()) {
			g.GenLiteralValueExpression(w, lit)
			return
		}
	}

	g.Fgen(w, "`")
	for _, expr := range expr.Parts {
		if lit, ok := expr.(*model.LiteralValueExpression); ok && model.StringType.AssignableFrom(lit.Type()) {
			g.Fgen(w, lit.Value.AsString())
		} else {
			g.Fgenf(w, "${%.v}", expr)
		}
	}
	g.Fgen(w, "`")
}

func (g *generator) GenTemplateJoinExpression(w io.Writer, expr *model.TemplateJoinExpression) {
	g.genNYI(w, "TemplateJoinExpression")
}

func (g *generator) GenTupleConsExpression(w io.Writer, expr *model.TupleConsExpression) {
	switch len(expr.Expressions) {
	case 0:
		g.Fgen(w, "[]")
	case 1:
		g.Fgenf(w, "[%.v]", expr.Expressions[0])
	default:
		g.Fgen(w, "[")
		g.Indented(func() {
			for _, v := range expr.Expressions {
				g.Fgenf(w, "\n%s%.v,", g.Indent, v)
			}
		})
		g.Fgen(w, "\n", g.Indent, "]")
	}
}

func (g *generator) GenUnaryOpExpression(w io.Writer, expr *model.UnaryOpExpression) {
	opstr, precedence := "", g.GetPrecedence(expr)
	switch expr.Operation {
	case hclsyntax.OpLogicalNot:
		opstr = "!"
	case hclsyntax.OpNegate:
		opstr = "-"
	}
	g.Fgenf(w, "%[2]v%.[1]*[3]v", precedence, opstr, expr.Operand)
}
