package d2dagrelayout

import (
	"context"
	"fmt"
	"math"
	"sort"
	"strings"

	"github.com/d2lang/util-go/xdefer"

	"github.com/d2lang/util-go/go2"

	"github.com/d2lang/d2/d2graph"
	"github.com/d2lang/d2/d2target"
	"github.com/d2lang/d2/lib/geo"
	"github.com/d2lang/d2/lib/label"
	"github.com/d2lang/d2/lib/shape"
	"github.com/d2lang/dagro"
)

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 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")

	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)
	}

	mapper := NewObjectMapper()
	for _, obj := range g.Objects {
		mapper.Register(obj)
	}
	resolver := newEndpointResolver(g)
	type endpointPair struct {
		src, dst *d2graph.Object
	}
	resolvedEndpoints := make(map[*d2graph.Edge]endpointPair, len(g.Edges))
	directedPairCounts := make(map[endpointPair]int, len(g.Edges))
	for _, edge := range g.Edges {
		src, dst := resolver.resolve(edge)
		pair := endpointPair{src: src, dst: dst}
		resolvedEndpoints[edge] = pair
		directedPairCounts[pair]++
	}

	dagreGraph := dagro.NewGraph(dagro.GraphOptions{Compound: true, Multigraph: true}).SetGraph(dagro.Attrs{
		"ranksep": float64(rootAttrs.ranksep),
		"edgesep": float64(rootAttrs.EdgeSep),
		"nodesep": float64(rootAttrs.NodeSep),
		"rankdir": rootAttrs.rankdir,
	})
	for _, obj := range g.Objects {
		id := mapper.ToID(obj)
		dagreGraph.SetNode(id, dagro.Attrs{
			"id":     id,
			"width":  float64(int(obj.Width)),
			"height": float64(int(obj.Height)),
		})
		if obj.Parent != g.Root {
			if err := dagreGraph.SetParent(id, mapper.ToID(obj.Parent)); err != nil {
				return err
			}
		}
	}

	for _, edge := range g.Edges {
		pair := resolvedEndpoints[edge]
		src, dst := pair.src, pair.dst

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

		numEdges := directedPairCounts[pair]
		if src != dst {
			numEdges += directedPairCounts[endpointPair{src: dst, dst: src}]
		}

		// 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
			}
		}

		dagreGraph.SetEdge(
			mapper.ToID(src),
			mapper.ToID(dst),
			dagro.Attrs{
				"width":    float64(width),
				"height":   float64(height),
				"labelpos": "c",
			},
			edge.AbsID(),
		)
	}

	if err := dagro.Layout(dagreGraph); err != nil {
		return err
	}

	for _, id := range dagreGraph.Nodes() {
		dn, ok := dagreGraph.Node(id).(dagro.Attrs)
		if !ok {
			return fmt.Errorf("dagro returned invalid node label for %q", id)
		}
		dnID, ok := dn["id"].(string)
		if !ok {
			return fmt.Errorf("dagro returned node %q without an id", id)
		}
		obj := mapper.ToObj(dnID)
		if obj == nil {
			return fmt.Errorf("dagro returned unknown node id %q", dnID)
		}
		x, xOK := dn["x"].(float64)
		y, yOK := dn["y"].(float64)
		width, widthOK := dn["width"].(float64)
		height, heightOK := dn["height"].(float64)
		if !xOK || !yOK || !widthOK || !heightOK {
			return fmt.Errorf("dagro returned invalid geometry for node %q", id)
		}

		// dagre gives center of node
		obj.TopLeft = geo.NewPoint(math.Round(x-width/2), math.Round(y-height/2))
		obj.Width = math.Ceil(width)
		obj.Height = math.Ceil(height)
	}

	dagreEdges := dagreGraph.Edges()
	if len(dagreEdges) != len(g.Edges) {
		return fmt.Errorf("dagro returned %d edges, expected %d", len(dagreEdges), len(g.Edges))
	}
	for i, edge := range g.Edges {
		de, ok := dagreGraph.Edge(dagreEdges[i]).(dagro.Attrs)
		if !ok {
			return fmt.Errorf("dagro returned invalid edge label for %q", dagreEdges[i].Name)
		}
		dagrePoints, ok := de["points"].([]dagro.Point)
		if !ok {
			return fmt.Errorf("dagro returned edge %q without points", dagreEdges[i].Name)
		}

		points := make([]*geo.Point, len(dagrePoints))
		for i, point := range dagrePoints {
			p := geo.NewPoint(point.X, point.Y)
			if edge.SrcArrow && !edge.DstArrow {
				points[len(dagrePoints)-i-1] = p
			} else {
				points[i] = p
			}
		}
		// Dagre 3.1 self-loop routes contain repeated control points. Remove only
		// consecutive duplicates so downstream tangent calculations never see a
		// zero-length segment.
		points = deduplicateRoutePoints(points)
		if edge.Src == edge.Dst {
			// The self-loop controls describe the loop outside the node, but do not
			// include usable node-facing endpoints. Center sentinels give
			// TraceToShape a direction from which to chop the route to each border.
			center := edge.Src.Center()
			points = append([]*geo.Point{center.Copy()}, points...)
			points = append(points, center.Copy())
		}
		if len(points) < 2 {
			return fmt.Errorf("dagro returned edge %q without a usable route", dagreEdges[i].Name)
		}

		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 deduplicateRoutePoints(points []*geo.Point) []*geo.Point {
	if len(points) < 2 {
		return points
	}
	deduplicated := points[:1]
	for _, point := range points[1:] {
		if !point.Equals(deduplicated[len(deduplicated)-1]) {
			deduplicated = append(deduplicated, point)
		}
	}
	return deduplicated
}

type containerEndpoints struct {
	head, tail *d2graph.Object
}

type endpointResolver struct {
	g          *d2graph.Graph
	containers map[*d2graph.Object]containerEndpoints
}

func newEndpointResolver(g *d2graph.Graph) *endpointResolver {
	return &endpointResolver{
		g:          g,
		containers: make(map[*d2graph.Object]containerEndpoints),
	}
}

func (r *endpointResolver) head(container *d2graph.Object) *d2graph.Object {
	return r.endpoints(container).head
}

func (r *endpointResolver) tail(container *d2graph.Object) *d2graph.Object {
	return r.endpoints(container).tail
}

func (r *endpointResolver) endpoints(container *d2graph.Object) containerEndpoints {
	endpoints, ok := r.containers[container]
	if !ok {
		endpoints.head, endpoints.tail = getLongestEdgeChainEndpoints(r.g, container)
		r.containers[container] = endpoints
	}
	return endpoints
}

func (r *endpointResolver) resolve(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 = r.tail(src)
	}
	dst := edge.Dst
	for len(dst.Children) > 0 && dst.Class == nil && dst.SQLTable == nil {
		dst = r.head(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
}

type containerTopology struct {
	container   *d2graph.Object
	hasIncoming map[*d2graph.Object]bool
	outgoing    map[*d2graph.Object][]*d2graph.Object
}

func newContainerTopology(g *d2graph.Graph, container *d2graph.Object) *containerTopology {
	topology := &containerTopology{
		container:   container,
		hasIncoming: make(map[*d2graph.Object]bool, len(container.ChildrenArray)),
		outgoing:    make(map[*d2graph.Object][]*d2graph.Object, len(container.ChildrenArray)+1),
	}
	for _, edge := range g.Edges {
		src := inContainer(edge.Src, container)
		dst := inContainer(edge.Dst, container)
		if src == nil || dst == nil {
			continue
		}

		// The original head test counts every edge from anywhere in the
		// container to a direct child's subtree, including internal edges.
		if dst != container {
			topology.hasIncoming[dst] = true
		}

		// Preserve edge order and duplicates: the breadth-first walk's rank
		// updates are order-sensitive even though the graph is usually acyclic.
		if src != dst {
			topology.outgoing[src] = append(topology.outgoing[src], dst)
		}
		// A container can enter the old traversal when an edge terminates on
		// it. From there, every edge whose source is inside it is reachable.
		if src != container && dst != container {
			topology.outgoing[container] = append(topology.outgoing[container], dst)
		}
	}
	return topology
}

func (t *containerTopology) longestChainEndpoints() (*d2graph.Object, *d2graph.Object) {
	rank := make(map[*d2graph.Object]int)
	chainLength := make(map[*d2graph.Object]int)

	for _, obj := range t.container.ChildrenArray {
		if t.hasIncoming[obj] {
			continue
		}
		rank[obj] = 1
		chainLength[obj] = 1
		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 _, child := range t.outgoing[curr] {
				if rank[curr]+1 > rank[child] {
					rank[child] = rank[curr] + 1
					chainLength[obj] = go2.Max(chainLength[obj], rank[child])
				}
				queue = append(queue, child)
			}
		}
	}

	maxChainLength := int(math.MinInt32)
	maxRank := int(math.MinInt32)
	for _, obj := range t.container.ChildrenArray {
		maxChainLength = go2.Max(maxChainLength, chainLength[obj])
		maxRank = go2.Max(maxRank, rank[obj])
	}

	var heads, tails []*d2graph.Object
	for _, obj := range t.container.ChildrenArray {
		if rank[obj] == 1 && chainLength[obj] == maxChainLength {
			heads = append(heads, obj)
		}
		if rank[obj] == maxRank {
			tails = append(tails, obj)
		}
	}

	head := t.container.ChildrenArray[0]
	if len(heads) > 0 {
		head = heads[len(heads)/2]
	}
	return head, tails[len(tails)/2]
}

func getLongestEdgeChainEndpoints(g *d2graph.Graph, container *d2graph.Object) (*d2graph.Object, *d2graph.Object) {
	return newContainerTopology(g, container).longestChainEndpoints()
}

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
		}
	}
}

type crossRankIndex struct {
	incident map[*d2graph.Object][]*d2graph.Edge
}

func newCrossRankIndex(g *d2graph.Graph) *crossRankIndex {
	index := &crossRankIndex{
		incident: make(map[*d2graph.Object][]*d2graph.Edge, len(g.Objects)),
	}
	for _, edge := range g.Edges {
		index.incident[edge.Src] = append(index.incident[edge.Src], edge)
		if edge.Dst != edge.Src {
			index.incident[edge.Dst] = append(index.incident[edge.Dst], edge)
		}
	}
	return index
}

func shiftReachableDownIndexed(g *d2graph.Graph, index *crossRankIndex, 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 index.incident[curr] {
				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) {
	index := newCrossRankIndex(g)
	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 := shiftReachableDownIndexed(g, index, 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 {
				shiftReachableDownIndexed(g, index, 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 := shiftReachableDownIndexed(g, index, obj, obj.TopLeft.Y, margin.Top, isHorizontal, true)
				for o := range increased {
					prevMarginTop[o] = math.Max(prevMarginTop[o], margin.Top)
				}
			}
			if padding.Top > 0 {
				shiftReachableDownIndexed(g, index, 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 := shiftReachableDownIndexed(g, index, 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 {
				shiftReachableDownIndexed(g, index, 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 := shiftReachableDownIndexed(g, index, obj, obj.TopLeft.X, margin.Left, isHorizontal, true)
				for o := range increased {
					prevMarginLeft[o] = math.Max(prevMarginLeft[o], margin.Left)
				}
			}
			if padding.Left > 0 {
				shiftReachableDownIndexed(g, index, 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])
		}
	}
}
