package d2layouts

import (
	"context"
	"fmt"
	"log/slog"
	"math"
	"sort"
	"strings"

	"github.com/d2lang/d2/d2graph"
	"github.com/d2lang/d2/d2layouts/d2grid"
	"github.com/d2lang/d2/d2layouts/d2near"
	"github.com/d2lang/d2/d2layouts/d2sequence"
	"github.com/d2lang/d2/lib/geo"
	"github.com/d2lang/d2/lib/label"
	"github.com/d2lang/d2/lib/log"
	"github.com/d2lang/util-go/go2"
)

type DiagramType string

// a grid diagram at a constant near is
const (
	DefaultGraphType  DiagramType = ""
	ConstantNearGraph DiagramType = "constant-near"
	GridDiagram       DiagramType = "grid-diagram"
	SequenceDiagram   DiagramType = "sequence-diagram"
)

type GraphInfo struct {
	IsConstantNear bool
	DiagramType    DiagramType
}

func (gi GraphInfo) isDefault() bool {
	return !gi.IsConstantNear && gi.DiagramType == DefaultGraphType
}

func SaveChildrenOrder(container *d2graph.Object) (restoreOrder func()) {
	objectOrder := make(map[string]int, len(container.ChildrenArray))
	for i, obj := range container.ChildrenArray {
		objectOrder[obj.AbsID()] = i
	}
	return func() {
		sort.SliceStable(container.ChildrenArray, func(i, j int) bool {
			return objectOrder[container.ChildrenArray[i].AbsID()] < objectOrder[container.ChildrenArray[j].AbsID()]
		})
	}
}

func SaveOrder(g *d2graph.Graph) (restoreOrder func()) {
	objectOrder := make(map[string]int, len(g.Objects))
	for i, obj := range g.Objects {
		objectOrder[obj.AbsID()] = i
	}
	edgeOrder := make(map[string]int, len(g.Edges))
	for i, edge := range g.Edges {
		edgeOrder[edge.AbsID()] = i
	}
	restoreRootOrder := SaveChildrenOrder(g.Root)
	return func() {
		sort.SliceStable(g.Objects, func(i, j int) bool {
			return objectOrder[g.Objects[i].AbsID()] < objectOrder[g.Objects[j].AbsID()]
		})
		sort.SliceStable(g.Edges, func(i, j int) bool {
			iIndex, iHas := edgeOrder[g.Edges[i].AbsID()]
			jIndex, jHas := edgeOrder[g.Edges[j].AbsID()]
			if iHas && jHas {
				return iIndex < jIndex
			}
			return iHas
		})
		restoreRootOrder()
	}
}

func LayoutNested(ctx context.Context, g *d2graph.Graph, graphInfo GraphInfo, coreLayout d2graph.LayoutGraph, edgeRouter d2graph.RouteEdges) error {
	g.Root.Box = &geo.Box{}

	// Before we can layout these nodes, we need to handle all nested diagrams first.
	extracted := make(map[string]*d2graph.Graph)
	var extractedOrder []string
	var extractedEdges []*d2graph.Edge
	var extractedEdgeIDs []edgeIDs

	var constantNears []*d2graph.Graph
	restoreOrder := SaveOrder(g)
	defer restoreOrder()

	// Iterate top-down from Root so all nested diagrams can process their own contents
	queue := make([]*d2graph.Object, 0, len(g.Root.ChildrenArray))
	queue = append(queue, g.Root.ChildrenArray...)
	var batchedExtractions map[*d2graph.Object]*subgraphExtraction
	var extractionBatch *subgraphExtractionBatch
	if graphInfo.DiagramType != GridDiagram {
		specs := collectNestedExtractions(g)
		if len(specs) > 1 {
			extractionBatch = extractSubgraphs(g, specs)
			batchedExtractions = extractionBatch.extractions
			queue = queue[:0]
			for _, spec := range specs {
				queue = append(queue, spec.container)
			}
		}
	}

	for len(queue) > 0 {
		curr := queue[0]
		queue = queue[1:]

		isGridCellContainer := graphInfo.DiagramType == GridDiagram &&
			curr.IsContainer() && curr.Parent == g.Root
		gi := NestedGraphInfo(curr)
		if extraction, ok := batchedExtractions[curr]; ok {
			gi = extraction.graphInfo
		}

		if isGridCellContainer && gi.isDefault() {
			// if we are in a grid diagram, and our children have descendants
			// we need to run layout on them first, even if they are not special diagram types

			// First we extract the grid cell container as a nested graph with includeSelf=true
			// resulting in externalEdges=[A, C] and nestedGraph.Edges=[B]
			// ┌grid(g.Root)───────────────────┐    ┌grid(g.Root)───────────────────┐
			// │ ┌────┐      ┌curr───────────┐ │    │ ┌────┐                        │
			// │ │    │      │  ┌──┐    ┌──┐ │ │    │ │    │                        │
			// │ │    ├──A──►│  │  ├─B─►│  │ │ │ => │ │    │                        │
			// │ │    ├──────┼C─┼──┼───►│  │ │ │    │ │    │                        │
			// │ │    │      │  └──┘    └──┘ │ │    │ │    │                        │
			// │ └────┘      └───────────────┘ │    │ └────┘                        │
			// └───────────────────────────────┘    └───────────────────────────────┘
			nestedGraph, externalEdges, externalEdgeIDs := ExtractSubgraph(curr, true)

			// Then we layout curr as a nested graph and re-inject it
			id := curr.AbsID()
			err := LayoutNested(ctx, nestedGraph, GraphInfo{}, coreLayout, edgeRouter)
			if err != nil {
				return err
			}
			InjectNested(g.Root, nestedGraph, false)
			g.Edges = append(g.Edges, externalEdges...)
			restoreOrder()

			// layout can replace Objects so we need to update the references we are holding onto (curr + externalEdges)
			idToObj := make(map[string]*d2graph.Object)
			for _, o := range g.Objects {
				idToObj[o.AbsID()] = o
			}
			lookup := func(idStr string) (*d2graph.Object, error) {
				o, exists := idToObj[idStr]
				if !exists {
					return nil, fmt.Errorf("could not find object %#v after layout", idStr)
				}
				return o, nil
			}
			curr, err = lookup(id)
			if err != nil {
				return err
			}
			for i, e := range externalEdges {
				src, err := lookup(externalEdgeIDs[i].srcID)
				if err != nil {
					return err
				}
				e.Src = src
				dst, err := lookup(externalEdgeIDs[i].dstID)
				if err != nil {
					return err
				}
				e.Dst = dst
			}

			// position nested graph (excluding curr) relative to curr
			dx := 0 - curr.TopLeft.X
			dy := 0 - curr.TopLeft.Y
			for _, o := range nestedGraph.Objects {
				if o == curr {
					continue
				}
				o.TopLeft.X += dx
				o.TopLeft.Y += dy
			}
			for _, e := range nestedGraph.Edges {
				e.Move(dx, dy)
			}

			// Then after re-injecting everything, we extract curr with includeSelf=false,
			// and externalEdges=[C], nestedGraph.Edges=[B], and graph.Edges=[A].
			// This will leave A in the graph to be routed by grid layout, and C will have cross-graph edge routing
			// Note: currently grid layout's cell-cell edge routing, and cross-graph edge routing behave the same,
			// but these are simple placeholder routings and they may be different in the future
			// ┌grid(g.Root)───────────────────┐    ┌grid(g.Root)───────────────────┐
			// │ ┌────┐      ┌curr───────────┐ │    │ ┌────┐      ┌curr───────────┐ │
			// │ │    │      │  ┌──┐    ┌──┐ │ │    │ │    │      │               │ │
			// │ │    ├──A──►│  │  ├─B─►│  │ │ │ => │ │    ├──A──►│               │ │
			// │ │    ├──────┼C─┼──┼───►│  │ │ │    │ │    │      │               │ │
			// │ │    │      │  └──┘    └──┘ │ │    │ │    │      │               │ │
			// │ └────┘      └───────────────┘ │    │ └────┘      └───────────────┘ │
			// └───────────────────────────────┘    └───────────────────────────────┘
			nestedGraph, externalEdges, externalEdgeIDs = ExtractSubgraph(curr, false)
			extractedEdges = append(extractedEdges, externalEdges...)
			extractedEdgeIDs = append(extractedEdgeIDs, externalEdgeIDs...)

			extracted[id] = nestedGraph
			extractedOrder = append(extractedOrder, id)
			continue
		}

		if !gi.isDefault() {
			// empty grid or sequence can have 0 objects..
			_, wasBatched := batchedExtractions[curr]
			if !wasBatched && !gi.IsConstantNear && len(curr.Children) == 0 {
				continue
			}

			// There is a nested diagram here, so extract its contents and process in the same way
			var nestedGraph *d2graph.Graph
			var externalEdges []*d2graph.Edge
			var externalEdgeIDs []edgeIDs
			if extraction, ok := batchedExtractions[curr]; ok {
				nestedGraph = extraction.nestedGraph
				externalEdges = extraction.externalEdges
				externalEdgeIDs = extraction.externalEdgeIDs
			} else {
				nestedGraph, externalEdges, externalEdgeIDs = ExtractSubgraph(curr, gi.IsConstantNear)
			}
			extractedEdges = append(extractedEdges, externalEdges...)
			extractedEdgeIDs = append(extractedEdgeIDs, externalEdgeIDs...)

			log.Debug(ctx, "layout nested", slog.Any("level", curr.Level()), slog.Any("child", curr.AbsID()), slog.Any("gi", gi))
			nestedInfo := gi
			nearKey := curr.NearKey
			if gi.IsConstantNear {
				// layout nested as a non-near
				nestedInfo = GraphInfo{}
				curr.NearKey = nil
			}

			err := LayoutNested(ctx, nestedGraph, nestedInfo, coreLayout, edgeRouter)
			if err != nil {
				if extraction, ok := batchedExtractions[curr]; ok {
					extractionBatch.rollbackAfter(extraction.index)
				}
				return err
			}
			// coreLayout can overwrite graph contents with newly created *Object pointers
			// so we need to update `curr` with nestedGraph's value
			if gi.IsConstantNear {
				curr = nestedGraph.Root.ChildrenArray[0]
			}

			if gi.IsConstantNear {
				curr.NearKey = nearKey
			} else {
				FitToGraph(curr, nestedGraph, geo.Spacing{})
				curr.TopLeft = geo.NewPoint(0, 0)
			}

			if gi.IsConstantNear {
				// near layout will inject these nestedGraphs
				constantNears = append(constantNears, nestedGraph)
			} else {
				// We will restore the contents after running layout with child as the placeholder
				// We need to reference using ID because there may be a new object to use after coreLayout
				id := curr.AbsID()
				extracted[id] = nestedGraph
				extractedOrder = append(extractedOrder, id)
			}
		} else if len(curr.ChildrenArray) > 0 {
			queue = append(queue, curr.ChildrenArray...)
		}
	}

	// We can now run layout with accurate sizes of nested layout containers
	// Layout according to the type of diagram
	var err error
	if len(g.Objects) > 0 {
		switch graphInfo.DiagramType {
		case GridDiagram:
			log.Debug(ctx, "layout grid", slog.Any("rootlevel", g.RootLevel), slog.Any("shapes", g.PrintString()))
			if err = d2grid.Layout(ctx, g); err != nil {
				return err
			}

		case SequenceDiagram:
			log.Debug(ctx, "layout sequence", slog.Any("rootlevel", g.RootLevel), slog.Any("shapes", g.PrintString()))
			err = d2sequence.Layout(ctx, g, coreLayout)
			if err != nil {
				return err
			}
		default:
			log.Debug(ctx, "default layout", slog.Any("rootlevel", g.RootLevel), slog.Any("shapes", g.PrintString()))
			err := coreLayout(ctx, g)
			if err != nil {
				return err
			}
		}
	}

	if len(constantNears) > 0 {
		err = d2near.Layout(ctx, g, constantNears)
		if err != nil {
			return err
		}
	}

	idToObj := make(map[string]*d2graph.Object)
	for _, o := range g.Objects {
		idToObj[o.AbsID()] = o
	}

	// With the layout set, inject all the extracted graphs
	for _, id := range extractedOrder {
		nestedGraph := extracted[id]
		// we have to find the object by ID because coreLayout can replace the Objects in graph
		obj, exists := idToObj[id]
		if !exists {
			return fmt.Errorf("could not find object %#v after layout", id)
		}
		InjectNested(obj, nestedGraph, true)
		PositionNested(obj, nestedGraph)
	}

	if len(extractedEdges) > 0 {
		// update map with injected objects
		for _, o := range g.Objects {
			idToObj[o.AbsID()] = o
		}

		// Restore cross-graph edges and route them
		g.Edges = append(g.Edges, extractedEdges...)
		for i, e := range extractedEdges {
			ids := extractedEdgeIDs[i]
			// update object references
			src, exists := idToObj[ids.srcID]
			if !exists {
				return fmt.Errorf("could not find object %#v after layout", ids.srcID)
			}
			e.Src = src
			dst, exists := idToObj[ids.dstID]
			if !exists {
				return fmt.Errorf("could not find object %#v after layout", ids.dstID)
			}
			e.Dst = dst
		}

		err = edgeRouter(ctx, g, extractedEdges)
		if err != nil {
			return err
		}
		// need to update pointers if plugin performs edge routing
		for i, e := range extractedEdges {
			ids := extractedEdgeIDs[i]
			src, exists := idToObj[ids.srcID]
			if !exists {
				return fmt.Errorf("could not find object %#v after routing", ids.srcID)
			}
			e.Src = src
			dst, exists := idToObj[ids.dstID]
			if !exists {
				return fmt.Errorf("could not find object %#v after routing", ids.dstID)
			}
			e.Dst = dst
		}
	}

	if err := validateObjectPositions(g.Objects); err != nil {
		return err
	}

	log.Debug(ctx, "done", slog.Any("rootlevel", g.RootLevel), slog.Any("shapes", g.PrintString()))
	return err
}

func DefaultRouter(ctx context.Context, graph *d2graph.Graph, edges []*d2graph.Edge) error {
	for _, e := range edges {
		// TODO replace simple straight line edge routing
		e.Route = []*geo.Point{e.Src.Center(), e.Dst.Center()}
		e.TraceToShape(e.Route, 0, 1)
		if e.Label.Value != "" {
			e.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
		}
	}
	return nil
}

func NestedGraphInfo(obj *d2graph.Object) (gi GraphInfo) {
	if obj.Graph.RootLevel == 0 && obj.IsConstantNear() {
		gi.IsConstantNear = true
	}
	if obj.IsSequenceDiagram() {
		gi.DiagramType = SequenceDiagram
	} else if obj.IsGridDiagram() {
		gi.DiagramType = GridDiagram
	}
	return gi
}

type edgeIDs struct {
	srcID, dstID string
}

type subgraphExtractionSpec struct {
	container   *d2graph.Object
	includeSelf bool
	graphInfo   GraphInfo
}

type subgraphExtraction struct {
	nestedGraph     *d2graph.Graph
	externalEdges   []*d2graph.Edge
	externalEdgeIDs []edgeIDs
	graphInfo       GraphInfo
	index           int
	restoreInto     *d2graph.Object
}

type subgraphExtractionBatch struct {
	g                  *d2graph.Graph
	specs              []subgraphExtractionSpec
	extractions        map[*d2graph.Object]*subgraphExtraction
	restoreParentOrder []func()
}

// collectNestedExtractions returns the topmost nested diagrams in the same
// breadth-first order used by LayoutNested. Their object sets are disjoint, so
// they can be partitioned out of the parent graph together.
func collectNestedExtractions(g *d2graph.Graph) []subgraphExtractionSpec {
	queue := make([]*d2graph.Object, 0, len(g.Root.ChildrenArray))
	queue = append(queue, g.Root.ChildrenArray...)
	var specs []subgraphExtractionSpec
	for len(queue) > 0 {
		curr := queue[0]
		queue = queue[1:]
		gi := NestedGraphInfo(curr)
		if !gi.isDefault() {
			if !gi.IsConstantNear && len(curr.Children) == 0 {
				continue
			}
			specs = append(specs, subgraphExtractionSpec{
				container:   curr,
				includeSelf: gi.IsConstantNear,
				graphInfo:   gi,
			})
			continue
		}
		queue = append(queue, curr.ChildrenArray...)
	}
	return specs
}

// extractSubgraphs is the one-pass equivalent of calling ExtractSubgraph for
// each disjoint nested diagram in order. An edge crossing two nested diagrams
// belongs to the first extraction, matching the sequential implementation.
func extractSubgraphs(g *d2graph.Graph, specs []subgraphExtractionSpec) *subgraphExtractionBatch {
	extractions := make(map[*d2graph.Object]*subgraphExtraction, len(specs))
	batch := &subgraphExtractionBatch{
		g:           g,
		specs:       specs,
		extractions: extractions,
	}
	ownerIndex := make(map[*d2graph.Object]int, len(specs))
	parentOrderSaved := make(map[*d2graph.Object]struct{})
	for i, spec := range specs {
		ownerIndex[spec.container] = i
		restoreInto := spec.container
		if spec.includeSelf {
			restoreInto = spec.container.Parent
			if restoreInto != nil {
				if _, ok := parentOrderSaved[restoreInto]; !ok {
					parentOrderSaved[restoreInto] = struct{}{}
					batch.restoreParentOrder = append(batch.restoreParentOrder, SaveChildrenOrder(restoreInto))
				}
			}
		}
		nestedGraph := d2graph.NewGraph()
		nestedGraph.RootLevel = int(spec.container.Level())
		if spec.includeSelf {
			nestedGraph.RootLevel--
		}
		nestedGraph.Root.Attributes = spec.container.Attributes
		nestedGraph.Root.Box = &geo.Box{}
		nestedGraph.Root.LabelPosition = spec.container.LabelPosition
		nestedGraph.Root.IconPosition = spec.container.IconPosition
		extractions[spec.container] = &subgraphExtraction{
			nestedGraph: nestedGraph,
			graphInfo:   spec.graphInfo,
			index:       i,
			restoreInto: restoreInto,
		}
	}

	objectOwner := make(map[*d2graph.Object]int, len(g.Objects))
	findOwner := func(obj *d2graph.Object) (int, bool) {
		if owner, ok := objectOwner[obj]; ok {
			return owner, true
		}
		for ancestor := obj; ancestor != nil; ancestor = ancestor.Parent {
			owner, ok := ownerIndex[ancestor]
			if !ok {
				continue
			}
			if specs[owner].includeSelf || ancestor != obj {
				return owner, true
			}
			break
		}
		return 0, false
	}

	remainingObjects := make([]*d2graph.Object, 0, len(g.Objects))
	for _, obj := range g.Objects {
		owner, ok := findOwner(obj)
		if !ok {
			remainingObjects = append(remainingObjects, obj)
			continue
		}
		objectOwner[obj] = owner
		extraction := extractions[specs[owner].container]
		extraction.nestedGraph.Objects = append(extraction.nestedGraph.Objects, obj)
	}
	g.Objects = remainingObjects

	remainingEdges := make([]*d2graph.Edge, 0, len(g.Edges))
	for _, edge := range g.Edges {
		srcOwner, srcNested := findOwner(edge.Src)
		if d2sequence.IsLifelineEnd(edge.Dst) {
			if srcNested {
				extraction := extractions[specs[srcOwner].container]
				extraction.nestedGraph.Edges = append(extraction.nestedGraph.Edges, edge)
			} else {
				remainingEdges = append(remainingEdges, edge)
			}
			continue
		}

		dstOwner, dstNested := findOwner(edge.Dst)
		if srcNested && dstNested && srcOwner == dstOwner {
			extraction := extractions[specs[srcOwner].container]
			extraction.nestedGraph.Edges = append(extraction.nestedGraph.Edges, edge)
			continue
		}
		if !srcNested && !dstNested {
			remainingEdges = append(remainingEdges, edge)
			continue
		}

		owner := srcOwner
		if !srcNested || dstNested && dstOwner < owner {
			owner = dstOwner
		}
		extraction := extractions[specs[owner].container]
		extraction.externalEdges = append(extraction.externalEdges, edge)
		extraction.externalEdgeIDs = append(extraction.externalEdgeIDs, edgeIDs{
			srcID: edge.Src.AbsID(),
			dstID: edge.Dst.AbsID(),
		})
	}
	g.Edges = remainingEdges

	// Mutate parent/root references only after all memberships have been
	// classified; doing so earlier would change ancestry for later diagrams.
	for _, spec := range specs {
		extraction := extractions[spec.container]
		for _, obj := range extraction.nestedGraph.Objects {
			obj.Graph = extraction.nestedGraph
		}
		if spec.includeSelf {
			if spec.container.Parent != nil {
				spec.container.Parent.RemoveChild(spec.container)
			}
			extraction.nestedGraph.Root.ChildrenArray = []*d2graph.Object{spec.container}
			spec.container.Parent = extraction.nestedGraph.Root
			extraction.nestedGraph.Root.Children[strings.ToLower(spec.container.ID)] = spec.container
		} else {
			extraction.nestedGraph.Root.ChildrenArray = append(extraction.nestedGraph.Root.ChildrenArray, spec.container.ChildrenArray...)
			for _, child := range spec.container.ChildrenArray {
				child.Parent = extraction.nestedGraph.Root
				extraction.nestedGraph.Root.Children[strings.ToLower(child.ID)] = child
			}
			for key := range spec.container.Children {
				delete(spec.container.Children, key)
			}
			spec.container.ChildrenArray = nil
		}
	}

	return batch
}

// rollbackAfter restores sibling diagrams that the batched fast path extracted
// ahead of the diagram whose recursive layout failed. The failing diagram and
// all earlier diagrams remain extracted, matching sequential ExtractSubgraph
// side effects at the same error point.
func (b *subgraphExtractionBatch) rollbackAfter(index int) {
	for i := index + 1; i < len(b.specs); i++ {
		spec := b.specs[i]
		extraction := b.extractions[spec.container]
		InjectNested(extraction.restoreInto, extraction.nestedGraph, false)
		b.g.Edges = append(b.g.Edges, extraction.externalEdges...)
	}
	for _, restoreOrder := range b.restoreParentOrder {
		restoreOrder()
	}
}

func ExtractSubgraph(container *d2graph.Object, includeSelf bool) (nestedGraph *d2graph.Graph, externalEdges []*d2graph.Edge, externalEdgeIDs []edgeIDs) {
	// includeSelf: when we have a constant near or a grid cell that is a container,
	// we want to include itself in the nested graph, not just its descendants,
	nestedGraph = d2graph.NewGraph()
	nestedGraph.RootLevel = int(container.Level())
	if includeSelf {
		nestedGraph.RootLevel--
	}
	nestedGraph.Root.Attributes = container.Attributes
	nestedGraph.Root.Box = &geo.Box{}
	nestedGraph.Root.LabelPosition = container.LabelPosition
	nestedGraph.Root.IconPosition = container.IconPosition

	isNestedObject := func(obj *d2graph.Object) bool {
		if includeSelf {
			return obj.IsDescendantOf(container)
		}
		return obj.Parent.IsDescendantOf(container)
	}

	// separate out nested edges
	g := container.Graph
	remainingEdges := make([]*d2graph.Edge, 0, len(g.Edges))
	for _, edge := range g.Edges {
		srcIsNested := isNestedObject(edge.Src)
		if d2sequence.IsLifelineEnd(edge.Dst) {
			// special handling for lifelines since their edge.Dst is a special Object
			if srcIsNested {
				nestedGraph.Edges = append(nestedGraph.Edges, edge)
			} else {
				remainingEdges = append(remainingEdges, edge)
			}
			continue
		}
		dstIsNested := isNestedObject(edge.Dst)
		if srcIsNested && dstIsNested {
			nestedGraph.Edges = append(nestedGraph.Edges, edge)
		} else if srcIsNested || dstIsNested {
			externalEdges = append(externalEdges, edge)
			// we need these AbsIDs when reconnecting since parent references may become stale
			externalEdgeIDs = append(externalEdgeIDs, edgeIDs{
				srcID: edge.Src.AbsID(),
				dstID: edge.Dst.AbsID(),
			})
		} else {
			remainingEdges = append(remainingEdges, edge)
		}
	}
	g.Edges = remainingEdges

	// separate out nested objects
	remainingObjects := make([]*d2graph.Object, 0, len(g.Objects))
	for _, obj := range g.Objects {
		if isNestedObject(obj) {
			nestedGraph.Objects = append(nestedGraph.Objects, obj)
		} else {
			remainingObjects = append(remainingObjects, obj)
		}
	}
	g.Objects = remainingObjects

	// update object and new root references
	for _, o := range nestedGraph.Objects {
		o.Graph = nestedGraph
	}

	if includeSelf {
		// remove container parent's references
		if container.Parent != nil {
			container.Parent.RemoveChild(container)
		}

		// set root references
		nestedGraph.Root.ChildrenArray = []*d2graph.Object{container}
		container.Parent = nestedGraph.Root
		nestedGraph.Root.Children[strings.ToLower(container.ID)] = container
	} else {
		// set root references
		nestedGraph.Root.ChildrenArray = append(nestedGraph.Root.ChildrenArray, container.ChildrenArray...)
		for _, child := range container.ChildrenArray {
			child.Parent = nestedGraph.Root
			nestedGraph.Root.Children[strings.ToLower(child.ID)] = child
		}

		// remove container's references
		for k := range container.Children {
			delete(container.Children, k)
		}
		container.ChildrenArray = nil
	}

	return nestedGraph, externalEdges, externalEdgeIDs
}

func InjectNested(container *d2graph.Object, nestedGraph *d2graph.Graph, isRoot bool) {
	g := container.Graph
	for _, obj := range nestedGraph.Root.ChildrenArray {
		obj.Parent = container
		if container.Children == nil {
			container.Children = make(map[string]*d2graph.Object)
		}
		container.Children[strings.ToLower(obj.ID)] = obj
		container.ChildrenArray = append(container.ChildrenArray, obj)
	}
	for _, obj := range nestedGraph.Objects {
		obj.Graph = g
	}
	g.Objects = append(g.Objects, nestedGraph.Objects...)
	g.Edges = append(g.Edges, nestedGraph.Edges...)

	if isRoot {
		if nestedGraph.Root.LabelPosition != nil {
			container.LabelPosition = nestedGraph.Root.LabelPosition
		}
		if nestedGraph.Root.IconPosition != nil {
			container.IconPosition = nestedGraph.Root.IconPosition
		}
		container.Attributes = nestedGraph.Root.Attributes
	}
}

func PositionNested(container *d2graph.Object, nestedGraph *d2graph.Graph) {
	// tl, _ := boundingBox(nestedGraph)
	// Note: assumes nestedGraph's layout has contents positioned relative to 0,0
	dx := container.TopLeft.X //- tl.X
	dy := container.TopLeft.Y //- tl.Y
	if dx == 0 && dy == 0 {
		return
	}
	for _, o := range nestedGraph.Objects {
		o.TopLeft.X += dx
		o.TopLeft.Y += dy
	}
	for _, e := range nestedGraph.Edges {
		e.Move(dx, dy)
	}
}

func boundingBox(g *d2graph.Graph) (tl, br *geo.Point) {
	if len(g.Objects) == 0 {
		return geo.NewPoint(0, 0), geo.NewPoint(0, 0)
	}
	tl = geo.NewPoint(math.Inf(1), math.Inf(1))
	br = geo.NewPoint(math.Inf(-1), math.Inf(-1))

	for _, obj := range g.Objects {
		if obj.TopLeft == nil {
			panic(obj.AbsID())
		}
		tl.X = math.Min(tl.X, obj.TopLeft.X)
		tl.Y = math.Min(tl.Y, obj.TopLeft.Y)
		br.X = math.Max(br.X, obj.TopLeft.X+obj.Width)
		br.Y = math.Max(br.Y, obj.TopLeft.Y+obj.Height)
	}

	return tl, br
}

func validateObjectPositions(objects []*d2graph.Object) error {
	for _, obj := range objects {
		if obj.TopLeft == nil {
			continue
		}
		if math.IsInf(obj.TopLeft.X, 0) || math.IsInf(obj.TopLeft.Y, 0) {
			return fmt.Errorf("object %q has invalid position with infinity value: x=%f, y=%f", obj.AbsID(), obj.TopLeft.X, obj.TopLeft.Y)
		}
	}
	return nil
}

func FitToGraph(container *d2graph.Object, nestedGraph *d2graph.Graph, padding geo.Spacing) {
	var width, height float64
	width = nestedGraph.Root.Width
	height = nestedGraph.Root.Height
	if width == 0 || height == 0 {
		tl, br := boundingBox(nestedGraph)
		width = br.X - tl.X
		height = br.Y - tl.Y
	}
	container.Width = padding.Left + width + padding.Right
	container.Height = padding.Top + height + padding.Bottom
}
