package d2dagrelayout

import (
	"context"
	_ "embed"
	"encoding/json"
	"fmt"
	"math"
	"sort"
	"strings"

	"log/slog"

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

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

	"oss.terrastruct.com/d2/d2graph"
	"oss.terrastruct.com/d2/d2target"
	"oss.terrastruct.com/d2/lib/geo"
	"oss.terrastruct.com/d2/lib/jsrunner"
	"oss.terrastruct.com/d2/lib/label"
	"oss.terrastruct.com/d2/lib/log"
	"oss.terrastruct.com/d2/lib/shape"
)

//go:embed setup.js
var setupJS string

const (
	MIN_RANK_SEP    = 60
	EDGE_LABEL_GAP  = 20
	DEFAULT_PADDING = 30.
	MIN_SPACING     = 10.
)

type ConfigurableOpts struct {
	NodeSep int `json:"nodesep"`
	EdgeSep int `json:"edgesep"`
}

var DefaultOpts = ConfigurableOpts{
	NodeSep: 60,
	EdgeSep: 20,
}

type DagreNode struct {
	ID     string  `json:"id"`
	X      float64 `json:"x"`
	Y      float64 `json:"y"`
	Width  float64 `json:"width"`
	Height float64 `json:"height"`
}

type DagreEdge struct {
	Points []*geo.Point `json:"points"`
}

type dagreOpts struct {
	// for a top to bottom graph: ranksep is y spacing, nodesep is x spacing, edgesep is x spacing
	ranksep int
	// graph direction: tb (top to bottom)| bt | lr | rl
	rankdir string

	ConfigurableOpts
}

func DefaultLayout(ctx context.Context, g *d2graph.Graph) (err error) {
	return Layout(ctx, g, nil)
}

func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err error) {
	if opts == nil {
		opts = &DefaultOpts
	}
	defer xdefer.Errorf(&err, "failed to dagre layout")

	debugJS := false
	runner := jsrunner.NewJSRunner()
	if _, err := runner.RunString(dagreJS); err != nil {
		return err
	}
	if _, err := runner.RunString(setupJS); err != nil {
		return err
	}

	rootAttrs := dagreOpts{
		ConfigurableOpts: ConfigurableOpts{
			EdgeSep: opts.EdgeSep,
			NodeSep: opts.NodeSep,
		},
	}
	isHorizontal := false
	switch g.Root.Direction.Value {
	case "down":
		rootAttrs.rankdir = "TB"
	case "right":
		rootAttrs.rankdir = "LR"
		isHorizontal = true
	case "left":
		rootAttrs.rankdir = "RL"
		isHorizontal = true
	case "up":
		rootAttrs.rankdir = "BT"
	default:
		rootAttrs.rankdir = "TB"
	}

	// set label and icon positions for dagre
	for _, obj := range g.Objects {
		positionLabelsIcons(obj)
	}

	maxLabelWidth := 0
	maxLabelHeight := 0
	for _, edge := range g.Edges {
		width := edge.LabelDimensions.Width
		height := edge.LabelDimensions.Height
		maxLabelWidth = go2.Max(maxLabelWidth, width)
		maxLabelHeight = go2.Max(maxLabelHeight, height)
	}

	if !isHorizontal {
		rootAttrs.ranksep = go2.Max(100, maxLabelHeight+40)
	} else {
		rootAttrs.ranksep = go2.Max(100, maxLabelWidth+40)
		// use existing config
		// rootAttrs.NodeSep = rootAttrs.EdgeSep
		// // configure vertical padding
		// rootAttrs.EdgeSep = maxLabelHeight + 40
		// Note: non-containers have both of these as padding (rootAttrs.NodeSep + rootAttrs.EdgeSep)
	}

	configJS := setGraphAttrs(rootAttrs)
	if _, err := runner.RunString(configJS); err != nil {
		return err
	}

	mapper := NewObjectMapper()
	for _, obj := range g.Objects {
		mapper.Register(obj)
	}
	loadScript := ""
	for _, obj := range g.Objects {
		loadScript += mapper.generateAddNodeLine(obj, int(obj.Width), int(obj.Height))
		if obj.Parent != g.Root {
			loadScript += mapper.generateAddParentLine(obj, obj.Parent)
		}
	}

	for _, edge := range g.Edges {
		src, dst := getEdgeEndpoints(g, edge)

		width := edge.LabelDimensions.Width
		height := edge.LabelDimensions.Height

		numEdges := 0
		for _, e := range g.Edges {
			otherSrc, otherDst := getEdgeEndpoints(g, e)
			if (otherSrc == src && otherDst == dst) || (otherSrc == dst && otherDst == src) {
				numEdges++
			}
		}

		// We want to leave some gap between multiple edges
		if numEdges > 1 {
			switch g.Root.Direction.Value {
			case "down", "up", "":
				width += EDGE_LABEL_GAP
			case "left", "right":
				height += EDGE_LABEL_GAP
			}
		}

		loadScript += mapper.generateAddEdgeLine(src, dst, edge.AbsID(), width, height)
	}

	if debugJS {
		log.Debug(ctx, "script", slog.Any("all", setupJS+configJS+loadScript))
	}

	if _, err := runner.RunString(loadScript); err != nil {
		return err
	}

	if _, err := runner.RunString(`dagre.layout(g)`); err != nil {
		if debugJS {
			log.Warn(ctx, "layout error", slog.Any("err", err))
		}
		return err
	}

	for i := range g.Objects {
		val, err := runner.RunString(fmt.Sprintf("JSON.stringify(g.node(g.nodes()[%d]))", i))
		if err != nil {
			return err
		}
		var dn DagreNode
		if err := json.Unmarshal([]byte(val.String()), &dn); err != nil {
			return err
		}
		if debugJS {
			log.Debug(ctx, "graph", slog.Any("json", dn))
		}

		obj := mapper.ToObj(dn.ID)

		// dagre gives center of node
		obj.TopLeft = geo.NewPoint(math.Round(dn.X-dn.Width/2), math.Round(dn.Y-dn.Height/2))
		obj.Width = math.Ceil(dn.Width)
		obj.Height = math.Ceil(dn.Height)
	}

	for i, edge := range g.Edges {
		val, err := runner.RunString(fmt.Sprintf("JSON.stringify(g.edge(g.edges()[%d]))", i))
		if err != nil {
			return err
		}
		var de DagreEdge
		if err := json.Unmarshal([]byte(val.String()), &de); err != nil {
			return err
		}
		if debugJS {
			log.Debug(ctx, "graph", slog.Any("json", de))
		}

		points := make([]*geo.Point, len(de.Points))
		for i := range de.Points {
			if edge.SrcArrow && !edge.DstArrow {
				points[len(de.Points)-i-1] = de.Points[i].Copy()
			} else {
				points[i] = de.Points[i].Copy()
			}
		}

		startIndex, endIndex := 0, len(points)-1
		start, end := points[startIndex], points[endIndex]

		// chop where edge crosses the source/target boxes since container edges were routed to a descendant
		if edge.Src != edge.Dst {
			for i := 1; i < len(points); i++ {
				segment := *geo.NewSegment(points[i-1], points[i])
				if intersections := edge.Src.Box.Intersections(segment); len(intersections) > 0 {
					start = intersections[0]
					startIndex = i - 1
				}

				if intersections := edge.Dst.Box.Intersections(segment); len(intersections) > 0 {
					end = intersections[0]
					endIndex = i
					break
				}
			}
		}
		points = points[startIndex : endIndex+1]
		points[0] = start
		points[len(points)-1] = end

		edge.Route = points
	}

	adjustRankSpacing(g, float64(rootAttrs.ranksep), isHorizontal)
	adjustCrossRankSpacing(g, float64(rootAttrs.ranksep), !isHorizontal)
	fitContainerPadding(g, float64(rootAttrs.ranksep), isHorizontal)

	for _, edge := range g.Edges {
		points := edge.Route
		startIndex, endIndex := 0, len(points)-1
		start, end := points[startIndex], points[endIndex]

		// arrowheads can appear broken if segments are very short from dagre routing a point just outside the shape
		// to fix this, we try extending the previous segment into the shape instead of having a very short segment
		if startIndex+2 < len(points) {
			newStartingSegment := *geo.NewSegment(start, points[startIndex+1])
			if newStartingSegment.Length() < d2graph.MIN_SEGMENT_LEN {
				// we don't want a very short segment right next to the source because it will mess up the arrowhead
				// instead we want to extend the next segment into the shape border if possible
				nextStart := points[startIndex+1]
				nextEnd := points[startIndex+2]

				// Note: in other direction to extend towards source
				nextSegment := *geo.NewSegment(nextStart, nextEnd)
				v := nextSegment.ToVector()
				extendedStart := nextEnd.ToVector().Add(v.AddLength(d2graph.MIN_SEGMENT_LEN)).ToPoint()
				extended := *geo.NewSegment(nextEnd, extendedStart)

				if intersections := edge.Src.Box.Intersections(extended); len(intersections) > 0 {
					startIndex++
					points[startIndex] = intersections[0]
					start = points[startIndex]
				}
			}
		}
		if endIndex-2 >= 0 {
			newEndingSegment := *geo.NewSegment(end, points[endIndex-1])
			if newEndingSegment.Length() < d2graph.MIN_SEGMENT_LEN {
				// extend the prev segment into the shape border if possible
				prevStart := points[endIndex-2]
				prevEnd := points[endIndex-1]

				prevSegment := *geo.NewSegment(prevStart, prevEnd)
				v := prevSegment.ToVector()
				extendedEnd := prevStart.ToVector().Add(v.AddLength(d2graph.MIN_SEGMENT_LEN)).ToPoint()
				extended := *geo.NewSegment(prevStart, extendedEnd)

				if intersections := edge.Dst.Box.Intersections(extended); len(intersections) > 0 {
					endIndex--
					points[endIndex] = intersections[0]
					end = points[endIndex]
				}
			}
		}

		var originalSrcTL, originalDstTL *geo.Point
		// if the edge passes through 3d/multiple, use the offset box for tracing to border
		if srcDx, srcDy := edge.Src.GetModifierElementAdjustments(); srcDx != 0 || srcDy != 0 {
			if start.X > edge.Src.TopLeft.X+srcDx &&
				start.Y < edge.Src.TopLeft.Y+edge.Src.Height-srcDy {
				originalSrcTL = edge.Src.TopLeft.Copy()
				edge.Src.TopLeft.X += srcDx
				edge.Src.TopLeft.Y -= srcDy
			}
		}
		if dstDx, dstDy := edge.Dst.GetModifierElementAdjustments(); dstDx != 0 || dstDy != 0 {
			if end.X > edge.Dst.TopLeft.X+dstDx &&
				end.Y < edge.Dst.TopLeft.Y+edge.Dst.Height-dstDy {
				originalDstTL = edge.Dst.TopLeft.Copy()
				edge.Dst.TopLeft.X += dstDx
				edge.Dst.TopLeft.Y -= dstDy
			}
		}

		startIndex, endIndex = edge.TraceToShape(points, startIndex, endIndex)
		points = points[startIndex : endIndex+1]

		// build a curved path from the dagre route
		vectors := make([]geo.Vector, 0, len(points)-1)
		for i := 1; i < len(points); i++ {
			vectors = append(vectors, points[i-1].VectorTo(points[i]))
		}

		path := make([]*geo.Point, 0)
		path = append(path, points[0])
		if len(vectors) > 1 {
			path = append(path, points[0].AddVector(vectors[0].Multiply(.8)))
			for i := 1; i < len(vectors)-2; i++ {
				p := points[i]
				v := vectors[i]
				path = append(path, p.AddVector(v.Multiply(.2)))
				path = append(path, p.AddVector(v.Multiply(.5)))
				path = append(path, p.AddVector(v.Multiply(.8)))
			}
			path = append(path, points[len(points)-2].AddVector(vectors[len(vectors)-1].Multiply(.2)))
			edge.IsCurve = true
		}
		path = append(path, points[len(points)-1])

		edge.Route = path
		// compile needs to assign edge label positions
		if edge.Label.Value != "" {
			edge.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
		}

		// undo 3d/multiple offset
		if originalSrcTL != nil {
			edge.Src.TopLeft.X = originalSrcTL.X
			edge.Src.TopLeft.Y = originalSrcTL.Y
		}
		if originalDstTL != nil {
			edge.Dst.TopLeft.X = originalDstTL.X
			edge.Dst.TopLeft.Y = originalDstTL.Y
		}
	}

	return nil
}

func getEdgeEndpoints(g *d2graph.Graph, edge *d2graph.Edge) (*d2graph.Object, *d2graph.Object) {
	// dagre doesn't work with edges to containers so we connect container edges to their first child instead (going all the way down)
	// we will chop the edge where it intersects the container border so it only shows the edge from the container
	src := edge.Src
	for len(src.Children) > 0 && src.Class == nil && src.SQLTable == nil {
		// We want to get the bottom node of sources, setting its rank higher than all children
		src = getLongestEdgeChainTail(g, src)
	}
	dst := edge.Dst
	for len(dst.Children) > 0 && dst.Class == nil && dst.SQLTable == nil {
		dst = getLongestEdgeChainHead(g, dst)
	}
	if edge.SrcArrow && !edge.DstArrow {
		// for `b <- a`, edge.Edge is `a -> b` and we expect this routing result
		src, dst = dst, src
	}
	return src, dst
}

func setGraphAttrs(attrs dagreOpts) string {
	return fmt.Sprintf(`g.setGraph({
  ranksep: %d,
  edgesep: %d,
  nodesep: %d,
  rankdir: "%s",
});
`,
		attrs.ranksep,
		attrs.ConfigurableOpts.EdgeSep,
		attrs.ConfigurableOpts.NodeSep,
		attrs.rankdir,
	)
}

// getLongestEdgeChainHead finds the longest chain in a container and gets its head
// If there are multiple chains of the same length, get the head closest to the center
func getLongestEdgeChainHead(g *d2graph.Graph, container *d2graph.Object) *d2graph.Object {
	rank := make(map[*d2graph.Object]int)
	chainLength := make(map[*d2graph.Object]int)

	for _, obj := range container.ChildrenArray {
		isHead := true
		for _, e := range g.Edges {
			if inContainer(e.Src, container) != nil && inContainer(e.Dst, obj) != nil {
				isHead = false
				break
			}
		}
		if !isHead {
			continue
		}
		rank[obj] = 1
		chainLength[obj] = 1
		// BFS
		queue := []*d2graph.Object{obj}
		visited := make(map[*d2graph.Object]struct{})
		for len(queue) > 0 {
			curr := queue[0]
			queue = queue[1:]
			if _, ok := visited[curr]; ok {
				continue
			}
			visited[curr] = struct{}{}
			for _, e := range g.Edges {
				child := inContainer(e.Dst, container)
				if child == curr {
					continue
				}
				if child != nil && inContainer(e.Src, curr) != nil {
					if rank[curr]+1 > rank[child] {
						rank[child] = rank[curr] + 1
						chainLength[obj] = go2.Max(chainLength[obj], rank[child])
					}
					queue = append(queue, child)
				}
			}
		}
	}
	max := int(math.MinInt32)
	for _, obj := range container.ChildrenArray {
		if chainLength[obj] > max {
			max = chainLength[obj]
		}
	}

	var heads []*d2graph.Object
	for i, obj := range container.ChildrenArray {
		if rank[obj] == 1 && chainLength[obj] == max {
			heads = append(heads, container.ChildrenArray[i])
		}
	}

	if len(heads) > 0 {
		return heads[int(math.Floor(float64(len(heads))/2.0))]
	}
	return container.ChildrenArray[0]
}

// getLongestEdgeChainTail gets the node at the end of the longest edge chain, because that will be the end of the container
// and is what external connections should connect with.
// If there are multiple of same length, get the one closest to the middle
func getLongestEdgeChainTail(g *d2graph.Graph, container *d2graph.Object) *d2graph.Object {
	rank := make(map[*d2graph.Object]int)

	for _, obj := range container.ChildrenArray {
		isHead := true
		for _, e := range g.Edges {
			if inContainer(e.Src, container) != nil && inContainer(e.Dst, obj) != nil {
				isHead = false
				break
			}
		}
		if !isHead {
			continue
		}
		rank[obj] = 1
		// BFS
		queue := []*d2graph.Object{obj}
		visited := make(map[*d2graph.Object]struct{})
		for len(queue) > 0 {
			curr := queue[0]
			queue = queue[1:]
			if _, ok := visited[curr]; ok {
				continue
			}
			visited[curr] = struct{}{}
			for _, e := range g.Edges {
				child := inContainer(e.Dst, container)
				if child == curr {
					continue
				}
				if child != nil && inContainer(e.Src, curr) != nil {
					rank[child] = go2.Max(rank[child], rank[curr]+1)
					queue = append(queue, child)
				}
			}
		}
	}
	max := int(math.MinInt32)
	for _, obj := range container.ChildrenArray {
		if rank[obj] > max {
			max = rank[obj]
		}
	}

	var tails []*d2graph.Object
	for i, obj := range container.ChildrenArray {
		if rank[obj] == max {
			tails = append(tails, container.ChildrenArray[i])
		}
	}

	return tails[int(math.Floor(float64(len(tails))/2.0))]
}

func inContainer(obj, container *d2graph.Object) *d2graph.Object {
	if obj == nil {
		return nil
	}
	if obj == container {
		return obj
	}
	if obj.Parent == container {
		return obj
	}
	return inContainer(obj.Parent, container)
}

func positionLabelsIcons(obj *d2graph.Object) {
	if obj.Icon != nil && obj.IconPosition == nil {
		if len(obj.ChildrenArray) > 0 {
			obj.IconPosition = go2.Pointer(label.OutsideTopLeft.String())
			if obj.LabelPosition == nil {
				obj.LabelPosition = go2.Pointer(label.OutsideTopRight.String())
				return
			}
		} else if obj.SQLTable != nil || obj.Class != nil || obj.Language != "" {
			obj.IconPosition = go2.Pointer(label.OutsideTopLeft.String())
		} else {
			obj.IconPosition = go2.Pointer(label.InsideMiddleCenter.String())
		}
	}
	if obj.HasLabel() && obj.LabelPosition == nil {
		if len(obj.ChildrenArray) > 0 {
			obj.LabelPosition = go2.Pointer(label.OutsideTopCenter.String())
		} else if obj.HasOutsideBottomLabel() {
			obj.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
		} else if obj.Icon != nil {
			obj.LabelPosition = go2.Pointer(label.InsideTopCenter.String())
		} else {
			obj.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
		}
		if float64(obj.LabelDimensions.Width) > obj.Width || float64(obj.LabelDimensions.Height) > obj.Height {
			if len(obj.ChildrenArray) > 0 {
				obj.LabelPosition = go2.Pointer(label.OutsideTopCenter.String())
			} else {
				obj.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
			}
		}
	}
}

func getRanks(g *d2graph.Graph, isHorizontal bool) (ranks [][]*d2graph.Object, objectRanks, startingParentRanks, endingParentRanks map[*d2graph.Object]int) {
	alignedObjects := make(map[float64][]*d2graph.Object)
	for _, obj := range g.Objects {
		if !obj.IsContainer() {
			if !isHorizontal {
				y := math.Ceil(obj.TopLeft.Y + obj.Height/2)
				alignedObjects[y] = append(alignedObjects[y], obj)
			} else {
				x := math.Ceil(obj.TopLeft.X + obj.Width/2)
				alignedObjects[x] = append(alignedObjects[x], obj)
			}
		}
	}

	levels := make([]float64, 0, len(alignedObjects))
	for l := range alignedObjects {
		levels = append(levels, l)
	}
	sort.Slice(levels, func(i, j int) bool {
		return levels[i] < levels[j]
	})

	ranks = make([][]*d2graph.Object, 0, len(levels))
	objectRanks = make(map[*d2graph.Object]int)
	for i, l := range levels {
		for _, obj := range alignedObjects[l] {
			objectRanks[obj] = i
		}
		ranks = append(ranks, alignedObjects[l])
	}

	startingParentRanks = make(map[*d2graph.Object]int)
	endingParentRanks = make(map[*d2graph.Object]int)
	for _, obj := range g.Objects {
		if obj.IsContainer() {
			continue
		}
		r := objectRanks[obj]
		// update all ancestor's min/max ranks
		for parent := obj.Parent; parent != nil && parent != g.Root; parent = parent.Parent {
			if start, has := startingParentRanks[parent]; !has || r < start {
				startingParentRanks[parent] = r
			}
			if end, has := endingParentRanks[parent]; !has || r > end {
				endingParentRanks[parent] = r
			}
		}
	}

	return ranks, objectRanks, startingParentRanks, endingParentRanks
}

// shift everything down by distance if it is at or below start position
func shiftDown(g *d2graph.Graph, start, distance float64, isHorizontal bool) {
	if isHorizontal {
		for _, edge := range g.Edges {
			first, last := edge.Route[0], edge.Route[len(edge.Route)-1]
			if start <= first.X {
				onStaticSrc := first.X == edge.Src.TopLeft.X+edge.Src.Width && edge.Src.TopLeft.X < start
				if !onStaticSrc {
					// src is not shifting and we are on src so don't shift
					first.X += distance
				}
			}
			if start <= last.X {
				onStaticDst := last.X == edge.Dst.TopLeft.X+edge.Dst.Width && edge.Dst.TopLeft.X < start
				if !onStaticDst {
					last.X += distance
				}
			}
			for i := 1; i < len(edge.Route)-1; i++ {
				p := edge.Route[i]
				if p.X < start {
					continue
				}
				p.X += distance
			}
		}
		for _, obj := range g.Objects {
			if obj.TopLeft.X < start {
				continue
			}
			obj.TopLeft.X += distance
		}
	} else {
		for _, edge := range g.Edges {
			first, last := edge.Route[0], edge.Route[len(edge.Route)-1]
			if start <= first.Y {
				onStaticSrc := first.Y == edge.Src.TopLeft.Y+edge.Src.Height && edge.Src.TopLeft.Y < start
				if !onStaticSrc {
					// src is not shifting and we are on src so don't shift
					first.Y += distance
				}
			}
			if start <= last.Y {
				onStaticDst := last.Y == edge.Dst.TopLeft.Y+edge.Dst.Height && edge.Dst.TopLeft.Y < start
				if !onStaticDst {
					last.Y += distance
				}
			}
			for i := 1; i < len(edge.Route)-1; i++ {
				p := edge.Route[i]
				if p.Y < start {
					continue
				}
				p.Y += distance
			}
		}
		for _, obj := range g.Objects {
			if obj.TopLeft.Y < start {
				continue
			}
			obj.TopLeft.Y += distance
		}
	}
}

func shiftUp(g *d2graph.Graph, start, distance float64, isHorizontal bool) {
	if isHorizontal {
		for _, edge := range g.Edges {
			first, last := edge.Route[0], edge.Route[len(edge.Route)-1]
			if first.X <= start {
				onStaticSrc := first.X == edge.Src.TopLeft.X && start < edge.Src.TopLeft.X+edge.Src.Width
				if !onStaticSrc {
					// src is not shifting and we are on src so don't shift
					first.X -= distance
				}
			}
			if last.X <= start {
				onStaticDst := last.X == edge.Dst.TopLeft.X && start < edge.Dst.TopLeft.X+edge.Dst.Width
				if !onStaticDst {
					last.X -= distance
				}
			}
			for i := 1; i < len(edge.Route)-1; i++ {
				p := edge.Route[i]
				if start < p.X {
					continue
				}
				p.X -= distance
			}
		}
		for _, obj := range g.Objects {
			if start < obj.TopLeft.X {
				continue
			}
			obj.TopLeft.X -= distance
		}
	} else {
		for _, edge := range g.Edges {
			first, last := edge.Route[0], edge.Route[len(edge.Route)-1]
			if first.Y <= start {
				// don't shift first edge point if src is not shifting and we are on src
				onStaticSrc := first.Y == edge.Src.TopLeft.Y && start < edge.Src.TopLeft.Y+edge.Src.Height
				if !onStaticSrc {
					first.Y -= distance
				}
			}
			if last.Y <= start {
				onStaticDst := last.Y == edge.Dst.TopLeft.Y && start < edge.Dst.TopLeft.Y
				if !onStaticDst {
					last.Y -= distance
				}
			}
			for i := 1; i < len(edge.Route)-1; i++ {
				p := edge.Route[i]
				// for _, p := range edge.Route {
				if start < p.Y {
					continue
				}
				p.Y -= distance
			}
		}
		for _, obj := range g.Objects {
			if start < obj.TopLeft.Y {
				continue
			}
			obj.TopLeft.Y -= distance
		}
	}
}

// shift down everything that is below start
// shift all nodes that are reachable via an edge or being directly below a shifting node or expanding container
// expand containers to wrap shifted nodes
func shiftReachableDown(g *d2graph.Graph, obj *d2graph.Object, start, distance float64, isHorizontal, isMargin bool) map[*d2graph.Object]struct{} {
	q := []*d2graph.Object{obj}

	needsMove := make(map[*d2graph.Object]struct{})
	seen := make(map[*d2graph.Object]struct{})
	shifted := make(map[*d2graph.Object]struct{})
	shiftedEdges := make(map[*d2graph.Edge]struct{})
	queue := func(o *d2graph.Object) {
		if _, in := seen[o]; in {
			return
		}
		q = append(q, o)
	}

	// if object below is within this distance after shifting, also shift it
	threshold := 100.
	checkBelow := func(curr *d2graph.Object) {
		currBottom := curr.TopLeft.Y + curr.Height
		currRight := curr.TopLeft.X + curr.Width
		if isHorizontal {
			originalRight := currRight
			if _, in := shifted[curr]; in {
				originalRight -= distance
			}
			for _, other := range g.Objects {
				if other == curr || curr.IsDescendantOf(other) {
					continue
				}
				if originalRight < other.TopLeft.X &&
					other.TopLeft.X < originalRight+distance+threshold &&
					curr.TopLeft.Y < other.TopLeft.Y+other.Height &&
					other.TopLeft.Y < currBottom {
					queue(other)
				}
			}
		} else {
			originalBottom := currBottom
			if _, in := shifted[curr]; in {
				originalBottom -= distance
			}
			for _, other := range g.Objects {
				if other == curr || curr.IsDescendantOf(other) {
					continue
				}
				if originalBottom < other.TopLeft.Y &&
					other.TopLeft.Y < originalBottom+distance+threshold &&
					curr.TopLeft.X < other.TopLeft.X+other.Width &&
					other.TopLeft.X < currRight {
					queue(other)
				}
			}
		}
	}

	processQueue := func() {
		for len(q) > 0 {
			curr := q[0]
			q = q[1:]
			if _, was := seen[curr]; was {
				continue
			}
			// skip other objects behind start
			if curr != obj {
				if _, in := needsMove[curr]; !in {
					if isHorizontal {
						if curr.TopLeft.X < start {
							continue
						}
					} else {
						if curr.TopLeft.Y < start {
							continue
						}
					}
				}
			}

			if isHorizontal {
				_, shift := needsMove[curr]
				if !shift {
					if !isMargin {
						shift = start < curr.TopLeft.X
					} else {
						shift = start <= curr.TopLeft.X
					}
				}

				if shift {
					curr.TopLeft.X += distance
					shifted[curr] = struct{}{}
				}
			} else {
				_, shift := needsMove[curr]
				if !shift {
					if !isMargin {
						shift = start < curr.TopLeft.Y
					} else {
						shift = start <= curr.TopLeft.Y
					}
				}
				if shift {
					curr.TopLeft.Y += distance
					shifted[curr] = struct{}{}
				}
			}
			seen[curr] = struct{}{}

			if curr.Parent != g.Root && !curr.IsDescendantOf(obj) {
				queue(curr.Parent)
			}

			for _, child := range curr.ChildrenArray {
				queue(child)
			}

			for _, e := range g.Edges {
				if _, in := shiftedEdges[e]; in {
					continue
				}
				if e.Src == curr && e.Dst == curr {
					if isHorizontal {
						for _, p := range e.Route {
							p.X += distance
						}
					} else {
						for _, p := range e.Route {
							p.Y += distance
						}
					}
					shiftedEdges[e] = struct{}{}
					continue
				} else if e.Src == curr {
					last := e.Route[len(e.Route)-1]
					if isHorizontal {
						if start <= last.X &&
							e.Dst.TopLeft.X+e.Dst.Width < last.X+distance {
							needsMove[e.Dst] = struct{}{}
						}
					} else {
						if start <= last.Y &&
							e.Dst.TopLeft.Y+e.Dst.Height < last.Y+distance {
							needsMove[e.Dst] = struct{}{}
						}
					}
					queue(e.Dst)
					first := e.Route[0]
					startIndex := 0
					_, wasShifted := shifted[curr]
					if isHorizontal {
						if wasShifted && first.X < curr.TopLeft.X && first.X < start {
							first.X += distance
							startIndex++
						}
						for i := startIndex; i < len(e.Route); i++ {
							p := e.Route[i]
							if start <= p.X {
								p.X += distance
							}
						}
					} else {
						if wasShifted && first.Y < curr.TopLeft.Y && first.Y < start {
							first.Y += distance
							startIndex++
						}
						for i := startIndex; i < len(e.Route); i++ {
							p := e.Route[i]
							if start <= p.Y {
								p.Y += distance
							}
						}
					}
					shiftedEdges[e] = struct{}{}
				} else if e.Dst == curr {
					first := e.Route[0]
					if isHorizontal {
						if start <= first.X &&
							e.Src.TopLeft.X+e.Src.Width < first.X+distance {
							needsMove[e.Src] = struct{}{}
						}
					} else {
						if start <= first.Y &&
							e.Src.TopLeft.Y+e.Src.Height < first.Y+distance {
							needsMove[e.Src] = struct{}{}
						}
					}
					queue(e.Src)
					last := e.Route[len(e.Route)-1]
					endIndex := len(e.Route)
					_, wasShifted := shifted[curr]
					if isHorizontal {
						if wasShifted && last.X < curr.TopLeft.X && last.X < start {
							last.X += distance
							endIndex--
						}
						for i := 0; i < endIndex; i++ {
							p := e.Route[i]
							if start <= p.X {
								p.X += distance
							}
						}
					} else {
						if wasShifted && last.Y < curr.TopLeft.Y && last.Y < start {
							last.Y += distance
							endIndex--
						}
						for i := 0; i < endIndex; i++ {
							p := e.Route[i]
							if start <= p.Y {
								p.Y += distance
							}
						}
					}
					shiftedEdges[e] = struct{}{}
				}
			}

			// check for nodes below that need to move from the shift
			checkBelow(curr)
		}
	}

	processQueue()

	grown := make(map[*d2graph.Object]struct{})
	for o := range seen {
		if o.Parent == g.Root {
			continue
		}
		if _, in := shifted[o.Parent]; in {
			continue
		}
		if _, in := grown[o.Parent]; in {
			continue
		}

		for parent := o.Parent; parent != g.Root; parent = parent.Parent {
			if _, in := shifted[parent]; in {
				break
			}
			if _, in := grown[parent]; in {
				break
			}

			if isHorizontal {
				if parent.TopLeft.X < start {
					parent.Width += distance
					grown[parent] = struct{}{}

					checkBelow(parent)
					processQueue()
				}
			} else {
				if parent.TopLeft.Y < start {
					parent.Height += distance
					grown[parent] = struct{}{}

					checkBelow(parent)
					processQueue()
				}
			}
		}
	}

	increasedMargins := make(map[*d2graph.Object]struct{})
	movedObjects := make([]*d2graph.Object, 0, len(shifted))
	for obj := range shifted {
		movedObjects = append(movedObjects, obj)
	}
	for obj := range grown {
		movedObjects = append(movedObjects, obj)
	}
	for _, moved := range movedObjects {
		counts := true
		// check if any other shifted is directly above
		for _, other := range movedObjects {
			if other == moved {
				continue
			}
			if isHorizontal {
				if other.TopLeft.Y+other.Height < moved.TopLeft.Y ||
					moved.TopLeft.Y+moved.Height < other.TopLeft.Y {
					// doesn't line up vertically
					continue
				}

				// above and within threshold
				if other.TopLeft.X < moved.TopLeft.X &&
					moved.TopLeft.X < other.TopLeft.X+other.Width+threshold {
					counts = false
					break
				}
			} else {
				if other.TopLeft.X+other.Width < moved.TopLeft.X ||
					moved.TopLeft.X+moved.Width < other.TopLeft.X {
					// doesn't line up horizontally
					continue
				}

				// above and within threshold
				if other.TopLeft.Y < moved.TopLeft.Y &&
					moved.TopLeft.Y < other.TopLeft.Y+other.Height+threshold {
					counts = false
					break
				}
			}
		}
		if counts {
			increasedMargins[moved] = struct{}{}
		}
	}

	return increasedMargins
}

func adjustRankSpacing(g *d2graph.Graph, rankSep float64, isHorizontal bool) {
	ranks, objectRanks, startingParentRanks, endingParentRanks := getRanks(g, isHorizontal)

	// shifting bottom rank down first, then moving up to next rank
	for rank := len(ranks) - 1; rank >= 0; rank-- {
		var startingParents []*d2graph.Object
		var endingParents []*d2graph.Object
		for _, obj := range ranks[rank] {
			if obj.Parent == g.Root {
				continue
			}
			if r, has := endingParentRanks[obj.Parent]; has && r == rank {
				endingParents = append(endingParents, obj.Parent)
			}
			if r, has := startingParentRanks[obj.Parent]; has && r == rank {
				startingParents = append(startingParents, obj.Parent)
			}
		}

		startingAncestorPositions := make(map[*d2graph.Object]float64)
		for len(startingParents) > 0 {
			var ancestors []*d2graph.Object
			for _, parent := range startingParents {
				_, padding := parent.Spacing()
				if _, has := startingAncestorPositions[parent]; !has {
					startingAncestorPositions[parent] = math.Inf(1)
				}
				var startPosition float64
				if isHorizontal {
					paddingIncrease := math.Max(0, padding.Left-rankSep/2)
					startPosition = parent.TopLeft.X - paddingIncrease
				} else {
					paddingIncrease := math.Max(0, padding.Top-rankSep/2)
					startPosition = parent.TopLeft.Y - paddingIncrease
				}
				startingAncestorPositions[parent] = math.Min(startingAncestorPositions[parent], startPosition)
				for _, child := range parent.ChildrenArray {
					if r, has := objectRanks[child]; has {
						if r != rank {
							continue
						}
					} else {
						if startingParentRanks[child] != rank {
							continue
						}
					}
					margin, _ := child.Spacing()
					if isHorizontal {
						startPosition = child.TopLeft.X - margin.Left - padding.Left
					} else {
						startPosition = child.TopLeft.Y - margin.Top - padding.Top
					}
					startingAncestorPositions[parent] = math.Min(startingAncestorPositions[parent], startPosition)
				}
				if parent.Parent != g.Root {
					ancestors = append(ancestors, parent.Parent)
				}
			}
			startingParents = ancestors
		}

		endingAncestorPositions := make(map[*d2graph.Object]float64)
		for len(endingParents) > 0 {
			var ancestors []*d2graph.Object
			for _, parent := range endingParents {
				_, padding := parent.Spacing()
				if _, has := endingAncestorPositions[parent]; !has {
					endingAncestorPositions[parent] = math.Inf(-1)
				}
				var endPosition float64
				if isHorizontal {
					endPosition = parent.TopLeft.X + parent.Width + padding.Right - rankSep/2.
				} else {
					endPosition = parent.TopLeft.Y + parent.Height + padding.Bottom - rankSep/2.
				}

				endingAncestorPositions[parent] = math.Max(endingAncestorPositions[parent], endPosition)
				for _, child := range parent.ChildrenArray {
					if r, has := objectRanks[child]; has {
						if r != rank {
							continue
						}
					} else {
						if endingParentRanks[child] != rank {
							continue
						}
					}
					margin, _ := child.Spacing()

					if isHorizontal {
						endPosition = child.TopLeft.X + child.Width + margin.Right + padding.Right
					} else {
						endPosition = child.TopLeft.Y + child.Height + margin.Bottom + padding.Bottom
					}
					endingAncestorPositions[parent] = math.Max(endingAncestorPositions[parent], endPosition)
				}
				if parent.Parent != g.Root {
					ancestors = append(ancestors, parent.Parent)
				}
			}
			endingParents = ancestors
		}

		startingAdjustmentOrder := make([]*d2graph.Object, 0, len(startingAncestorPositions))
		for ancestor := range startingAncestorPositions {
			startingAdjustmentOrder = append(startingAdjustmentOrder, ancestor)
		}
		// adjust starting ancestors top-down
		sort.Slice(startingAdjustmentOrder, func(i, j int) bool {
			iPos := startingAncestorPositions[startingAdjustmentOrder[i]]
			jPos := startingAncestorPositions[startingAdjustmentOrder[j]]
			return iPos < jPos
		})

		endingAdjustmentOrder := make([]*d2graph.Object, 0, len(endingAncestorPositions))
		for ancestor := range endingAncestorPositions {
			endingAdjustmentOrder = append(endingAdjustmentOrder, ancestor)
		}

		// adjust ending ancestors bottom-up
		sort.Slice(endingAdjustmentOrder, func(i, j int) bool {
			iPos := endingAncestorPositions[endingAdjustmentOrder[i]]
			jPos := endingAncestorPositions[endingAdjustmentOrder[j]]
			return jPos < iPos
		})

		for _, ancestor := range endingAdjustmentOrder {
			var position float64
			if isHorizontal {
				position = ancestor.TopLeft.X + ancestor.Width
			} else {
				position = ancestor.TopLeft.Y + ancestor.Height
			}
			endDelta := endingAncestorPositions[ancestor] - position
			if endDelta > 0 {
				for _, obj := range g.Objects {
					if !obj.IsContainer() {
						continue
					}
					start := startingParentRanks[obj]
					end := endingParentRanks[obj]
					if start <= rank && rank <= end {
						if isHorizontal && position <= obj.TopLeft.X+obj.Width {
							obj.Width += endDelta
						} else if !isHorizontal &&
							position <= obj.TopLeft.Y+obj.Height {
							obj.Height += endDelta
						}
					}
				}
				shiftDown(g, position, endDelta, isHorizontal)
			}
		}

		for _, ancestor := range startingAdjustmentOrder {
			var position float64
			if isHorizontal {
				position = ancestor.TopLeft.X
			} else {
				position = ancestor.TopLeft.Y
			}
			startDelta := position - startingAncestorPositions[ancestor]
			if startDelta > 0 {
				for _, obj := range g.Objects {
					if !obj.IsContainer() {
						continue
					}
					start := startingParentRanks[obj]
					end := endingParentRanks[obj]
					if start <= rank && rank <= end {
						if isHorizontal && obj.TopLeft.X <= position {
							obj.Width += startDelta
						} else if !isHorizontal && obj.TopLeft.Y <= position {
							obj.Height += startDelta
						}
					}
				}
				shiftUp(g, position, startDelta, isHorizontal)
			}
		}
	}
}

func adjustCrossRankSpacing(g *d2graph.Graph, rankSep float64, isHorizontal bool) {
	var prevMarginTop, prevMarginBottom, prevMarginLeft, prevMarginRight map[*d2graph.Object]float64
	if isHorizontal {
		prevMarginLeft = make(map[*d2graph.Object]float64)
		prevMarginRight = make(map[*d2graph.Object]float64)
	} else {
		prevMarginTop = make(map[*d2graph.Object]float64)
		prevMarginBottom = make(map[*d2graph.Object]float64)
	}
	for _, obj := range g.Objects {
		if obj.IsGridDiagram() {
			continue
		}
		margin, padding := obj.Spacing()
		if !isHorizontal {
			if prevShift, has := prevMarginBottom[obj]; has {
				margin.Bottom -= prevShift
			}
			if margin.Bottom > 0 {
				increased := shiftReachableDown(g, obj, obj.TopLeft.Y+obj.Height, margin.Bottom, isHorizontal, true)
				for o := range increased {
					prevMarginBottom[o] = math.Max(prevMarginBottom[o], margin.Bottom)
				}
			}
			if padding.Bottom > 0 {
				shiftReachableDown(g, obj, obj.TopLeft.Y+obj.Height, padding.Bottom, isHorizontal, false)
				obj.Height += padding.Bottom
			}
			if prevShift, has := prevMarginTop[obj]; has {
				margin.Top -= prevShift
			}
			if margin.Top > 0 {
				increased := shiftReachableDown(g, obj, obj.TopLeft.Y, margin.Top, isHorizontal, true)
				for o := range increased {
					prevMarginTop[o] = math.Max(prevMarginTop[o], margin.Top)
				}
			}
			if padding.Top > 0 {
				shiftReachableDown(g, obj, obj.TopLeft.Y, padding.Top, isHorizontal, false)
				obj.Height += padding.Top
			}
		} else {
			if prevShift, has := prevMarginRight[obj]; has {
				margin.Right -= prevShift
			}
			if margin.Right > 0 {
				increased := shiftReachableDown(g, obj, obj.TopLeft.X+obj.Width, margin.Right, isHorizontal, true)
				for o := range increased {
					prevMarginRight[o] = math.Max(prevMarginRight[o], margin.Right)
				}
			}
			if padding.Right > 0 {
				shiftReachableDown(g, obj, obj.TopLeft.X+obj.Width, padding.Right, isHorizontal, false)
				obj.Width += padding.Right
			}
			if prevShift, has := prevMarginLeft[obj]; has {
				margin.Left -= prevShift
			}
			if margin.Left > 0 {
				increased := shiftReachableDown(g, obj, obj.TopLeft.X, margin.Left, isHorizontal, true)
				for o := range increased {
					prevMarginLeft[o] = math.Max(prevMarginLeft[o], margin.Left)
				}
			}
			if padding.Left > 0 {
				shiftReachableDown(g, obj, obj.TopLeft.X, padding.Left, isHorizontal, false)
				obj.Width += padding.Left
			}
		}
	}
}

func fitContainerPadding(g *d2graph.Graph, rankSep float64, isHorizontal bool) {
	for _, obj := range g.Root.ChildrenArray {
		fitPadding(obj)
	}
}

func fitPadding(obj *d2graph.Object) {
	dslShape := strings.ToLower(obj.Shape.Value)
	shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[dslShape]
	// Note: there's no shape-specific padding/placement in dagre yet
	if !obj.IsContainer() || shapeType != shape.SQUARE_TYPE {
		return
	}
	for _, child := range obj.ChildrenArray {
		fitPadding(child)
	}

	// we will compute a perfectly fit innerBox merging our padding with children's margin,
	// but we need to add padding and margin together if an outside child label will overlap with our inside label
	_, padding := obj.Spacing()
	padding.Top = math.Max(padding.Top, DEFAULT_PADDING)
	padding.Bottom = math.Max(padding.Bottom, DEFAULT_PADDING)
	padding.Left = math.Max(padding.Left, DEFAULT_PADDING)
	padding.Right = math.Max(padding.Right, DEFAULT_PADDING)

	// where we are (current*) vs where we want to fit each side to (inner*)
	currentTop := obj.TopLeft.Y
	currentBottom := obj.TopLeft.Y + obj.Height
	currentLeft := obj.TopLeft.X
	currentRight := obj.TopLeft.X + obj.Width

	innerTop := math.Inf(1)
	innerBottom := math.Inf(-1)
	innerLeft := math.Inf(1)
	innerRight := math.Inf(-1)

	// we create boxes for our inside label and icon, and will check against overlaps with any internal boxes
	var labelPosition, iconPosition label.Position
	var labelBox, iconBox *geo.Box
	if obj.HasLabel() && obj.LabelPosition != nil {
		labelPosition = label.FromString(*obj.LabelPosition)
		switch labelPosition {
		case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight,
			label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight,
			label.InsideMiddleLeft, label.InsideMiddleRight:
			labelTL := obj.GetLabelTopLeft()
			if labelTL != nil {
				labelBox = geo.NewBox(labelTL, float64(obj.LabelDimensions.Width)+2*label.PADDING, float64(obj.LabelDimensions.Height))
			}
		}
	}
	if obj.HasIcon() && obj.IconPosition != nil {
		iconPosition = label.FromString(*obj.IconPosition)
		switch iconPosition {
		case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight,
			label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight,
			label.InsideMiddleLeft, label.InsideMiddleRight:
			iconTL := obj.GetIconTopLeft()
			if iconTL != nil {
				iconBox = geo.NewBox(iconTL, d2target.MAX_ICON_SIZE, d2target.MAX_ICON_SIZE)
			}
		}
	}

	// update the inner positions for children's margin and collect the outside boxes that we cannot overlap with
	var innerBoxes []geo.Box
	for _, child := range obj.ChildrenArray {
		margin, _ := child.Spacing()
		dx, dy := child.GetModifierElementAdjustments()

		if labelBox != nil || iconBox != nil {
			var childLabelBox *geo.Box
			var childLabelPosition, childIconPosition label.Position
			if child.HasLabel() && child.LabelPosition != nil {
				childLabelPosition = label.FromString(*child.LabelPosition)
				if childLabelPosition.IsOutside() {
					childLabelTL := child.GetLabelTopLeft()

					childLabelBox = geo.NewBox(
						childLabelTL,
						float64(child.LabelDimensions.Width),
						float64(child.LabelDimensions.Height),
					)
					innerBoxes = append(innerBoxes, *childLabelBox)
				}
			}
			if child.HasIcon() && child.IconPosition != nil {
				childIconPosition = label.FromString(*child.IconPosition)
				if childIconPosition.IsOutside() {
					childIconTL := child.GetIconTopLeft()

					childIconBox := geo.NewBox(childIconTL, d2target.MAX_ICON_SIZE, d2target.MAX_ICON_SIZE)
					innerBoxes = append(innerBoxes, *childIconBox)
				}
			}
		}

		innerTop = math.Min(innerTop, child.TopLeft.Y-dy-math.Max(margin.Top, padding.Top))
		innerBottom = math.Max(innerBottom, child.TopLeft.Y+child.Height+math.Max(margin.Bottom, padding.Bottom))
		innerLeft = math.Min(innerLeft, child.TopLeft.X-math.Max(margin.Left, padding.Left))
		innerRight = math.Max(innerRight, child.TopLeft.X+child.Width+dx+math.Max(margin.Right, padding.Right))
	}

	// collect edge label boxes and update inner box for internal edges
	for _, edge := range obj.Graph.Edges {
		if !edge.Src.IsDescendantOf(obj) || !edge.Dst.IsDescendantOf(obj) {
			continue
		}
		// check internal edge + their labels
		if edge.Label.Value != "" {
			labelPosition := label.InsideMiddleCenter
			if edge.LabelPosition != nil {
				labelPosition = label.FromString(*edge.LabelPosition)
			}
			labelWidth := float64(edge.LabelDimensions.Width)
			labelHeight := float64(edge.LabelDimensions.Height)
			point, _ := labelPosition.GetPointOnRoute(edge.Route, 2, 0, labelWidth, labelHeight)

			if labelBox != nil || iconBox != nil {
				innerBoxes = append(innerBoxes, geo.Box{TopLeft: point, Width: labelWidth, Height: labelHeight})
			}

			innerTop = math.Min(innerTop, point.Y-padding.Top)
			innerBottom = math.Max(innerBottom, point.Y+labelHeight+padding.Bottom)
			innerLeft = math.Min(innerLeft, point.X-padding.Left)
			innerRight = math.Max(innerRight, point.X+labelWidth+padding.Right)
		}
		for _, point := range edge.Route {
			innerTop = math.Min(innerTop, point.Y-padding.Top)
			innerBottom = math.Max(innerBottom, point.Y+padding.Bottom)
			innerLeft = math.Min(innerLeft, point.X-padding.Left)
			innerRight = math.Max(innerRight, point.X+padding.Right)
		}
	}

	// how much do we need to shrink each side
	topDelta := innerTop - currentTop
	bottomDelta := currentBottom - innerBottom
	leftDelta := innerLeft - currentLeft
	rightDelta := currentRight - innerRight

	if topDelta > 0 || bottomDelta > 0 || leftDelta > 0 || rightDelta > 0 {
		var leftOverlap, rightOverlap, topOverlap, bottomOverlap float64
		var labelSide, iconSide geo.Orientation
		if labelBox != nil {
			switch labelPosition {
			case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight:
				labelSide = geo.Top
			case label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight:
				labelSide = geo.Bottom
			case label.InsideMiddleLeft:
				labelSide = geo.Left
			case label.InsideMiddleRight:
				labelSide = geo.Right
			default:
				labelSide = geo.NONE
			}
			// move labelBox to its position with the merged delta and check for overlaps
			switch labelSide {
			case geo.Top:
				if topDelta > 0 {
					labelBox.TopLeft.Y += topDelta
				}
			case geo.Bottom:
				if bottomDelta > 0 {
					labelBox.TopLeft.Y -= bottomDelta
				}
			case geo.Left:
				if leftDelta > 0 {
					labelBox.TopLeft.X += leftDelta
				}
			case geo.Right:
				if rightDelta > 0 {
					labelBox.TopLeft.X -= rightDelta
				}
			}
			switch labelSide {
			case geo.Top:
				if topDelta > 0 {
					for _, box := range innerBoxes {
						if labelBox.Overlaps(box) {
							dy := labelBox.TopLeft.Y + labelBox.Height - box.TopLeft.Y
							topOverlap = go2.Max(topOverlap, dy)
						}
					}
				}
			case geo.Bottom:
				if bottomDelta > 0 {
					for _, box := range innerBoxes {
						if labelBox.Overlaps(box) {
							dy := box.TopLeft.Y + box.Height - labelBox.TopLeft.Y
							bottomOverlap = go2.Max(bottomOverlap, dy)
						}
					}
				}
			case geo.Left:
				if leftDelta > 0 {
					for _, box := range innerBoxes {
						if labelBox.Overlaps(box) {
							dx := labelBox.TopLeft.X + labelBox.Width - box.TopLeft.X
							leftOverlap = go2.Max(leftOverlap, dx)
						}
					}
				}
			case geo.Right:
				if rightDelta > 0 {
					for _, box := range innerBoxes {
						if labelBox.Overlaps(box) {
							dx := box.TopLeft.X + box.Width - labelBox.TopLeft.X
							rightOverlap = go2.Max(rightOverlap, dx)
						}
					}
				}
			}
		}
		if iconBox != nil {
			switch iconPosition {
			case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight:
				iconSide = geo.Top
			case label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight:
				iconSide = geo.Bottom
			case label.InsideMiddleLeft:
				iconSide = geo.Left
			case label.InsideMiddleRight:
				iconSide = geo.Right
			default:
				iconSide = geo.NONE
			}
			// move iconBox to its position with the merged delta and check for overlaps
			switch iconSide {
			case geo.Top:
				if topDelta > 0 {
					iconBox.TopLeft.Y += topDelta
				}
			case geo.Bottom:
				if bottomDelta > 0 {
					iconBox.TopLeft.Y -= bottomDelta
				}
			case geo.Left:
				if leftDelta > 0 {
					iconBox.TopLeft.X += leftDelta
				}
			case geo.Right:
				if rightDelta > 0 {
					iconBox.TopLeft.X -= rightDelta
				}
			}
			switch iconSide {
			case geo.Top:
				if topDelta > 0 {
					for _, box := range innerBoxes {
						if iconBox.Overlaps(box) {
							dy := iconBox.TopLeft.Y + iconBox.Height - box.TopLeft.Y
							topOverlap = go2.Max(topOverlap, dy)
						}
					}
				}
			case geo.Bottom:
				if bottomDelta > 0 {
					for _, box := range innerBoxes {
						if iconBox.Overlaps(box) {
							dy := box.TopLeft.Y + box.Height - iconBox.TopLeft.Y
							bottomOverlap = go2.Max(bottomOverlap, dy)
						}
					}
				}
			case geo.Left:
				if leftDelta > 0 {
					for _, box := range innerBoxes {
						if iconBox.Overlaps(box) {
							dx := iconBox.TopLeft.X + iconBox.Width - box.TopLeft.X
							leftOverlap = go2.Max(leftOverlap, dx)
						}
					}
				}
			case geo.Right:
				if rightDelta > 0 {
					for _, box := range innerBoxes {
						if iconBox.Overlaps(box) {
							dx := box.TopLeft.X + box.Width - iconBox.TopLeft.X
							rightOverlap = go2.Max(rightOverlap, dx)
						}
					}
				}
			}
		}

		if leftOverlap > 0 {
			leftDelta -= leftOverlap + MIN_SPACING
		}
		if rightOverlap > 0 {
			rightDelta -= rightOverlap + MIN_SPACING
		}
		if topOverlap > 0 {
			topDelta -= topOverlap + MIN_SPACING
		}
		if bottomOverlap > 0 {
			bottomDelta -= bottomOverlap + MIN_SPACING
		}
	}

	if 0 < topDelta {
		topDelta = adjustDeltaForEdges(obj, currentTop, topDelta, false)
		if 0 < topDelta {
			adjustEdges(obj, currentTop, topDelta, false)
			obj.TopLeft.Y += topDelta
			obj.Height -= topDelta
		}
	}
	if 0 < bottomDelta {
		bottomDelta = adjustDeltaForEdges(obj, currentBottom, -bottomDelta, false)
		if 0 < bottomDelta {
			adjustEdges(obj, currentBottom, -bottomDelta, false)
			obj.Height -= bottomDelta
		}
	}
	if 0 < leftDelta {
		leftDelta = adjustDeltaForEdges(obj, currentLeft, leftDelta, true)
		if 0 < leftDelta {
			adjustEdges(obj, currentLeft, leftDelta, true)
			obj.TopLeft.X += leftDelta
			obj.Width -= leftDelta
		}
	}
	if 0 < rightDelta {
		rightDelta = adjustDeltaForEdges(obj, currentRight, -rightDelta, true)
		if 0 < rightDelta {
			adjustEdges(obj, currentRight, -rightDelta, true)
			obj.Width -= rightDelta
		}
	}
}

func adjustDeltaForEdges(obj *d2graph.Object, objPosition, delta float64, isHorizontal bool) (newMagnitude float64) {
	isOnCollapsingSide := func(p *geo.Point) bool {
		var position float64
		if isHorizontal {
			position = p.X
		} else {
			position = p.Y
		}
		if geo.PrecisionCompare(position, objPosition, 1) == 0 {
			return false
		}
		// check for edges on side corners
		var isOnSide bool
		if isHorizontal {
			if geo.PrecisionCompare(p.Y, obj.TopLeft.Y, 1) == 0 ||
				geo.PrecisionCompare(p.Y, obj.TopLeft.Y+obj.Height, 1) == 0 {
				isOnSide = true
			}
		} else {
			if geo.PrecisionCompare(p.X, obj.TopLeft.X, 1) == 0 ||
				geo.PrecisionCompare(p.X, obj.TopLeft.X+obj.Width, 1) == 0 {
				isOnSide = true
			}
		}
		if !isOnSide {
			return false
		}
		buffer := MIN_SPACING
		var isInRange bool
		if delta > 0 {
			if objPosition <= position && position <= objPosition+delta+buffer {
				isInRange = true
			}
		} else {
			if objPosition+delta-buffer <= position && position <= objPosition {
				isInRange = true
			}
		}
		return isInRange
	}
	hasEdgeOnCollapsingSide := false
	outermost := objPosition + delta
	for _, edge := range obj.Graph.Edges {
		if edge.Src == obj {
			p := edge.Route[0]
			if isOnCollapsingSide(p) {
				hasEdgeOnCollapsingSide = true
				var position float64
				if isHorizontal {
					position = p.X
				} else {
					position = p.Y
				}
				if delta < 0 {
					outermost = math.Max(outermost, position)
				} else {
					outermost = math.Min(outermost, position)
				}
			}
		}
		if edge.Dst == obj {
			p := edge.Route[len(edge.Route)-1]
			if isOnCollapsingSide(p) {
				hasEdgeOnCollapsingSide = true
				var position float64
				if isHorizontal {
					position = p.X
				} else {
					position = p.Y
				}
				if delta < 0 {
					outermost = math.Max(outermost, position)
				} else {
					outermost = math.Min(outermost, position)
				}
			}
		}
	}
	newMagnitude = math.Abs(delta)
	if hasEdgeOnCollapsingSide {
		// only reduce to outermost + DEFAULT_PADDING
		if delta < 0 {
			newMagnitude = math.Max(0, objPosition-(outermost+DEFAULT_PADDING))
		} else {
			newMagnitude = math.Max(0, (outermost-DEFAULT_PADDING)-objPosition)
		}
	}
	return newMagnitude
}

func adjustEdges(obj *d2graph.Object, objPosition, delta float64, isHorizontal bool) {
	adjust := func(p *geo.Point) {
		var position float64
		if isHorizontal {
			position = p.X
		} else {
			position = p.Y
		}
		if geo.PrecisionCompare(position, objPosition, 1) == 0 {
			if isHorizontal {
				p.X += delta
			} else {
				p.Y += delta
			}
		} else {
			// check side corners
			var isOnSide bool
			if isHorizontal {
				if geo.PrecisionCompare(p.Y, obj.TopLeft.Y, 1) == 0 ||
					geo.PrecisionCompare(p.Y, obj.TopLeft.Y+obj.Height, 1) == 0 {
					isOnSide = true
				}
			} else {
				if geo.PrecisionCompare(p.X, obj.TopLeft.X, 1) == 0 ||
					geo.PrecisionCompare(p.X, obj.TopLeft.X+obj.Width, 1) == 0 {
					isOnSide = true
				}
			}
			if isOnSide {
				var isInRange bool
				if delta > 0 {
					if objPosition < position && position < objPosition+delta {
						isInRange = true
					}
				} else {
					if objPosition+delta < position && position < objPosition {
						isInRange = true
					}
				}
				if isInRange {
					if isHorizontal {
						p.X = objPosition + delta
					} else {
						p.Y = objPosition + delta
					}
				}
			}
		}
	}

	for _, edge := range obj.Graph.Edges {
		if edge.Src == obj {
			adjust(edge.Route[0])
		}
		if edge.Dst == obj {
			adjust(edge.Route[len(edge.Route)-1])
		}
	}
}
