package placement

import (
	
	
	
	
	

	

	
	
	
)

// sizelessOptimizer holds the state during the first part (sizeless) of the optimization routine described in
// Graph Compact Orthogonal Layout Algorithm by Freivalds and Glagolevs
type sizelessOptimizer struct {
	mutationScratch optimizerMutationSnapshot

	nodes         []*layoutgraph.Node
	g             *layoutgraph.Graph
	randGenerator *rand.Rand

	occupied map[geo.Point]*layoutgraph.Node
}

func newSizelessOptimizer( context.Context,  *layoutgraph.Graph,  *rand.Rand) (*sizelessOptimizer, error) {
	if  == nil {
		return nil, fmt.Errorf("TALA sizelessOptimizer requires a graph")
	}
	if len(.Nodes) > limits.MaxEngineNodes {
		return nil, fmt.Errorf("TALA sizelessOptimizer node count exceeds limit %d", limits.MaxEngineNodes)
	}
	if len(.Edges) > limits.MaxEngineEdges {
		return nil, fmt.Errorf("TALA sizelessOptimizer edge count exceeds limit %d", limits.MaxEngineEdges)
	}
	,  := limits.NewOptimizationWorkGuard(, "sizelessOptimizer.setup", limits.MaxOptimizationWorkUnits)
	if  != nil {
		return nil, 
	}
	for ,  := range .Nodes {
		if  := .Step();  != nil {
			return nil, 
		}
		if  == nil || .Graph == nil {
			return nil, fmt.Errorf("TALA sizelessOptimizer found a node without a graph")
		}
		if len(.Edges) > limits.MaxEngineEdges || len(.Nears) > limits.MaxEngineNodes {
			return nil, fmt.Errorf("TALA sizelessOptimizer node %s adjacency references exceed engine limits", .DebugID())
		}
		if .Graph.CellSize != .CellSize {
			return nil, fmt.Errorf("mismatch of cell size, graph=%f, node=%f", .CellSize, .Graph.CellSize)
		}
	}
	 := &sizelessOptimizer{
		nodes:         make([]*layoutgraph.Node, 0, len(.Nodes)),
		g:             ,
		randGenerator: ,
	}
	.resetOccupied()
	for ,  := range .Nodes {
		if  := .Step();  != nil {
			return nil, 
		}
		,  := canOptimizeNodeGuarded(, , )
		if  != nil {
			return nil, 
		}
		if  {
			.nodes = append(.nodes, )
		}
	}
	if  := .Finish();  != nil {
		return nil, 
	}
	return , nil
}

// resetOccupied resets the occpupied positions of the optimizer
// this is useful if the nodes changed their positions outside `optimize`
func ( *sizelessOptimizer) () {
	.occupied = make(map[geo.Point]*layoutgraph.Node, len(.g.Nodes))
	for ,  := range .g.Nodes {
		if .TopLeft == nil {
			continue
		}
		.occupied[*.TopLeft] = 
	}
}

// optimize optimizes sizeless node placement following the routine describe in
// Graph Compact Orthogonal Layout Algorithm by Freivalds and Glagolevs
// Nodes are optimized in random order
// For each node:
// - Compute the median to its connected nodes (or `nears`)
// - Find the closest distance from this median point that is not occupied
// - Generate a set of points from median to (closest distance + 1)
// - Moves the node to the best position, if any
// - If there's no position improves edge length, tries to swap the node with adjacent nodes
func ( *sizelessOptimizer) ( context.Context,  float64) ( error) {
	return .optimizeWithLimit(, , limits.MaxOptimizationWorkUnits)
}

func ( *sizelessOptimizer) ( context.Context,  float64,  uint64) ( error) {
	,  := limits.NewOptimizationWorkGuard(, "sizelessOptimizer.optimize", )
	if  != nil {
		return 
	}
	if  == nil || .g == nil {
		return fmt.Errorf("TALA sizelessOptimizer.optimize requires an optimizer with a graph")
	}
	if .randGenerator == nil {
		return fmt.Errorf("TALA sizelessOptimizer.optimize requires a random generator")
	}
	defer .mutationScratch.release()
	,  := captureOptimizerMutationStateInto(.g, , &.mutationScratch)
	if  != nil {
		return 
	}
	 := .occupied
	 := make(map[geo.Point]*layoutgraph.Node, len(.occupied))
	for ,  := range .occupied {
		if  := .Step();  != nil {
			return 
		}
		[] = 
	}
	 := func() {
		if  == nil {
			.occupied = nil
			return
		}
		clear()
		maps.Copy(, )
		.occupied = 
	}
	 := false
	defer func() {
		if  := recover();  != nil {
			.restore()
			()
			panic()
		}
		if ! {
			.restore()
			()
		}
	}()
	if  := .optimizeGuarded(, , );  != nil {
		return 
	}
	if  := .Finish();  != nil {
		return 
	}
	 = true
	return nil
}

func ( *sizelessOptimizer) ( context.Context,  float64,  *limits.OptimizationWorkGuard) error {
	if len(.nodes) > limits.MaxEngineNodes {
		return fmt.Errorf("TALA sizelessOptimizer.optimize node count exceeds limit %d", limits.MaxEngineNodes)
	}

	 := make([]int, len(.nodes))
	for  := 0;  < len(.nodes); ++ {
		if  := .Step();  != nil {
			return 
		}
		[] = 
	}
	if  := limits.Shuffle(, .randGenerator, );  != nil {
		return 
	}

	for ,  := range  {
		if  := .Step();  != nil {
			return 
		}
		 := .nodes[]
		if  == nil || .TopLeft == nil {
			return fmt.Errorf("TALA sizelessOptimizer.optimize found an unpositioned node")
		}

		// we should always be able to move to the original position
		delete(.occupied, *.TopLeft)
		,  := .medianPointGuarded(, , )
		if  != nil {
			return 
		}
		,  := .findClosestUnoccupiedDistanceGuarded(, , )
		if  != nil {
			return fmt.Errorf("could not find an unoccupied distance for %s: %w", .DebugID(), )
		}
		,  := .placementPointsGuarded(, , , )
		if  != nil {
			return 
		}
		if  := limits.Shuffle(, .randGenerator, );  != nil {
			return 
		}

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

		if ! {
			,  := .bestSwapCandidateGuarded(, , )
			if  != nil {
				return 
			}
			if  != nil {
				if  := optimizerSwapPositions(, , );  != nil {
					return 
				}
				.occupied[*.TopLeft] = 
			}
		}
		.occupied[*.TopLeft] = 
	}
	return .Finish()
}

func ( *sizelessOptimizer) ( *layoutgraph.Node,  float64,  *limits.OptimizationWorkGuard) (*geo.Point, error) {
	, ,  := optimizerMedianToNeighbors(, false, nil, )
	if  != nil {
		return nil, 
	}

	 := .g.CellSize
	,  := optimizerFixedOrigin(.g, .OwningContainer(), )
	if  != nil {
		return nil, 
	}
	if  != nil {
		if  <= 0 || math.IsNaN() || math.IsInf(, 0) {
			return nil, fmt.Errorf("TALA %s fixed-origin placement requires a finite positive cell size", .Location())
		}
		// we can't have node positions past the fixedOrigin so set a floor
		if  < .X/ {
			 = .X / 
			 += 
		}
		if  < .Y/ {
			 = .Y / 
			 += 
		}
	}

	 += (- + .randGenerator.Float64()*(2.0*))
	 += (- + .randGenerator.Float64()*(2.0*))

	if  != nil {
		 = math.Max(, .X/)
		 = math.Max(, .Y/)
	}

	return geo.NewPoint(math.Round(), math.Round()), nil
}

// FindClosestUnoccupiedDistance finds the closest distance from `medianPoint` which is not occupied by any other node
// example with distance=3, we iterate points from least to most distance
// . 	      3
// . 	   3  2  3
// . 	3  2  1  2  3
// . 3──2──1──0──1──2──3
// . 	3  2  1  2  3
// . 	   3  2  3
// . 	      3
func ( *sizelessOptimizer) ( context.Context,  *layoutgraph.Node,  *geo.Point) (float64, error) {
	,  := limits.NewOptimizationWorkGuard(, "sizelessOptimizer.optimize", limits.MaxOptimizationWorkUnits)
	if  != nil {
		return 0, 
	}
	return .findClosestUnoccupiedDistanceGuarded(, , )
}

func ( *sizelessOptimizer) ( *layoutgraph.Node,  *geo.Point,  *limits.OptimizationWorkGuard) (float64, error) {
	if  == nil || .Graph == nil ||  == nil {
		return 0, fmt.Errorf("TALA %s placement search requires a node, graph, and median", .Location())
	}
	,  := optimizerFixedOrigin(.Graph, .OwningContainer(), )
	if  != nil {
		return 0, 
	}
	if  != nil {
		if .Graph.CellSize <= 0 || math.IsNaN(.Graph.CellSize) || math.IsInf(.Graph.CellSize, 0) {
			return 0, fmt.Errorf("TALA %s fixed-origin placement requires a finite positive cell size", .Location())
		}
		.X = math.Round(.X / .Graph.CellSize)
		.Y = math.Round(.Y / .Graph.CellSize)
	}
	 := func(,  float64) bool {
		return  != nil && ( < .X ||  < .Y)
	}

	for  := 0.;  <= 100.; ++ {
		if  := .Step();  != nil {
			return 0, 
		}
		// for points at the same distance we iterate right to left (& negative then positive):
		// . curr=0       =1            =2               =3        6
		// .                                   4                8     4
		// .                  2             6     2         10           2
		// .     ──0──     3─────0       7───────────0   12─────────────────0
		// .                  1             5     1          9           1
		// .                                   3                7     3
		// .                                                       5
		for ,  := , 0.;  >= -; -- {
			if  := .Step();  != nil {
				return 0, 
			}
			if  != 0 {
				 := geo.NewPoint(.X+, .Y-)
				if !(.X, .Y) && !.isOccupied() {
					return , nil
				}
			}
			 := geo.NewPoint(.X+, .Y+)
			if !(.X, .Y) && !.isOccupied() {
				return , nil
			}
			if  > 0 {
				++
			} else {
				--
			}
		}
	}
	return 0, fmt.Errorf("no unoccupied points within d=100 of p=%v", )
}

func ( *sizelessOptimizer) ( *geo.Point) bool {
	,  := .occupied[*]
	return 
}

// placementPointsGuarded generates points up to minUnoccupiedDistance+1 from medianPoint.
// The resulting points contain only valid positions (unoccupied and positive if there are fixed position nodes)
// Example:
// minUnoccupiedDistance = 0
// # = medianPoint
// * = resulting points (3 points, the median is included)
// . n5 n1
// . n2    n4
// .    n3 #*  *
// .        *
func ( *sizelessOptimizer) ( *layoutgraph.Node,  *geo.Point,  float64,  *limits.OptimizationWorkGuard) ([]*geo.Point, error) {
	if  == nil || .Graph == nil ||  == nil {
		return nil, fmt.Errorf("TALA %s placement generation requires a node, graph, and median", .Location())
	}
	if  < 0 ||  > 100 || math.IsNaN() || math.IsInf(, 0) {
		return nil, fmt.Errorf("TALA %s placement distance must be finite and within [0, 100]", .Location())
	}
	 :=  + 1
	 := numPointsWithinManhattanDistance() + 1
	if  < 0 ||  > maxOptimizerPlacementCandidates {
		return nil, fmt.Errorf("%w: TALA sizelessOptimizer.optimize placement candidates exceed limit %d", limits.ErrOptimizationResourceLimit, maxOptimizerPlacementCandidates)
	}
	 := make([]*geo.Point, 0, )

	,  := optimizerFixedOrigin(.Graph, .OwningContainer(), )
	if  != nil {
		return nil, 
	}
	if  != nil {
		if .Graph.CellSize <= 0 || math.IsNaN(.Graph.CellSize) || math.IsInf(.Graph.CellSize, 0) {
			return nil, fmt.Errorf("TALA %s fixed-origin placement requires a finite positive cell size", .Location())
		}
		.X = math.Round(.X / .Graph.CellSize)
		.Y = math.Round(.Y / .Graph.CellSize)
	}
	 := func(,  float64) bool {
		return  != nil && ( < .X ||  < .Y)
	}

	for  := ;  >= -; -- {
		if  := .Step();  != nil {
			return nil, 
		}
		for  := 0.0;  <= -math.Abs(); ++ {
			if  := .Step();  != nil {
				return nil, 
			}
			if  != 0 {
				 := geo.NewPoint(.X+, .Y-)
				if !(.X, .Y) && !.isOccupied() {
					 = append(, )
				}
			}
			 := geo.NewPoint(.X+, .Y+)
			if !(.X, .Y) && !.isOccupied() {
				 = append(, )
			}
		}
	}

	return , nil
}

func ( *sizelessOptimizer) ( context.Context,  *layoutgraph.Node,  []*geo.Point,  *limits.OptimizationWorkGuard) (bool, error) {
	 := placementcost.NewNodeEdgeLengthScorer(, placementcost.EdgeLengthOptions{
		EdgeAbductions: nil, IncludeNodeSizes: false,
		EnforceMinimumGap: false, PenalizeDirection: true,
	})
	defer .Close()
	 := math.Inf(1)
	 := .TopLeft.Copy()
	 := .TopLeft.X
	 := .TopLeft.Y
	,  := captureOptimizerCandidateMovement(, )
	if  != nil {
		return false, 
	}
	 := .positions
	 := false
	defer func() {
		if ! {
			restoreNodePositions()
		}
	}()

	for ,  := range  {
		if  := .Step();  != nil {
			restoreNodePositions()
			return false, 
		}
		if  == nil {
			restoreNodePositions()
			return false, fmt.Errorf("TALA sizelessOptimizer.optimize found a nil placement point")
		}
		 := .X
		 := .Y
		if  := .moveAbs(, , );  != nil {
			restoreNodePositions()
			return false, 
		}
		if  := chargeOptimizerScoring(, nil, false, );  != nil {
			restoreNodePositions()
			return false, 
		}
		,  := .Score()
		if  != nil {
			restoreNodePositions()
			return false, 
		}
		switch geo.PrecisionCompare(, , geo.PRECISION) {
		case -1:
			 = 
			 = 
		case 0:
			if .X ==  && .Y ==  {
				 = 
				 = 
			}
		}
	}

	if  := .Err();  != nil {
		restoreNodePositions()
		return false, fmt.Errorf("EdgeLength: %w", )
	}
	if math.IsInf(, 1) {
		restoreNodePositions()
		return false, fmt.Errorf("sizelessOptimizer.moveNodeToBest: could not find any placement")
	}

	if  := .moveAbs(.X, .Y, );  != nil {
		restoreNodePositions()
		return false, 
	}

	 = true
	return (.TopLeft.X != ) || (.TopLeft.Y != ), nil
}

func ( *sizelessOptimizer) ( context.Context,  *layoutgraph.Node,  *limits.OptimizationWorkGuard) (*layoutgraph.Node, error) {
	// TODO (Mon Sep 20 10:57:17 2021) These maybe should be true on enforcing gap size, since when we swap, we don't want resulting swap to be too squished
	if  := chargeOptimizerScoring(, nil, false, );  != nil {
		return nil, 
	}
	,  := placementcost.NodeEdgeLength(, , placementcost.EdgeLengthOptions{EdgeAbductions: nil, IncludeNodeSizes: false, EnforceMinimumGap: false, PenalizeDirection: true})
	if  != nil {
		return nil, 
	}
	var  *layoutgraph.Node = nil

	,  := .swapCandidatesGuarded(, )
	if  != nil {
		return nil, 
	}
	if  := limits.Shuffle(, .randGenerator, );  != nil {
		return nil, 
	}

	for ,  := range  {
		if  := .Step();  != nil {
			return nil, 
		}
		if  := chargeOptimizerScoring(, nil, false, );  != nil {
			return nil, 
		}
		,  := placementcost.NodeEdgeLength(, , placementcost.EdgeLengthOptions{EdgeAbductions: nil, IncludeNodeSizes: false, EnforceMinimumGap: false, PenalizeDirection: true})
		if  != nil {
			return nil, 
		}

		var ,  float64
		 = withOptimizerPositionsSwapped(, , , func() error {
			var  error
			if  = chargeOptimizerScoring(, nil, false, );  != nil {
				return 
			}
			,  = placementcost.NodeEdgeLength(, , placementcost.EdgeLengthOptions{EdgeAbductions: nil, IncludeNodeSizes: false, EnforceMinimumGap: false, PenalizeDirection: true})
			if  != nil {
				return 
			}
			// No need to compute it if we already know it won't be a good swap.
			if geo.PrecisionCompare(, , geo.PRECISION) < 0 {
				if  = chargeOptimizerScoring(, nil, false, );  != nil {
					return 
				}
				,  = placementcost.NodeEdgeLength(, , placementcost.EdgeLengthOptions{EdgeAbductions: nil, IncludeNodeSizes: false, EnforceMinimumGap: false, PenalizeDirection: true})
				if  != nil {
					return 
				}
			}
			return nil
		})
		if  != nil {
			return nil, 
		}

		// Both positions have to be better than before
		if geo.PrecisionCompare(, , geo.PRECISION) < 0 && geo.PrecisionCompare(, , geo.PRECISION) <= 0 {
			 = 
			 = 
		}
	}

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

// swapCandidatesGuarded finds the optimizable nodes adjacent to node.
// Adjacent to `node`:
// . 	           (node.X, node.Y-1)
// . (node.X-1, node.Y)  node  (node.X+1, node.Y)
// . 	           (node.X, node.Y+1)
func ( *sizelessOptimizer) ( *layoutgraph.Node,  *limits.OptimizationWorkGuard) ([]*layoutgraph.Node, error) {
	 := make([]*layoutgraph.Node, 0, 4)
	 := .TopLeft

	var  error
	 := func(,  float64) *layoutgraph.Node {
		if  := .Step();  != nil {
			 = 
			return nil
		}
		if ,  := .occupied[*geo.NewPoint(.X+, .Y+)];  {
			,  := canOptimizeNodeGuarded(, .g, )
			if  != nil {
				 = 
				return nil
			}
			if  {
				return 
			}
		}
		return nil
	}

	if  := (0, -1);  != nil {
		 = append(, )
	}
	if  := (0, 1);  != nil {
		 = append(, )
	}
	if  := (-1, 0);  != nil {
		 = append(, )
	}
	if  := (1, 0);  != nil {
		 = append(, )
	}
	if  != nil {
		return nil, 
	}

	return , nil
}

// canOptimizeNodeGuarded reports whether a node can be optimized.
// In order to be optimizable, a node must have edges, or at least `nears`, can't have a fixed position and can't be in a tree
func canOptimizeNodeGuarded( *layoutgraph.Node,  *layoutgraph.Graph,  *limits.OptimizationWorkGuard) (bool, error) {
	if  == nil ||  == nil {
		return false, fmt.Errorf("TALA %s found a nil optimizer node", .Location())
	}
	if len(.Edges) == 0 {
		 := false
		for  := range .Nears {
			if  := .Step();  != nil {
				return false, 
			}
			,  := optimizerIsDescendantOf(, .Container, )
			if  != nil {
				return false, 
			}
			if  {
				 = true
				break
			}
		}
		if ! {
			return false, nil
		}
	}
	if .FixedTopLeft != nil {
		return false, nil
	}
	,  := .NodeToTree[]
	return !, nil
}