package loops

import (
	
	

	
	
	
)

const sharedPortLoopGap = 30.0

// ComputeOffsets refreshes loop extents for every graph node.
func ( *layoutgraph.Graph) {
	for ,  := range .Nodes {
		UpdateOffsets()
	}
}

// UpdateOffsets refreshes the space reserved around node for self-edge routes.
func ( *layoutgraph.Node) {
	// need to set node TopLeft because the node position is required to get the ports
	// which are required for routing
	 := .TopLeft == nil
	if  {
		.TopLeft = geo.NewPoint(0, 0)
	}
	computeOffsets()
	if  {
		.TopLeft = nil
	}
}

// computeLoopOffsets precomputes loops and their contribution to node bounds
// for later delta and bounding-box calculations.
func computeOffsets( *layoutgraph.Node) {
	.LoopOffsets = make(map[geo.Orientation]float64)

	,  := .BoundingBox(nil)
	,  := .BoundingBox(nil)
	for ,  := range routeLoops() {
		if !.IsLoop() {
			continue
		}
		,  := .BoundingBoxValues()
		.X = math.Min(.X, .X)
		.Y = math.Min(.Y, .Y)
		.X = math.Max(.X, .X)
		.Y = math.Max(.Y, .Y)
		// need to reset because if these are set based on `0, 0`, graph bounding box breaks during placement
		// as nodes move around during placement and these edge points would stay at `0, 0`
		// the graph would reach a bad state
		.Points = nil
	}

	if nonNilEquals(, ) && nonNilEquals(, ) {
		return
	}

	.LoopOffsets[geo.Left] = math.Round(math.Abs(.X - .X))
	.LoopOffsets[geo.Right] = math.Round(math.Abs(.X - .X))
	.LoopOffsets[geo.Top] = math.Round(math.Abs(.Y - .Y))
	.LoopOffsets[geo.Bottom] = math.Round(math.Abs(.Y - .Y))

	.LoopOffsets[geo.TopLeft] = math.Max(.LoopOffsets[geo.Top], .LoopOffsets[geo.Left])
	.LoopOffsets[geo.TopRight] = math.Max(.LoopOffsets[geo.Top], .LoopOffsets[geo.Right])
	.LoopOffsets[geo.BottomLeft] = math.Max(.LoopOffsets[geo.Bottom], .LoopOffsets[geo.Left])
	.LoopOffsets[geo.BottomRight] = math.Max(.LoopOffsets[geo.Bottom], .LoopOffsets[geo.Right])
}

// loopPorts works as sort of context for a given ports pair while routing loops
type loopPorts struct {
	verticalOffset     float64
	horizontalOffset   float64
	hasSourceArrowhead *bool
	hasTargetArrowhead *bool
	sourceArrowhead    layoutgraph.Arrowhead
	targetArrowhead    layoutgraph.Arrowhead
	from               *geo.Point
	fromDirection      geo.Orientation
	to                 *geo.Point
	toDirection        geo.Orientation
}

func newLoopPorts(,  *geo.Point, ,  geo.Orientation) *loopPorts {
	return &loopPorts{
		verticalOffset:     0.,
		horizontalOffset:   0.,
		hasSourceArrowhead: nil,
		hasTargetArrowhead: nil,
		from:               ,
		fromDirection:      ,
		to:                 ,
		toDirection:        ,
	}
}

// Route computes every self-edge route for node.
func ( *layoutgraph.Node) []*layoutgraph.Edge {
	return routeLoops()
}

// routeLoops routes loops in a rule based approach and sets the proper label position
// edges are routed based on their arrowheads and label size
// there are 4 possible positions for loops, one at each corner
func routeLoops( *layoutgraph.Node) []*layoutgraph.Edge {
	 := makeLoopPorts()

	var  []*layoutgraph.Edge
	for ,  := range edgesInOrder(.Edges) {
		if !.IsLoop() {
			continue
		}

		 = append(, )
		 := findLoopPortsForEdge(, )
		.Points = routeLoop()
		if .Label != nil {
			.Label.Position = label.OutsideTopCenter
		}
		.verticalOffset, .horizontalOffset = computeEdgeOffset()
		.hasSourceArrowhead = new(.HasSourceArrow())
		.hasTargetArrowhead = new(.HasTargetArrow())
		.sourceArrowhead = .SourceArrowhead
		.targetArrowhead = .TargetArrowhead
	}
	return 
}

// edgesInOrder sorts edges based on their arrowheads and label sizes
// sorting by arrowhead is required so that, if a given `diamond` node has all edge types (->, --, <->)
// the arrowheads will match the ports as expected (see `self_edges_all_shapes` test case)
// then, sorts by label so that the smallest ones will use the routes closes to the shape, avoiding empty space
func edgesInOrder( []*layoutgraph.Edge) []*layoutgraph.Edge {
	 := make([]*layoutgraph.Edge, len())
	copy(, )
	 := func( *layoutgraph.Edge) int {
		// ->
		if .HasSourceArrow() != .HasTargetArrow() {
			return 0
		} else if .HasSourceArrow() {
			return 1
		} else {
			return 2
		}
	}
	slices.SortStableFunc(, func(,  *layoutgraph.Edge) int {
		// 1st ->
		// 2nd <->
		// 3rd --
		if .HasSourceArrow() != .HasSourceArrow() || .HasTargetArrow() != .HasTargetArrow() {
			 := ()
			 := ()
			switch {
			case  < :
				return -1
			case  < :
				return 1
			default:
				return 0
			}
		}

		// if arrowheads are equal, sort by label area
		 := 0.
		 := 0.
		if .Label != nil {
			 = .Label.Width * .Label.Height
		}
		if .Label != nil {
			 = .Label.Width * .Label.Height
		}
		switch {
		case  < :
			return -1
		case  < :
			return 1
		default:
			return 0
		}
	})
	return 
}

// findLoopPortsForEdge finds the ports that are using the same arrowheads in `availablePorts`
func findLoopPortsForEdge( *layoutgraph.Edge,  []*loopPorts) *loopPorts {
	// first find a matching set if it exists
	for ,  := range  {
		if .hasSourceArrowhead != nil && .hasTargetArrowhead != nil {
			if .sourceArrowhead == .SourceArrowhead && .targetArrowhead == .TargetArrowhead {
				return 
			}
		}
	}
	// look for an open pair
	for ,  := range  {
		if .hasSourceArrowhead == nil && .hasTargetArrowhead == nil {
			return 
		}
	}
	// find a pair that have arrows at the same end
	for ,  := range  {
		if .hasSourceArrowhead != nil && .hasTargetArrowhead != nil &&
			*.hasSourceArrowhead == .HasSourceArrow() && *.hasTargetArrowhead == .HasTargetArrow() {
			return 
		}
	}
	return nil
}

// makeLoopPorts makes the 4 possible port pairs, one at each corner, using the closest ports for each combination
// closest ports because this avoids crossings with other routes
// the ports order is related to edgesInOrder and the `shape: diamond` condition (see test case)
// Four pairs are required because the declaration orientation makes source-only
// and target-only arrows distinct categories in addition to -- and <->.
func makeLoopPorts( *layoutgraph.Node) []*loopPorts {
	 := .Ports()
	 := .PortIndices(geo.Top)
	 := .PortIndices(geo.Right)
	 := .PortIndices(geo.Bottom)
	 := .PortIndices(geo.Left)

	,  := closestPorts(, , )
	,  := closestPorts(, , )
	,  := closestPorts(, , )
	,  := closestPorts(, , )

	return []*loopPorts{
		newLoopPorts(, , geo.Left, geo.Top),
		newLoopPorts(, , geo.Top, geo.Right),
		newLoopPorts(, , geo.Bottom, geo.Left),
		newLoopPorts(, , geo.Right, geo.Bottom),
	}
}

func closestPorts( []geo.Point, ,  []int) (*geo.Point, *geo.Point) {
	var ,  geo.Point
	 := math.Inf(1)

	for ,  := range  {
		for ,  := range  {
			 := []
			 := []
			 := geo.EuclideanDistance(.X, .Y, .X, .Y)
			if  <  {
				 = 
				 = 
				 = 
			}
		}
	}

	return &, &
}

// routeLoop creates the route shown below with ports `*`, bend points and intersection (`#`)
// . #───────────#
// . │           │
// . │      ┌────*───┐
// . │      │        │
// . #──────*        │
// .        │  node  │
// .        │        │
// .        └────────┘
func routeLoop( *loopPorts) []*geo.Point {
	 := makeBendPoint(.from, .fromDirection, .verticalOffset, .horizontalOffset)
	 := makeBendPoint(.to, .toDirection, .verticalOffset, .horizontalOffset)
	 := makeIntersectionPoint(, , .fromDirection)

	return []*geo.Point{
		.from,
		,
		,
		,
		.to,
	}
}

func makeBendPoint( *geo.Point,  geo.Orientation, ,  float64) *geo.Point {
	switch  {
	case geo.Top:
		return geo.NewPoint(.X, .Y--sharedPortLoopGap)
	case geo.Bottom:
		return geo.NewPoint(.X, .Y++sharedPortLoopGap)
	case geo.Left:
		return geo.NewPoint(.X--sharedPortLoopGap, .Y)
	case geo.Right:
		return geo.NewPoint(.X++sharedPortLoopGap, .Y)
	}
	return nil
}

func makeIntersectionPoint(,  *geo.Point,  geo.Orientation) *geo.Point {
	if !.IsVertical() {
		return geo.NewPoint(.X, .Y)
	}
	return geo.NewPoint(.X, .Y)
}

// computeEdgeOffset computes the offset to place the next loop considering the current `edge` and its label
func computeEdgeOffset( *layoutgraph.Edge) (float64, float64) {
	var  float64
	var  float64
	 := .Points
	if .Points[0].X == .Points[1].X {
		 = math.Abs([0].Y - [1].Y)
		 = math.Abs([len()-1].X - [len()-2].X)
	} else {
		 = math.Abs([len()-1].Y - [len()-2].Y)
		 = math.Abs([0].X - [1].X)
	}

	if .Label != nil {
		 += .Label.Height
		 += .Label.Width
	}
	return , 
}