package configupgrade

import (
	"bytes"
	"fmt"
	"log"
	"strconv"
	"strings"

	hcl2 "github.com/hashicorp/hcl/v2"
	hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/zclconf/go-cty/cty"

	hcl1ast "github.com/hashicorp/hcl/hcl/ast"
	hcl1printer "github.com/hashicorp/hcl/hcl/printer"
	hcl1token "github.com/hashicorp/hcl/hcl/token"

	"github.com/hashicorp/hil"
	hilast "github.com/hashicorp/hil/ast"

	"github.com/hashicorp/terraform/addrs"
	"github.com/hashicorp/terraform/configs/configschema"
	"github.com/hashicorp/terraform/tfdiags"
)

func upgradeExpr(val interface{}, filename string, interp bool, an *analysis) ([]byte, tfdiags.Diagnostics) {
	var buf bytes.Buffer
	var diags tfdiags.Diagnostics

	// "val" here can be either a hcl1ast.Node or a hilast.Node, since both
	// of these correspond to expressions in HCL2. Therefore we need to
	// comprehensively handle every possible HCL1 *and* HIL AST node type
	// and, at minimum, print it out as-is in HCL2 syntax.
Value:
	switch tv := val.(type) {

	case *hcl1ast.LiteralType:
		return upgradeExpr(tv.Token, filename, interp, an)

	case hcl1token.Token:
		switch tv.Type {
		case hcl1token.STRING:
			litVal := tv.Value()
			if !interp {
				// Easy case, then.
				printQuotedString(&buf, litVal.(string))
				break
			}

			hilNode, err := hil.Parse(litVal.(string))
			if err != nil {
				diags = diags.Append(&hcl2.Diagnostic{
					Severity: hcl2.DiagError,
					Summary:  "Invalid interpolated string",
					Detail:   fmt.Sprintf("Interpolation parsing failed: %s", err),
					Subject:  hcl1PosRange(filename, tv.Pos).Ptr(),
				})
				return nil, diags
			}

			interpSrc, interpDiags := upgradeExpr(hilNode, filename, interp, an)
			buf.Write(interpSrc)
			diags = diags.Append(interpDiags)

		case hcl1token.HEREDOC:
			// HCL1's "Value" method for tokens pulls out the body and removes
			// any indents in the source for a flush heredoc, which throws away
			// information we need to upgrade. Therefore we're going to
			// re-implement a subset of that logic here where we want to retain
			// the whitespace verbatim even in flush mode.

			firstNewlineIdx := strings.IndexByte(tv.Text, '\n')
			if firstNewlineIdx < 0 {
				// Should never happen, because tv.Value would already have
				// panicked above in this case.
				panic("heredoc doesn't contain newline")
			}
			introducer := tv.Text[:firstNewlineIdx+1]
			marker := introducer[2:] // trim off << prefix
			if marker[0] == '-' {
				marker = marker[1:] // also trim of - prefix for flush heredoc
			}
			body := tv.Text[len(introducer) : len(tv.Text)-len(marker)]
			flush := introducer[2] == '-'
			if flush {
				// HCL1 treats flush heredocs differently, trimming off any
				// spare whitespace that might appear after the trailing
				// newline, and so we must replicate that here to avoid
				// introducing additional whitespace in the output.
				body = strings.TrimRight(body, " \t")
			}

			// Now we have:
			//  - introducer is the first line, like "<<-FOO\n"
			//  - marker is the end marker, like "FOO\n"
			//  - body is the raw data between the introducer and the marker,
			//    which we need to do recursive upgrading for.

			buf.WriteString(introducer)
			if !interp {
				// Easy case: escape all interpolation-looking sequences.
				printHeredocLiteralFromHILOutput(&buf, body)
			} else {
				hilNode, err := hil.Parse(body)
				if err != nil {
					diags = diags.Append(&hcl2.Diagnostic{
						Severity: hcl2.DiagError,
						Summary:  "Invalid interpolated string",
						Detail:   fmt.Sprintf("Interpolation parsing failed: %s", err),
						Subject:  hcl1PosRange(filename, tv.Pos).Ptr(),
					})
				}
				if hilNode != nil {
					if _, ok := hilNode.(*hilast.Output); !ok {
						// hil.Parse usually produces an output, but it can sometimes
						// produce an isolated expression if the input is entirely
						// a single interpolation.
						if hilNode != nil {
							hilNode = &hilast.Output{
								Exprs: []hilast.Node{hilNode},
								Posx:  hilNode.Pos(),
							}
						}
					}
					interpDiags := upgradeHeredocBody(&buf, hilNode.(*hilast.Output), filename, an)
					diags = diags.Append(interpDiags)
				}
			}
			if !strings.HasSuffix(body, "\n") {
				// The versions of HCL1 vendored into Terraform <=0.11
				// incorrectly allowed the end marker to appear at the end of
				// the final line of the body, rather than on a line of its own.
				// That is no longer valid in HCL2, so we need to fix it up.
				buf.WriteByte('\n')
			}
			// NOTE: Marker intentionally contains an extra newline here because
			// we need to ensure that any follow-on expression bits end up on
			// a separate line, or else the HCL2 parser won't be able to
			// recognize the heredoc marker. This causes an extra empty line
			// in some cases, which we accept for simplicity's sake.
			buf.WriteString(marker)

		case hcl1token.BOOL:
			litVal := tv.Value()
			if litVal.(bool) {
				buf.WriteString("true")
			} else {
				buf.WriteString("false")
			}

		case hcl1token.NUMBER:
			num, err := strconv.ParseInt(tv.Text, 0, 64)
			if err != nil {
				diags = diags.Append(&hcl2.Diagnostic{
					Severity: hcl2.DiagError,
					Summary:  "Invalid number value",
					Detail:   fmt.Sprintf("Parsing failed: %s", err),
					Subject:  hcl1PosRange(filename, tv.Pos).Ptr(),
				})
			}
			buf.WriteString(strconv.FormatInt(num, 10))

		case hcl1token.FLOAT:
			num, err := strconv.ParseFloat(tv.Text, 64)
			if err != nil {
				diags = diags.Append(&hcl2.Diagnostic{
					Severity: hcl2.DiagError,
					Summary:  "Invalid float value",
					Detail:   fmt.Sprintf("Parsing failed: %s", err),
					Subject:  hcl1PosRange(filename, tv.Pos).Ptr(),
				})
			}
			buf.WriteString(strconv.FormatFloat(num, 'f', -1, 64))

		default:
			// For everything else we'll just pass through the given bytes verbatim,
			// but we should't get here because the above is intended to be exhaustive.
			buf.WriteString(tv.Text)

		}

	case *hcl1ast.ListType:
		multiline := tv.Lbrack.Line != tv.Rbrack.Line
		buf.WriteString("[")
		if multiline {
			buf.WriteString("\n")
		}
		for i, node := range tv.List {
			src, moreDiags := upgradeExpr(node, filename, interp, an)
			diags = diags.Append(moreDiags)
			buf.Write(src)
			if lit, ok := node.(*hcl1ast.LiteralType); ok && lit.LineComment != nil {
				for _, comment := range lit.LineComment.List {
					buf.WriteString(", " + comment.Text)
					buf.WriteString("\n")
				}
			} else {
				if multiline {
					buf.WriteString(",\n")
				} else if i < len(tv.List)-1 {
					buf.WriteString(", ")
				}
			}
		}
		buf.WriteString("]")

	case *hcl1ast.ObjectType:
		if len(tv.List.Items) == 0 {
			buf.WriteString("{}")
			break
		}
		buf.WriteString("{\n")
		for _, item := range tv.List.Items {
			if len(item.Keys) != 1 {
				diags = diags.Append(&hcl2.Diagnostic{
					Severity: hcl2.DiagError,
					Summary:  "Invalid map element",
					Detail:   "A map element may not have any block-style labels.",
					Subject:  hcl1PosRange(filename, item.Pos()).Ptr(),
				})
				continue
			}
			keySrc, moreDiags := upgradeExpr(item.Keys[0].Token, filename, interp, an)
			diags = diags.Append(moreDiags)
			valueSrc, moreDiags := upgradeExpr(item.Val, filename, interp, an)
			diags = diags.Append(moreDiags)
			if item.LeadComment != nil {
				for _, c := range item.LeadComment.List {
					buf.WriteString(c.Text)
					buf.WriteByte('\n')
				}
			}

			buf.Write(keySrc)
			buf.WriteString(" = ")
			buf.Write(valueSrc)
			if item.LineComment != nil {
				for _, c := range item.LineComment.List {
					buf.WriteByte(' ')
					buf.WriteString(c.Text)
				}
			}
			buf.WriteString("\n")
		}
		buf.WriteString("}")

	case hcl1ast.Node:
		// If our more-specific cases above didn't match this then we'll
		// ask the hcl1printer package to print the expression out
		// itself, and assume it'll still be valid in HCL2.
		// (We should rarely end up here, since our cases above should
		// be comprehensive.)
		log.Printf("[TRACE] configupgrade: Don't know how to upgrade %T as expression, so just passing it through as-is", tv)
		hcl1printer.Fprint(&buf, tv)

	case *hilast.LiteralNode:
		switch tl := tv.Value.(type) {
		case string:
			// This shouldn't generally happen because literal strings are
			// always wrapped in hilast.Output in HIL, but we'll allow it anyway.
			printQuotedString(&buf, tl)
		case int:
			buf.WriteString(strconv.Itoa(tl))
		case float64:
			buf.WriteString(strconv.FormatFloat(tl, 'f', -1, 64))
		case bool:
			if tl {
				buf.WriteString("true")
			} else {
				buf.WriteString("false")
			}
		}

	case *hilast.VariableAccess:
		// In HIL a variable access is just a single string which might contain
		// a mixture of identifiers, dots, integer indices, and splat expressions.
		// All of these concepts were formerly interpreted by Terraform itself,
		// rather than by HIL. We're going to process this one chunk at a time
		// here so we can normalize and introduce some newer syntax where it's
		// safe to do so.
		parts := strings.Split(tv.Name, ".")

		transformed := transformCountPseudoAttribute(&buf, parts, an)
		if transformed {
			break Value
		}

		parts = upgradeTraversalParts(parts, an) // might add/remove/change parts

		vDiags := validateHilAddress(tv.Name, filename)
		if len(vDiags) > 0 {
			diags = diags.Append(vDiags)
			break
		}

		printHilTraversalPartsAsHcl2(&buf, parts)

	case *hilast.Arithmetic:
		op, exists := hilArithmeticOpSyms[tv.Op]
		if !exists {
			panic(fmt.Errorf("arithmetic node with unsupported operator %#v", tv.Op))
		}

		lhsExpr := tv.Exprs[0]
		rhsExpr := tv.Exprs[1]
		lhsSrc, exprDiags := upgradeExpr(lhsExpr, filename, true, an)
		diags = diags.Append(exprDiags)
		rhsSrc, exprDiags := upgradeExpr(rhsExpr, filename, true, an)
		diags = diags.Append(exprDiags)

		// HIL's AST represents -foo as (0 - foo), so we'll recognize
		// that here and normalize it back.
		if tv.Op == hilast.ArithmeticOpSub && len(lhsSrc) == 1 && lhsSrc[0] == '0' {
			buf.WriteString("-")
			buf.Write(rhsSrc)
			break
		}

		buf.Write(lhsSrc)
		buf.WriteString(op)
		buf.Write(rhsSrc)

	case *hilast.Call:
		name := tv.Func
		args := tv.Args

		// Some adaptations must happen prior to upgrading the arguments,
		// because they depend on the original argument AST nodes.
		switch name {
		case "base64sha256", "base64sha512", "md5", "sha1", "sha256", "sha512":
			// These functions were sometimes used in conjunction with the
			// file() function to take the hash of the contents of a file.
			// Prior to Terraform 0.11 there was a chance of silent corruption
			// of strings containing non-UTF8 byte sequences, and so we have
			// made it illegal to use file() with non-text files in 0.12 even
			// though in this _particular_ situation (passing the function
			// result directly to another function) there would not be any
			// corruption; the general rule keeps things consistent.
			// However, to still meet those use-cases we now have variants of
			// the hashing functions that have a "file" prefix on their names
			// and read the contents of a given file, rather than hashing
			// directly the given string.
			if len(args) > 0 {
				if subCall, ok := args[0].(*hilast.Call); ok && subCall.Func == "file" {
					// We're going to flatten this down into a single call, so
					// we actually want the arguments of the sub-call here.
					name = "file" + name
					args = subCall.Args

					// For this one, we'll fall through to the normal upgrade
					// handling now that we've fixed up the name and args...
				}
			}

		}

		argExprs := make([][]byte, len(args))
		multiline := false
		totalLen := 0
		for i, arg := range args {
			if i > 0 {
				totalLen += 2
			}
			exprSrc, exprDiags := upgradeExpr(arg, filename, true, an)
			diags = diags.Append(exprDiags)
			argExprs[i] = exprSrc
			if bytes.Contains(exprSrc, []byte{'\n'}) {
				// If any of our arguments are multi-line then we'll also be multiline
				multiline = true
			}
			totalLen += len(exprSrc)
		}

		if totalLen > 60 { // heuristic, since we don't know here how indented we are already
			multiline = true
		}

		// Some functions are now better expressed as native language constructs.
		// These cases will return early if they emit anything, or otherwise
		// fall through to the default emitter.
		switch name {
		case "list":
			// Should now use tuple constructor syntax
			buf.WriteByte('[')
			if multiline {
				buf.WriteByte('\n')
			}
			for i, exprSrc := range argExprs {
				buf.Write(exprSrc)
				if multiline {
					buf.WriteString(",\n")
				} else {
					if i < len(args)-1 {
						buf.WriteString(", ")
					}
				}
			}
			buf.WriteByte(']')
			break Value
		case "map":
			// Should now use object constructor syntax, but we can only
			// achieve that if the call is valid, which requires an even
			// number of arguments.
			if len(argExprs) == 0 {
				buf.WriteString("{}")
				break Value
			} else if len(argExprs)%2 == 0 {
				buf.WriteString("{\n")
				for i := 0; i < len(argExprs); i += 2 {
					k := argExprs[i]
					v := argExprs[i+1]

					buf.Write(k)
					buf.WriteString(" = ")
					buf.Write(v)
					buf.WriteByte('\n')
				}
				buf.WriteByte('}')
				break Value
			}
		case "lookup":
			// A lookup call with only two arguments is equivalent to native
			// index syntax. (A third argument would specify a default value,
			// so calls like that must be left alone.)
			// (Note that we can't safely do this for element(...) because
			// the user may be relying on its wraparound behavior.)
			if len(argExprs) == 2 {
				buf.Write(argExprs[0])
				buf.WriteByte('[')
				buf.Write(argExprs[1])
				buf.WriteByte(']')
				break Value
			}
		case "element":
			// We cannot replace element with index syntax safely in general
			// because users may be relying on its special modulo wraparound
			// behavior that the index syntax doesn't do. However, if it seems
			// like the user is trying to use element with a set, we'll insert
			// an explicit conversion to list to mimic the implicit conversion
			// that we used to do as an unintended side-effect of how functions
			// work in HIL.
			if len(argExprs) > 0 {
				argTy := an.InferExpressionType(argExprs[0], nil)
				if argTy.IsSetType() {
					newExpr := []byte(`tolist(`)
					newExpr = append(newExpr, argExprs[0]...)
					newExpr = append(newExpr, ')')
					argExprs[0] = newExpr
				}
			}

			// HIL used some undocumented special functions to implement certain
			// operations, but since those were actually callable in real expressions
			// some users inevitably depended on them, so we'll fix them up here.
			// These each become two function calls to preserve the old behavior
			// of implicitly converting to the source type first. Usage of these
			// is relatively rare, so the result doesn't need to be too pretty.
		case "__builtin_BoolToString":
			buf.WriteString("tostring(tobool(")
			buf.Write(argExprs[0])
			buf.WriteString("))")
			break Value
		case "__builtin_FloatToString":
			buf.WriteString("tostring(tonumber(")
			buf.Write(argExprs[0])
			buf.WriteString("))")
			break Value
		case "__builtin_IntToString":
			buf.WriteString("tostring(floor(")
			buf.Write(argExprs[0])
			buf.WriteString("))")
			break Value
		case "__builtin_StringToInt":
			buf.WriteString("floor(tostring(")
			buf.Write(argExprs[0])
			buf.WriteString("))")
			break Value
		case "__builtin_StringToFloat":
			buf.WriteString("tonumber(tostring(")
			buf.Write(argExprs[0])
			buf.WriteString("))")
			break Value
		case "__builtin_StringToBool":
			buf.WriteString("tobool(tostring(")
			buf.Write(argExprs[0])
			buf.WriteString("))")
			break Value
		case "__builtin_FloatToInt", "__builtin_IntToFloat":
			// Since "floor" already has an implicit conversion of its argument
			// to number, and the result is a whole number in either case,
			// these ones are easier. (We no longer distinguish int and float
			// as types in HCL2, even though HIL did.)
			name = "floor"
		}

		buf.WriteString(name)
		buf.WriteByte('(')
		if multiline {
			buf.WriteByte('\n')
		}
		for i, exprSrc := range argExprs {
			buf.Write(exprSrc)
			if multiline {
				buf.WriteString(",\n")
			} else {
				if i < len(args)-1 {
					buf.WriteString(", ")
				}
			}
		}
		buf.WriteByte(')')

	case *hilast.Conditional:
		condSrc, exprDiags := upgradeExpr(tv.CondExpr, filename, true, an)
		diags = diags.Append(exprDiags)
		trueSrc, exprDiags := upgradeExpr(tv.TrueExpr, filename, true, an)
		diags = diags.Append(exprDiags)
		falseSrc, exprDiags := upgradeExpr(tv.FalseExpr, filename, true, an)
		diags = diags.Append(exprDiags)

		buf.Write(condSrc)
		buf.WriteString(" ? ")
		buf.Write(trueSrc)
		buf.WriteString(" : ")
		buf.Write(falseSrc)

	case *hilast.Index:
		target, ok := tv.Target.(*hilast.VariableAccess)
		if !ok {
			panic(fmt.Sprintf("Index node with unsupported target type (%T)", tv.Target))
		}
		parts := strings.Split(target.Name, ".")

		keySrc, exprDiags := upgradeExpr(tv.Key, filename, true, an)
		diags = diags.Append(exprDiags)

		transformed := transformCountPseudoAttribute(&buf, parts, an)
		if transformed {
			break Value
		}

		parts = upgradeTraversalParts(parts, an) // might add/remove/change parts

		vDiags := validateHilAddress(target.Name, filename)
		if len(vDiags) > 0 {
			diags = diags.Append(vDiags)
			break
		}

		first, remain := parts[0], parts[1:]

		var rAddr addrs.Resource
		switch parts[0] {
		case "data":
			if len(parts) == 5 && parts[3] == "*" {
				rAddr.Mode = addrs.DataResourceMode
				rAddr.Type = parts[1]
				rAddr.Name = parts[2]
			}
		default:
			if len(parts) == 4 && parts[2] == "*" {
				rAddr.Mode = addrs.ManagedResourceMode
				rAddr.Type = parts[0]
				rAddr.Name = parts[1]
			}
		}

		// We need to check if the thing being referenced has count
		// to retain backward compatibility
		hasCount := false
		if v, exists := an.ResourceHasCount[rAddr]; exists {
			hasCount = v
		}

		hasSplat := false

		buf.WriteString(first)
		for _, part := range remain {
			// Attempt to convert old-style splat indexing to new one
			// e.g. res.label.*.attr[idx] to res.label[idx].attr
			if part == "*" && hasCount {
				hasSplat = true
				buf.WriteString(fmt.Sprintf("[%s]", keySrc))
				continue
			}

			buf.WriteByte('.')
			buf.WriteString(part)
		}

		if !hasSplat {
			buf.WriteString("[")
			buf.Write(keySrc)
			buf.WriteString("]")
		}

	case *hilast.Output:
		if len(tv.Exprs) == 1 {
			item := tv.Exprs[0]
			naked := true
			if lit, ok := item.(*hilast.LiteralNode); ok {
				if _, ok := lit.Value.(string); ok {
					naked = false
				}
			}
			if naked {
				// If there's only one expression and it isn't a literal string
				// then we'll just output it naked, since wrapping a single
				// expression in interpolation is no longer idiomatic.
				interped, interpDiags := upgradeExpr(item, filename, true, an)
				diags = diags.Append(interpDiags)
				buf.Write(interped)
				break
			}
		}

		buf.WriteString(`"`)
		for _, item := range tv.Exprs {
			if lit, ok := item.(*hilast.LiteralNode); ok {
				if litStr, ok := lit.Value.(string); ok {
					printStringLiteralFromHILOutput(&buf, litStr)
					continue
				}
			}

			interped, interpDiags := upgradeExpr(item, filename, true, an)
			diags = diags.Append(interpDiags)

			buf.WriteString("${")
			buf.Write(interped)
			buf.WriteString("}")
		}
		buf.WriteString(`"`)

	case hilast.Node:
		// Nothing reasonable we can do here, so we should've handled all of
		// the possibilities above.
		panic(fmt.Errorf("upgradeExpr doesn't handle HIL node type %T", tv))

	default:
		// If we end up in here then the caller gave us something completely invalid.
		panic(fmt.Errorf("upgradeExpr on unsupported type %T", val))

	}

	return buf.Bytes(), diags
}

func validateHilAddress(address, filename string) tfdiags.Diagnostics {
	parts := strings.Split(address, ".")
	var diags tfdiags.Diagnostics

	label, ok := getResourceLabel(parts)
	if ok && !hcl2syntax.ValidIdentifier(label) {
		// We can't get any useful source location out of HIL unfortunately
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			fmt.Sprintf("Invalid address (%s) in ./%s", address, filename),
			// The label could be invalid for another reason
			// but this is the most likely, so we add it as hint
			"Names of objects (resources, modules, etc) may no longer start with digits."))
	}

	return diags
}

func getResourceLabel(parts []string) (string, bool) {
	if len(parts) < 1 {
		return "", false
	}

	if parts[0] == "data" {
		if len(parts) < 3 {
			return "", false
		}
		return parts[2], true
	}

	if len(parts) < 2 {
		return "", false
	}

	return parts[1], true
}

// transformCountPseudoAttribute deals with the .count pseudo-attributes
// that 0.11 and prior allowed for resources. These no longer exist,
// because they don't do anything we can't do with the length(...) function.
func transformCountPseudoAttribute(buf *bytes.Buffer, parts []string, an *analysis) (transformed bool) {
	if len(parts) > 0 {
		var rAddr addrs.Resource
		switch parts[0] {
		case "data":
			if len(parts) == 4 && parts[3] == "count" {
				rAddr.Mode = addrs.DataResourceMode
				rAddr.Type = parts[1]
				rAddr.Name = parts[2]
			}
		default:
			if len(parts) == 3 && parts[2] == "count" {
				rAddr.Mode = addrs.ManagedResourceMode
				rAddr.Type = parts[0]
				rAddr.Name = parts[1]
			}
		}
		// We need to check if the thing being referenced is actually an
		// existing resource, because other three-part traversals might
		// coincidentally end with "count".
		if hasCount, exists := an.ResourceHasCount[rAddr]; exists {
			if hasCount {
				buf.WriteString("length(")
				buf.WriteString(rAddr.String())
				buf.WriteString(")")
			} else {
				// If the resource does not have count, the .count
				// attr would've always returned 1 before.
				buf.WriteString("1")
			}
			transformed = true
			return
		}
	}
	return
}

func printHilTraversalPartsAsHcl2(buf *bytes.Buffer, parts []string) {
	first, remain := parts[0], parts[1:]
	buf.WriteString(first)
	seenSplat := false
	for _, part := range remain {
		if part == "*" {
			seenSplat = true
			buf.WriteString(".*")
			continue
		}

		// Other special cases apply only if we've not previously
		// seen a splat expression marker, since attribute vs. index
		// syntax have different interpretations after a simple splat.
		if !seenSplat {
			if v, err := strconv.Atoi(part); err == nil {
				// Looks like it's old-style index traversal syntax foo.0.bar
				// so we'll replace with canonical index syntax foo[0].bar.
				fmt.Fprintf(buf, "[%d]", v)
				continue
			}
			if !hcl2syntax.ValidIdentifier(part) {
				// This should be rare since HIL's identifier syntax is _close_
				// to HCL2's, but we'll get here if one of the intervening
				// parts is not a valid identifier in isolation, since HIL
				// did not consider these to be separate identifiers.
				// e.g. foo.1bar would be invalid in HCL2; must instead be foo["1bar"].
				buf.WriteByte('[')
				printQuotedString(buf, part)
				buf.WriteByte(']')
				continue
			}
		}

		buf.WriteByte('.')
		buf.WriteString(part)
	}
}

func upgradeHeredocBody(buf *bytes.Buffer, val *hilast.Output, filename string, an *analysis) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	for _, item := range val.Exprs {
		if lit, ok := item.(*hilast.LiteralNode); ok {
			if litStr, ok := lit.Value.(string); ok {
				printHeredocLiteralFromHILOutput(buf, litStr)
				continue
			}
		}
		interped, interpDiags := upgradeExpr(item, filename, true, an)
		diags = diags.Append(interpDiags)

		buf.WriteString("${")
		buf.Write(interped)
		buf.WriteString("}")
	}

	return diags
}

func upgradeTraversalExpr(val interface{}, filename string, an *analysis) ([]byte, tfdiags.Diagnostics) {
	if lit, ok := val.(*hcl1ast.LiteralType); ok && lit.Token.Type == hcl1token.STRING {
		trStr := lit.Token.Value().(string)
		if strings.HasSuffix(trStr, ".%") || strings.HasSuffix(trStr, ".#") {
			// Terraform 0.11 would often not validate traversals given in
			// strings and so users would get away with this sort of
			// flatmap-implementation-detail reference, particularly inside
			// ignore_changes. We'll just trim these off to tolerate it,
			// rather than failing below in ParseTraversalAbs.
			trStr = trStr[:len(trStr)-2]
		}
		trSrc := []byte(trStr)
		_, trDiags := hcl2syntax.ParseTraversalAbs(trSrc, "", hcl2.Pos{})
		if !trDiags.HasErrors() {
			return trSrc, nil
		}
	}
	return upgradeExpr(val, filename, false, an)
}

var hilArithmeticOpSyms = map[hilast.ArithmeticOp]string{
	hilast.ArithmeticOpAdd: " + ",
	hilast.ArithmeticOpSub: " - ",
	hilast.ArithmeticOpMul: " * ",
	hilast.ArithmeticOpDiv: " / ",
	hilast.ArithmeticOpMod: " % ",

	hilast.ArithmeticOpLogicalAnd: " && ",
	hilast.ArithmeticOpLogicalOr:  " || ",

	hilast.ArithmeticOpEqual:              " == ",
	hilast.ArithmeticOpNotEqual:           " != ",
	hilast.ArithmeticOpLessThan:           " < ",
	hilast.ArithmeticOpLessThanOrEqual:    " <= ",
	hilast.ArithmeticOpGreaterThan:        " > ",
	hilast.ArithmeticOpGreaterThanOrEqual: " >= ",
}

// upgradeTraversalParts might alter the given split parts from a HIL-style
// variable access to account for renamings made in Terraform v0.12.
func upgradeTraversalParts(parts []string, an *analysis) []string {
	parts = upgradeCountTraversalParts(parts, an)
	parts = upgradeTerraformRemoteStateTraversalParts(parts, an)
	return parts
}

func upgradeCountTraversalParts(parts []string, an *analysis) []string {
	// test_instance.foo.id needs to become test_instance.foo[0].id if
	// count is set for test_instance.foo. Likewise, if count _isn't_ set
	// then test_instance.foo.0.id must become test_instance.foo.id.
	if len(parts) < 3 {
		return parts
	}
	var addr addrs.Resource
	var idxIdx int
	switch parts[0] {
	case "data":
		addr.Mode = addrs.DataResourceMode
		addr.Type = parts[1]
		addr.Name = parts[2]
		idxIdx = 3
	default:
		addr.Mode = addrs.ManagedResourceMode
		addr.Type = parts[0]
		addr.Name = parts[1]
		idxIdx = 2
	}

	hasCount, exists := an.ResourceHasCount[addr]
	if !exists {
		// Probably not actually a resource instance at all, then.
		return parts
	}

	// Since at least one attribute is required after a resource reference
	// prior to Terraform v0.12, we can assume there will be at least enough
	// parts to contain the index even if no index is actually present.
	if idxIdx >= len(parts) {
		return parts
	}

	maybeIdx := parts[idxIdx]
	switch {
	case hasCount:
		if _, err := strconv.Atoi(maybeIdx); err == nil || maybeIdx == "*" {
			// Has an index already, so no changes required.
			return parts
		}
		// Need to insert index zero at idxIdx.
		log.Printf("[TRACE] configupgrade: %s has count but reference does not have index, so adding one", addr)
		newParts := make([]string, len(parts)+1)
		copy(newParts, parts[:idxIdx])
		newParts[idxIdx] = "0"
		copy(newParts[idxIdx+1:], parts[idxIdx:])
		return newParts
	default:
		// For removing indexes we'll be more conservative and only remove
		// exactly index "0", because other indexes on a resource without
		// count are invalid anyway and we're better off letting the normal
		// configuration parser deal with that.
		if maybeIdx != "0" {
			return parts
		}

		// Need to remove the index zero.
		log.Printf("[TRACE] configupgrade: %s does not have count but reference has index, so removing it", addr)
		newParts := make([]string, len(parts)-1)
		copy(newParts, parts[:idxIdx])
		copy(newParts[idxIdx:], parts[idxIdx+1:])
		return newParts
	}
}

func upgradeTerraformRemoteStateTraversalParts(parts []string, an *analysis) []string {
	// data.terraform_remote_state.x.foo needs to become
	// data.terraform_remote_state.x.outputs.foo unless "foo" is a real
	// attribute in the object type implied by the remote state schema.
	if len(parts) < 4 {
		return parts
	}
	if parts[0] != "data" || parts[1] != "terraform_remote_state" {
		return parts
	}

	attrIdx := 3
	if parts[attrIdx] == "*" {
		attrIdx = 4 // data.terraform_remote_state.x.*.foo
	} else if _, err := strconv.Atoi(parts[attrIdx]); err == nil {
		attrIdx = 4 // data.terraform_remote_state.x.1.foo
	}
	if attrIdx >= len(parts) {
		return parts
	}

	attrName := parts[attrIdx]

	// Now we'll use the schema of data.terraform_remote_state to decide if
	// the user intended this to be an output, or whether it's one of the real
	// attributes of this data source.
	var schema *configschema.Block
	if providerSchema := an.ProviderSchemas["terraform"]; providerSchema != nil {
		schema, _ = providerSchema.SchemaForResourceType(addrs.DataResourceMode, "terraform_remote_state")
	}
	// Schema should be available in all reasonable cases, but might be nil
	// if input configuration contains a reference to a remote state data resource
	// without actually defining that data resource. In that weird edge case,
	// we'll just assume all attributes are outputs.
	if schema != nil && schema.ImpliedType().HasAttribute(attrName) {
		// User is accessing one of the real attributes, then, and we have
		// no need to rewrite it.
		return parts
	}

	// If we get down here then our task is to produce a new parts slice
	// that has the fixed additional attribute name "outputs" inserted at
	// attrIdx, retaining all other parts.
	newParts := make([]string, len(parts)+1)
	copy(newParts, parts[:attrIdx])
	newParts[attrIdx] = "outputs"
	copy(newParts[attrIdx+1:], parts[attrIdx:])
	return newParts
}

func typeIsSettableFromTupleCons(ty cty.Type) bool {
	return ty.IsListType() || ty.IsTupleType() || ty.IsSetType()
}
