package placement

import (
	
	
	
	
	

	
	
	
	

	
)

// compaction serves two functions:
// 1. Inflate nodes to create room for more optimal node placements (escaping local maximums)
// 2. Reduce distance between nodes too far apart
//
// We use compaction to inflate when transitioning from sizeless phase to sized phase, which the transition flag is for
type compactionOptions struct {
	edgeAbductions []*layoutgraph.EdgeAbduction
	axis           layoutAxis
	includeSizes   bool
	factor         float64
	transition     bool
	moveWorkLimit  uint64
}

func compaction( context.Context,  *layoutgraph.Graph,  compactionOptions) ( error) {
	if !.axis.valid() {
		return fmt.Errorf("TALA Compaction requires an axis")
	}
	if .factor <= 0 || math.IsNaN(.factor) || math.IsInf(.factor, 0) {
		return fmt.Errorf("TALA Compaction requires a finite positive factor")
	}
	 := .edgeAbductions
	 := .axis.isHorizontal()
	 := .includeSizes
	 := .factor
	 := .transition
	if  := layoutgraph.Validate(, "Compaction", );  != nil {
		return 
	}
	 := .RoutingCosts()
	,  := snapshotNodePositionsContext(, "Compaction", .Nodes)
	if  != nil {
		return 
	}
	 := false
	defer func() {
		if ! {
			restoreNodePositions()
			.RestoreRoutingCosts()
		}
	}()

	,  := visibilityEdges(, , , )
	if  != nil {
		return 
	}

	inflateAlongAxis(, , , , , )
	if  := .Err();  != nil {
		return fmt.Errorf("Compaction: %w", )
	}

	// We're returning early during transition because the below calls assume no overlap
	// And during transition, there can be temporary overlaps along a dimension
	// The main point of the transition is just to inflate anyway, there shouldn't be any compacting needed because
	// they inflate to the minimum possible value
	if  {
		 = true
		return nil
	}

	for  := 0;  < 20; ++ {
		,  := shiftSubgraphs(, , , , , , )
		if  != nil {
			return 
		}
		if ! {
			break
		}
	}
	 := .moveWorkLimit
	if  == 0 ||  > limits.MaxOptimizationWorkUnits {
		 = limits.MaxOptimizationWorkUnits
	}
	,  := limits.NewOptimizationWorkGuard(, "CompactionMoves", )
	if  != nil {
		return 
	}

	for  := 0;  < 20; ++ {
		,  := compactAlongAxis(, , , , , , , )
		if  != nil {
			return 
		}
		if ! {
			break
		}
	}
	if  := .Err();  != nil {
		return fmt.Errorf("Compaction: %w", )
	}
	 = true
	return nil
}

func orderedAlongAxis( *layoutgraph.Graph,  bool) []*layoutgraph.Node {
	 := make([]*layoutgraph.Node, len(.Nodes))
	copy(, .Nodes)

	sort.Slice(, func(,  int) bool {
		if  {
			if [].TopLeft.X == [].TopLeft.X {
				return [].ID < [].ID
			}
			return [].TopLeft.X < [].TopLeft.X
		} else {
			if [].TopLeft.Y == [].TopLeft.Y {
				return [].ID < [].ID
			}
			return [].TopLeft.Y < [].TopLeft.Y
		}
	})
	return 
}

// shiftSubgraphs tries to move entire subgraphs of the visibility graph, for a more global attempt to close gaps
// The primary purpose is to escape local maximas, like in this case below, where no individual nodes would move closer to each other
// .        ┌───┐  ┌───┐
// .   ┌────►   ├──►   │
// .   │    └───┘  └───┘
// . ┌─┴─┐
// . │   │
// . └─▲─┘
// .   │
// . ┌─┴─┐
// . │   │
// . └───┘
func shiftSubgraphs( context.Context,  *layoutgraph.Graph, ,  bool,  float64,  []*layoutgraph.EdgeAbduction,  layoutgraph.Edges) (bool, error) {
	 := false
	 := make(map[*layoutgraph.Node][]*layoutgraph.Node)
	 := []*layoutgraph.Node{}

	 := orderedAlongAxis(, )

	for ,  := range  {
		 := nearestFrom(, , , )
		if  == nil {
			[] = []*layoutgraph.Node{}
			 = append(, )
		} else {
			[] = append([], )
		}
	}

	 := []*layoutgraph.Node{}
	for ,  := range  {
		 := false
		for ,  := range [] {
			if .FixedTopLeft != nil {
				 = true
				break
			}
		}
		if  {
			delete(, )
		} else {
			 = append(, )
		}
	}
	 = 
	 := .FixedNodes()

	sort.Slice(, func(,  int) bool {
		if  {
			if [].TopLeft.X == [].TopLeft.X {
				return [].ID < [].ID
			}
			return [].TopLeft.X < [].TopLeft.X
		} else {
			if [].TopLeft.Y == [].TopLeft.Y {
				return [].ID < [].ID
			}
			return [].TopLeft.Y < [].TopLeft.Y
		}
	})

	 := globallyFurthestBehind(, )
	for ,  := range  {
		 := []
		var  []*geo.Point
		var  error
		if  {
			if .TopLeft.X == .TopLeft.X {
				,  = candidateMoves(, , , , , , 2, )
			} else {
				,  = candidateMoves(, , , , , , 0, )
			}
		} else {
			if .TopLeft.Y == .TopLeft.Y {
				,  = candidateMoves(, , , , , , 2, )
			} else {
				,  = candidateMoves(, , , , , , 0, )
			}
		}
		if  != nil {
			return false, 
		}
		 = append(, .TopLeft.Copy())

		// shifting subgraphs shouldn't introduce any overlaps between subgraph nodes, but we still need to check for overlaps with nodes not in this subgraph
		 := []*layoutgraph.Node{}
		 = append(, ...)
		for ,  := range .Nodes {
			 := slices.Contains(, )
			if ! {
				 = append(, )
			}
		}

		 := 1.0
		if  {
			 = .CellSize
		}
		 *= float64(layoutgraph.Nodes().NumAdjacent())

		,  := placementcost.NodesEdgeLength(, layoutgraph.Nodes(), placementcost.EdgeLengthOptions{EdgeAbductions: , IncludeNodeSizes: , EnforceMinimumGap: false, PenalizeDirection: true})
		if  != nil {
			return false, 
		}
		 := 0.0

		,  := snapshotNodePositionsContext(, "Compaction", )
		if  != nil {
			return false, 
		}
		for ,  := range  {
			var  float64
			if  {
				 = .TopLeft.X - .X
			} else {
				 = .TopLeft.Y - .Y
			}

			 := false
			for ,  := range  {
				 := .TopLeft.Copy()
				if  {
					.X -= 
				} else {
					.Y -= 
				}
				if len() > 0 {
					if .PointPastFixedOrigin(.X, .Y, ) {
						 = true
						break
					}
				}
				for ,  := range  {
					if  {
						if .DoesOverlapAt(, ) {
							 = true
							break
						}
					} else {
						if nonNilEquals(.TopLeft, ) {
							 = true
							break
						}
					}
				}
				if  {
					break
				}
			}
			if  {
				continue
			}

			for ,  := range  {
				if  {
					.MoveWithChildren(-, 0)
				} else {
					.MoveWithChildren(0, -)
				}
			}

			,  := placementcost.NodesEdgeLength(, layoutgraph.Nodes(), placementcost.EdgeLengthOptions{EdgeAbductions: , IncludeNodeSizes: , EnforceMinimumGap: false, PenalizeDirection: true})
			if  != nil {
				restoreNodePositions()
				return false, 
			}
			if  {
				,  := placementcost.NodesSymmetry(, layoutgraph.Nodes(), )
				if  != nil {
					restoreNodePositions()
					return false, 
				}
				 -=  * 
			}

			restoreNodePositions()

			if  <  {
				 = 
				 = 
			}
		}
		if  != 0 {
			 = true
			for ,  := range  {
				if  {
					.MoveWithChildren(-, 0)
				} else {
					.MoveWithChildren(0, -)
				}
			}
		}
	}
	return , nil
}

// inflateAlongAxis makes sure that the appropriate amount of distance (based on factor) between connected (in vGraph) nodes
func inflateAlongAxis( *layoutgraph.Graph, ,  bool,  float64,  layoutgraph.Edges,  bool) {
	 := orderedAlongAxis(, )
	 := .HasFixedNode()

	for ,  := range  {
		 := nearestFrom(, , , )
		if  == nil {
			// TODO reconsider this
			if  &&  && .FixedTopLeft == nil {
				// during transition we want all nodes to move to a cellSize even if they don't have a nearestFrom
				if  {
					.MoveAbsWithChildren(roundToPreviousCellSize(.TopLeft.X, .CellSize), .TopLeft.Y)
				} else {
					.MoveAbsWithChildren(.TopLeft.X, roundToPreviousCellSize(.TopLeft.Y, .CellSize))
				}
			}
			continue
		}
		// we don't want to move fixed nodes but we transitioning from sizeless to sized is an exception
		if .FixedTopLeft != nil && ! {
			continue
		}

		// Edge case:
		// During transition, because there are temporary overlaps, we want to maintain those overlaps for visibility edges in
		// the subsequent iteration of compaction. If one is connected and another is not, then that overlap can be lost.
		// So we force it to be maintained.
		var  int
		if  {
			 = layoutgraph.ConnectedNodeGap
		} else {
			 = .DeltaTo(, .TopLeft)
		}
		 := compactionFloor(, , , , , )
		if  {
			if  {
				if *.CellSize > .TopLeft.X {
					.MoveAbsWithChildren(*.CellSize, .TopLeft.Y)
				}
			} else {
				if  > .TopLeft.X {
					.MoveAbsWithChildren(, .TopLeft.Y)
				}
			}
		} else {
			if  {
				if *.CellSize > .TopLeft.Y {
					.MoveAbsWithChildren(.TopLeft.X, *.CellSize)
				}
			} else {
				if  > .TopLeft.Y {
					.MoveAbsWithChildren(.TopLeft.X, )
				}
			}
		}
	}
}

// Each node moves as far back as possible while making progress towards an adjacent node
// (this is a visibility edge)
// Before
// ┌───┐                       ┌───┐
// │ A ├──────────────────────►│B  │
// └───┘                       └───┘
// After
// ┌───┐     ┌───┐
// │ A ├────►│B  │
// └───┘     └───┘
//
// E.g. if it's moving away from all nodes, then it shouldn't keep moving back, even if there's a gap between it and the nearest behind
func compactAlongAxis( context.Context,  *layoutgraph.Graph, ,  bool,  float64,  []*layoutgraph.EdgeAbduction,  layoutgraph.Edges,  *limits.OptimizationWorkGuard) (bool, error) {
	 := false

	 := orderedAlongAxis(, )

	for ,  := range  {
		if .FixedTopLeft != nil {
			continue
		}
		,  := candidateMoves(, , , , , , 0, )
		if  != nil {
			return false, 
		}
		 = append(, .TopLeft.Copy())

		,  := moveNodeToBest(, , , , , , )
		if  != nil {
			return false, 
		}
		if  {
			 = true
		}
	}

	return , nil
}

func visibilityEdges( context.Context,  *layoutgraph.Graph, ,  bool) (layoutgraph.Edges, error) {
	,  := limits.NewWorkGuard(, "CompactionVisibility", limits.MaxEngineWorkUnits)
	if  != nil {
		return nil, 
	}
	 := []*layoutgraph.Edge{}
	for ,  := range .Nodes {
		if  := .Step();  != nil {
			return nil, 
		}
		for ,  := range .Nodes {
			if  := .Step();  != nil {
				return nil, 
			}
			if .ID == .ID {
				continue
			}

			 := float64(.DeltaTo(, .TopLeft))
			if !.VisibilityGraphCandidate(, true, , , ) {
				continue
			}

			,  := isVisibilityGraphEdgeBlocked(, , , , , )
			if  != nil {
				return nil, 
			}
			if ! {
				 = append(, layoutgraph.NewEdge(, ))
			}
		}
	}

	if  := .Finish();  != nil {
		return nil, 
	}
	return , nil
}

func isVisibilityGraphEdgeBlocked( *layoutgraph.Graph,
	,  bool,
	,  *layoutgraph.Node,
	 *limits.WorkGuard,
) (bool, error) {
	for ,  := range .Nodes {
		if  := .Step();  != nil {
			return false, 
		}
		if (.ID == .ID) || (.ID == .ID) {
			continue
		}

		if .IsBlocked(, , , ) {
			return true, nil
		}
	}

	return false, nil
}

func nearestFrom( layoutgraph.Edges,  *layoutgraph.Node, ,  bool) *layoutgraph.Node {
	var  *layoutgraph.Node

	 := math.Inf(-1)

	for ,  := range  {
		if .To ==  {
			if  {
				if  {
					if .From.TopLeft.X+.From.Width >  {
						 = .From.TopLeft.X + .From.Width
						 = .From
					}
				} else {
					if .From.TopLeft.X >  {
						 = .From.TopLeft.X
						 = .From
					}
				}
			} else {
				if  {
					if .From.TopLeft.Y+.From.Height >  {
						 = .From.TopLeft.Y + .From.Height
						 = .From
					}
				} else {
					if .From.TopLeft.Y >  {
						 = .From.TopLeft.Y
						 = .From
					}
				}
			}
		}
	}

	return 
}

// compactionFloor returns the floor value along an axis that a node must satisfy, given the anchor.
// if include sizes, it returns it as a number of cell size
func compactionFloor( *layoutgraph.Graph,  *layoutgraph.Node,  float64, ,  bool,  int) float64 {
	var  float64

	if  {
		if  {
			 = math.Ceil((.TopLeft.X + ( * .Width)) / .CellSize)
		} else {
			 = .TopLeft.X + math.Floor()
		}
	} else {
		if  {
			 = math.Ceil((.TopLeft.Y + ( * .Height)) / .CellSize)
		} else {
			 = .TopLeft.Y + math.Floor()
		}
	}

	if  {
		var  float64
		if  {
			 = .TopLeft.X + .Width
		} else {
			 = .TopLeft.Y + .Height
		}
		for (*.CellSize - ) <= float64() {
			 += 1
		}
	}

	return 
}

func globallyFurthestBehind( *layoutgraph.Graph,  bool) *layoutgraph.Node {
	 := .Nodes[0]
	for ,  := range .Nodes {
		if  {
			if .TopLeft.X < .TopLeft.X {
				 = 
			}
		} else {
			if .TopLeft.Y < .TopLeft.Y {
				 = 
			}
		}
	}
	return 
}

// candidateMoves returns the points a node may move to while compacting toward the nearest node behind it.
// floorDecrease is only safe to add when called with the node being the globally most behind
func candidateMoves( context.Context,  *layoutgraph.Graph,  *layoutgraph.Node,  float64, ,  bool,  int,  layoutgraph.Edges) ([]*geo.Point, error) {
	,  := limits.NewWorkGuard(, "CompactionCandidates", limits.MaxEngineWorkUnits)
	if  != nil {
		return nil, 
	}
	 := nearestFrom(, , , )

	// Node is at the start of visibility graph chain
	if  == nil {
		 = globallyFurthestBehind(, )
	}

	 := compactionFloor(, , , , , .DeltaTo(, .TopLeft))

	 := .HasFixedNode()

	if  {
		// If it's same node or a node on another axis, no need add size buffer
		var  bool
		if  {
			 = (.TopLeft.Y > .TopLeft.Y+.Height) ||
				(.TopLeft.Y+.Height < .TopLeft.Y)
		} else {
			 = .TopLeft.Y != .TopLeft.Y
		}
		if (.TopLeft.X == .TopLeft.X) ||  {
			if  {
				 = .TopLeft.X / .CellSize
				// with fixed positions floor may not be exactly on a cellSize
				if  {
					 = math.Round()
				}
			} else {
				 = .TopLeft.X
			}
		}
	} else {
		var  bool
		if  {
			 = (.TopLeft.X > .TopLeft.X+.Width) ||
				(.TopLeft.X+.Width < .TopLeft.X)
		} else {
			 = .TopLeft.X != .TopLeft.X
		}
		if (.TopLeft.Y == .TopLeft.Y) ||  {
			if  {
				 = .TopLeft.Y / .CellSize
				// with fixed positions floor may not be exactly on a cellSize
				if  {
					 = math.Round()
				}
			} else {
				 = .TopLeft.Y
			}
		}
	}

	// Every node needs to include at least itself
	var  float64
	if  {
		if  {
			 = .TopLeft.X / .CellSize
		} else {
			 = .TopLeft.X
		}
	} else {
		if  {
			 = .TopLeft.Y / .CellSize
		} else {
			 = .TopLeft.Y
		}
	}
	 :=  - float64()
	if math.IsNaN() || math.IsInf(, 0) || math.IsNaN() || math.IsInf(, 0) {
		return nil, invariant.New("compaction candidate range is not finite")
	}
	 := 0
	if  <=  {
		 := math.Floor(-) + 1
		if  > maxCompactionCandidateCount {
			return nil, fmt.Errorf(
				"TALA compaction candidate count %.0f exceeds limit %d",
				,
				maxCompactionCandidateCount,
			)
		}
		 = int()
	}
	 := make([]*geo.Point, 0, )
	for  := ;  <= ; ++ {
		if  := .Step();  != nil {
			return nil, 
		}
		var  geo.Point
		if  {
			if  {
				 = *geo.NewPoint(*.CellSize, .TopLeft.Y)
			} else {
				 = *geo.NewPoint(, .TopLeft.Y)
			}
		} else {
			if  {
				 = *geo.NewPoint(.TopLeft.X, *.CellSize)
			} else {
				 = *geo.NewPoint(.TopLeft.X, )
			}
		}

		 = append(, &)
	}
	if  := .Finish();  != nil {
		return nil, 
	}
	return , nil
}