package format

import (
	"bufio"
	"bytes"
	"fmt"
	"log"
	"sort"
	"strings"

	"github.com/mitchellh/colorstring"
	"github.com/zclconf/go-cty/cty"
	ctyjson "github.com/zclconf/go-cty/cty/json"

	"github.com/hashicorp/terraform/addrs"
	"github.com/hashicorp/terraform/configs/configschema"
	"github.com/hashicorp/terraform/plans"
	"github.com/hashicorp/terraform/plans/objchange"
	"github.com/hashicorp/terraform/states"
)

// ResourceChange returns a string representation of a change to a particular
// resource, for inclusion in user-facing plan output.
//
// The resource schema must be provided along with the change so that the
// formatted change can reflect the configuration structure for the associated
// resource.
//
// If "color" is non-nil, it will be used to color the result. Otherwise,
// no color codes will be included.
func ResourceChange(
	change *plans.ResourceInstanceChangeSrc,
	schema *configschema.Block,
	color *colorstring.Colorize,
) string {
	addr := change.Addr
	var buf bytes.Buffer

	if color == nil {
		color = &colorstring.Colorize{
			Colors:  colorstring.DefaultColors,
			Disable: true,
			Reset:   false,
		}
	}

	dispAddr := addr.String()
	if change.DeposedKey != states.NotDeposed {
		dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, change.DeposedKey)
	}

	switch change.Action {
	case plans.Create:
		buf.WriteString(color.Color(fmt.Sprintf("[bold]  # %s[reset] will be created", dispAddr)))
	case plans.Read:
		buf.WriteString(color.Color(fmt.Sprintf("[bold]  # %s[reset] will be read during apply\n  # (config refers to values not yet known)", dispAddr)))
	case plans.Update:
		buf.WriteString(color.Color(fmt.Sprintf("[bold]  # %s[reset] will be updated in-place", dispAddr)))
	case plans.CreateThenDelete, plans.DeleteThenCreate:
		switch change.ActionReason {
		case plans.ResourceInstanceReplaceBecauseTainted:
			buf.WriteString(color.Color(fmt.Sprintf("[bold]  # %s[reset] is tainted, so must be [bold][red]replaced", dispAddr)))
		case plans.ResourceInstanceReplaceByRequest:
			buf.WriteString(color.Color(fmt.Sprintf("[bold]  # %s[reset] will be [bold][red]replaced[reset], as requested", dispAddr)))
		default:
			buf.WriteString(color.Color(fmt.Sprintf("[bold]  # %s[reset] must be [bold][red]replaced", dispAddr)))
		}
	case plans.Delete:
		buf.WriteString(color.Color(fmt.Sprintf("[bold]  # %s[reset] will be [bold][red]destroyed", dispAddr)))
	default:
		// should never happen, since the above is exhaustive
		buf.WriteString(fmt.Sprintf("%s has an action the plan renderer doesn't support (this is a bug)", dispAddr))
	}
	buf.WriteString(color.Color("[reset]\n"))

	buf.WriteString(color.Color(DiffActionSymbol(change.Action)) + " ")

	switch addr.Resource.Resource.Mode {
	case addrs.ManagedResourceMode:
		buf.WriteString(fmt.Sprintf(
			"resource %q %q",
			addr.Resource.Resource.Type,
			addr.Resource.Resource.Name,
		))
	case addrs.DataResourceMode:
		buf.WriteString(fmt.Sprintf(
			"data %q %q ",
			addr.Resource.Resource.Type,
			addr.Resource.Resource.Name,
		))
	default:
		// should never happen, since the above is exhaustive
		buf.WriteString(addr.String())
	}

	buf.WriteString(" {")

	p := blockBodyDiffPrinter{
		buf:             &buf,
		color:           color,
		action:          change.Action,
		requiredReplace: change.RequiredReplace,
	}

	// Most commonly-used resources have nested blocks that result in us
	// going at least three traversals deep while we recurse here, so we'll
	// start with that much capacity and then grow as needed for deeper
	// structures.
	path := make(cty.Path, 0, 3)

	changeV, err := change.Decode(schema.ImpliedType())
	if err != nil {
		// Should never happen in here, since we've already been through
		// loads of layers of encode/decode of the planned changes before now.
		panic(fmt.Sprintf("failed to decode plan for %s while rendering diff: %s", addr, err))
	}

	// We currently have an opt-out that permits the legacy SDK to return values
	// that defy our usual conventions around handling of nesting blocks. To
	// avoid the rendering code from needing to handle all of these, we'll
	// normalize first.
	// (Ideally we'd do this as part of the SDK opt-out implementation in core,
	// but we've added it here for now to reduce risk of unexpected impacts
	// on other code in core.)
	changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, schema)
	changeV.Change.After = objchange.NormalizeObjectFromLegacySDK(changeV.Change.After, schema)

	// Now that the change is decoded, add back the marks at the defined paths
	if len(change.BeforeValMarks) > 0 {
		changeV.Change.Before = changeV.Change.Before.MarkWithPaths(change.BeforeValMarks)
	}
	if len(change.AfterValMarks) > 0 {
		changeV.Change.After = changeV.Change.After.MarkWithPaths(change.AfterValMarks)
	}

	result := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path)
	if result.bodyWritten {
		buf.WriteString("\n")
		buf.WriteString(strings.Repeat(" ", 4))
	}
	buf.WriteString("}\n")

	return buf.String()
}

// OutputChanges returns a string representation of a set of changes to output
// values for inclusion in user-facing plan output.
//
// If "color" is non-nil, it will be used to color the result. Otherwise,
// no color codes will be included.
func OutputChanges(
	changes []*plans.OutputChangeSrc,
	color *colorstring.Colorize,
) string {
	var buf bytes.Buffer
	p := blockBodyDiffPrinter{
		buf:    &buf,
		color:  color,
		action: plans.Update, // not actually used in this case, because we're not printing a containing block
	}

	// We're going to reuse the codepath we used for printing resource block
	// diffs, by pretending that the set of defined outputs are the attributes
	// of some resource. It's a little forced to do this, but it gives us all
	// the same formatting heuristics as we normally use for resource
	// attributes.
	oldVals := make(map[string]cty.Value, len(changes))
	newVals := make(map[string]cty.Value, len(changes))
	synthSchema := &configschema.Block{
		Attributes: make(map[string]*configschema.Attribute, len(changes)),
	}
	for _, changeSrc := range changes {
		name := changeSrc.Addr.OutputValue.Name
		change, err := changeSrc.Decode()
		if err != nil {
			// It'd be weird to get a decoding error here because that would
			// suggest that Terraform itself just produced an invalid plan, and
			// we don't have any good way to ignore it in this codepath, so
			// we'll just log it and ignore it.
			log.Printf("[ERROR] format.OutputChanges: Failed to decode planned change for output %q: %s", name, err)
			continue
		}
		synthSchema.Attributes[name] = &configschema.Attribute{
			Type:      cty.DynamicPseudoType, // output types are decided dynamically based on the given value
			Optional:  true,
			Sensitive: change.Sensitive,
		}
		oldVals[name] = change.Before
		newVals[name] = change.After
	}

	p.writeBlockBodyDiff(synthSchema, cty.ObjectVal(oldVals), cty.ObjectVal(newVals), 2, nil)

	return buf.String()
}

type blockBodyDiffPrinter struct {
	buf             *bytes.Buffer
	color           *colorstring.Colorize
	action          plans.Action
	requiredReplace cty.PathSet
	// verbose is set to true when using the "diff" printer to format state
	verbose bool
}

type blockBodyDiffResult struct {
	bodyWritten       bool
	skippedAttributes int
	skippedBlocks     int
}

const forcesNewResourceCaption = " [red]# forces replacement[reset]"

// writeBlockBodyDiff writes attribute or block differences
// and returns true if any differences were found and written
func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, old, new cty.Value, indent int, path cty.Path) blockBodyDiffResult {
	path = ctyEnsurePathCapacity(path, 1)
	result := blockBodyDiffResult{}

	// write the attributes diff
	blankBeforeBlocks := p.writeAttrsDiff(schema.Attributes, old, new, indent, path, &result)
	p.writeSkippedAttr(result.skippedAttributes, indent+2)

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

		for _, name := range blockTypeNames {
			blockS := schema.BlockTypes[name]
			oldVal := ctyGetAttrMaybeNull(old, name)
			newVal := ctyGetAttrMaybeNull(new, name)

			result.bodyWritten = true
			skippedBlocks := p.writeNestedBlockDiffs(name, blockS, oldVal, newVal, blankBeforeBlocks, indent, path)
			if skippedBlocks > 0 {
				result.skippedBlocks += skippedBlocks
			}

			// Always include a blank for any subsequent block types.
			blankBeforeBlocks = true
		}
		if result.skippedBlocks > 0 {
			noun := "blocks"
			if result.skippedBlocks == 1 {
				noun = "block"
			}
			p.buf.WriteString("\n")
			p.buf.WriteString(strings.Repeat(" ", indent+2))
			p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", result.skippedBlocks, noun)))
		}
	}

	return result
}

func (p *blockBodyDiffPrinter) writeAttrsDiff(
	attrsS map[string]*configschema.Attribute,
	old, new cty.Value,
	indent int,
	path cty.Path,
	result *blockBodyDiffResult) bool {

	blankBeforeBlocks := false

	attrNames := make([]string, 0, len(attrsS))
	attrNameLen := 0
	for name := range attrsS {
		oldVal := ctyGetAttrMaybeNull(old, name)
		newVal := ctyGetAttrMaybeNull(new, name)
		if oldVal.IsNull() && newVal.IsNull() {
			// Skip attributes where both old and new values are null
			// (we do this early here so that we'll do our value alignment
			// based on the longest attribute name that has a change, rather
			// than the longest attribute name in the full set.)
			continue
		}

		attrNames = append(attrNames, name)
		if len(name) > attrNameLen {
			attrNameLen = len(name)
		}
	}
	sort.Strings(attrNames)
	if len(attrNames) > 0 {
		blankBeforeBlocks = true
	}

	for _, name := range attrNames {
		attrS := attrsS[name]
		oldVal := ctyGetAttrMaybeNull(old, name)
		newVal := ctyGetAttrMaybeNull(new, name)

		result.bodyWritten = true
		skipped := p.writeAttrDiff(name, attrS, oldVal, newVal, attrNameLen, indent, path)
		if skipped {
			result.skippedAttributes++
		}
	}

	return blankBeforeBlocks
}

// getPlanActionAndShow returns the action value
// and a boolean for showJustNew. In this function we
// modify the old and new values to remove any possible marks
func getPlanActionAndShow(old cty.Value, new cty.Value) (plans.Action, bool) {
	var action plans.Action
	showJustNew := false
	switch {
	case old.IsNull():
		action = plans.Create
		showJustNew = true
	case new.IsNull():
		action = plans.Delete
	case ctyEqualWithUnknown(old, new):
		action = plans.NoOp
		showJustNew = true
	default:
		action = plans.Update
	}
	return action, showJustNew
}

func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) bool {
	path = append(path, cty.GetAttrStep{Name: name})
	action, showJustNew := getPlanActionAndShow(old, new)

	if action == plans.NoOp && !p.verbose && !identifyingAttribute(name, attrS) {
		return true
	}

	if attrS.NestedType != nil {
		p.writeNestedAttrDiff(name, attrS.NestedType, old, new, nameLen, indent, path, action, showJustNew)
		return false
	}

	p.buf.WriteString("\n")

	p.writeSensitivityWarning(old, new, indent, action, false)

	p.buf.WriteString(strings.Repeat(" ", indent))
	p.writeActionSymbol(action)

	p.buf.WriteString(p.color.Color("[bold]"))
	p.buf.WriteString(name)
	p.buf.WriteString(p.color.Color("[reset]"))
	p.buf.WriteString(strings.Repeat(" ", nameLen-len(name)))
	p.buf.WriteString(" = ")

	if attrS.Sensitive {
		p.buf.WriteString("(sensitive value)")
		if p.pathForcesNewResource(path) {
			p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
		}
	} else {
		switch {
		case showJustNew:
			p.writeValue(new, action, indent+2)
			if p.pathForcesNewResource(path) {
				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
			}
		default:
			// We show new even if it is null to emphasize the fact
			// that it is being unset, since otherwise it is easy to
			// misunderstand that the value is still set to the old value.
			p.writeValueDiff(old, new, indent+2, path)
		}
	}

	return false
}

// writeNestedAttrDiff is responsible for formatting Attributes with NestedTypes
// in the diff.
func (p *blockBodyDiffPrinter) writeNestedAttrDiff(
	name string, objS *configschema.Object, old, new cty.Value,
	nameLen, indent int, path cty.Path, action plans.Action, showJustNew bool) {

	p.buf.WriteString("\n")
	p.buf.WriteString(strings.Repeat(" ", indent))
	p.writeActionSymbol(action)

	p.buf.WriteString(p.color.Color("[bold]"))
	p.buf.WriteString(name)
	p.buf.WriteString(p.color.Color("[reset]"))
	p.buf.WriteString(strings.Repeat(" ", nameLen-len(name)))

	result := &blockBodyDiffResult{}
	switch objS.Nesting {
	case configschema.NestingSingle:
		p.buf.WriteString(" = {")
		if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
			p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
		}
		p.writeAttrsDiff(objS.Attributes, old, new, indent+2, path, result)
		p.writeSkippedAttr(result.skippedAttributes, indent+4)
		p.buf.WriteString("\n")
		p.buf.WriteString(strings.Repeat(" ", indent))
		p.buf.WriteString("}")

	case configschema.NestingList:
		p.buf.WriteString(" = [")
		if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
			p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
		}
		p.buf.WriteString("\n")
		p.buf.WriteString(strings.Repeat(" ", indent+4))
		p.writeActionSymbol(action)
		p.buf.WriteString("{")

		oldItems := ctyCollectionValues(old)
		newItems := ctyCollectionValues(new)
		// Here we intentionally preserve the index-based correspondance
		// between old and new, rather than trying to detect insertions
		// and removals in the list, because this more accurately reflects
		// how Terraform Core and providers will understand the change,
		// particularly when the nested block contains computed attributes
		// that will themselves maintain correspondance by index.

		// commonLen is number of elements that exist in both lists, which
		// will be presented as updates (~). Any additional items in one
		// of the lists will be presented as either creates (+) or deletes (-)
		// depending on which list they belong to.
		var commonLen int
		// unchanged is the number of unchanged elements
		var unchanged int

		switch {
		case len(oldItems) < len(newItems):
			commonLen = len(oldItems)
		default:
			commonLen = len(newItems)
		}
		for i := 0; i < commonLen; i++ {
			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
			oldItem := oldItems[i]
			newItem := newItems[i]
			if oldItem.RawEquals(newItem) {
				action = plans.NoOp
				unchanged++
			}
			if action != plans.NoOp {
				p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+6, path, result)
				p.writeSkippedAttr(result.skippedAttributes, indent+8)
				p.buf.WriteString("\n")
			}
		}
		for i := commonLen; i < len(oldItems); i++ {
			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
			oldItem := oldItems[i]
			newItem := cty.NullVal(oldItem.Type())
			p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+6, path, result)
			p.buf.WriteString("\n")
		}
		for i := commonLen; i < len(newItems); i++ {
			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
			newItem := newItems[i]
			oldItem := cty.NullVal(newItem.Type())
			p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+6, path, result)
			p.buf.WriteString("\n")
		}
		p.buf.WriteString(strings.Repeat(" ", indent+4))
		p.buf.WriteString("},\n")
		p.writeSkippedElems(unchanged, indent+4)
		p.buf.WriteString(strings.Repeat(" ", indent+2))
		p.buf.WriteString("]")

	case configschema.NestingSet:
		oldItems := ctyCollectionValues(old)
		newItems := ctyCollectionValues(new)

		allItems := make([]cty.Value, 0, len(oldItems)+len(newItems))
		allItems = append(allItems, oldItems...)
		allItems = append(allItems, newItems...)
		all := cty.SetVal(allItems)

		p.buf.WriteString(" = [")

		for it := all.ElementIterator(); it.Next(); {
			_, val := it.Element()
			var action plans.Action
			var oldValue, newValue cty.Value
			switch {
			case !val.IsKnown():
				action = plans.Update
				newValue = val
			case !old.HasElement(val).True():
				action = plans.Create
				oldValue = cty.NullVal(val.Type())
				newValue = val
			case !new.HasElement(val).True():
				action = plans.Delete
				oldValue = val
				newValue = cty.NullVal(val.Type())
			default:
				action = plans.NoOp
				oldValue = val
				newValue = val
			}

			p.buf.WriteString("\n")
			p.buf.WriteString(strings.Repeat(" ", indent+4))
			p.writeActionSymbol(action)
			p.buf.WriteString("{")

			if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
			}

			path := append(path, cty.IndexStep{Key: val})
			p.writeAttrsDiff(objS.Attributes, oldValue, newValue, indent+6, path, result)

			p.buf.WriteString("\n")
			p.buf.WriteString(strings.Repeat(" ", indent+4))
			p.buf.WriteString("},")
		}
		p.buf.WriteString("\n")
		p.buf.WriteString(strings.Repeat(" ", indent+2))
		p.buf.WriteString("]")

	case configschema.NestingMap:
		oldItems := old.AsValueMap()
		newItems := new.AsValueMap()

		allKeys := make(map[string]bool)
		for k := range oldItems {
			allKeys[k] = true
		}
		for k := range newItems {
			allKeys[k] = true
		}
		allKeysOrder := make([]string, 0, len(allKeys))
		for k := range allKeys {
			allKeysOrder = append(allKeysOrder, k)
		}
		sort.Strings(allKeysOrder)

		p.buf.WriteString(" = {\n")

		// unchanged tracks the number of unchanged elements
		unchanged := 0
		for _, k := range allKeysOrder {
			var action plans.Action
			oldValue := oldItems[k]
			newValue := newItems[k]
			switch {
			case oldValue == cty.NilVal:
				oldValue = cty.NullVal(newValue.Type())
				action = plans.Create
			case newValue == cty.NilVal:
				newValue = cty.NullVal(oldValue.Type())
				action = plans.Delete
			case !newValue.RawEquals(oldValue):
				action = plans.Update
			default:
				action = plans.NoOp
				unchanged++
			}

			if action != plans.NoOp {
				p.buf.WriteString(strings.Repeat(" ", indent+4))
				p.writeActionSymbol(action)
				fmt.Fprintf(p.buf, "%q = {", k)
				if p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1]) {
					p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
				}

				path := append(path, cty.IndexStep{Key: cty.StringVal(k)})
				p.writeAttrsDiff(objS.Attributes, oldValue, newValue, indent+6, path, result)
				p.writeSkippedAttr(result.skippedAttributes, indent+8)
				p.buf.WriteString("\n")
				p.buf.WriteString(strings.Repeat(" ", indent+4))
				p.buf.WriteString("},\n")
			}
		}

		p.writeSkippedElems(unchanged, indent+4)
		p.buf.WriteString(strings.Repeat(" ", indent+2))
		p.buf.WriteString("}")
	}

	return
}

func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *configschema.NestedBlock, old, new cty.Value, blankBefore bool, indent int, path cty.Path) int {
	skippedBlocks := 0
	path = append(path, cty.GetAttrStep{Name: name})
	if old.IsNull() && new.IsNull() {
		// Nothing to do if both old and new is null
		return skippedBlocks
	}

	// If either the old or the new value is marked,
	// Display a special diff because it is irrelevant
	// to list all obfuscated attributes as (sensitive)
	if old.IsMarked() || new.IsMarked() {
		p.writeSensitiveNestedBlockDiff(name, old, new, indent, blankBefore, path)
		return 0
	}

	// Where old/new are collections representing a nesting mode other than
	// NestingSingle, we assume the collection value can never be unknown
	// since we always produce the container for the nested objects, even if
	// the objects within are computed.

	switch blockS.Nesting {
	case configschema.NestingSingle, configschema.NestingGroup:
		var action plans.Action
		eqV := new.Equals(old)
		switch {
		case old.IsNull():
			action = plans.Create
		case new.IsNull():
			action = plans.Delete
		case !new.IsWhollyKnown() || !old.IsWhollyKnown():
			// "old" should actually always be known due to our contract
			// that old values must never be unknown, but we'll allow it
			// anyway to be robust.
			action = plans.Update
		case !eqV.IsKnown() || !eqV.True():
			action = plans.Update
		}

		if blankBefore {
			p.buf.WriteRune('\n')
		}
		skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, old, new, indent, path)
		if skipped {
			return 1
		}
	case configschema.NestingList:
		// For the sake of handling nested blocks, we'll treat a null list
		// the same as an empty list since the config language doesn't
		// distinguish these anyway.
		old = ctyNullBlockListAsEmpty(old)
		new = ctyNullBlockListAsEmpty(new)

		oldItems := ctyCollectionValues(old)
		newItems := ctyCollectionValues(new)

		// Here we intentionally preserve the index-based correspondance
		// between old and new, rather than trying to detect insertions
		// and removals in the list, because this more accurately reflects
		// how Terraform Core and providers will understand the change,
		// particularly when the nested block contains computed attributes
		// that will themselves maintain correspondance by index.

		// commonLen is number of elements that exist in both lists, which
		// will be presented as updates (~). Any additional items in one
		// of the lists will be presented as either creates (+) or deletes (-)
		// depending on which list they belong to.
		var commonLen int
		switch {
		case len(oldItems) < len(newItems):
			commonLen = len(oldItems)
		default:
			commonLen = len(newItems)
		}

		if blankBefore && (len(oldItems) > 0 || len(newItems) > 0) {
			p.buf.WriteRune('\n')
		}

		for i := 0; i < commonLen; i++ {
			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
			oldItem := oldItems[i]
			newItem := newItems[i]
			action := plans.Update
			if oldItem.RawEquals(newItem) {
				action = plans.NoOp
			}
			skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldItem, newItem, indent, path)
			if skipped {
				skippedBlocks++
			}
		}
		for i := commonLen; i < len(oldItems); i++ {
			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
			oldItem := oldItems[i]
			newItem := cty.NullVal(oldItem.Type())
			skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Delete, oldItem, newItem, indent, path)
			if skipped {
				skippedBlocks++
			}
		}
		for i := commonLen; i < len(newItems); i++ {
			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
			newItem := newItems[i]
			oldItem := cty.NullVal(newItem.Type())
			skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Create, oldItem, newItem, indent, path)
			if skipped {
				skippedBlocks++
			}
		}
	case configschema.NestingSet:
		// For the sake of handling nested blocks, we'll treat a null set
		// the same as an empty set since the config language doesn't
		// distinguish these anyway.
		old = ctyNullBlockSetAsEmpty(old)
		new = ctyNullBlockSetAsEmpty(new)

		oldItems := ctyCollectionValues(old)
		newItems := ctyCollectionValues(new)

		if (len(oldItems) + len(newItems)) == 0 {
			// Nothing to do if both sets are empty
			return 0
		}

		allItems := make([]cty.Value, 0, len(oldItems)+len(newItems))
		allItems = append(allItems, oldItems...)
		allItems = append(allItems, newItems...)
		all := cty.SetVal(allItems)

		if blankBefore {
			p.buf.WriteRune('\n')
		}

		for it := all.ElementIterator(); it.Next(); {
			_, val := it.Element()
			var action plans.Action
			var oldValue, newValue cty.Value
			switch {
			case !val.IsKnown():
				action = plans.Update
				newValue = val
			case !old.HasElement(val).True():
				action = plans.Create
				oldValue = cty.NullVal(val.Type())
				newValue = val
			case !new.HasElement(val).True():
				action = plans.Delete
				oldValue = val
				newValue = cty.NullVal(val.Type())
			default:
				action = plans.NoOp
				oldValue = val
				newValue = val
			}
			path := append(path, cty.IndexStep{Key: val})
			skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldValue, newValue, indent, path)
			if skipped {
				skippedBlocks++
			}
		}

	case configschema.NestingMap:
		// For the sake of handling nested blocks, we'll treat a null map
		// the same as an empty map since the config language doesn't
		// distinguish these anyway.
		old = ctyNullBlockMapAsEmpty(old)
		new = ctyNullBlockMapAsEmpty(new)

		oldItems := old.AsValueMap()
		newItems := new.AsValueMap()
		if (len(oldItems) + len(newItems)) == 0 {
			// Nothing to do if both maps are empty
			return 0
		}

		allKeys := make(map[string]bool)
		for k := range oldItems {
			allKeys[k] = true
		}
		for k := range newItems {
			allKeys[k] = true
		}
		allKeysOrder := make([]string, 0, len(allKeys))
		for k := range allKeys {
			allKeysOrder = append(allKeysOrder, k)
		}
		sort.Strings(allKeysOrder)

		if blankBefore {
			p.buf.WriteRune('\n')
		}

		for _, k := range allKeysOrder {
			var action plans.Action
			oldValue := oldItems[k]
			newValue := newItems[k]
			switch {
			case oldValue == cty.NilVal:
				oldValue = cty.NullVal(newValue.Type())
				action = plans.Create
			case newValue == cty.NilVal:
				newValue = cty.NullVal(oldValue.Type())
				action = plans.Delete
			case !newValue.RawEquals(oldValue):
				action = plans.Update
			default:
				action = plans.NoOp
			}

			path := append(path, cty.IndexStep{Key: cty.StringVal(k)})
			skipped := p.writeNestedBlockDiff(name, &k, &blockS.Block, action, oldValue, newValue, indent, path)
			if skipped {
				skippedBlocks++
			}
		}
	}
	return skippedBlocks
}

func (p *blockBodyDiffPrinter) writeSensitiveNestedBlockDiff(name string, old, new cty.Value, indent int, blankBefore bool, path cty.Path) {
	var action plans.Action
	switch {
	case old.IsNull():
		action = plans.Create
	case new.IsNull():
		action = plans.Delete
	case !new.IsWhollyKnown() || !old.IsWhollyKnown():
		// "old" should actually always be known due to our contract
		// that old values must never be unknown, but we'll allow it
		// anyway to be robust.
		action = plans.Update
	case !ctyEqualValueAndMarks(old, new):
		action = plans.Update
	}

	if blankBefore {
		p.buf.WriteRune('\n')
	}

	// New line before warning printing
	p.buf.WriteRune('\n')
	p.writeSensitivityWarning(old, new, indent, action, true)
	p.buf.WriteString(strings.Repeat(" ", indent))
	p.writeActionSymbol(action)
	fmt.Fprintf(p.buf, "%s {", name)
	if action != plans.NoOp && p.pathForcesNewResource(path) {
		p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
	}
	p.buf.WriteRune('\n')
	p.buf.WriteString(strings.Repeat(" ", indent+4))
	p.buf.WriteString("# At least one attribute in this block is (or was) sensitive,\n")
	p.buf.WriteString(strings.Repeat(" ", indent+4))
	p.buf.WriteString("# so its contents will not be displayed.")
	p.buf.WriteRune('\n')
	p.buf.WriteString(strings.Repeat(" ", indent+2))
	p.buf.WriteString("}")
}

func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string, blockS *configschema.Block, action plans.Action, old, new cty.Value, indent int, path cty.Path) bool {
	if action == plans.NoOp && !p.verbose {
		return true
	}

	p.buf.WriteString("\n")
	p.buf.WriteString(strings.Repeat(" ", indent))
	p.writeActionSymbol(action)

	if label != nil {
		fmt.Fprintf(p.buf, "%s %q {", name, *label)
	} else {
		fmt.Fprintf(p.buf, "%s {", name)
	}

	if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
		p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
	}

	result := p.writeBlockBodyDiff(blockS, old, new, indent+4, path)
	if result.bodyWritten {
		p.buf.WriteString("\n")
		p.buf.WriteString(strings.Repeat(" ", indent+2))
	}
	p.buf.WriteString("}")

	return false
}

func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) {
	// Could check specifically for the sensitivity marker
	if val.IsMarked() {
		p.buf.WriteString("(sensitive)")
		return
	}

	if !val.IsKnown() {
		p.buf.WriteString("(known after apply)")
		return
	}
	if val.IsNull() {
		p.buf.WriteString(p.color.Color("[dark_gray]null[reset]"))
		return
	}

	ty := val.Type()

	switch {
	case ty.IsPrimitiveType():
		switch ty {
		case cty.String:
			{
				// Special behavior for JSON strings containing array or object
				src := []byte(val.AsString())
				ty, err := ctyjson.ImpliedType(src)
				// check for the special case of "null", which decodes to nil,
				// and just allow it to be printed out directly
				if err == nil && !ty.IsPrimitiveType() && strings.TrimSpace(val.AsString()) != "null" {
					jv, err := ctyjson.Unmarshal(src, ty)
					if err == nil {
						p.buf.WriteString("jsonencode(")
						if jv.LengthInt() == 0 {
							p.writeValue(jv, action, 0)
						} else {
							p.buf.WriteByte('\n')
							p.buf.WriteString(strings.Repeat(" ", indent+4))
							p.writeValue(jv, action, indent+4)
							p.buf.WriteByte('\n')
							p.buf.WriteString(strings.Repeat(" ", indent))
						}
						p.buf.WriteByte(')')
						break // don't *also* do the normal behavior below
					}
				}
			}

			if strings.Contains(val.AsString(), "\n") {
				// It's a multi-line string, so we want to use the multi-line
				// rendering so it'll be readable. Rather than re-implement
				// that here, we'll just re-use the multi-line string diff
				// printer with no changes, which ends up producing the
				// result we want here.
				// The path argument is nil because we don't track path
				// information into strings and we know that a string can't
				// have any indices or attributes that might need to be marked
				// as (requires replacement), which is what that argument is for.
				p.writeValueDiff(val, val, indent, nil)
				break
			}

			fmt.Fprintf(p.buf, "%q", val.AsString())
		case cty.Bool:
			if val.True() {
				p.buf.WriteString("true")
			} else {
				p.buf.WriteString("false")
			}
		case cty.Number:
			bf := val.AsBigFloat()
			p.buf.WriteString(bf.Text('f', -1))
		default:
			// should never happen, since the above is exhaustive
			fmt.Fprintf(p.buf, "%#v", val)
		}
	case ty.IsListType() || ty.IsSetType() || ty.IsTupleType():
		p.buf.WriteString("[")

		it := val.ElementIterator()
		for it.Next() {
			_, val := it.Element()

			p.buf.WriteString("\n")
			p.buf.WriteString(strings.Repeat(" ", indent+2))
			p.writeActionSymbol(action)
			p.writeValue(val, action, indent+4)
			p.buf.WriteString(",")
		}

		if val.LengthInt() > 0 {
			p.buf.WriteString("\n")
			p.buf.WriteString(strings.Repeat(" ", indent))
		}
		p.buf.WriteString("]")
	case ty.IsMapType():
		p.buf.WriteString("{")

		keyLen := 0
		for it := val.ElementIterator(); it.Next(); {
			key, _ := it.Element()
			if keyStr := key.AsString(); len(keyStr) > keyLen {
				keyLen = len(keyStr)
			}
		}

		for it := val.ElementIterator(); it.Next(); {
			key, val := it.Element()

			p.buf.WriteString("\n")
			p.buf.WriteString(strings.Repeat(" ", indent+2))
			p.writeActionSymbol(action)
			p.writeValue(key, action, indent+4)
			p.buf.WriteString(strings.Repeat(" ", keyLen-len(key.AsString())))
			p.buf.WriteString(" = ")
			p.writeValue(val, action, indent+4)
		}

		if val.LengthInt() > 0 {
			p.buf.WriteString("\n")
			p.buf.WriteString(strings.Repeat(" ", indent))
		}
		p.buf.WriteString("}")
	case ty.IsObjectType():
		p.buf.WriteString("{")

		atys := ty.AttributeTypes()
		attrNames := make([]string, 0, len(atys))
		nameLen := 0
		for attrName := range atys {
			attrNames = append(attrNames, attrName)
			if len(attrName) > nameLen {
				nameLen = len(attrName)
			}
		}
		sort.Strings(attrNames)

		for _, attrName := range attrNames {
			val := val.GetAttr(attrName)

			p.buf.WriteString("\n")
			p.buf.WriteString(strings.Repeat(" ", indent+2))
			p.writeActionSymbol(action)
			p.buf.WriteString(attrName)
			p.buf.WriteString(strings.Repeat(" ", nameLen-len(attrName)))
			p.buf.WriteString(" = ")
			p.writeValue(val, action, indent+4)
		}

		if len(attrNames) > 0 {
			p.buf.WriteString("\n")
			p.buf.WriteString(strings.Repeat(" ", indent))
		}
		p.buf.WriteString("}")
	}
}

func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, path cty.Path) {
	ty := old.Type()
	typesEqual := ctyTypesEqual(ty, new.Type())

	// We have some specialized diff implementations for certain complex
	// values where it's useful to see a visualization of the diff of
	// the nested elements rather than just showing the entire old and
	// new values verbatim.
	// However, these specialized implementations can apply only if both
	// values are known and non-null.
	if old.IsKnown() && new.IsKnown() && !old.IsNull() && !new.IsNull() && typesEqual {
		if old.IsMarked() || new.IsMarked() {
			p.buf.WriteString("(sensitive)")
			if p.pathForcesNewResource(path) {
				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
			}
			return
		}

		switch {
		case ty == cty.String:
			// We have special behavior for both multi-line strings in general
			// and for strings that can parse as JSON. For the JSON handling
			// to apply, both old and new must be valid JSON.
			// For single-line strings that don't parse as JSON we just fall
			// out of this switch block and do the default old -> new rendering.
			oldS := old.AsString()
			newS := new.AsString()

			{
				// Special behavior for JSON strings containing object or
				// list values.
				oldBytes := []byte(oldS)
				newBytes := []byte(newS)
				oldType, oldErr := ctyjson.ImpliedType(oldBytes)
				newType, newErr := ctyjson.ImpliedType(newBytes)
				if oldErr == nil && newErr == nil && !(oldType.IsPrimitiveType() && newType.IsPrimitiveType()) {
					oldJV, oldErr := ctyjson.Unmarshal(oldBytes, oldType)
					newJV, newErr := ctyjson.Unmarshal(newBytes, newType)
					if oldErr == nil && newErr == nil {
						if !oldJV.RawEquals(newJV) { // two JSON values may differ only in insignificant whitespace
							p.buf.WriteString("jsonencode(")
							p.buf.WriteByte('\n')
							p.buf.WriteString(strings.Repeat(" ", indent+2))
							p.writeActionSymbol(plans.Update)
							p.writeValueDiff(oldJV, newJV, indent+4, path)
							p.buf.WriteByte('\n')
							p.buf.WriteString(strings.Repeat(" ", indent))
							p.buf.WriteByte(')')
						} else {
							// if they differ only in insignificant whitespace
							// then we'll note that but still expand out the
							// effective value.
							if p.pathForcesNewResource(path) {
								p.buf.WriteString(p.color.Color("jsonencode( [red]# whitespace changes force replacement[reset]"))
							} else {
								p.buf.WriteString(p.color.Color("jsonencode( [dim]# whitespace changes[reset]"))
							}
							p.buf.WriteByte('\n')
							p.buf.WriteString(strings.Repeat(" ", indent+4))
							p.writeValue(oldJV, plans.NoOp, indent+4)
							p.buf.WriteByte('\n')
							p.buf.WriteString(strings.Repeat(" ", indent))
							p.buf.WriteByte(')')
						}
						return
					}
				}
			}

			if !strings.Contains(oldS, "\n") && !strings.Contains(newS, "\n") {
				break
			}

			p.buf.WriteString("<<-EOT")
			if p.pathForcesNewResource(path) {
				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
			}
			p.buf.WriteString("\n")

			var oldLines, newLines []cty.Value
			{
				r := strings.NewReader(oldS)
				sc := bufio.NewScanner(r)
				for sc.Scan() {
					oldLines = append(oldLines, cty.StringVal(sc.Text()))
				}
			}
			{
				r := strings.NewReader(newS)
				sc := bufio.NewScanner(r)
				for sc.Scan() {
					newLines = append(newLines, cty.StringVal(sc.Text()))
				}
			}

			// Optimization for strings which are exactly equal: just print
			// directly without calculating the sequence diff. This makes a
			// significant difference when this code path is reached via a
			// writeValue call with a large multi-line string.
			if oldS == newS {
				for _, line := range newLines {
					p.buf.WriteString(strings.Repeat(" ", indent+4))
					p.buf.WriteString(line.AsString())
					p.buf.WriteString("\n")
				}
			} else {
				diffLines := ctySequenceDiff(oldLines, newLines)
				for _, diffLine := range diffLines {
					p.buf.WriteString(strings.Repeat(" ", indent+2))
					p.writeActionSymbol(diffLine.Action)

					switch diffLine.Action {
					case plans.NoOp, plans.Delete:
						p.buf.WriteString(diffLine.Before.AsString())
					case plans.Create:
						p.buf.WriteString(diffLine.After.AsString())
					default:
						// Should never happen since the above covers all
						// actions that ctySequenceDiff can return for strings
						p.buf.WriteString(diffLine.After.AsString())

					}
					p.buf.WriteString("\n")
				}
			}

			p.buf.WriteString(strings.Repeat(" ", indent)) // +4 here because there's no symbol
			p.buf.WriteString("EOT")

			return

		case ty.IsSetType():
			p.buf.WriteString("[")
			if p.pathForcesNewResource(path) {
				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
			}
			p.buf.WriteString("\n")

			var addedVals, removedVals, allVals []cty.Value
			for it := old.ElementIterator(); it.Next(); {
				_, val := it.Element()
				allVals = append(allVals, val)
				if new.HasElement(val).False() {
					removedVals = append(removedVals, val)
				}
			}
			for it := new.ElementIterator(); it.Next(); {
				_, val := it.Element()
				allVals = append(allVals, val)
				if val.IsKnown() && old.HasElement(val).False() {
					addedVals = append(addedVals, val)
				}
			}

			var all, added, removed cty.Value
			if len(allVals) > 0 {
				all = cty.SetVal(allVals)
			} else {
				all = cty.SetValEmpty(ty.ElementType())
			}
			if len(addedVals) > 0 {
				added = cty.SetVal(addedVals)
			} else {
				added = cty.SetValEmpty(ty.ElementType())
			}
			if len(removedVals) > 0 {
				removed = cty.SetVal(removedVals)
			} else {
				removed = cty.SetValEmpty(ty.ElementType())
			}

			suppressedElements := 0
			for it := all.ElementIterator(); it.Next(); {
				_, val := it.Element()

				var action plans.Action
				switch {
				case !val.IsKnown():
					action = plans.Update
				case added.HasElement(val).True():
					action = plans.Create
				case removed.HasElement(val).True():
					action = plans.Delete
				default:
					action = plans.NoOp
				}

				if action == plans.NoOp && !p.verbose {
					suppressedElements++
					continue
				}

				p.buf.WriteString(strings.Repeat(" ", indent+2))
				p.writeActionSymbol(action)
				p.writeValue(val, action, indent+4)
				p.buf.WriteString(",\n")
			}

			if suppressedElements > 0 {
				p.writeActionSymbol(plans.NoOp)
				p.buf.WriteString(strings.Repeat(" ", indent+2))
				noun := "elements"
				if suppressedElements == 1 {
					noun = "element"
				}
				p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", suppressedElements, noun)))
				p.buf.WriteString("\n")
			}

			p.buf.WriteString(strings.Repeat(" ", indent))
			p.buf.WriteString("]")
			return
		case ty.IsListType() || ty.IsTupleType():
			p.buf.WriteString("[")
			if p.pathForcesNewResource(path) {
				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
			}
			p.buf.WriteString("\n")

			elemDiffs := ctySequenceDiff(old.AsValueSlice(), new.AsValueSlice())

			// Maintain a stack of suppressed lines in the diff for later
			// display or elision
			var suppressedElements []*plans.Change
			var changeShown bool

			for i := 0; i < len(elemDiffs); i++ {
				if !p.verbose {
					for i < len(elemDiffs) && elemDiffs[i].Action == plans.NoOp {
						suppressedElements = append(suppressedElements, elemDiffs[i])
						i++
					}
				}

				// If we have some suppressed elements on the stack…
				if len(suppressedElements) > 0 {
					// If we've just rendered a change, display the first
					// element in the stack as context
					if changeShown {
						elemDiff := suppressedElements[0]
						p.buf.WriteString(strings.Repeat(" ", indent+4))
						p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
						p.buf.WriteString(",\n")
						suppressedElements = suppressedElements[1:]
					}

					hidden := len(suppressedElements)

					// If we're not yet at the end of the list, capture the
					// last element on the stack as context for the upcoming
					// change to be rendered
					var nextContextDiff *plans.Change
					if hidden > 0 && i < len(elemDiffs) {
						hidden--
						nextContextDiff = suppressedElements[hidden]
					}

					// If there are still hidden elements, show an elision
					// statement counting them
					if hidden > 0 {
						p.writeActionSymbol(plans.NoOp)
						p.buf.WriteString(strings.Repeat(" ", indent+2))
						noun := "elements"
						if hidden == 1 {
							noun = "element"
						}
						p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", hidden, noun)))
						p.buf.WriteString("\n")
					}

					// Display the next context diff if it was captured above
					if nextContextDiff != nil {
						p.buf.WriteString(strings.Repeat(" ", indent+4))
						p.writeValue(nextContextDiff.After, nextContextDiff.Action, indent+4)
						p.buf.WriteString(",\n")
					}

					// Suppressed elements have now been handled so clear them again
					suppressedElements = nil
				}

				if i >= len(elemDiffs) {
					break
				}

				elemDiff := elemDiffs[i]
				p.buf.WriteString(strings.Repeat(" ", indent+2))
				p.writeActionSymbol(elemDiff.Action)
				switch elemDiff.Action {
				case plans.NoOp, plans.Delete:
					p.writeValue(elemDiff.Before, elemDiff.Action, indent+4)
				case plans.Update:
					p.writeValueDiff(elemDiff.Before, elemDiff.After, indent+4, path)
				case plans.Create:
					p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
				default:
					// Should never happen since the above covers all
					// actions that ctySequenceDiff can return.
					p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
				}

				p.buf.WriteString(",\n")
				changeShown = true
			}

			p.buf.WriteString(strings.Repeat(" ", indent))
			p.buf.WriteString("]")

			return

		case ty.IsMapType():
			p.buf.WriteString("{")
			if p.pathForcesNewResource(path) {
				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
			}
			p.buf.WriteString("\n")

			var allKeys []string
			keyLen := 0
			for it := old.ElementIterator(); it.Next(); {
				k, _ := it.Element()
				keyStr := k.AsString()
				allKeys = append(allKeys, keyStr)
				if len(keyStr) > keyLen {
					keyLen = len(keyStr)
				}
			}
			for it := new.ElementIterator(); it.Next(); {
				k, _ := it.Element()
				keyStr := k.AsString()
				allKeys = append(allKeys, keyStr)
				if len(keyStr) > keyLen {
					keyLen = len(keyStr)
				}
			}

			sort.Strings(allKeys)

			suppressedElements := 0
			lastK := ""
			for i, k := range allKeys {
				if i > 0 && lastK == k {
					continue // skip duplicates (list is sorted)
				}
				lastK = k

				kV := cty.StringVal(k)
				var action plans.Action
				if old.HasIndex(kV).False() {
					action = plans.Create
				} else if new.HasIndex(kV).False() {
					action = plans.Delete
				}

				if old.HasIndex(kV).True() && new.HasIndex(kV).True() {
					if ctyEqualValueAndMarks(old.Index(kV), new.Index(kV)) {
						action = plans.NoOp
					} else {
						action = plans.Update
					}
				}

				if action == plans.NoOp && !p.verbose {
					suppressedElements++
					continue
				}

				path := append(path, cty.IndexStep{Key: kV})

				oldV := old.Index(kV)
				newV := new.Index(kV)
				p.writeSensitivityWarning(oldV, newV, indent+2, action, false)

				p.buf.WriteString(strings.Repeat(" ", indent+2))
				p.writeActionSymbol(action)
				p.writeValue(kV, action, indent+4)
				p.buf.WriteString(strings.Repeat(" ", keyLen-len(k)))
				p.buf.WriteString(" = ")
				switch action {
				case plans.Create, plans.NoOp:
					v := new.Index(kV)
					if v.IsMarked() {
						p.buf.WriteString("(sensitive)")
					} else {
						p.writeValue(v, action, indent+4)
					}
				case plans.Delete:
					oldV := old.Index(kV)
					newV := cty.NullVal(oldV.Type())
					p.writeValueDiff(oldV, newV, indent+4, path)
				default:
					if oldV.IsMarked() || newV.IsMarked() {
						p.buf.WriteString("(sensitive)")
					} else {
						p.writeValueDiff(oldV, newV, indent+4, path)
					}
				}

				p.buf.WriteByte('\n')
			}

			if suppressedElements > 0 {
				p.writeActionSymbol(plans.NoOp)
				p.buf.WriteString(strings.Repeat(" ", indent+2))
				noun := "elements"
				if suppressedElements == 1 {
					noun = "element"
				}
				p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", suppressedElements, noun)))
				p.buf.WriteString("\n")
			}

			p.buf.WriteString(strings.Repeat(" ", indent))
			p.buf.WriteString("}")

			return
		case ty.IsObjectType():
			p.buf.WriteString("{")
			p.buf.WriteString("\n")

			forcesNewResource := p.pathForcesNewResource(path)

			var allKeys []string
			keyLen := 0
			for it := old.ElementIterator(); it.Next(); {
				k, _ := it.Element()
				keyStr := k.AsString()
				allKeys = append(allKeys, keyStr)
				if len(keyStr) > keyLen {
					keyLen = len(keyStr)
				}
			}
			for it := new.ElementIterator(); it.Next(); {
				k, _ := it.Element()
				keyStr := k.AsString()
				allKeys = append(allKeys, keyStr)
				if len(keyStr) > keyLen {
					keyLen = len(keyStr)
				}
			}

			sort.Strings(allKeys)

			suppressedElements := 0
			lastK := ""
			for i, k := range allKeys {
				if i > 0 && lastK == k {
					continue // skip duplicates (list is sorted)
				}
				lastK = k

				kV := k
				var action plans.Action
				if !old.Type().HasAttribute(kV) {
					action = plans.Create
				} else if !new.Type().HasAttribute(kV) {
					action = plans.Delete
				} else if ctyEqualValueAndMarks(old.GetAttr(kV), new.GetAttr(kV)) {
					action = plans.NoOp
				} else {
					action = plans.Update
				}

				if action == plans.NoOp && !p.verbose {
					suppressedElements++
					continue
				}

				path := append(path, cty.GetAttrStep{Name: kV})

				p.buf.WriteString(strings.Repeat(" ", indent+2))
				p.writeActionSymbol(action)
				p.buf.WriteString(k)
				p.buf.WriteString(strings.Repeat(" ", keyLen-len(k)))
				p.buf.WriteString(" = ")

				switch action {
				case plans.Create, plans.NoOp:
					v := new.GetAttr(kV)
					p.writeValue(v, action, indent+4)
				case plans.Delete:
					oldV := old.GetAttr(kV)
					newV := cty.NullVal(oldV.Type())
					p.writeValueDiff(oldV, newV, indent+4, path)
				default:
					oldV := old.GetAttr(kV)
					newV := new.GetAttr(kV)
					p.writeValueDiff(oldV, newV, indent+4, path)
				}

				p.buf.WriteString("\n")
			}

			if suppressedElements > 0 {
				p.writeActionSymbol(plans.NoOp)
				p.buf.WriteString(strings.Repeat(" ", indent+2))
				noun := "elements"
				if suppressedElements == 1 {
					noun = "element"
				}
				p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", suppressedElements, noun)))
				p.buf.WriteString("\n")
			}

			p.buf.WriteString(strings.Repeat(" ", indent))
			p.buf.WriteString("}")

			if forcesNewResource {
				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
			}
			return
		}
	}

	// In all other cases, we just show the new and old values as-is
	p.writeValue(old, plans.Delete, indent)
	if new.IsNull() {
		p.buf.WriteString(p.color.Color(" [dark_gray]->[reset] "))
	} else {
		p.buf.WriteString(p.color.Color(" [yellow]->[reset] "))
	}

	p.writeValue(new, plans.Create, indent)
	if p.pathForcesNewResource(path) {
		p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
	}
}

// writeActionSymbol writes a symbol to represent the given action, followed
// by a space.
//
// It only supports the actions that can be represented with a single character:
// Create, Delete, Update and NoAction.
func (p *blockBodyDiffPrinter) writeActionSymbol(action plans.Action) {
	switch action {
	case plans.Create:
		p.buf.WriteString(p.color.Color("[green]+[reset] "))
	case plans.Delete:
		p.buf.WriteString(p.color.Color("[red]-[reset] "))
	case plans.Update:
		p.buf.WriteString(p.color.Color("[yellow]~[reset] "))
	case plans.NoOp:
		p.buf.WriteString("  ")
	default:
		// Should never happen
		p.buf.WriteString(p.color.Color("? "))
	}
}

func (p *blockBodyDiffPrinter) writeSensitivityWarning(old, new cty.Value, indent int, action plans.Action, isBlock bool) {
	// Dont' show this warning for create or delete
	if action == plans.Create || action == plans.Delete {
		return
	}

	// Customize the warning based on if it is an attribute or block
	diffType := "attribute value"
	if isBlock {
		diffType = "block"
	}

	if new.IsMarked() && !old.IsMarked() {
		p.buf.WriteString(strings.Repeat(" ", indent))
		p.buf.WriteString(p.color.Color(fmt.Sprintf("# [yellow]Warning:[reset] this %s will be marked as sensitive and will\n", diffType)))
		p.buf.WriteString(strings.Repeat(" ", indent))
		p.buf.WriteString(p.color.Color("# not display in UI output after applying this change\n"))
	}

	// Note if changing this attribute will change its sensitivity
	if old.IsMarked() && !new.IsMarked() {
		p.buf.WriteString(strings.Repeat(" ", indent))
		p.buf.WriteString(p.color.Color(fmt.Sprintf("# [yellow]Warning:[reset] this %s will no longer be marked as sensitive\n", diffType)))
		p.buf.WriteString(strings.Repeat(" ", indent))
		p.buf.WriteString(p.color.Color("# after applying this change\n"))
	}
}

func (p *blockBodyDiffPrinter) pathForcesNewResource(path cty.Path) bool {
	if !p.action.IsReplace() || p.requiredReplace.Empty() {
		// "requiredReplace" only applies when the instance is being replaced,
		// and we should only inspect that set if it is not empty
		return false
	}
	return p.requiredReplace.Has(path)
}

func ctyEmptyString(value cty.Value) bool {
	if !value.IsNull() && value.IsKnown() {
		valueType := value.Type()
		if valueType == cty.String && value.AsString() == "" {
			return true
		}
	}
	return false
}

func ctyGetAttrMaybeNull(val cty.Value, name string) cty.Value {
	attrType := val.Type().AttributeType(name)

	if val.IsNull() {
		return cty.NullVal(attrType)
	}

	// We treat "" as null here
	// as existing SDK doesn't support null yet.
	// This allows us to avoid spurious diffs
	// until we introduce null to the SDK.
	attrValue := val.GetAttr(name)
	// If the value is marked, the ctyEmptyString function will fail
	if !val.ContainsMarked() && ctyEmptyString(attrValue) {
		return cty.NullVal(attrType)
	}

	return attrValue
}

func ctyCollectionValues(val cty.Value) []cty.Value {
	if !val.IsKnown() || val.IsNull() {
		return nil
	}

	ret := make([]cty.Value, 0, val.LengthInt())
	for it := val.ElementIterator(); it.Next(); {
		_, value := it.Element()
		ret = append(ret, value)
	}
	return ret
}

// ctySequenceDiff returns differences between given sequences of cty.Value(s)
// in the form of Create, Delete, or Update actions (for objects).
func ctySequenceDiff(old, new []cty.Value) []*plans.Change {
	var ret []*plans.Change
	lcs := objchange.LongestCommonSubsequence(old, new)
	var oldI, newI, lcsI int
	for oldI < len(old) || newI < len(new) || lcsI < len(lcs) {
		for oldI < len(old) && (lcsI >= len(lcs) || !old[oldI].RawEquals(lcs[lcsI])) {
			isObjectDiff := old[oldI].Type().IsObjectType() && newI < len(new) && new[newI].Type().IsObjectType() && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI]))
			if isObjectDiff {
				ret = append(ret, &plans.Change{
					Action: plans.Update,
					Before: old[oldI],
					After:  new[newI],
				})
				oldI++
				newI++ // we also consume the next "new" in this case
				continue
			}

			ret = append(ret, &plans.Change{
				Action: plans.Delete,
				Before: old[oldI],
				After:  cty.NullVal(old[oldI].Type()),
			})
			oldI++
		}
		for newI < len(new) && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI])) {
			ret = append(ret, &plans.Change{
				Action: plans.Create,
				Before: cty.NullVal(new[newI].Type()),
				After:  new[newI],
			})
			newI++
		}
		if lcsI < len(lcs) {
			ret = append(ret, &plans.Change{
				Action: plans.NoOp,
				Before: lcs[lcsI],
				After:  lcs[lcsI],
			})

			// All of our indexes advance together now, since the line
			// is common to all three sequences.
			lcsI++
			oldI++
			newI++
		}
	}
	return ret
}

// ctyEqualValueAndMarks checks equality of two possibly-marked values,
// considering partially-unknown values and equal values with different marks
// as inequal
func ctyEqualWithUnknown(old, new cty.Value) bool {
	if !old.IsWhollyKnown() || !new.IsWhollyKnown() {
		return false
	}
	return ctyEqualValueAndMarks(old, new)
}

// ctyEqualValueAndMarks checks equality of two possibly-marked values,
// considering equal values with different marks as inequal
func ctyEqualValueAndMarks(old, new cty.Value) bool {
	oldUnmarked, oldMarks := old.UnmarkDeep()
	newUnmarked, newMarks := new.UnmarkDeep()
	sameValue := oldUnmarked.Equals(newUnmarked)
	return sameValue.IsKnown() && sameValue.True() && oldMarks.Equal(newMarks)
}

// ctyTypesEqual checks equality of two types more loosely
// by avoiding checks of object/tuple elements
// as we render differences on element-by-element basis anyway
func ctyTypesEqual(oldT, newT cty.Type) bool {
	if oldT.IsObjectType() && newT.IsObjectType() {
		return true
	}
	if oldT.IsTupleType() && newT.IsTupleType() {
		return true
	}
	return oldT.Equals(newT)
}

func ctyEnsurePathCapacity(path cty.Path, minExtra int) cty.Path {
	if cap(path)-len(path) >= minExtra {
		return path
	}
	newCap := cap(path) * 2
	if newCap < (len(path) + minExtra) {
		newCap = len(path) + minExtra
	}
	newPath := make(cty.Path, len(path), newCap)
	copy(newPath, path)
	return newPath
}

// ctyNullBlockListAsEmpty either returns the given value verbatim if it is non-nil
// or returns an empty value of a suitable type to serve as a placeholder for it.
//
// In particular, this function handles the special situation where a "list" is
// actually represented as a tuple type where nested blocks contain
// dynamically-typed values.
func ctyNullBlockListAsEmpty(in cty.Value) cty.Value {
	if !in.IsNull() {
		return in
	}
	if ty := in.Type(); ty.IsListType() {
		return cty.ListValEmpty(ty.ElementType())
	}
	return cty.EmptyTupleVal // must need a tuple, then
}

// ctyNullBlockMapAsEmpty either returns the given value verbatim if it is non-nil
// or returns an empty value of a suitable type to serve as a placeholder for it.
//
// In particular, this function handles the special situation where a "map" is
// actually represented as an object type where nested blocks contain
// dynamically-typed values.
func ctyNullBlockMapAsEmpty(in cty.Value) cty.Value {
	if !in.IsNull() {
		return in
	}
	if ty := in.Type(); ty.IsMapType() {
		return cty.MapValEmpty(ty.ElementType())
	}
	return cty.EmptyObjectVal // must need an object, then
}

// ctyNullBlockSetAsEmpty either returns the given value verbatim if it is non-nil
// or returns an empty value of a suitable type to serve as a placeholder for it.
func ctyNullBlockSetAsEmpty(in cty.Value) cty.Value {
	if !in.IsNull() {
		return in
	}
	// Dynamically-typed attributes are not supported inside blocks backed by
	// sets, so our result here is always a set.
	return cty.SetValEmpty(in.Type().ElementType())
}

// DiffActionSymbol returns a string that, once passed through a
// colorstring.Colorize, will produce a result that can be written
// to a terminal to produce a symbol made of three printable
// characters, possibly interspersed with VT100 color codes.
func DiffActionSymbol(action plans.Action) string {
	switch action {
	case plans.DeleteThenCreate:
		return "[red]-[reset]/[green]+[reset]"
	case plans.CreateThenDelete:
		return "[green]+[reset]/[red]-[reset]"
	case plans.Create:
		return "  [green]+[reset]"
	case plans.Delete:
		return "  [red]-[reset]"
	case plans.Read:
		return " [cyan]<=[reset]"
	case plans.Update:
		return "  [yellow]~[reset]"
	default:
		return "  ?"
	}
}

// Extremely coarse heuristic for determining whether or not a given attribute
// name is important for identifying a resource. In the future, this may be
// replaced by a flag in the schema, but for now this is likely to be good
// enough.
func identifyingAttribute(name string, attrSchema *configschema.Attribute) bool {
	return name == "id" || name == "tags" || name == "name"
}

func (p *blockBodyDiffPrinter) writeSkippedAttr(skipped, indent int) {
	if skipped > 0 {
		noun := "attributes"
		if skipped == 1 {
			noun = "attribute"
		}
		p.buf.WriteString("\n")
		p.buf.WriteString(strings.Repeat(" ", indent))
		p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", skipped, noun)))
	}
}

func (p *blockBodyDiffPrinter) writeSkippedElems(skipped, indent int) {
	if skipped > 0 {
		noun := "elements"
		if skipped == 1 {
			noun = "element"
		}
		p.buf.WriteString(strings.Repeat(" ", indent))
		p.buf.WriteString(p.color.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", skipped, noun)))
		p.buf.WriteString("\n")
	}
}
