package d2oracle

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
	"unicode"

	"oss.terrastruct.com/util-go/xdefer"

	"oss.terrastruct.com/util-go/xrand"

	"oss.terrastruct.com/util-go/go2"

	"oss.terrastruct.com/d2/d2ast"
	"oss.terrastruct.com/d2/d2compiler"
	"oss.terrastruct.com/d2/d2format"
	"oss.terrastruct.com/d2/d2graph"
	"oss.terrastruct.com/d2/d2ir"
	"oss.terrastruct.com/d2/d2parser"
	"oss.terrastruct.com/d2/d2target"
)

type OutsideScopeError struct{}

func (e OutsideScopeError) Error() string {
	return "operation would modify AST outside of given scope"
}

func Create(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph, newKey string, err error) {
	defer xdefer.Errorf(&err, "failed to create %#v", key)

	boardG := g
	baseAST := g.AST

	if len(boardPath) > 0 {
		// When compiling a nested board, we can read from boardG but only write to baseBoardG
		boardG = GetBoardGraph(g, boardPath)
		if boardG == nil {
			return nil, "", fmt.Errorf("board %v not found", boardPath)
		}
		// TODO beter name
		baseAST = boardG.BaseAST
		if baseAST == nil {
			return nil, "", fmt.Errorf("board %v cannot be modified through this file", boardPath)
		}
	}

	newKey, edge, err := generateUniqueKey(boardG, key, nil, nil)
	if err != nil {
		return nil, "", err
	}

	if edge {
		err = _set(boardG, baseAST, key, nil, nil)
	} else {
		err = _set(boardG, baseAST, newKey, nil, nil)
	}

	if len(boardPath) > 0 {
		replaced := ReplaceBoardNode(g.AST, baseAST, boardPath)
		if !replaced {
			return nil, "", fmt.Errorf("board %v AST not found", boardPath)
		}
	}

	if err != nil {
		return nil, "", err
	}
	g, err = recompile(g)
	if err != nil {
		return nil, "", err
	}
	return g, newKey, nil
}

// TODO: update graph in place when compiler can accept single modifications
// TODO: go through all references to decide best spot to insert something
func Set(g *d2graph.Graph, boardPath []string, key string, tag, value *string) (_ *d2graph.Graph, err error) {
	var valueHelp string
	if value == nil {
		valueHelp = fmt.Sprintf("%#v", value)
	} else {
		valueHelp = fmt.Sprintf("%#v", *value)
	}
	if tag != nil {
		defer xdefer.Errorf(&err, "failed to set %#v to %#v %#v", key, *tag, valueHelp)
	} else {
		defer xdefer.Errorf(&err, "failed to set %#v to %#v", key, valueHelp)
	}

	boardG := g
	baseAST := g.AST

	if len(boardPath) > 0 {
		// When compiling a nested board, we can read from boardG but only write to baseBoardG
		boardG = GetBoardGraph(g, boardPath)
		if boardG == nil {
			return nil, fmt.Errorf("board %v not found", boardPath)
		}
		// TODO beter name
		baseAST = boardG.BaseAST
		if baseAST == nil {
			return nil, fmt.Errorf("board %v cannot be modified through this file", boardPath)
		}
	}

	err = _set(boardG, baseAST, key, tag, value)
	if err != nil {
		return nil, err
	}

	if len(boardPath) > 0 {
		replaced := ReplaceBoardNode(g.AST, baseAST, boardPath)
		if !replaced {
			return nil, fmt.Errorf("board %v AST not found", boardPath)
		}
	}

	return recompile(g)
}

func ReconnectEdge(g *d2graph.Graph, boardPath []string, edgeKey string, srcKey, dstKey *string) (_ *d2graph.Graph, err error) {
	mk, err := d2parser.ParseMapKey(edgeKey)
	if err != nil {
		return nil, err
	}

	if len(mk.Edges) == 0 {
		return nil, errors.New("edgeKey must be an edge")
	}

	if mk.EdgeIndex == nil {
		return nil, errors.New("edgeKey must refer to an existing edge")
	}

	edgeTrimCommon(mk)

	boardG := g
	baseAST := g.AST

	if len(boardPath) > 0 {
		// When compiling a nested board, we can read from boardG but only write to baseBoardG
		boardG = GetBoardGraph(g, boardPath)
		if boardG == nil {
			return nil, fmt.Errorf("board %v not found", boardPath)
		}
		// TODO beter name
		baseAST = boardG.BaseAST
		if baseAST == nil {
			return nil, fmt.Errorf("board %v cannot be modified through this file", boardPath)
		}
	}

	obj := boardG.Root
	if mk.Key != nil {
		var ok bool
		obj, ok = boardG.Root.HasChild(d2graph.Key(mk.Key))
		if !ok {
			return nil, errors.New("edge not found")
		}
	}

	edge, ok := obj.HasEdge(mk)
	if !ok {
		return nil, errors.New("edge not found")
	}

	if srcKey != nil {
		if edge.Src.AbsID() == *srcKey {
			srcKey = nil
		}
	}

	if dstKey != nil {
		if edge.Dst.AbsID() == *dstKey {
			dstKey = nil
		}
	}

	if srcKey == nil && dstKey == nil {
		return g, nil
	}

	var src *d2graph.Object
	var dst *d2graph.Object
	if srcKey != nil {
		srcmk, err := d2parser.ParseMapKey(*srcKey)
		if err != nil {
			return nil, err
		}
		src, ok = boardG.Root.HasChild(d2graph.Key(srcmk.Key))
		if !ok {
			return nil, errors.New("newSrc not found")
		}
	}
	if dstKey != nil {
		dstmk, err := d2parser.ParseMapKey(*dstKey)
		if err != nil {
			return nil, err
		}
		dst, ok = boardG.Root.HasChild(d2graph.Key(dstmk.Key))
		if !ok {
			return nil, errors.New("newDst not found")
		}
	}

	refs := edge.References
	if baseAST != g.AST {
		refs = GetWriteableEdgeRefs(edge, baseAST)
		if len(refs) == 0 || refs[0].ScopeAST != baseAST {
			// TODO null
			return nil, OutsideScopeError{}
		}
	}
	ref := refs[0]

	// for loops where only one end is changing, node is always ensured
	if edge.Src != edge.Dst && (srcKey == nil || dstKey == nil) {
		var refEdges []*d2ast.Edge
		for _, ref := range refs {
			refEdges = append(refEdges, ref.Edge)
		}

		if srcKey != nil {
			ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, ref.MapKey.Edges[ref.MapKeyEdgeIndex].Src, true)
		}
		if dstKey != nil {
			ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, ref.MapKey.Edges[ref.MapKeyEdgeIndex].Dst, false)
		}
	}

	for i := range refs {
		ref := refs[i]
		// it's a chain
		if len(ref.MapKey.Edges) > 1 && ref.MapKey.EdgeIndex == nil {
			splitChain := true
			// Changing the start of a chain is okay
			if ref.MapKeyEdgeIndex == 0 && dstKey == nil {
				splitChain = false
			}
			// Changing the end of a chain is okay
			if ref.MapKeyEdgeIndex == len(ref.MapKey.Edges)-1 && srcKey == nil {
				splitChain = false
			}
			if splitChain {
				tmp := *ref.MapKey
				mk2 := &tmp
				mk2.Edges = []*d2ast.Edge{ref.MapKey.Edges[ref.MapKeyEdgeIndex]}
				ref.Scope.InsertAfter(ref.MapKey, mk2)

				if ref.MapKeyEdgeIndex < len(ref.MapKey.Edges)-1 {
					tmp := *ref.MapKey
					mk2 := &tmp
					mk2.Edges = ref.MapKey.Edges[ref.MapKeyEdgeIndex+1:]
					ref.Scope.InsertAfter(ref.MapKey, mk2)
				}
				ref.MapKey.Edges = ref.MapKey.Edges[:ref.MapKeyEdgeIndex]
			}
		}

		if src != nil {
			srcmk, _ := d2parser.ParseMapKey(*srcKey)
			ref.Edge.Src = srcmk.Key
			newPath, err := pathFromScopeObj(boardG, srcmk, ref.ScopeObj)
			if err != nil {
				return nil, err
			}
			ref.Edge.Src.Path = newPath
		}
		if dst != nil {
			dstmk, _ := d2parser.ParseMapKey(*dstKey)
			ref.Edge.Dst = dstmk.Key
			newPath, err := pathFromScopeObj(boardG, dstmk, ref.ScopeObj)
			if err != nil {
				return nil, err
			}
			ref.Edge.Dst.Path = newPath
		}
	}

	return recompile(g)
}

func pathFromScopeKey(g *d2graph.Graph, key *d2ast.Key, scopeak []string) ([]*d2ast.StringBox, error) {
	ak2 := d2graph.Key(key.Key)

	commonPath := getCommonPath(scopeak, ak2)

	var newPath []*d2ast.StringBox
	// Move out to most common scope
	for i := len(commonPath); i < len(scopeak); i++ {
		newPath = append(newPath, d2ast.MakeValueBox(d2ast.RawString("_", true)).StringBox())
	}
	// From most common scope, target the toKey
	newPath = append(newPath, key.Key.Path[len(commonPath):]...)

	return newPath, nil
}

func pathFromScopeObj(g *d2graph.Graph, key *d2ast.Key, fromScope *d2graph.Object) ([]*d2ast.StringBox, error) {
	// We don't want this to be underscore-resolved scope. We want to ignore underscores
	var scopeak []string
	if fromScope != g.Root {
		scopek, err := d2parser.ParseKey(fromScope.AbsID())
		if err != nil {
			return nil, err
		}
		scopeak = d2graph.Key(scopek)
	}
	return pathFromScopeKey(g, key, scopeak)
}

func recompile(g *d2graph.Graph) (*d2graph.Graph, error) {
	s := d2format.Format(g.AST)
	g2, _, err := d2compiler.Compile(g.AST.Range.Path, strings.NewReader(s), &d2compiler.CompileOptions{
		FS: g.FS,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to recompile:\n%s\n%w", s, err)
	}
	return g2, nil
}

// TODO merge flat styles
func _set(g *d2graph.Graph, baseAST *d2ast.Map, key string, tag, value *string) error {
	if tag != nil {
		if hasSpace(*tag) {
			return fmt.Errorf("spaces are not allowed in blockstring tags")
		}
	}

	mk, err := d2parser.ParseMapKey(key)
	if err != nil {
		return err
	}

	if len(mk.Edges) > 1 {
		return errors.New("can only set one edge at a time")
	}

	if value != nil {
		mk.Value = d2ast.MakeValueBox(d2ast.RawString(*value, false))
	} else {
		mk.Value = d2ast.ValueBox{}
	}
	if tag != nil && value != nil {
		mk.Value = d2ast.MakeValueBox(&d2ast.BlockString{
			Tag:   *tag,
			Value: *value,
		})
	}

	scope := baseAST
	edgeTrimCommon(mk)
	obj := g.Root
	toSkip := 1

	reserved := false
	imported := false

	// If you're setting `(x -> y)[0].style.opacity`
	// There's 3 cases you need to handle:
	// 1. The edge has no map.
	// 2. The edge has a style map with opacity not existing
	// 3. The edge has a style map with opacity existing
	//
	// How each case is handled:
	// 1. Append that mapkey to edge.
	// 2. Append opacity to the style map
	// 3. Set opacity
	//
	// There's certainly cleaner code to achieve this, but currently, there's a lot of logic to correctly scope, merge, append.
	// The tests should be comprehensive enough for a safe refactor someday
	//
	// reservedKey = "style"
	// reservedTargetKey = "opacity"
	reservedKey := ""
	reservedTargetKey := ""
	if mk.Key != nil {
		found := true
		for _, idel := range d2graph.Key(mk.Key) {
			_, ok := d2ast.ReservedKeywords[idel]
			if ok {
				reserved = true
				break
			}
			o, ok := obj.HasChild([]string{idel})
			if !ok {
				found = false
				break
			}
			obj = o
			imported = IsImportedObj(baseAST, obj)

			var maybeNewScope *d2ast.Map
			if baseAST != g.AST || imported {
				writeableRefs := GetWriteableRefs(obj, baseAST)
				for _, ref := range writeableRefs {
					if ref.MapKey != nil && ref.MapKey.Value.Map != nil && ref.MapKey.Key == mk.Key {
						maybeNewScope = ref.MapKey.Value.Map
					}
				}
			} else {
				maybeNewScope = obj.Map
			}

			if maybeNewScope == nil {
				// If we find a deeper obj.Map we need to skip this key too.
				toSkip++
				continue
			}

			scope = maybeNewScope
			mk.Key.Path = mk.Key.Path[toSkip:]
			toSkip = 1
			if len(mk.Key.Path) == 0 {
				mk.Key = nil
			}
		}

		if mk.Key != nil && len(mk.Key.Path) == 2 {
			boardType := mk.Key.Path[0].Unbox().ScalarString()
			if boardType == "layers" || boardType == "scenarios" || boardType == "steps" {
				// Force map structure
				var containerMap *d2ast.Map
				for _, n := range scope.Nodes {
					if n.MapKey != nil && n.MapKey.Key != nil && len(n.MapKey.Key.Path) == 1 &&
						n.MapKey.Key.Path[0].Unbox().ScalarString() == boardType {
						containerMap = n.MapKey.Value.Map
						break
					}
				}

				if containerMap == nil {
					containerMap = &d2ast.Map{
						Range: d2ast.MakeRange(",1:0:0-1:0:0"),
					}
					containerMK := &d2ast.Key{
						Key: &d2ast.KeyPath{
							Path: []*d2ast.StringBox{
								d2ast.MakeValueBox(d2ast.RawString(boardType, true)).StringBox(),
							},
						},
						Value: d2ast.MakeValueBox(containerMap),
					}
					appendMapKey(scope, containerMK)
				}

				itemMK := &d2ast.Key{
					Key: &d2ast.KeyPath{
						Path: []*d2ast.StringBox{
							d2ast.MakeValueBox(d2ast.RawString(mk.Key.Path[1].Unbox().ScalarString(), true)).StringBox(),
						},
					},
					Value: mk.Value,
				}
				appendMapKey(containerMap, itemMK)
				return nil
			}
		}

		writeableLabelMK := true
		var objK *d2ast.Key
		if baseAST != g.AST || imported {
			writeableRefs := GetWriteableRefs(obj, baseAST)
			if len(writeableRefs) > 0 {
				objK = writeableRefs[0].MapKey
			}
			if objK == nil {
				appendMapKey(scope, mk)
				return nil
			}
			writeableLabelMK = false
			for _, ref := range writeableRefs {
				if ref.MapKey == obj.Label.MapKey {
					writeableLabelMK = true
					break
				}
			}
		} else {
			// Even if not imported or different board, a label can be not writeable if it's in a class or var or glob
			// In those cases, the label is not a direct object reference
			found := false
			for _, ref := range obj.References {
				if ref.MapKey == obj.Label.MapKey {
					found = true
					break
				}
			}
			if !found {
				writeableLabelMK = false
			}
		}
		var m *d2ast.Map
		if objK != nil {
			m = objK.Value.Map
		} else {
			m = obj.Map
		}

		if (obj.Label.MapKey != nil && writeableLabelMK) && m == nil && (!found || reserved || len(mk.Edges) > 0) &&
			// Label is not set like `hey.label: mylabel`
			// This should only work when label is set like `hey: mylabel`
			(obj.Label.MapKey.Key == nil ||
				len(obj.Label.MapKey.Key.Path) == 0 ||
				obj.Label.MapKey.Key.Path[len(obj.Label.MapKey.Key.Path)-1].Unbox().ScalarString() != "label") {
			m2 := &d2ast.Map{
				Range: d2ast.MakeRange(",1:0:0-1:0:0"),
			}
			if objK == nil {
				obj.Map = m2
				objK = obj.Label.MapKey
			} else {
				objK.Value.Map = m2
			}
			objK.Primary = objK.Value.ScalarBox()
			objK.Value = d2ast.MakeValueBox(m2)
			scope = m2

			mk.Key.Path = mk.Key.Path[toSkip-1:]
			toSkip = 1
			if len(mk.Key.Path) == 0 {
				mk.Key = nil
			}
		}

		if !found {
			appendMapKey(scope, mk)
			return nil
		}
	}
	ir, _, err := d2ir.Compile(g.AST, &d2ir.CompileOptions{
		FS: g.FS,
	})
	if err != nil {
		return err
	}
	attrs := obj.Attributes
	var edge *d2graph.Edge
	if len(mk.Edges) == 1 {
		if mk.EdgeIndex == nil {
			appendMapKey(scope, mk)
			return nil
		}
		var ok bool
		edge, ok = obj.HasEdge(mk)
		if !ok {
			return errors.New("edge not found")
		}
		imported = IsImportedEdge(baseAST, edge)
		refs := edge.References
		if baseAST != g.AST || imported {
			refs = GetWriteableEdgeRefs(edge, baseAST)
		}
		onlyInChain := true
		var earliestRef *d2graph.EdgeReference
		for i, ref := range refs {
			if earliestRef == nil || ref.MapKey.Range.Before(earliestRef.MapKey.Range) {
				earliestRef = &refs[i]
			}
			// TODO merge flat edgekeys
			// E.g. this can group into a map
			// (y -> z)[0].style.opacity: 0.4
			// (y -> z)[0].style.animated: true
			if len(ref.MapKey.Edges) == 1 {
				if ref.MapKey.EdgeIndex == nil || ref.MapKey.Value.Map != nil {
					onlyInChain = false
				}
			}

			if ref.MapKey.EdgeIndex == nil || !ref.MapKey.EdgeIndex.Glob {
				// If a ref has an exact match on this key, just change the value
				tmp1 := *ref.MapKey
				tmp2 := *mk
				noVal1 := &tmp1
				noVal2 := &tmp2
				noVal1.Value = d2ast.ValueBox{}
				noVal2.Value = d2ast.ValueBox{}
				if noVal1.D2OracleEquals(noVal2) {
					ref.MapKey.Value = mk.Value
					return nil
				}
			}
		}
		if onlyInChain {
			if earliestRef != nil && scope.Range.Before(earliestRef.MapKey.Range) {
				// Since the original mk was trimmed to common, we set to the edge that
				// the ref's scope is in
				// Only replace with earliestRef.Edge if it's not a glob pattern
				if !earliestRef.Edge.Src.HasGlob() && !earliestRef.Edge.Dst.HasGlob() {
					mk.Edges[0] = earliestRef.Edge
				}
				// We can't reference an edge before it's been defined
				earliestRef.Scope.InsertAfter(earliestRef.MapKey, mk)
			} else {
				appendMapKey(scope, mk)
			}
			return nil
		}
		attrs = edge.Attributes

		if mk.EdgeKey != nil {
			if _, ok := d2ast.ReservedKeywords[mk.EdgeKey.Path[0].Unbox().ScalarString()]; !ok {
				return errors.New("edge key must be reserved")
			}
			reserved = true

			toSkip = 1
			mk = &d2ast.Key{
				Key:   cloneKey(mk.EdgeKey),
				Value: mk.Value,
			}

			foundMap := false
			for _, ref := range refs {
				// TODO get the most nested one
				if ref.MapKey.Value.Map != nil {
					foundMap = true
					scope = ref.MapKey.Value.Map
					for _, n := range scope.Nodes {
						if n.MapKey == nil || n.MapKey.Value.Map == nil {
							continue
						}
						if n.MapKey.Key == nil || len(n.MapKey.Key.Path) != 1 {
							continue
						}
						if n.MapKey.Key.Path[0].Unbox().ScalarString() == mk.Key.Path[toSkip-1].Unbox().ScalarString() {
							scope = n.MapKey.Value.Map
							if mk.Key.Path[0].Unbox().ScalarString() == "source-arrowhead" && edge.SrcArrowhead != nil {
								attrs = *edge.SrcArrowhead
							}
							if mk.Key.Path[0].Unbox().ScalarString() == "target-arrowhead" && edge.DstArrowhead != nil {
								attrs = *edge.DstArrowhead
							}
							reservedKey = mk.Key.Path[0].Unbox().ScalarString()
							mk.Key.Path = mk.Key.Path[1:]
							reservedTargetKey = mk.Key.Path[0].Unbox().ScalarString()
							break
						}
					}
					break
				}
			}
			if !foundMap && attrs.Label.MapKey != nil {
				attrs.Label.MapKey.Primary = attrs.Label.MapKey.Value.ScalarBox()
				edgeMap := &d2ast.Map{
					Range: d2ast.MakeRange(",1:0:0-1:0:0"),
				}
				attrs.Label.MapKey.Value = d2ast.MakeValueBox(edgeMap)
				scope = edgeMap
			}
		}
	}

	if reserved {
		inlined := func(s *d2graph.Scalar) bool {
			if s != nil && s.MapKey != nil {
				// The value was set outside of what's writeable
				if s.MapKey.Range.Path != baseAST.Range.Path {
					return false
				}
				// Globs are also not writeable
				if s.MapKey.HasGlob() {
					return false
				}
			}
			return s != nil && s.MapKey != nil && !ir.InClass(s.MapKey)
		}
		reservedIndex := toSkip - 1
		if mk.Key != nil && len(mk.Key.Path) > 0 {
			if reservedKey == "" {
				reservedKey = mk.Key.Path[reservedIndex].Unbox().ScalarString()
			}
			switch reservedKey {
			case "shape":
				if inlined(&attrs.Shape) {
					attrs.Shape.MapKey.SetScalar(mk.Value.ScalarBox())
					return nil
				}
			case "link":
				if inlined(attrs.Link) {
					attrs.Link.MapKey.SetScalar(mk.Value.ScalarBox())
					return nil
				}
			case "tooltip":
				if inlined(attrs.Tooltip) {
					attrs.Tooltip.MapKey.SetScalar(mk.Value.ScalarBox())
					return nil
				}
			case "width":
				if inlined(attrs.WidthAttr) {
					attrs.WidthAttr.MapKey.SetScalar(mk.Value.ScalarBox())
					return nil
				}
			case "height":
				if inlined(attrs.HeightAttr) {
					attrs.HeightAttr.MapKey.SetScalar(mk.Value.ScalarBox())
					return nil
				}
			case "top":
				if inlined(attrs.Top) {
					attrs.Top.MapKey.SetScalar(mk.Value.ScalarBox())
					return nil
				}
			case "left":
				if inlined(attrs.Left) {
					attrs.Left.MapKey.SetScalar(mk.Value.ScalarBox())
					return nil
				}
			case "grid-rows":
				if inlined(attrs.GridRows) {
					attrs.GridRows.MapKey.SetScalar(mk.Value.ScalarBox())
					return nil
				}
			case "grid-columns":
				if inlined(attrs.GridColumns) {
					attrs.GridColumns.MapKey.SetScalar(mk.Value.ScalarBox())
					return nil
				}
			case "grid-gap":
				if inlined(attrs.GridGap) {
					attrs.GridGap.MapKey.SetScalar(mk.Value.ScalarBox())
					return nil
				}
			case "vertical-gap":
				if inlined(attrs.VerticalGap) {
					attrs.VerticalGap.MapKey.SetScalar(mk.Value.ScalarBox())
					return nil
				}
			case "horizontal-gap":
				if inlined(attrs.HorizontalGap) {
					attrs.HorizontalGap.MapKey.SetScalar(mk.Value.ScalarBox())
					return nil
				}
			case "source-arrowhead", "target-arrowhead":
				var arrowhead *d2graph.Attributes
				if reservedKey == "source-arrowhead" {
					if edge.SrcArrowhead != nil {
						attrs = *edge.SrcArrowhead
					}
					arrowhead = edge.SrcArrowhead
				} else {
					if edge.DstArrowhead != nil {
						attrs = *edge.DstArrowhead
					}
					arrowhead = edge.DstArrowhead
				}
				if arrowhead != nil {
					if reservedTargetKey == "" {
						if len(mk.Key.Path[reservedIndex:]) < 2 {
							return errors.New("malformed style setting, expected >= 2 part path")
						}
						reservedTargetKey = mk.Key.Path[reservedIndex+1].Unbox().ScalarString()
					}
					switch reservedTargetKey {
					case "shape":
						if inlined(&arrowhead.Shape) {
							arrowhead.Shape.MapKey.SetScalar(mk.Value.ScalarBox())
							return nil
						}
					case "label":
						if inlined(&arrowhead.Label) {
							arrowhead.Label.MapKey.SetScalar(mk.Value.ScalarBox())
							return nil
						}
					case "style":
						reservedTargetKey = mk.Key.Path[len(mk.Key.Path)-1].Unbox().ScalarString()
						if inlined(attrs.Style.Filled) {
							attrs.Style.Filled.MapKey.SetScalar(mk.Value.ScalarBox())
							return nil
						}
					}
				}
			case "style":
				if reservedTargetKey == "" {
					if len(mk.Key.Path[reservedIndex:]) != 2 {
						return errors.New("malformed style setting, expected 2 part path")
					}
					reservedTargetKey = mk.Key.Path[reservedIndex+1].Unbox().ScalarString()
				}
				switch reservedTargetKey {
				case "opacity":
					if inlined(attrs.Style.Opacity) {
						attrs.Style.Opacity.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "stroke":
					if inlined(attrs.Style.Stroke) {
						attrs.Style.Stroke.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "fill":
					if inlined(attrs.Style.Fill) {
						attrs.Style.Fill.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "stroke-width":
					if inlined(attrs.Style.StrokeWidth) {
						attrs.Style.StrokeWidth.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "stroke-dash":
					if inlined(attrs.Style.StrokeDash) {
						attrs.Style.StrokeDash.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "border-radius":
					if inlined(attrs.Style.BorderRadius) {
						attrs.Style.BorderRadius.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "shadow":
					if inlined(attrs.Style.Shadow) {
						attrs.Style.Shadow.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "3d":
					if inlined(attrs.Style.ThreeDee) {
						attrs.Style.ThreeDee.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "multiple":
					if inlined(attrs.Style.Multiple) {
						attrs.Style.Multiple.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "double-border":
					if inlined(attrs.Style.DoubleBorder) {
						attrs.Style.DoubleBorder.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "font":
					if inlined(attrs.Style.Font) {
						attrs.Style.Font.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "font-size":
					if inlined(attrs.Style.FontSize) {
						attrs.Style.FontSize.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "font-color":
					if inlined(attrs.Style.FontColor) {
						attrs.Style.FontColor.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "animated":
					if inlined(attrs.Style.Animated) {
						attrs.Style.Animated.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "bold":
					if inlined(attrs.Style.Bold) {
						attrs.Style.Bold.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "italic":
					if inlined(attrs.Style.Italic) {
						attrs.Style.Italic.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "underline":
					if inlined(attrs.Style.Underline) {
						attrs.Style.Underline.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				case "fill-pattern":
					if inlined(attrs.Style.FillPattern) {
						attrs.Style.FillPattern.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				}
			case "label":
				if len(mk.Key.Path[reservedIndex:]) > 1 {
					reservedTargetKey = mk.Key.Path[reservedIndex+1].Unbox().ScalarString()
					switch reservedTargetKey {
					case "near":
						if inlined(attrs.LabelPosition) {
							attrs.LabelPosition.MapKey.SetScalar(mk.Value.ScalarBox())
							return nil
						}
					}
				} else {
					if inlined(&attrs.Label) {
						attrs.Label.MapKey.SetScalar(mk.Value.ScalarBox())
						return nil
					}
				}
			}
		}
	} else if attrs.Label.MapKey != nil {
		attrs.Label.MapKey.SetScalar(mk.Value.ScalarBox())
		return nil
	}
	appendMapKey(scope, mk)
	return nil
}

func appendUniqueMapKey(m *d2ast.Map, mk *d2ast.Key) {
	for _, n := range m.Nodes {
		if n.MapKey != nil && n.MapKey.D2OracleEquals(mk) {
			return
		}
	}
	appendMapKey(m, mk)
}

func appendMapKey(m *d2ast.Map, mk *d2ast.Key) {
	m.Nodes = append(m.Nodes, d2ast.MapNodeBox{
		MapKey: mk,
	})
	if len(m.Nodes) == 1 &&
		mk.Key != nil &&
		len(mk.Key.Path) > 0 {
		_, ok := d2ast.ReservedKeywords[mk.Key.Path[0].Unbox().ScalarString()]
		if ok {
			// Allow one line reserved key (like shape) maps.
			// TODO: This needs to be smarter as certain keys are only reserved in context.
			// e.g. all keys under style are reserved. And constraint is only reserved
			// under sql_table shapes.
			return
		}
	}
	if !m.IsFileMap() && m.Range.OneLine() {
		// This doesn't require any shenanigans to prevent consuming sibling spacing because
		// d2format will use the mapkey's range to determine whether to insert extra newlines.
		// See TestCreate/make_scope_multiline_spacing_2
		m.Range.End.Line++
	}
}

func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph, err error) {
	defer xdefer.Errorf(&err, "failed to delete %#v", key)

	mk, err := d2parser.ParseMapKey(key)
	if err != nil {
		return nil, err
	}

	if len(mk.Edges) > 1 {
		return nil, errors.New("can only delete one edge at a time")
	}

	if len(mk.Edges) == 1 {
		edgeTrimCommon(mk)
	}

	boardG := g
	baseAST := g.AST

	if len(boardPath) > 0 {
		// When compiling a nested board, we can read from boardG but only write to baseBoardG
		boardG = GetBoardGraph(g, boardPath)
		if boardG == nil {
			return nil, fmt.Errorf("board %v not found", boardPath)
		}
		// TODO beter name
		baseAST = boardG.BaseAST
		if baseAST == nil {
			return nil, fmt.Errorf("board %v cannot be modified through this file", boardPath)
		}
	}

	g2, err := deleteReserved(g, boardPath, baseAST, mk)
	if err != nil {
		return nil, err
	}
	if g != g2 {
		return g2, nil
	}

	if len(mk.Edges) == 1 {
		obj := boardG.Root
		if mk.Key != nil {
			var ok bool
			obj, ok = boardG.Root.HasChild(d2graph.Key(mk.Key))
			if !ok {
				return g, nil
			}
		}
		e, ok := obj.HasEdge(mk)
		if !ok {
			return g, nil
		}

		imported := IsImportedEdge(baseAST, e)

		if imported {
			mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
			appendMapKey(baseAST, mk)
		} else {
			refs := e.References
			if len(boardPath) > 0 {
				refs := GetWriteableEdgeRefs(e, baseAST)
				if len(refs) != len(e.References) {
					mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
				}
			}

			if _, ok := mk.Value.Unbox().(*d2ast.Null); !ok {
				ref := refs[0]
				var refEdges []*d2ast.Edge
				for _, ref := range refs {
					refEdges = append(refEdges, ref.Edge)
				}
				ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, ref.MapKey.Edges[ref.MapKeyEdgeIndex].Src, true)
				ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, ref.MapKey.Edges[ref.MapKeyEdgeIndex].Dst, false)

				for i := len(e.References) - 1; i >= 0; i-- {
					ref := e.References[i]
					// Leave glob setters alone
					if !(ref.MapKey.EdgeIndex != nil && ref.MapKey.EdgeIndex.Glob) {
						deleteEdge(g, ref.Scope, ref.MapKey, ref.MapKeyEdgeIndex)
					}
				}

				edges, ok := obj.FindEdges(mk)
				if ok {
					for _, e2 := range edges {
						if e2.Index <= e.Index {
							continue
						}
						for i := len(e2.References) - 1; i >= 0; i-- {
							ref := e2.References[i]
							if ref.MapKey.EdgeIndex != nil {
								*ref.MapKey.EdgeIndex.Int--
							}
						}
					}
				}
			} else {
				// NOTE: it only needs to be after the last ref, but perhaps simplest and cleanest to append all nulls at the end
				appendMapKey(baseAST, mk)
			}
		}
		if len(boardPath) > 0 {
			replaced := ReplaceBoardNode(g.AST, baseAST, boardPath)
			if !replaced {
				return nil, fmt.Errorf("board %v AST not found", boardPath)
			}
			return recompile(g)
		}
		return recompile(boardG)
	}

	prevG, err := recompile(boardG)
	if err != nil {
		return nil, err
	}

	obj, ok := boardG.Root.HasChild(d2graph.Key(mk.Key))
	if !ok {
		return g, nil
	}

	imported := IsImportedObj(baseAST, obj)

	if imported {
		mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
		appendMapKey(baseAST, mk)
	} else {
		boardG, err = renameConflictsToParent(boardG, mk.Key)
		if err != nil {
			return nil, err
		}
		obj, ok = boardG.Root.HasChild(d2graph.Key(mk.Key))
		if !ok {
			return g, nil
		}
		if len(boardPath) > 0 {
			writeableRefs := GetWriteableRefs(obj, baseAST)
			if len(writeableRefs) != len(obj.References) {
				mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
			}
		}

		if _, ok := mk.Value.Unbox().(*d2ast.Null); !ok {
			boardG, err = deleteObject(boardG, baseAST, mk.Key, obj)
			if err != nil {
				return nil, err
			}

			if err := updateNear(prevG, boardG, &key, nil, false); err != nil {
				return nil, err
			}
		} else {
			appendMapKey(baseAST, mk)
		}
	}

	if len(boardPath) > 0 {
		replaced := ReplaceBoardNode(g.AST, baseAST, boardPath)
		if !replaced {
			return nil, fmt.Errorf("board %v AST not found", boardPath)
		}
		return recompile(g)
	}

	return recompile(boardG)
}

func bumpChildrenUnderscores(m *d2ast.Map) {
	for _, n := range m.Nodes {
		if n.MapKey == nil {
			continue
		}
		if n.MapKey.Key != nil {
			if n.MapKey.Key.Path[0].Unbox().ScalarString() == "_" {
				n.MapKey.Key.Path = n.MapKey.Key.Path[1:]
			}
		}
		for _, e := range n.MapKey.Edges {
			if e.Src.Path[0].Unbox().ScalarString() == "_" {
				e.Src.Path = e.Src.Path[1:]
			}
			if e.Dst.Path[0].Unbox().ScalarString() == "_" {
				e.Dst.Path = e.Dst.Path[1:]
			}
		}
		if n.MapKey.Value.Map != nil {
			bumpChildrenUnderscores(n.MapKey.Value.Map)
		}
	}
}

func hoistRefChildren(g *d2graph.Graph, key *d2ast.KeyPath, ref d2graph.Reference) {
	if ref.MapKey == nil || ref.MapKey.Value.Map == nil {
		return
	}

	bumpChildrenUnderscores(ref.MapKey.Value.Map)
	scopeKey, scope := findNearestParentScope(g, key)
	for i := 0; i < len(ref.MapKey.Value.Map.Nodes); i++ {
		n := ref.MapKey.Value.Map.Nodes[i]
		if n.MapKey == nil {
			continue
		}
		if n.MapKey.Key != nil {
			_, ok := d2ast.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
			if ok {
				continue
			}
		}
		scopeKey := cloneKey(scopeKey)
		scopeKey.Path = scopeKey.Path[:len(scopeKey.Path)-1]
		if n.MapKey.Key != nil {
			scopeKey.Path = append(scopeKey.Path, n.MapKey.Key.Path...)
		}
		if len(scopeKey.Path) > 0 {
			n.MapKey.Key = scopeKey
		}
		scope.InsertBefore(ref.MapKey, n.Unbox())
	}
}

// renameConflictsToParent renames would-be ID conflicts.
func renameConflictsToParent(g *d2graph.Graph, key *d2ast.KeyPath) (*d2graph.Graph, error) {
	obj, ok := g.Root.HasChild(d2graph.Key(key))
	if !ok {
		return g, nil
	}
	if obj.Shape.Value == d2target.ShapeSQLTable || obj.Shape.Value == d2target.ShapeClass {
		return g, nil
	}

	// Usually ignore the object when generating, but if a sibling has the same ID, can't ignore
	ignored := obj
	for _, ch := range obj.ChildrenArray {
		if ch.ID == obj.ID {
			ignored = nil
			break
		}
	}

	// Keep a list of newly generated IDs, so that generateUniqueKey considers them for conflict
	var newIDs []string
	// If we already renamed the key from another reference, no need to touch
	dedupedRenames := map[string]struct{}{}
	for _, ref := range obj.References {
		var absKeys []*d2ast.KeyPath

		if len(ref.Key.Path)-1 == ref.KeyPathIndex {
			if ref.MapKey == nil || ref.MapKey.Value.Map == nil {
				continue
			}
			var mapKeys []*d2ast.KeyPath
			for _, n := range ref.MapKey.Value.Map.Nodes {
				if n.MapKey == nil {
					continue
				}
				if n.MapKey.Key != nil {
					_, ok := d2ast.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
					if ok {
						continue
					}
					mapKeys = append(mapKeys, n.MapKey.Key)
				}
				for _, e := range n.MapKey.Edges {
					mapKeys = append(mapKeys, e.Src)
					mapKeys = append(mapKeys, e.Dst)
				}
			}
			for _, k := range mapKeys {
				absKey, err := d2parser.ParseKey(ref.ScopeObj.AbsID())
				if err != nil {
					absKey = &d2ast.KeyPath{}
				}
				absKey.Path = append(absKey.Path, ref.Key.Path...)
				absKey.Path = append(absKey.Path, k.Path[0])
				absKeys = append(absKeys, absKey)
			}
		} else if _, ok := d2ast.ReservedKeywords[ref.Key.Path[len(ref.Key.Path)-1].Unbox().ScalarString()]; !ok {
			absKey, err := d2parser.ParseKey(ref.ScopeObj.AbsID())
			if err != nil {
				absKey = &d2ast.KeyPath{}
			}
			absKey.Path = append(absKey.Path, ref.Key.Path[:ref.KeyPathIndex+2]...)
			absKeys = append(absKeys, absKey)
		}

		renames := make(map[string]string)
		for _, absKey := range absKeys {
			ida := d2graph.Key(absKey)
			absKeyStr := strings.Join(ida, ".")
			if _, ok := dedupedRenames[absKeyStr]; ok {
				continue
			}
			// Stale reference
			dedupedRenames[absKeyStr] = struct{}{}
			// Do not consider the parent for conflicts, assume the parent will be deleted
			if ida[len(ida)-1] == ida[len(ida)-2] {
				continue
			}

			hoistedAbsKey, err := d2parser.ParseKey(ref.ScopeObj.AbsID())
			if err != nil {
				hoistedAbsKey = &d2ast.KeyPath{}
			}
			hoistedAbsKey.Path = append(hoistedAbsKey.Path, ref.Key.Path[:ref.KeyPathIndex]...)
			hoistedAbsKey.Path = append(hoistedAbsKey.Path, absKey.Path[len(absKey.Path)-1])

			// Can't generate a key that'd conflict with sibling
			var siblingHoistedIDs []string
			for _, otherAbsKey := range absKeys {
				if absKey == otherAbsKey {
					continue
				}
				ida := d2graph.Key(otherAbsKey)
				absKeyStr := strings.Join(ida, ".")
				if _, ok := dedupedRenames[absKeyStr]; ok {
					continue
				}
				hoistedAbsKey, err := d2parser.ParseKey(ref.ScopeObj.AbsID())
				if err != nil {
					hoistedAbsKey = &d2ast.KeyPath{}
				}
				hoistedAbsKey.Path = append(hoistedAbsKey.Path, ref.Key.Path[:ref.KeyPathIndex]...)
				hoistedAbsKey.Path = append(hoistedAbsKey.Path, otherAbsKey.Path[len(otherAbsKey.Path)-1])
				siblingHoistedIDs = append(siblingHoistedIDs, strings.Join(d2graph.Key(hoistedAbsKey), "."))
			}

			uniqueKeyStr, _, err := generateUniqueKey(g, strings.Join(d2graph.Key(hoistedAbsKey), "."), ignored, append(newIDs, siblingHoistedIDs...))
			if err != nil {
				return nil, err
			}
			newIDs = append(newIDs, uniqueKeyStr)
			uniqueKey, err := d2parser.ParseKey(uniqueKeyStr)
			if err != nil {
				return nil, err
			}

			renamedKey := cloneKey(absKey)
			renamedKey.Path[len(renamedKey.Path)-1].Unbox().SetString(uniqueKey.Path[len(uniqueKey.Path)-1].Unbox().ScalarString())

			renamedKeyStr := strings.Join(d2graph.Key(renamedKey), ".")
			if absKeyStr != renamedKeyStr {
				renames[absKeyStr] = renamedKeyStr
			}
			dedupedRenames[renamedKeyStr] = struct{}{}
		}
		// We need to rename in a conflict-free order
		// E.g. imagine you have children `Text 4` and `Text`.
		// `Text 4` would get renamed to `Text` and `Text` gets renamed to `Text 2`
		// But if we follow that order, then both would get named to `Text 2`
		// So order such that the ones that have a conflict are done last, after the no-conflict ones are done
		// A cycle would never occur, as the uniqueness constraint is guaranteed
		var renameOrder []string
		for k, v := range renames {
			// conflict
			if _, ok := renames[v]; ok {
				renameOrder = append(renameOrder, k)
			} else {
				renameOrder = append([]string{k}, renameOrder...)
			}
		}
		for _, k := range renameOrder {
			var err error
			// TODO boardPath
			g, err = move(g, nil, k, renames[k], false)
			if err != nil {
				return nil, err
			}
		}
	}
	return g, nil
}

func deleteReserved(g *d2graph.Graph, boardPath []string, baseAST *d2ast.Map, mk *d2ast.Key) (*d2graph.Graph, error) {
	targetKey := mk.Key
	if len(mk.Edges) == 1 {
		if mk.EdgeKey == nil {
			return g, nil
		}
		targetKey = mk.EdgeKey
	}
	_, ok := d2ast.ReservedKeywords[targetKey.Path[len(targetKey.Path)-1].Unbox().ScalarString()]
	if !ok {
		return g, nil
	}

	var e *d2graph.Edge
	obj := g.Root
	if len(boardPath) > 0 {
		boardG := GetBoardGraph(g, boardPath)
		if boardG == nil {
			return nil, fmt.Errorf("board %v not found", boardPath)
		}
		obj = boardG.Root
	}
	if len(mk.Edges) == 1 {
		if mk.Key != nil {
			var ok bool
			obj, ok = obj.HasChild(d2graph.Key(mk.Key))
			if !ok {
				return g, nil
			}
		}
		e, ok = obj.HasEdge(mk)
		if !ok {
			return g, nil
		}
		imported := IsImportedEdge(baseAST, e)

		deleted, err := deleteEdgeField(g, baseAST, e, targetKey.Path[len(targetKey.Path)-1].Unbox().ScalarString())
		if err != nil {
			return nil, err
		}
		if !deleted && imported {
			mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
			appendMapKey(baseAST, mk)
		}
		return recompile(g)
	}

	isNestedKey := false
	imported := false
	parts := d2graph.Key(targetKey)
	for i, id := range parts {
		_, ok := d2ast.ReservedKeywords[id]
		if ok {
			if id == "style" {
				isNestedKey = true
				continue
			}
			if id == "label" || id == "icon" {
				if i < len(parts)-1 {
					isNestedKey = true
					continue
				}
			}
			if isNestedKey {
				deleted, err := deleteObjField(g, baseAST, obj, id)
				if err != nil {
					return nil, err
				}
				if !deleted && imported {
					mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
					appendMapKey(baseAST, mk)
				}
				continue
			}

			if id == "near" ||
				id == "tooltip" ||
				id == "icon" ||
				id == "width" ||
				id == "height" ||
				id == "left" ||
				id == "top" ||
				id == "link" {
				deleted, err := deleteObjField(g, baseAST, obj, id)
				if err != nil {
					return nil, err
				}
				if !deleted && imported {
					mk.Value = d2ast.MakeValueBox(&d2ast.Null{})
					appendMapKey(baseAST, mk)
				} else {
				}
			}
			break
		}
		obj, ok = obj.HasChild([]string{id})
		if !ok {
			return nil, fmt.Errorf("object not found")
		}
		imported = IsImportedObj(baseAST, obj)
	}

	return recompile(g)
}

func deleteMapField(m *d2ast.Map, field string) (deleted bool) {
	for i := 0; i < len(m.Nodes); i++ {
		n := m.Nodes[i]
		if n.MapKey != nil && n.MapKey.Key != nil {
			if n.MapKey.Key.Path[0].Unbox().ScalarString() == field {
				deleteFromMap(m, n.MapKey)
			} else if n.MapKey.Key.Path[0].Unbox().ScalarString() == "style" ||
				n.MapKey.Key.Path[0].Unbox().ScalarString() == "label" ||
				n.MapKey.Key.Path[0].Unbox().ScalarString() == "icon" ||
				n.MapKey.Key.Path[0].Unbox().ScalarString() == "source-arrowhead" ||
				n.MapKey.Key.Path[0].Unbox().ScalarString() == "target-arrowhead" {
				if n.MapKey.Value.Map != nil {
					deleted2 := deleteMapField(n.MapKey.Value.Map, field)
					if deleted2 {
						deleted = true
					}
					if len(n.MapKey.Value.Map.Nodes) == 0 {
						deleted2 := deleteFromMap(m, n.MapKey)
						if deleted2 {
							deleted = true
						}
					}
				} else if len(n.MapKey.Key.Path) == 2 && n.MapKey.Key.Path[1].Unbox().ScalarString() == field {
					deleted2 := deleteFromMap(m, n.MapKey)
					if deleted2 {
						deleted = true
					}
				}
			}
		}
	}
	return deleted
}

func deleteEdgeField(g *d2graph.Graph, ast *d2ast.Map, e *d2graph.Edge, field string) (deleted bool, _ error) {
	for _, ref := range e.References {
		// Edge chains can't have fields
		if len(ref.MapKey.Edges) > 1 {
			continue
		}
		if ref.MapKey.Range.Path != ast.Range.Path {
			continue
		}
		if ref.MapKey.Value.Map != nil {
			deleted2 := deleteMapField(ref.MapKey.Value.Map, field)
			if deleted2 {
				deleted = true
			}
		} else if ref.MapKey.EdgeKey != nil && ref.MapKey.EdgeKey.Path[len(ref.MapKey.EdgeKey.Path)-1].Unbox().ScalarString() == field {
			// It's always safe to delete, since edge references must coexist with edge definition elsewhere
			deleted2 := deleteFromMap(ref.Scope, ref.MapKey)
			if deleted2 {
				deleted = true
			}
		}
	}
	return deleted, nil
}

func deleteObjField(g *d2graph.Graph, ast *d2ast.Map, obj *d2graph.Object, field string) (deleted bool, _ error) {
	objK, err := d2parser.ParseKey(obj.AbsID())
	if err != nil {
		return false, err
	}
	objGK := d2graph.Key(objK)
	for _, ref := range obj.References {
		if ref.InEdge() {
			continue
		}
		if ref.Key.Range.Path != ast.Range.Path {
			continue
		}
		if ref.MapKey.Value.Map != nil {
			deleteMapField(ref.MapKey.Value.Map, field)
		} else if (len(ref.Key.Path) >= 2 &&
			ref.Key.Path[len(ref.Key.Path)-1].Unbox().ScalarString() == field &&
			ref.Key.Path[len(ref.Key.Path)-2].Unbox().ScalarString() == obj.ID) ||
			(len(ref.Key.Path) >= 3 &&
				ref.Key.Path[len(ref.Key.Path)-1].Unbox().ScalarString() == field &&
				(ref.Key.Path[len(ref.Key.Path)-2].Unbox().ScalarString() == "style" ||
					ref.Key.Path[len(ref.Key.Path)-2].Unbox().ScalarString() == "label" ||
					ref.Key.Path[len(ref.Key.Path)-2].Unbox().ScalarString() == "icon") &&
				ref.Key.Path[len(ref.Key.Path)-3].Unbox().ScalarString() == obj.ID) {
			tmpNodes := make([]d2ast.MapNodeBox, len(ref.Scope.Nodes))
			copy(tmpNodes, ref.Scope.Nodes)
			// If I delete this, will the object still exist?
			deleted2 := deleteFromMap(ref.Scope, ref.MapKey)
			if deleted2 {
				deleted = true
			}
			g2, err := recompile(g)
			if err != nil {
				return false, err
			}
			if _, ok := g2.Root.HasChild(objGK); !ok {
				// Nope, so can't delete it, just remove the field then
				ref.Scope.Nodes = tmpNodes
				ref.MapKey.Value = d2ast.ValueBox{}
				ref.Key.Path = ref.Key.Path[:ref.KeyPathIndex+1]
			}

		}
	}
	return deleted, nil
}

func deleteObject(g *d2graph.Graph, baseAST *d2ast.Map, key *d2ast.KeyPath, obj *d2graph.Object) (*d2graph.Graph, error) {
	var refEdges []*d2ast.Edge
	for _, ref := range obj.References {
		if ref.InEdge() {
			refEdges = append(refEdges, ref.MapKey.Edges[ref.MapKeyEdgeIndex])
		}
	}

	for i := len(obj.References) - 1; i >= 0; i-- {
		ref := obj.References[i]

		if len(ref.MapKey.Edges) == 0 {
			isSuffix := ref.KeyPathIndex == len(ref.Key.Path)-1
			if isSuffix && ref.MapKey != nil {
				ref.MapKey.Primary = d2ast.ScalarBox{}
			}
			ref.Key.Path = append(ref.Key.Path[:ref.KeyPathIndex], ref.Key.Path[ref.KeyPathIndex+1:]...)
			withoutSpecial := go2.Filter(ref.Key.Path, func(x *d2ast.StringBox) bool {
				_, isReserved := d2ast.ReservedKeywords[x.Unbox().ScalarString()]
				isSpecial := isReserved || x.Unbox().ScalarString() == "_"
				return !isSpecial
			})
			if obj.Shape.Value == d2target.ShapeSQLTable || obj.Shape.Value == d2target.ShapeClass {
				deleteFromMap(ref.Scope, ref.MapKey)
			} else if len(withoutSpecial) == 0 {
				hoistRefChildren(g, key, ref)
				deleteFromMap(ref.Scope, ref.MapKey)
			} else if ref.MapKey.Value.Unbox() == nil &&
				obj.Parent != nil &&
				isSuffix &&
				len(obj.Parent.References) > 1 {
				// Redundant key.
				deleteFromMap(ref.Scope, ref.MapKey)
			} else if ref.MapKey.Value.Map != nil && isSuffix {
				for i := 0; i < len(ref.MapKey.Value.Map.Nodes); i++ {
					n := ref.MapKey.Value.Map.Nodes[i]
					if n.MapKey != nil && n.MapKey.Key != nil {
						_, ok := d2ast.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
						if ok {
							deleteFromMap(ref.MapKey.Value.Map, n.MapKey)
							i--
							continue
						}
					}
				}
			} else if isSuffix {
				ref.MapKey.Value = d2ast.ValueBox{}
			}
		} else if ref.InEdge() {
			edge := ref.MapKey.Edges[ref.MapKeyEdgeIndex]

			if obj.Shape.Value == d2target.ShapeSQLTable || obj.Shape.Value == d2target.ShapeClass {
				if ref.MapKeyEdgeDest() {
					ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, edge.Src, true)
				} else {
					ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, edge.Dst, false)
				}
				deleteEdge(g, ref.Scope, ref.MapKey, ref.MapKeyEdgeIndex)
			} else if ref.KeyPathIndex == len(ref.Key.Path)-1 {
				if ref.MapKeyEdgeDest() {
					ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, edge.Src, true)
				} else {
					ensureNode(g, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, edge.Dst, false)
				}
				deleteEdge(g, ref.Scope, ref.MapKey, ref.MapKeyEdgeIndex)
			} else {
				ref.Key.Path = append(ref.Key.Path[:ref.KeyPathIndex], ref.Key.Path[ref.KeyPathIndex+1:]...)

				// Skip visiting the same middle key in an edge chain
				if !ref.MapKeyEdgeDest() && i > 0 {
					nextRef := obj.References[i-1]
					if nextRef.InEdge() && nextRef.MapKey == ref.MapKey {
						i--
					}
				}
			}
		} else {
			// MapKey.Key with edge.
			ref.Key.Path = append(ref.Key.Path[:ref.KeyPathIndex], ref.Key.Path[ref.KeyPathIndex+1:]...)
			if len(ref.Key.Path) == 0 {
				ref.MapKey.Key = nil
			}
		}
	}

	return g, nil
}

func findNearestParentScope(g *d2graph.Graph, k *d2ast.KeyPath) (prefix *d2ast.KeyPath, _ *d2ast.Map) {
	for i := 1; i < len(k.Path); i++ {
		scopeKey := cloneKey(k)
		scopeKey.Path = scopeKey.Path[:len(k.Path)-i]
		obj, ok := g.Root.HasChild(d2graph.Key(scopeKey))
		if ok && obj.Map != nil {
			prefix := cloneKey(k)
			prefix.Path = prefix.Path[len(k.Path)-i:]
			return prefix, obj.Map
		}
	}
	return k, g.AST
}

func deleteEdge(g *d2graph.Graph, scope *d2ast.Map, mk *d2ast.Key, i int) {
	edgesAfter := mk.Edges[i+1:]
	mk.Edges = mk.Edges[:i]

	for _, obj := range g.Objects {
		for j := range obj.References {
			ref := obj.References[j]
			if ref.InEdge() {
				if ref.MapKey == mk && ref.MapKeyEdgeIndex >= i {
					obj.References[j].MapKeyEdgeIndex -= i
				}
			}
		}
	}

	if len(edgesAfter) > 0 {
		tmp := *mk
		mk2 := &tmp
		mk2.Edges = edgesAfter
		scope.InsertAfter(mk, mk2)
	}
	if len(mk.Edges) == 0 {
		deleteFromMap(scope, mk)
	}
}

// ensureNode ensures that `k` exists in `scope` if `excludedEdges` were removed
func ensureNode(g *d2graph.Graph, excludedEdges []*d2ast.Edge, scopeObj *d2graph.Object, scope *d2ast.Map, cursor *d2ast.Key, k *d2ast.KeyPath, before bool) {
	if k == nil || len(k.Path) == 0 {
		return
	}
	if cursor.Key != nil && len(cursor.Key.Path) > 0 {
		k = cloneKey(k)
		k.Path = append(cursor.Key.Path, k.Path...)
	}

	obj, ok := scopeObj.HasChild(d2graph.Key(k))
	if ok {
		// If this key only exists as part of excludedEdges (edges that'll be deleted), we need to make a new one
		hasPersistingRef := false
		for _, ref := range obj.References {
			if !ref.InEdge() {
				hasPersistingRef = true
				break
			}
			if len(ref.MapKey.Edges) == 0 {
				continue
			}
			if !go2.Contains(excludedEdges, ref.MapKey.Edges[ref.MapKeyEdgeIndex]) {
				hasPersistingRef = true
				break
			}
		}
		if hasPersistingRef {
			return
		}
	}
	mk := &d2ast.Key{
		Key: k,
	}

	for _, n := range scope.Nodes {
		if n.MapKey != nil && n.MapKey.D2OracleEquals(mk) {
			return
		}
	}

	if before {
		scope.InsertBefore(cursor, mk)
	} else {
		scope.InsertAfter(cursor, mk)
	}
}

func Rename(g *d2graph.Graph, boardPath []string, key, newName string) (_ *d2graph.Graph, newKey string, err error) {
	defer xdefer.Errorf(&err, "failed to rename %#v to %#v", key, newName)

	mk, err := d2parser.ParseMapKey(key)
	if err != nil {
		return nil, "", err
	}

	boardG := g

	if len(boardPath) > 0 {
		boardG = GetBoardGraph(g, boardPath)
		if boardG == nil {
			return nil, "", fmt.Errorf("board %v not found", boardPath)
		}
	}

	if len(mk.Edges) > 0 && mk.EdgeKey == nil {
		// TODO: Not a fan of this dual interpretation depending on mk.Edges.
		// Maybe we remove Rename and just have Move.
		mk2, err := d2parser.ParseMapKey(newName)
		if err != nil {
			return nil, "", err
		}

		mk2.Key = mk.Key
		mk = mk2
	} else {
		_, ok := d2ast.ReservedKeywords[newName]
		if ok {
			return nil, "", fmt.Errorf("cannot rename to reserved keyword: %#v", newName)
		}
		if mk.Key != nil {
			obj, ok := boardG.Root.HasChild(d2graph.Key(mk.Key))
			if !ok {
				return nil, "", fmt.Errorf("key does not exist")
			}
			// If attempt to name something "x", but "x" already exists, rename it "x 2" instead
			generatedName, _, err := generateUniqueKey(boardG, newName, obj, nil)
			if err == nil {
				newName = generatedName
			}
		}
		// TODO: Handle mk.EdgeKey
		mk.Key.Path[len(mk.Key.Path)-1] = d2ast.MakeValueBox(d2ast.RawString(newName, true)).StringBox()
	}

	g, err = move(g, boardPath, key, d2format.Format(mk), false)
	return g, newName, err
}

func trimReservedSuffix(path []*d2ast.StringBox) []*d2ast.StringBox {
	for i, p := range path {
		if _, ok := d2ast.ReservedKeywords[p.Unbox().ScalarString()]; ok {
			return path[:i]
		}
	}
	return path
}

// Does not handle edge keys, on account of edge keys can only be reserved, e.g. (a->b).style.color: red
func Move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDescendants bool) (_ *d2graph.Graph, err error) {
	defer xdefer.Errorf(&err, "failed to move: %#v to %#v", key, newKey)
	return move(g, boardPath, key, newKey, includeDescendants)
}

func move(g *d2graph.Graph, boardPath []string, key, newKey string, includeDescendants bool) (*d2graph.Graph, error) {
	if key == newKey {
		return g, nil
	}

	boardG := g
	baseAST := g.AST

	if len(boardPath) > 0 {
		// When compiling a nested board, we can read from boardG but only write to baseBoardG
		boardG = GetBoardGraph(g, boardPath)
		if boardG == nil {
			return nil, fmt.Errorf("board %v not found", boardPath)
		}
		// TODO beter name
		baseAST = boardG.BaseAST
		if baseAST == nil {
			return nil, fmt.Errorf("board %v cannot be modified through this file", boardPath)
		}
	}

	newKey, _, err := generateUniqueKey(boardG, newKey, nil, nil)
	if err != nil {
		return nil, err
	}

	mk, err := d2parser.ParseMapKey(key)
	if err != nil {
		return nil, err
	}

	mk2, err := d2parser.ParseMapKey(newKey)
	if err != nil {
		return nil, err
	}
	edgeTrimCommon(mk)
	edgeTrimCommon(mk2)

	if len(mk.Edges) > 0 && mk.EdgeKey == nil {
		if d2format.Format(mk.Key) != d2format.Format(mk2.Key) {
			// TODO just prevent moving edges at all
			return nil, errors.New("moving across scopes isn't supported for edges")
		}
		obj := g.Root
		if mk.Key != nil {
			var ok bool
			obj, ok = g.Root.HasChild(d2graph.Key(mk.Key))
			if !ok {
				return nil, fmt.Errorf("edge referenced by from does not exist")
			}
		}
		e, ok := obj.HasEdge(mk)
		if !ok {
			return nil, fmt.Errorf("edge referenced by to does not exist")
		}
		_, ok = obj.HasEdge(mk2)
		if ok {
			return nil, fmt.Errorf("to edge already exists")
		}

		for i := len(e.References) - 1; i >= 0; i-- {
			ref := e.References[i]
			ref.MapKey.Edges[ref.MapKeyEdgeIndex].SrcArrow = mk2.Edges[0].SrcArrow
			ref.MapKey.Edges[ref.MapKeyEdgeIndex].DstArrow = mk2.Edges[0].DstArrow
		}
		return recompile(g)
	}

	prevG, err := recompile(boardG)
	if err != nil {
		return nil, err
	}

	ak := d2graph.Key(mk.Key)
	ak2 := d2graph.Key(mk2.Key)

	isCrossScope := strings.Join(ak[:len(ak)-1], ".") != strings.Join(ak2[:len(ak2)-1], ".")

	if isCrossScope && !includeDescendants {
		boardG, err = renameConflictsToParent(boardG, mk.Key)
		if err != nil {
			return nil, err
		}
	}

	obj, ok := boardG.Root.HasChild(ak)
	if !ok {
		return nil, fmt.Errorf("key referenced by from does not exist")
	}

	if len(boardPath) > 0 {
		writeableRefs := GetWriteableRefs(obj, baseAST)
		if len(writeableRefs) != len(obj.References) {
			return nil, OutsideScopeError{}
		}
	}

	toParent := boardG.Root
	if isCrossScope && len(ak2) > 1 {
		toParent, ok = boardG.Root.HasChild(ak2[:len(ak2)-1])
		if !ok {
			return nil, fmt.Errorf("key referenced by to parent does not exist")
		}
	}

	// Cross-scope move:
	// 1. Ensure parent node exists as a Key
	// 2. Ensure parent node Key has a map to accept moved node
	// 3. Rename
	// 4. Update all Key references
	// 5. Update all Edge references

	// 1. Ensure parent node exists as a Key
	// The toParent may only exist as an implicit, edge-created node
	//
	// For example, d.e here
	// a.b.c -> d.e.f
	// MOVE(a.b, d.e.q)
	// We can't open d.e as a map, so we need to create a new key

	// If the key targeted exists only as implicit edge creation, e.g. the b in `a.b -> ...`,
	// then we'll be able to move it anywhere by changing that key, e.g. `_._.c.x -> ...`
	// Otherwise, we'll need to make sure the parent exists as a map to move into
	needsLandingMap := false
	if isCrossScope {
		for _, ref := range obj.References {
			if !ref.InEdge() {
				needsLandingMap = true
				break
			}
			if ref.KeyPathIndex != len(ref.Key.Path)-1 {
				needsLandingMap = true
				break
			}
		}
	}
	if isCrossScope && len(ak2) > 1 && needsLandingMap {
		parentExistsAsKey := false
		for _, ref := range toParent.References {
			if len(ref.MapKey.Edges) == 0 {
				parentExistsAsKey = true
				break
			}
		}
		if !parentExistsAsKey {
			// Choose the most nested edge as cursor for this new node
			var mostNestedRef d2graph.Reference
			for _, ref := range toParent.References {
				if mostNestedRef == (d2graph.Reference{}) || len(ref.ScopeObj.AbsIDArray()) > len(mostNestedRef.ScopeObj.AbsIDArray()) {
					mostNestedRef = ref
				}
			}
			detachedMK := &d2ast.Key{
				Key: cloneKey(mostNestedRef.MapKey.Key),
			}
			detachedMK.Key.Path = mostNestedRef.Key.Path[:mostNestedRef.KeyPathIndex+1]
			detachedMK.Range = d2ast.MakeRange(",1:0:0-1:0:0")
			mostNestedRef.Scope.InsertAfter(mostNestedRef.MapKey, detachedMK)

			mostNestedRef.ScopeObj.AppendReferences(d2graph.Key(detachedMK.Key), d2graph.Reference{
				Key:    detachedMK.Key,
				MapKey: detachedMK,
				Scope:  mostNestedRef.Scope,
			}, mostNestedRef.ScopeObj)
		}
	}

	// 2. Ensure parent node Key has a map to accept moved node.
	// This map will be what MOVE will append the new key to
	toScope := boardG.AST
	if isCrossScope && len(ak2) > 1 && needsLandingMap {
		mostNestedParentRefs := getMostNestedRefs(toParent)
		mapExists := false
		for _, ref := range mostNestedParentRefs {
			if ref.KeyPathIndex == len(ref.Key.Path)-1 && ref.MapKey.Value.Map != nil {
				toScope = ref.MapKey.Value.Map
				mapExists = true
				break
			}
		}

		if !mapExists {
			toScope = &d2ast.Map{
				Range: d2ast.MakeRange(",1:0:0-1:0:0"),
			}

			ref := mostNestedParentRefs[len(mostNestedParentRefs)-1]
			// Parent node key exists as part of a flat key, need to split up
			if ref.KeyPathIndex < len(ref.Key.Path)-1 {
				detachedMK := &d2ast.Key{
					Key: cloneKey(ref.MapKey.Key),
				}
				detachedMK.Value = ref.MapKey.Value
				detachedMK.Key.Path = ref.Key.Path[ref.KeyPathIndex+1:]
				detachedMK.Range = d2ast.MakeRange(",1:0:0-1:0:0")

				ref.Key.Path = ref.Key.Path[:ref.KeyPathIndex+1]
				appendUniqueMapKey(toScope, detachedMK)
			} else {
				ref.MapKey.Primary = ref.MapKey.Value.ScalarBox()
			}
			ref.MapKey.Value = d2ast.MakeValueBox(toScope)
		}
	}
	mostNestedRefs := getMostNestedRefs(obj)
	for _, ref := range obj.References {
		isExplicit := ref.KeyPathIndex == len(trimReservedSuffix(ref.Key.Path))-1 && ref.MapKey.Value.Map == nil

		// 3. Rename
		if ak[len(ak)-1] != ak2[len(ak2)-1] {
			ref.Key.Path[ref.KeyPathIndex] = d2ast.MakeValueBox(d2ast.RawString(
				mk2.Key.Path[len(mk2.Key.Path)-1].Unbox().ScalarString(),
				true,
			)).StringBox()
		}

		// 4. Update all Key references
		if len(ref.MapKey.Edges) != 0 {
			continue
		}

		firstNonUnderscoreIndex := 0
		ida := d2graph.Key(ref.Key)
		for i, id := range ida {
			if id != "_" {
				firstNonUnderscoreIndex = i
				break
			}
		}
		resolvedObj, resolvedIDA, err := d2graph.ResolveUnderscoreKey(ida, ref.ScopeObj)
		if err != nil {
			return nil, err
		}
		if resolvedObj != obj {
			ida = resolvedIDA
		}
		// e.g. "a.b.shape: circle"
		_, endsWithReserved := d2ast.ReservedKeywords[ida[len(ida)-1]]
		ida = go2.Filter(ida, func(x string) bool {
			_, ok := d2ast.ReservedKeywords[x]
			return !ok
		})

		// There are 3 cases of what we want to do with Key references in cross scope
		// 1. Transplant. Remove from its current scope, plop it into new scope
		// -- The ref key is the key being moved
		// 2. Split. One node remains, while another gets added to new scope
		// -- The ref key is a flat map with more than just the key being moved
		// 3. Extend.
		// -- The key is moving from its current scope into a more nested scope
		// 4. Slice.
		// -- The key is moving from its current scope out to a less nested scope
		if isCrossScope {
			if (!includeDescendants && len(ida) == 1) || (includeDescendants && ref.KeyPathIndex == firstNonUnderscoreIndex) {
				// 1. Transplant
				absKey, err := d2parser.ParseKey(ref.ScopeObj.AbsID())
				if err != nil {
					absKey = &d2ast.KeyPath{}
				}
				absKey.Path = append(absKey.Path, ref.Key.Path...)
				if !includeDescendants {
					hoistRefChildren(boardG, absKey, ref)
				}
				deleteFromMap(ref.Scope, ref.MapKey)
				detachedMK := &d2ast.Key{Primary: ref.MapKey.Primary, Key: cloneKey(ref.MapKey.Key)}
				detachedMK.Key.Path = go2.Filter(detachedMK.Key.Path, func(x *d2ast.StringBox) bool {
					return x.Unbox().ScalarString() != "_"
				})
				detachedMK.Value = ref.MapKey.Value
				if ref.MapKey != nil && ref.MapKey.Value.Map != nil {
					// Without including descendants, just copy over the reserved
					if !includeDescendants {
						detachedMK.Value.Map = &d2ast.Map{
							Range: ref.MapKey.Value.Map.Range,
						}
						for _, n := range ref.MapKey.Value.Map.Nodes {
							if n.MapKey == nil {
								continue
							}
							if n.MapKey.Key != nil {
								_, ok := d2ast.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
								if ok {
									detachedMK.Value.Map.Nodes = append(detachedMK.Value.Map.Nodes, n)
								}
							}
						}
						if len(detachedMK.Value.Map.Nodes) == 0 {
							detachedMK.Value.Map = nil
						}
					} else {
						// Usually copy everything as is when including descendants
						// The exception is underscored keys, which need to be updated
						for _, n := range ref.MapKey.Value.Map.Nodes {
							if n.MapKey == nil {
								continue
							}
							if n.MapKey.Key != nil {
								if n.MapKey.Key.Path[0].Unbox().ScalarString() == "_" {
									resolvedParent, resolvedScopeKey, err := d2graph.ResolveUnderscoreKey(d2graph.Key(n.MapKey.Key), obj)
									if err != nil {
										return nil, err
									}

									newPath, err := pathFromScopeKey(boardG, &d2ast.Key{Key: d2ast.MakeKeyPath(append(resolvedParent.AbsIDArray(), resolvedScopeKey...))}, ak2)
									if err != nil {
										return nil, err
									}
									n.MapKey.Key.Path = newPath
								}
							}
							for _, e := range n.MapKey.Edges {
								if e.Src.Path[0].Unbox().ScalarString() == "_" {
									resolvedParent, resolvedScopeKey, err := d2graph.ResolveUnderscoreKey(d2graph.Key(e.Src), obj)
									if err != nil {
										return nil, err
									}

									newPath, err := pathFromScopeKey(boardG, &d2ast.Key{Key: d2ast.MakeKeyPath(append(resolvedParent.AbsIDArray(), resolvedScopeKey...))}, ak2)
									if err != nil {
										return nil, err
									}
									e.Src.Path = newPath
								}
								if e.Dst.Path[0].Unbox().ScalarString() == "_" {
									resolvedParent, resolvedScopeKey, err := d2graph.ResolveUnderscoreKey(d2graph.Key(e.Dst), obj)
									if err != nil {
										return nil, err
									}

									newPath, err := pathFromScopeKey(boardG, &d2ast.Key{Key: d2ast.MakeKeyPath(append(resolvedParent.AbsIDArray(), resolvedScopeKey...))}, ak2)
									if err != nil {
										return nil, err
									}
									e.Dst.Path = newPath
								}
							}
						}
					}
				}
				appendUniqueMapKey(toScope, detachedMK)
			} else if len(ida) > 1 && (endsWithReserved || !isExplicit || go2.Contains(mostNestedRefs, ref)) {
				// 2. Split
				detachedMK := &d2ast.Key{Key: cloneKey(ref.MapKey.Key)}
				if includeDescendants {
					detachedMK.Key.Path = append([]*d2ast.StringBox{}, ref.Key.Path[ref.KeyPathIndex:]...)
				} else {
					detachedMK.Key.Path = []*d2ast.StringBox{ref.Key.Path[ref.KeyPathIndex]}
				}
				if includeDescendants {
					detachedMK.Value = ref.MapKey.Value
					ref.MapKey.Value = d2ast.ValueBox{}
				} else if ref.KeyPathIndex == len(filterReservedPath(ref.Key.Path))-1 {
					withReserved, withoutReserved := filterReserved(ref.MapKey.Value)
					detachedMK.Value = withReserved
					ref.MapKey.Value = withoutReserved
					detachedMK.Key.Path = append([]*d2ast.StringBox{}, ref.Key.Path[ref.KeyPathIndex:]...)
					ref.Key.Path = ref.Key.Path[:ref.KeyPathIndex+1]
				}
				if includeDescendants {
					ref.Key.Path = ref.Key.Path[:ref.KeyPathIndex]
				} else {
					ref.Key.Path = append(ref.Key.Path[:ref.KeyPathIndex], ref.Key.Path[ref.KeyPathIndex+1:]...)
				}
				appendUniqueMapKey(toScope, detachedMK)
			} else if len(getCommonPath(ak, ak2)) > 0 {
				// 3. Extend
				// This case does not make sense for includeDescendants
				newKeyPath := ref.Key.Path[:ref.KeyPathIndex]
				newKeyPath = append(newKeyPath, mk2.Key.Path[len(getCommonPath(ak, ak2)):]...)
				ref.Key.Path = append(newKeyPath, ref.Key.Path[ref.KeyPathIndex+1:]...)
			} else {
				// 4. Slice
				scopePath := ref.ScopeObj.AbsIDArray()
				if len(getCommonPath(scopePath, ak2)) != len(scopePath) {
					deleteFromMap(ref.Scope, ref.MapKey)
				} else {
					ref.Key.Path = ref.Key.Path[ref.KeyPathIndex:]
					exists := false
					for _, n := range toScope.Nodes {
						if n.MapKey != nil && n.MapKey != ref.MapKey && n.MapKey.D2OracleEquals(ref.MapKey) {
							exists = true
						}
					}
					if exists {
						deleteFromMap(ref.Scope, ref.MapKey)
					}
				}
			}
		}
	}
	var refEdges []*d2ast.Edge
	for _, ref := range obj.References {
		if ref.InEdge() {
			refEdges = append(refEdges, ref.MapKey.Edges[ref.MapKeyEdgeIndex])
		}
	}
	for i := 0; i < len(obj.References); i++ {
		if !isCrossScope {
			break
		}

		ref := obj.References[i]
		// 5. Update all Edge references
		if len(ref.MapKey.Edges) == 0 {
			continue
		}

		if i > 0 && ref.Key == obj.References[i-1].Key {
			continue
		}

		firstNonUnderscoreIndex := 0
		ida := d2graph.Key(ref.Key)
		for i, id := range ida {
			if id != "_" {
				firstNonUnderscoreIndex = i
				break
			}
		}

		if ref.KeyPathIndex != len(ref.Key.Path)-1 {
			// When moving a node out of an edge, e.g. the `b` out of `a.b.c -> ...`,
			// The edge needs to continue targeting the same thing (c)
			// Split
			detachedMK := &d2ast.Key{
				Key: cloneKey(ref.Key),
			}
			oldPath, err := pathFromScopeObj(boardG, mk, ref.ScopeObj)
			if err != nil {
				return nil, err
			}
			newPath, err := pathFromScopeObj(boardG, mk2, ref.ScopeObj)
			if err != nil {
				return nil, err
			}
			if includeDescendants {
				// When including descendants, the only thing that gets dropped, if any, is the uncommon leading path of new key and key
				// E.g. when moving `a.b` to `x.b` with edge ref key of `a.b.c`, then changing it to `x.b.c` will drop `a`
				diff := len(oldPath) - len(newPath)
				// Only need to check uncommon path if the lengths are the same
				if diff == 0 {
					diff = len(getUncommonPath(d2graph.Key(&d2ast.KeyPath{Path: oldPath}), d2graph.Key(&d2ast.KeyPath{Path: newPath})))
				}
				// If the old key is longer than the new key, we already know all the diff would be dropped
				if diff > 0 && ref.KeyPathIndex != firstNonUnderscoreIndex {
					detachedMK.Key.Path = append([]*d2ast.StringBox{}, ref.Key.Path[ref.KeyPathIndex-diff:ref.KeyPathIndex]...)
					appendUniqueMapKey(ref.Scope, detachedMK)
				}
			} else {
				detachedMK.Key.Path = []*d2ast.StringBox{ref.Key.Path[ref.KeyPathIndex]}
				appendUniqueMapKey(toScope, detachedMK)
			}

			if includeDescendants {
				ref.Key.Path = append(ref.Key.Path[:ref.KeyPathIndex-len(oldPath)+1], append(newPath, ref.Key.Path[ref.KeyPathIndex+1:]...)...)
			} else {
				ref.Key.Path = append(ref.Key.Path[:ref.KeyPathIndex], ref.Key.Path[ref.KeyPathIndex+1:]...)
			}
		} else {
			// When moving a node connected to an edge, we have to ensure parents continue to exist
			// e.g. the `c` out of `a.b.c -> ...`
			// `a.b` needs to exist
			newPath, err := pathFromScopeObj(boardG, mk2, ref.ScopeObj)
			if err != nil {
				return nil, err
			}
			if len(go2.Filter(ref.Key.Path, func(x *d2ast.StringBox) bool { return x.Unbox().ScalarString() != "_" })) > 1 {
				detachedK := cloneKey(ref.Key)
				detachedK.Path = detachedK.Path[:len(detachedK.Path)-1]
				ensureNode(boardG, refEdges, ref.ScopeObj, ref.Scope, ref.MapKey, detachedK, false)
			}

			if includeDescendants {
				ref.Key.Path = append(newPath, ref.Key.Path[go2.Min(len(ref.Key.Path), ref.KeyPathIndex+len(newPath)):]...)
			} else {
				ref.Key.Path = newPath
			}
		}
	}

	if err := updateNear(prevG, boardG, &key, &newKey, includeDescendants); err != nil {
		return nil, err
	}

	if len(boardPath) > 0 {
		replaced := ReplaceBoardNode(g.AST, baseAST, boardPath)
		if !replaced {
			return nil, fmt.Errorf("board %v AST not found", boardPath)
		}
		return recompile(g)
	}

	return recompile(boardG)
}

// filterReserved takes a Value and splits it into 2
// 1. Value with reserved keywords
// 2. Without reserved keywords
// Maintains structure, so if reserved keywords were part of map, the output will keep them in a map
func filterReserved(value d2ast.ValueBox) (with, without d2ast.ValueBox) {
	with, without = d2ast.MakeValueBox(value.Unbox()), d2ast.ValueBox{}

	if value.Map != nil {
		var forWith []d2ast.MapNodeBox
		var forWithout []d2ast.MapNodeBox

		// assume comments are above what they describe
		// going down the map line by line, we batch here as we encounter, and flush to either forWith or forWithout, whichever hits first
		var commentBatch []d2ast.MapNodeBox
		flushComments := func(to *[]d2ast.MapNodeBox) {
			*to = append(*to, commentBatch...)
			commentBatch = nil
		}

		for _, n := range value.Map.Nodes {
			if n.MapKey == nil {
				if n.Comment != nil || n.BlockComment != nil {
					commentBatch = append(commentBatch, n)
				}
				continue
			}
			if n.MapKey.Key == nil || (len(n.MapKey.Key.Path) > 1) {
				flushComments(&forWithout)
				forWithout = append(forWithout, n)
				continue
			}
			_, ok := d2ast.ReservedKeywords[n.MapKey.Key.Path[0].Unbox().ScalarString()]
			if !ok {
				flushComments(&forWithout)
				forWithout = append(forWithout, n)
				continue
			}
			flushComments(&forWith)
			forWith = append(forWith, n)
		}

		if len(forWith) > 0 {
			if with.Map == nil {
				with.Map = &d2ast.Map{
					Range: d2ast.MakeRange(",1:0:0-1:0:0"),
				}
			}
			with.Map.Nodes = forWith
		} else {
			with.Map = nil
		}
		if len(forWithout) > 0 {
			if without.Map == nil {
				without.Map = &d2ast.Map{
					Range: value.Map.Range,
				}
			}
			without.Map.Nodes = forWithout
		} else {
			without.Map = nil
		}
	}

	return
}

// updateNear updates all the Near fields
// prevG is the graph before the update (i.e. deletion, rename, move)
func updateNear(prevG, g *d2graph.Graph, from, to *string, includeDescendants bool) error {
	mk, _ := d2parser.ParseMapKey(*from)
	if len(mk.Edges) > 0 {
		return nil
	}
	if mk.Key == nil {
		return nil
	}
	if len(mk.Key.Path) == 0 {
		return nil
	}

	// TODO get rid of repetition

	// Update all the `near` keys that are one level nested
	// x: {
	//   near: z
	// }
	for _, obj := range g.Objects {
		if obj.Map == nil {
			continue
		}
		for _, n := range obj.Map.Nodes {
			if n.MapKey == nil {
				continue
			}
			if n.MapKey.Key == nil {
				continue
			}
			if len(n.MapKey.Key.Path) == 0 {
				continue
			}
			if len(n.MapKey.Key.Path) > 1 {
				if n.MapKey.Key.Path[len(n.MapKey.Key.Path)-2].Unbox().ScalarString() == "label" ||
					n.MapKey.Key.Path[len(n.MapKey.Key.Path)-2].Unbox().ScalarString() == "icon" {
					continue
				}
			}
			if n.MapKey.Key.Path[len(n.MapKey.Key.Path)-1].Unbox().ScalarString() == "near" {
				k := n.MapKey.Value.ScalarBox().Unbox().ScalarString()
				if _, ok := d2ast.NearConstants[k]; ok {
					continue
				}
				if strings.EqualFold(k, *from) && to == nil {
					deleteFromMap(obj.Map, n.MapKey)
				} else {
					valueMK, err := d2parser.ParseMapKey(k)
					if err != nil {
						return err
					}
					tmpG, _ := recompile(prevG)
					appendMapKey(tmpG.AST, valueMK)
					if to == nil {
						deltas, err := DeleteIDDeltas(tmpG, nil, *from)
						if err != nil {
							return err
						}
						if v, ok := deltas[k]; ok {
							n.MapKey.Value = d2ast.MakeValueBox(d2ast.RawString(v, false))
						}
					} else {
						deltas, err := MoveIDDeltas(tmpG, *from, *to, includeDescendants)
						if err != nil {
							return err
						}
						if v, ok := deltas[k]; ok {
							n.MapKey.Value = d2ast.MakeValueBox(d2ast.RawString(v, false))
						}
					}
				}
			}
		}
	}

	// Update all the `near` keys that are flat (x.near: z)
	for _, obj := range g.Objects {
		for _, ref := range obj.References {
			if ref.MapKey == nil {
				continue
			}
			if ref.MapKey.Key == nil {
				continue
			}
			if len(ref.MapKey.Key.Path) == 0 {
				continue
			}
			if ref.MapKey.Key.Path[len(ref.MapKey.Key.Path)-1].Unbox().ScalarString() == "near" {
				k := ref.MapKey.Value.ScalarBox().Unbox().ScalarString()
				if strings.EqualFold(k, *from) && to == nil {
					deleteFromMap(obj.Map, ref.MapKey)
				} else {
					valueMK, err := d2parser.ParseMapKey(k)
					if err != nil {
						return err
					}
					tmpG, _ := recompile(prevG)
					appendMapKey(tmpG.AST, valueMK)
					if to == nil {
						deltas, err := DeleteIDDeltas(tmpG, nil, *from)
						if err != nil {
							return err
						}
						if v, ok := deltas[k]; ok {
							ref.MapKey.Value = d2ast.MakeValueBox(d2ast.RawString(v, false))
						}
					} else {
						deltas, err := MoveIDDeltas(tmpG, *from, *to, includeDescendants)
						if err != nil {
							return err
						}
						if v, ok := deltas[k]; ok {
							ref.MapKey.Value = d2ast.MakeValueBox(d2ast.RawString(v, false))
						}
					}
				}
			}
		}
	}

	return nil
}

func deleteFromMap(m *d2ast.Map, mk *d2ast.Key) bool {
	for i, n := range m.Nodes {
		if n.MapKey == mk {
			m.Nodes = append(m.Nodes[:i], m.Nodes[i+1:]...)
			return true
		}
	}
	return false
}

func ReparentIDDelta(g *d2graph.Graph, boardPath []string, key, parentKey string) (string, error) {
	mk, err := d2parser.ParseMapKey(key)
	if err != nil {
		return "", err
	}

	boardG := g

	if len(boardPath) > 0 {
		// When compiling a nested board, we can read from boardG but only write to baseBoardG
		boardG = GetBoardGraph(g, boardPath)
		if boardG == nil {
			return "", fmt.Errorf("board %v not found", boardPath)
		}
	}

	obj, ok := boardG.Root.HasChild(d2graph.Key(mk.Key))
	if !ok {
		return "", errors.New("not found")
	}

	parent := boardG.Root
	if parentKey != "" {
		mk2, err := d2parser.ParseMapKey(parentKey)
		if err != nil {
			return "", err
		}
		parent, ok = boardG.Root.HasChild(d2graph.Key(mk2.Key))
		if !ok {
			return "", errors.New("not found")
		}
	}

	prevParent := obj.Parent
	obj.Parent = parent
	id := obj.AbsID()
	obj.Parent = prevParent
	return id, nil
}

func ReconnectEdgeIDDeltas(g *d2graph.Graph, boardPath []string, edgeKey string, srcKey, dstKey *string) (deltas map[string]string, err error) {
	defer xdefer.Errorf(&err, "failed to get deltas for reconnect edge %#v", edgeKey)
	deltas = make(map[string]string)
	// Reconnection: nothing is created or destroyed, the edge just gets a new ID
	// For deltas, it's indices that change:
	// - old sibling edges may decrement index
	// -- happens when the edge is not the last edge index
	// - new sibling edges may increment index
	// -- happens when the edge is not the last edge index
	// - new edge of course always needs an entry

	// The change happens at the first ref, since that is what changes index
	mk, err := d2parser.ParseMapKey(edgeKey)
	if err != nil {
		return nil, err
	}

	if len(mk.Edges) == 0 {
		return nil, errors.New("edgeKey must be an edge")
	}

	if mk.EdgeIndex == nil {
		return nil, errors.New("edgeKey must refer to an existing edge")
	}

	edgeTrimCommon(mk)

	boardG := g

	if len(boardPath) > 0 {
		// When compiling a nested board, we can read from boardG but only write to baseBoardG
		boardG = GetBoardGraph(g, boardPath)
		if boardG == nil {
			return nil, fmt.Errorf("board %v not found", boardPath)
		}
	}

	obj := boardG.Root
	if mk.Key != nil {
		var ok bool
		obj, ok = boardG.Root.HasChild(d2graph.Key(mk.Key))
		if !ok {
			return nil, errors.New("edge not found")
		}
	}
	edge, ok := obj.HasEdge(mk)
	if !ok {
		return nil, errors.New("edge not found")
	}

	if srcKey != nil {
		if edge.Src.AbsID() == *srcKey {
			srcKey = nil
		}
	}

	if dstKey != nil {
		if edge.Dst.AbsID() == *dstKey {
			dstKey = nil
		}
	}

	if srcKey == nil && dstKey == nil {
		return nil, nil
	}

	newSrc := edge.Src
	newDst := edge.Dst
	var src *d2graph.Object
	var dst *d2graph.Object
	if srcKey != nil {
		srcmk, err := d2parser.ParseMapKey(*srcKey)
		if err != nil {
			return nil, err
		}
		src, ok = boardG.Root.HasChild(d2graph.Key(srcmk.Key))
		if !ok {
			return nil, errors.New("newSrc not found")
		}
		newSrc = src
	}
	if dstKey != nil {
		dstmk, err := d2parser.ParseMapKey(*dstKey)
		if err != nil {
			return nil, err
		}
		dst, ok = boardG.Root.HasChild(d2graph.Key(dstmk.Key))
		if !ok {
			return nil, errors.New("newDst not found")
		}
		newDst = dst
	}

	// The first ref is always the definition
	firstRef := edge.References[0]
	line := firstRef.MapKey.Range.Start.Line
	newIndex := 0

	// For the edge's own delta, it just needs to know how many edges came before it with the same src and dst
	for _, otherEdge := range boardG.Edges {
		if otherEdge.Src == newSrc && otherEdge.Dst == newDst {
			firstRef := otherEdge.References[0]
			if firstRef.MapKey.Range.Start.Line <= line {
				newIndex++
			}
		}
		if otherEdge.Src == edge.Src && otherEdge.Dst == edge.Dst && otherEdge.Index > edge.Index {
			before := otherEdge.AbsID()
			otherEdge.Index--
			after := otherEdge.AbsID()
			deltas[before] = after
			otherEdge.Index++
		}
	}

	for _, otherEdge := range g.Edges {
		if otherEdge.Src == newSrc && otherEdge.Dst == newDst {
			if otherEdge.Index >= newIndex {
				before := otherEdge.AbsID()
				otherEdge.Index++
				after := otherEdge.AbsID()
				deltas[before] = after
				otherEdge.Index--
			}
		}
	}

	newEdge := &d2graph.Edge{
		Src:      newSrc,
		Dst:      newDst,
		SrcArrow: edge.SrcArrow,
		DstArrow: edge.DstArrow,
		Index:    newIndex,
	}

	deltas[edge.AbsID()] = newEdge.AbsID()

	return deltas, nil
}

// generateUniqueKey generates a unique key by appending a number after `prefix` such that it doesn't conflict with any IDs in `g`
// If `ignored` is not nil, a conflict with the ignored object is allowed. An example use case is to generate a unique ID for a child being
// hoisted out of its container, and you know the container is going to be deleted.
//
// If `included` is not nil, the generated key must also not conflict with a key in `included`, on top of not conflicting with any IDs in `g`.
// This is for when an operation needs to generate multiple unique keys in one go, like deleting a container and giving new IDs to all children
func generateUniqueKey(g *d2graph.Graph, prefix string, ignored *d2graph.Object, included []string) (key string, edge bool, _ error) {
	mk, err := d2parser.ParseMapKey(prefix)
	if err != nil {
		return "", false, err
	}

	if len(mk.Edges) > 1 {
		return "", false, errors.New("cannot generate unique key for edge chain")
	}

	if len(mk.Edges) == 1 {
		if mk.EdgeIndex == nil || mk.EdgeIndex.Int == nil {
			mk.EdgeIndex = &d2ast.EdgeIndex{
				Int: go2.Pointer(0),
			}
		}

		edgeTrimCommon(mk)
		obj := g.Root
		if mk.Key != nil {
			var ok bool
			obj, ok = g.Root.HasChild(d2graph.Key(mk.Key))
			if !ok {
				return d2format.Format(mk), true, nil
			}
		}
		for {
			_, ok := obj.HasEdge(mk)
			if !ok {
				return d2format.Format(mk), true, nil
			}
			mk.EdgeIndex.Int = go2.Pointer(*mk.EdgeIndex.Int + 1)
		}
	}

	// If a key is not provided, we generate one.
	if mk.Key == nil {
		mk.Key = &d2ast.KeyPath{
			Path: []*d2ast.StringBox{d2ast.MakeValueBox(d2ast.RawString(xrand.Base64(16), true)).StringBox()},
		}
	} else if obj, ok := g.Root.HasChild(d2graph.Key(mk.Key)); ok && obj != ignored {
		// The key may already have an index, e.g. "x 2"
		spaced := strings.Split(prefix, " ")
		if len(spaced) > 1 {
			if _, err := strconv.Atoi(spaced[len(spaced)-1]); err == nil {
				withoutIndex := strings.Join(spaced[:len(spaced)-1], " ")
				mk, err = d2parser.ParseMapKey(withoutIndex)
				if err != nil {
					return "", false, err
				}
			}
		}
	}

	k2 := cloneKey(mk.Key)
	i := 0
	for {
		conflictsWithIncluded := false
		for _, s := range included {
			if d2format.Format(k2) == s {
				conflictsWithIncluded = true
				break
			}
		}
		if !conflictsWithIncluded {
			obj, ok := g.Root.HasChild(d2graph.Key(k2))
			if !ok || obj == ignored {
				return d2format.Format(k2), false, nil
			}
		}

		rr := fmt.Sprintf("%s %d", mk.Key.Path[len(mk.Key.Path)-1].Unbox().ScalarString(), i+2)
		k2.Path[len(k2.Path)-1] = d2ast.MakeValueBox(d2ast.RawString(rr, true)).StringBox()
		i++
	}
}

func cloneKey(k *d2ast.KeyPath) *d2ast.KeyPath {
	if k == nil {
		return &d2ast.KeyPath{}
	}
	tmp := *k
	k2 := &tmp
	k2.Path = nil
	for _, p := range k.Path {
		k2.Path = append(k2.Path, d2ast.MakeValueBox(p.Unbox().Copy()).StringBox())
	}
	return k2
}

func getCommonPath(a, b []string) []string {
	var out []string
	for i := 0; i < len(a) && i < len(b); i++ {
		if a[i] == b[i] {
			out = a[:i+1]
		}
	}
	return out
}

func getUncommonPath(a, b []string) []string {
	var out []string
	for i := 0; i < len(a) && i < len(b); i++ {
		if a[i] != b[i] {
			out = a[:i+1]
		}
	}
	return out
}

func edgeTrimCommon(mk *d2ast.Key) {
	if len(mk.Edges) != 1 {
		return
	}
	e := mk.Edges[0]
	for len(e.Src.Path) > 1 && len(e.Dst.Path) > 1 {
		if !strings.EqualFold(e.Src.Path[0].Unbox().ScalarString(), e.Dst.Path[0].Unbox().ScalarString()) {
			return
		}
		if mk.Key == nil {
			mk.Key = &d2ast.KeyPath{}
		}
		mk.Key.Path = append(mk.Key.Path, e.Src.Path[0])
		e.Src.Path = e.Src.Path[1:]
		e.Dst.Path = e.Dst.Path[1:]
	}
}

func MoveIDDeltas(g *d2graph.Graph, key, newKey string, includeDescendants bool) (deltas map[string]string, err error) {
	defer xdefer.Errorf(&err, "failed to get deltas for move from %#v to %#v", key, newKey)
	deltas = make(map[string]string)

	if key == newKey {
		return deltas, nil
	}

	mk, err := d2parser.ParseMapKey(key)
	if err != nil {
		return nil, err
	}

	newKey, _, err = generateUniqueKey(g, newKey, nil, nil)
	if err != nil {
		return nil, err
	}

	mk2, err := d2parser.ParseMapKey(newKey)
	if err != nil {
		return nil, err
	}

	ak := d2graph.Key(mk.Key)
	ak2 := d2graph.Key(mk2.Key)
	isCrossScope := strings.Join(ak[:len(ak)-1], ".") != strings.Join(ak2[:len(ak2)-1], ".")

	edgeTrimCommon(mk)
	obj := g.Root

	// Conflict IDs are when a container is moved and the children conflict with something in parent
	conflictNewIDs := make(map[*d2graph.Object]string)
	conflictOldIDs := make(map[*d2graph.Object]string)
	var newIDs []string
	if mk.Key != nil {
		var ok bool
		obj, ok = g.Root.HasChild(d2graph.Key(mk.Key))
		if !ok {
			return nil, nil
		}

		ignored := obj
		for _, ch := range obj.ChildrenArray {
			if ch.ID == obj.ID {
				ignored = nil
				break
			}
		}

		if !includeDescendants {
			for _, ch := range obj.ChildrenArray {
				chMK, err := d2parser.ParseMapKey(ch.AbsID())
				if err != nil {
					return nil, err
				}
				ida := d2graph.Key(chMK.Key)
				if ida[len(ida)-1] == ida[len(ida)-2] {
					continue
				}

				hoistedAbsID := ch.ID
				if obj.Parent != g.Root {
					hoistedAbsID = obj.Parent.AbsID() + "." + ch.ID
				}
				hoistedMK, err := d2parser.ParseMapKey(hoistedAbsID)
				if err != nil {
					return nil, err
				}

				conflictsWithNewID := false
				for _, id := range newIDs {
					if id == d2format.Format(hoistedMK.Key) {
						conflictsWithNewID = true
						break
					}
				}

				if _, ok := g.Root.HasChild(d2graph.Key(hoistedMK.Key)); ok || conflictsWithNewID {
					newKey, _, err := generateUniqueKey(g, hoistedAbsID, ignored, newIDs)
					if err != nil {
						return nil, err
					}
					newMK, err := d2parser.ParseMapKey(newKey)
					if err != nil {
						return nil, err
					}
					newAK := d2graph.Key(newMK.Key)
					conflictOldIDs[ch] = ch.ID
					conflictNewIDs[ch] = newAK[len(newAK)-1]
					newIDs = append(newIDs, d2format.Format(newMK.Key))
				} else {
					newIDs = append(newIDs, d2format.Format(hoistedMK.Key))
				}
			}
		}
	}

	if len(mk.Edges) > 1 {
		return nil, nil
	}
	if len(mk.Edges) == 1 {
		if len(mk.Edges) == 0 {
			return nil, errors.New("cannot rename edge to node")
		}
		if len(mk.Edges) > 1 {
			return nil, errors.New("cannot rename edge to edge chain")
		}

		e, ok := obj.HasEdge(mk)
		if !ok {
			return nil, nil
		}
		beforeID := e.AbsID()
		tmp := *e
		e2 := &tmp
		e2.SrcArrow = mk2.Edges[0].SrcArrow == "<"
		e2.DstArrow = mk2.Edges[0].DstArrow == ">"
		deltas[beforeID] = e2.AbsID()
		return deltas, nil
	}

	beforeObjID := obj.ID

	toParent := g.Root
	if len(ak2) > 1 {
		var ok bool
		toParent, ok = g.Root.HasChild(ak2[:len(ak2)-1])
		if !ok {
			return nil, errors.New("to parent not found")
		}
	}
	id := ak2[len(ak2)-1]

	tmpRenames := func() func() {
		if isCrossScope && !includeDescendants {
			for _, ch := range obj.ChildrenArray {
				ch.Parent = obj.Parent
			}
		}

		prevParent := obj.Parent
		obj.Parent = toParent
		obj.ID = id

		for k, v := range conflictNewIDs {
			k.ID = v
		}
		return func() {
			for k, v := range conflictOldIDs {
				k.ID = v
			}
			obj.ID = beforeObjID
			obj.Parent = prevParent

			if isCrossScope && !includeDescendants {
				for _, ch := range obj.ChildrenArray {
					ch.Parent = obj
				}
			}
		}
	}

	appendNodeDelta := func(ch *d2graph.Object) {
		beforeID := ch.AbsID()
		revert := tmpRenames()
		deltas[beforeID] = ch.AbsID()
		revert()
	}

	appendEdgeDelta := func(ch *d2graph.Object) {
		for _, e := range obj.Graph.Edges {
			if e.Src == ch || e.Dst == ch {
				beforeID := e.AbsID()
				revert := tmpRenames()
				deltas[beforeID] = e.AbsID()
				revert()
			}
		}
	}

	var recurse func(ch *d2graph.Object)
	recurse = func(ch *d2graph.Object) {
		for _, ch := range ch.ChildrenArray {
			appendNodeDelta(ch)
			appendEdgeDelta(ch)
			recurse(ch)
		}
	}
	appendNodeDelta(obj)
	appendEdgeDelta(obj)
	recurse(obj)
	return deltas, nil
}

func DeleteIDDeltas(g *d2graph.Graph, boardPath []string, key string) (deltas map[string]string, err error) {
	defer xdefer.Errorf(&err, "failed to get deltas for deletion of %#v", key)
	deltas = make(map[string]string)

	mk, err := d2parser.ParseMapKey(key)
	if err != nil {
		return nil, err
	}

	edgeTrimCommon(mk)

	boardG := g
	if len(boardPath) > 0 {
		boardG = GetBoardGraph(g, boardPath)
		if boardG == nil {
			return nil, fmt.Errorf("board %v not found", boardPath)
		}
	}

	obj := boardG.Root
	conflictNewIDs := make(map[*d2graph.Object]string)
	conflictOldIDs := make(map[*d2graph.Object]string)
	var newIDs []string
	if mk.Key != nil {
		ida := d2graph.Key(mk.Key)
		// Deleting a reserved field cannot possibly have any deltas
		if _, ok := d2ast.ReservedKeywords[ida[len(ida)-1]]; ok {
			return nil, nil
		}

		var ok bool
		obj, ok = boardG.Root.HasChild(d2graph.Key(mk.Key))
		if !ok {
			return nil, nil
		}

		ignored := obj
		for _, ch := range obj.ChildrenArray {
			if ch.ID == obj.ID {
				ignored = nil
				break
			}
		}

		for _, ch := range obj.ChildrenArray {
			// Record siblings as the unique key generated should not conflict with any siblings either
			var siblingsToBeHoisted []string
			for _, ch2 := range obj.ChildrenArray {
				if ch2 != ch {
					chMK, err := d2parser.ParseMapKey(ch2.AbsID())
					if err != nil {
						return nil, err
					}
					ida := d2graph.Key(chMK.Key)
					if ida[len(ida)-1] == ida[len(ida)-2] {
						continue
					}
					hoistedAbsID := ch2.ID
					if obj.Parent != boardG.Root {
						hoistedAbsID = obj.Parent.AbsID() + "." + ch2.ID
					}
					siblingsToBeHoisted = append(siblingsToBeHoisted, hoistedAbsID)
				}
			}
			chMK, err := d2parser.ParseMapKey(ch.AbsID())
			if err != nil {
				return nil, err
			}
			ida := d2graph.Key(chMK.Key)
			if ida[len(ida)-1] == ida[len(ida)-2] {
				continue
			}
			hoistedAbsID := ch.ID
			if obj.Parent != boardG.Root {
				hoistedAbsID = obj.Parent.AbsID() + "." + ch.ID
			}
			hoistedMK, err := d2parser.ParseMapKey(hoistedAbsID)
			if err != nil {
				return nil, err
			}

			conflictsWithNewID := false
			for _, id := range newIDs {
				if id == d2format.Format(hoistedMK.Key) {
					conflictsWithNewID = true
					break
				}
			}

			if conflictingObj, ok := boardG.Root.HasChild(d2graph.Key(hoistedMK.Key)); (ok && conflictingObj != obj) || conflictsWithNewID {
				newKey, _, err := generateUniqueKey(boardG, hoistedAbsID, ignored, append(newIDs, siblingsToBeHoisted...))
				if err != nil {
					return nil, err
				}
				newMK, err := d2parser.ParseMapKey(newKey)
				if err != nil {
					return nil, err
				}
				newAK := d2graph.Key(newMK.Key)
				conflictOldIDs[ch] = ch.ID
				conflictNewIDs[ch] = newAK[len(newAK)-1]
				newIDs = append(newIDs, d2format.Format(newMK.Key))
			} else {
				newIDs = append(newIDs, d2format.Format(hoistedMK.Key))
			}
		}
	}
	if len(mk.Edges) > 1 {
		return nil, nil
	}
	if len(mk.Edges) == 1 {
		// Anything deleted in an edge key cannot affect deltas
		if mk.EdgeKey != nil {
			return nil, nil
		}
		e, ok := obj.HasEdge(mk)
		if !ok {
			return nil, nil
		}
		ea, ok := obj.FindEdges(mk)
		if !ok {
			return nil, nil
		}
		for _, e2 := range ea {
			if e2.Index > e.Index {
				beforeID := e2.AbsID()
				e2.Index--
				deltas[beforeID] = e2.AbsID()
				e2.Index++
			}
		}
		return deltas, nil
	}

	for _, ch := range obj.ChildrenArray {
		tmpRenames := func() func() {
			prevIDs := make(map[*d2graph.Object]string)
			for _, ch := range obj.ChildrenArray {
				prevIDs[ch] = ch.ID
				ch.Parent = obj.Parent
			}
			for k, v := range conflictNewIDs {
				k.ID = v
			}

			return func() {
				for k, v := range conflictOldIDs {
					k.ID = v
				}
				for _, ch := range obj.ChildrenArray {
					ch.Parent = obj
					ch.ID = prevIDs[ch]
				}
			}
		}

		appendNodeDelta := func(ch2 *d2graph.Object) {
			beforeAbsID := ch2.AbsID()
			revert := tmpRenames()
			deltas[beforeAbsID] = ch2.AbsID()
			revert()

		}
		appendEdgeDelta := func(ch2 *d2graph.Object) {
			for _, e := range obj.Graph.Edges {
				if e.Src == ch2 || e.Dst == ch2 {
					beforeAbsID := e.AbsID()
					revert := tmpRenames()
					deltas[beforeAbsID] = e.AbsID()
					revert()

				}
			}
		}

		var recurse func(ch2 *d2graph.Object)
		recurse = func(ch2 *d2graph.Object) {
			for _, ch2 := range ch2.ChildrenArray {
				appendNodeDelta(ch2)
				appendEdgeDelta(ch2)
				recurse(ch2)
			}
		}

		appendNodeDelta(ch)
		appendEdgeDelta(ch)
		recurse(ch)
	}
	return deltas, nil
}

func RenameIDDeltas(g *d2graph.Graph, boardPath []string, key, newName string) (deltas map[string]string, err error) {
	defer xdefer.Errorf(&err, "failed to get deltas for renaming of %#v to %#v", key, newName)
	deltas = make(map[string]string)

	mk, err := d2parser.ParseMapKey(key)
	if err != nil {
		return nil, err
	}

	boardG := g
	if len(boardPath) > 0 {
		boardG = GetBoardGraph(g, boardPath)
		if boardG == nil {
			return nil, fmt.Errorf("board %v not found", boardPath)
		}
	}

	edgeTrimCommon(mk)
	obj := boardG.Root
	if mk.Key != nil {
		var ok bool
		obj, ok = boardG.Root.HasChild(d2graph.Key(mk.Key))
		if !ok {
			return nil, nil
		}
	}
	if len(mk.Edges) > 1 {
		return nil, nil
	}
	if len(mk.Edges) == 1 {
		mk2, err := d2parser.ParseMapKey(newName)
		if err != nil {
			return nil, err
		}
		if len(mk.Edges) == 0 {
			return nil, errors.New("cannot rename edge to node")
		}
		if len(mk.Edges) > 1 {
			return nil, errors.New("cannot rename edge to edge chain")
		}

		e, ok := obj.HasEdge(mk)
		if !ok {
			return nil, nil
		}
		beforeID := e.AbsID()
		tmp := *e
		e2 := &tmp
		e2.SrcArrow = mk2.Edges[0].SrcArrow == "<"
		e2.DstArrow = mk2.Edges[0].DstArrow == ">"
		deltas[beforeID] = e2.AbsID()
		return deltas, nil
	}

	if mk.Key.Path[len(mk.Key.Path)-1].Unbox().ScalarString() == newName {
		return deltas, nil
	}

	mk.Key.Path[len(mk.Key.Path)-1].Unbox().SetString(newName)
	uniqueKeyStr, _, err := generateUniqueKey(boardG, strings.Join(d2graph.Key(mk.Key), "."), obj, nil)
	if err != nil {
		return nil, err
	}
	uniqueKey, err := d2parser.ParseKey(uniqueKeyStr)
	if err != nil {
		return nil, err
	}
	newNameKey := uniqueKey.Path[len(uniqueKey.Path)-1].Unbox().ScalarString()
	newNameKey = d2format.Format(d2ast.RawString(newNameKey, true))

	beforeObjID := obj.ID

	appendNodeDelta := func(ch *d2graph.Object) {
		if obj.ID != newNameKey {
			beforeID := ch.AbsID()
			obj.ID = newNameKey
			deltas[beforeID] = ch.AbsID()
			obj.ID = beforeObjID
		}
	}

	appendEdgeDelta := func(ch *d2graph.Object) {
		for _, e := range obj.Graph.Edges {
			if e.Src == ch || e.Dst == ch {
				if obj.ID != newNameKey {
					beforeID := e.AbsID()
					obj.ID = newNameKey
					deltas[beforeID] = e.AbsID()
					obj.ID = beforeObjID
				}
			}
		}
	}

	var recurse func(ch *d2graph.Object)
	recurse = func(ch *d2graph.Object) {
		for _, ch := range ch.ChildrenArray {
			appendNodeDelta(ch)
			appendEdgeDelta(ch)
			recurse(ch)
		}
	}
	appendNodeDelta(obj)
	appendEdgeDelta(obj)
	recurse(obj)
	return deltas, nil
}

func hasSpace(tag string) bool {
	for _, r := range tag {
		if unicode.IsSpace(r) {
			return true
		}
	}
	return false
}

func getMostNestedRefs(obj *d2graph.Object) []d2graph.Reference {
	var most d2graph.Reference
	for _, ref := range obj.References {
		if len(ref.MapKey.Edges) == 0 {
			most = ref
			break
		}
	}
	for _, ref := range obj.References {
		if len(ref.MapKey.Edges) != 0 {
			continue
		}

		scopeKey, err := d2parser.ParseKey(ref.ScopeObj.AbsID())
		if err != nil {
			scopeKey = &d2ast.KeyPath{}
		}
		mostKey, err := d2parser.ParseKey(most.ScopeObj.AbsID())
		if err != nil {
			mostKey = &d2ast.KeyPath{}
		}
		_, resolvedScopeKey, err := d2graph.ResolveUnderscoreKey(d2graph.Key(scopeKey), ref.ScopeObj)
		if err != nil {
			continue
		}
		_, resolvedMostKey, err := d2graph.ResolveUnderscoreKey(d2graph.Key(mostKey), ref.ScopeObj)
		if err != nil {
			continue
		}
		if len(resolvedScopeKey) > len(resolvedMostKey) {
			most = ref
		}
	}

	var out []d2graph.Reference
	for _, ref := range obj.References {
		if len(ref.MapKey.Edges) != 0 {
			continue
		}
		if ref.ScopeObj.AbsID() == most.ScopeObj.AbsID() {
			out = append(out, ref)
		}
	}

	return out
}

func filterReservedPath(path []*d2ast.StringBox) (filtered []*d2ast.StringBox) {
	for _, box := range path {
		if _, ok := d2ast.ReservedKeywords[strings.ToLower(box.Unbox().ScalarString())]; ok {
			return
		}
		filtered = append(filtered, box)
	}
	return
}

func UpdateImport(dsl, path string, newPath *string) (_ string, err error) {
	if newPath == nil {
		defer xdefer.Errorf(&err, "failed to remove import %#v", path)
	} else {
		defer xdefer.Errorf(&err, "failed to update import from %#v to %#v", path, *newPath)
	}

	ast, err := d2parser.Parse("", strings.NewReader(dsl), nil)
	if err != nil {
		return "", err
	}

	_updateImport(ast, path, newPath)

	return d2format.Format(ast), nil
}

func _updateImport(m *d2ast.Map, oldPath string, newPath *string) {
	for i := 0; i < len(m.Nodes); i++ {
		node := m.Nodes[i]

		if node.Import != nil {
			importPath := node.Import.PathWithPre()
			if matchesImportPath(importPath, oldPath) {
				if newPath == nil {
					if node.Import.Spread {
						m.Nodes = append(m.Nodes[:i], m.Nodes[i+1:]...)
						i--
					} else {
						node.Import = nil
					}
				} else {
					updateImportPath(node.Import, getNewImportPath(importPath, oldPath, *newPath))
				}
				continue
			}
		}

		if node.MapKey != nil {
			if node.MapKey.Value.Import != nil {
				importPath := node.MapKey.Value.Import.PathWithPre()
				if matchesImportPath(importPath, oldPath) {
					if newPath == nil {
						if node.MapKey.Value.Import.Spread && node.MapKey.Value.Map == nil {
							m.Nodes = append(m.Nodes[:i], m.Nodes[i+1:]...)
							i--
						} else {
							node.MapKey.Value.Import = nil
						}
					} else {
						updateImportPath(node.MapKey.Value.Import, getNewImportPath(importPath, oldPath, *newPath))
					}
				}
			}

			primaryImport := node.MapKey.Primary.Unbox()
			if primaryImport != nil {
				value, ok := primaryImport.(d2ast.Value)
				if ok {
					importBox := d2ast.MakeValueBox(value)
					if importBox.Import != nil {
						importPath := importBox.Import.PathWithPre()
						if matchesImportPath(importPath, oldPath) {
							if newPath == nil {
								node.MapKey.Primary = d2ast.ScalarBox{}
							} else {
								updateImportPath(importBox.Import, getNewImportPath(importPath, oldPath, *newPath))
							}
						}
					}
				}
			}

			if node.MapKey.Value.Map != nil {
				_updateImport(node.MapKey.Value.Map, oldPath, newPath)
			}
		}
	}
}

func updateImportPath(imp *d2ast.Import, newPath string) {
	var pre string
	pathPart := newPath

	for i, r := range newPath {
		if r != '.' && r != '/' {
			pre = newPath[:i]
			pathPart = newPath[i:]
			break
		}
	}

	if pre == "" && len(newPath) > 0 && (newPath[0] == '.' || newPath[0] == '/') {
		pre = newPath
		pathPart = ""
	}

	imp.Pre = pre

	if pathPart != "" {
		if len(imp.Path) > 0 {
			imp.Path[0] = d2ast.MakeValueBox(d2ast.RawString(pathPart, true)).StringBox()
		} else {
			imp.Path = []*d2ast.StringBox{
				d2ast.MakeValueBox(d2ast.RawString(pathPart, true)).StringBox(),
			}
		}
	} else if len(imp.Path) == 0 {
		imp.Path = []*d2ast.StringBox{
			d2ast.MakeValueBox(d2ast.RawString("", true)).StringBox(),
		}
	}
}

func matchesImportPath(importPath, oldPath string) bool {
	isDir := strings.HasSuffix(oldPath, "/")
	if isDir {
		return strings.HasPrefix(importPath, oldPath)
	}
	return importPath == oldPath
}

func getNewImportPath(importPath, oldPath, newPath string) string {
	isOldDir := strings.HasSuffix(oldPath, "/")
	isNewDir := strings.HasSuffix(newPath, "/")
	if isOldDir && isNewDir {
		relPath := importPath[len(oldPath):]
		return newPath + relPath
	}
	return newPath
}
