package d2isometricimg

import (
	
	
)

const (
	maxRouteCaptionRects      = 19000
	maxRouteCaptionCandidates = 192
	maxRouteCaptionWork       = 2_000_000
	maxRouteCaptionGridRefs   = 250_000
	maxRouteCaptionCells      = 128
	routeCaptionGap           = .055
)

type routeCaptionCell struct{ x, z int }

// All obstacles share the camera's projection onto Y=0. This retains the
// footprint's orientation while accounting for different surface elevations.
// The grid is only a broad phase; separating axes reject false intersections.
type routeCaptionRect struct {
	x, z, ux, uz, hw, hd     float64
	left, right, front, back float64
	hard                     bool
	hull                     []routeCaptionPoint
}

type routeCaptionPlacer struct {
	rects      []routeCaptionRect
	grid       map[routeCaptionCell][]int
	wide       []int
	work, refs int
}

func newRouteCaptionPlacer() *routeCaptionPlacer {
	return &routeCaptionPlacer{grid: make(map[routeCaptionCell][]int)}
}

// Avoid reserves an axis-aligned footprint, for example a visible component or
// board heading. It changes caption placement only, never the underlying route.
func ( *routeCaptionPlacer) ( Vec, ,  float64) {
	if !captionFinite(.X, .Y, .Z, , ) ||  <= 0 ||  <= 0 {
		return
	}
	.reserve(captionRect(labelSurface{center: , width: , depth: }, true))
}

type routeCaptionLeg struct {
	a, b          Vec
	start, length float64
}

// Place keeps the supplied print dimensions and searches straight legs, along-
// leg offsets, then both sides of a route. Existing caption rectangles and any
// Avoid footprints are considered. Work exhaustion returns the best checked
// candidate (or the original anchor), rather than hiding or shrinking a label.
func ( *routeCaptionPlacer) ( []Vec, , ,  float64) labelSurface {
	if len() < 2 || !captionFinite(, , ) ||  <= 0 ||  <= 0 {
		return labelSurface{}
	}
	if !captionFinite([0].Y) {
		return labelSurface{}
	}
	 = math.Max(0, math.Min(1, ))
	if len() > 10000 {
		 = []Vec{[0], [len()-1]}
	}
	if len(.rects) >= maxRouteCaptionRects || .work+len() > maxRouteCaptionWork {
		return captionFallback(, , , )
	}
	.work += len()
	,  := captionLegs()
	if len() == 0 {
		return labelSurface{}
	}
	 :=  * 
	 := .35
	if  != .5 {
		 = 3
	}
	sort.SliceStable(, func(,  int) bool {
		 := func( routeCaptionLeg) float64 { return .length - math.Abs(.start+.length/2-)* }
		return ([]) > ([])
	})
	 := func( routeCaptionLeg) float64 {
		if  == .5 {
			return .length / 2
		}
		return math.Max(.length*.15, math.Min(.length*.85, -.start))
	}
	 := math.Max(.25, /2+.075)
	 := captionOnLeg([0], ([0]), , , , [0].Y)
	,  := , math.Inf(1)
	 := 0
	for ,  := range  {
		if  >= 8 ||  >= maxRouteCaptionCandidates || .work >= maxRouteCaptionWork {
			break
		}
		 := math.Min(.035, .length*.03)
		,  := /2+, .length-/2-
		if  >  {
			continue
		}
		 := math.Max(, math.Min(, ()))
		 :=  + 2*routeCaptionGap
		 := []float64{,  - ,  + ,  - 2*,  + 2*, , }
		for ,  := range []float64{, -,  +  + .1, - -  - .1} {
			for ,  := range  {
				if  <  ||  >  ||  >= maxRouteCaptionCandidates {
					continue
				}
				// Endpoint labels remain in their end of the route. If the original
				// long-leg heuristic itself lies outside this range, retain it as
				// the fallback rather than moving to the opposite endpoint.
				 := (.start + ) / 
				if  < .25 &&  > .4 ||  > .75 &&  < .6 {
					continue
				}
				 := captionOnLeg(, , , , , [0].Y)
				++
				,  := .score(captionRect(, false))
				if ! {
					.reserve(captionRect(, false))
					return 
				}
				if  <  {
					,  = , 
				}
				if  == 0 {
					.reserve(captionRect(, false))
					return 
				}
			}
		}
	}
	.reserve(captionRect(, false))
	return 
}

func captionFinite( ...float64) bool {
	for ,  := range  {
		if math.IsNaN() || math.IsInf(, 0) || math.Abs() > 1e9 {
			return false
		}
	}
	return true
}

func captionLegs( []Vec) ([]routeCaptionLeg, float64) {
	 := make([]routeCaptionLeg, 0, min(len()-1, 32))
	 := 0.
	for  := 1;  < len(); ++ {
		,  := [-1], []
		if !captionFinite(.X, .Y, .Z, .X, .Y, .Z) {
			continue
		}
		 := math.Hypot(.X-.X, .Z-.Z)
		if  < 1e-9 {
			continue
		}
		if len() > 0 {
			 := &[len()-1]
			,  := .b.X-.a.X, .b.Z-.a.Z
			,  := .X-.X, .Z-.Z
			if math.Hypot(.b.X-.X, .b.Z-.Z) < 1e-9 && math.Abs(*-*) <= 1e-9*.length* && *+* > 0 {
				.b, .length = , .length+
				 += 
				continue
			}
		}
		 = append(, routeCaptionLeg{a: , b: , start: , length: })
		 += 
	}
	return , 
}

func captionFallback( []Vec, , ,  float64) labelSurface {
	// Even after exhausting the shared budget, inspect only a bounded local
	// neighborhood and retain the text on a real route leg, with its full size.
	 := min(len()-2, int(*float64(len()-1)))
	for  := 0;  < 32; ++ {
		 :=  + (+1)/2
		if %2 != 0 {
			 =  - (+1)/2
		}
		if  < 0 || +1 >= len() {
			continue
		}
		,  := [], [+1]
		 := math.Hypot(.X-.X, .Z-.Z)
		if  > 1e-9 && captionFinite(.X, .Z, .X, .Z) {
			return captionOnLeg(routeCaptionLeg{a: , b: , length: }, /2, math.Max(.25, /2+.075), , , [0].Y)
		}
	}
	return labelSurface{}
}

func captionOnLeg( routeCaptionLeg, , , , ,  float64) labelSurface {
	,  := (.b.X-.a.X)/.length, (.b.Z-.a.Z)/.length
	,  := .a.X+*, .a.Z+*
	// Reversing an edge does not turn its text upside down.
	if  < -1e-6 || math.Abs() < 1e-6 &&  < 0 {
		,  = -, -
	}
	return labelSurface{center: Vec{X:  + *, Y:  + .006, Z:  - *}, width: , depth: , angle: math.Atan2(, )}
}

func captionRect( labelSurface,  bool) routeCaptionRect {
	 := captionProjection(.center)
	 := routeCaptionRect{x: .x, z: .z, ux: math.Cos(.angle), uz: math.Sin(.angle), hw: .width/2 + routeCaptionGap, hd: .depth/2 + routeCaptionGap, hard: }
	,  := math.Abs(.ux)*.hw+math.Abs(.uz)*.hd, math.Abs(.uz)*.hw+math.Abs(.ux)*.hd
	.left, .right, .front, .back = .x-, .x+, .z-, .z+
	return 
}

func captionOverlap(,  routeCaptionRect) bool {
	if .right <= .left || .right <= .left || .back <= .front || .back <= .front {
		return false
	}
	if len(.hull) > 0 || len(.hull) > 0 {
		return captionHullOverlap(, )
	}
	,  := .x-.x, .z-.z
	for ,  := range [][2]float64{{.ux, .uz}, {-.uz, .ux}, {.ux, .uz}, {-.uz, .ux}} {
		 := func( routeCaptionRect) float64 {
			return .hw*math.Abs(.ux*[0]+.uz*[1]) + .hd*math.Abs(-.uz*[0]+.ux*[1])
		}
		if math.Abs(*[0]+*[1]) >= ()+() {
			return false
		}
	}
	return true
}

func captionCells( routeCaptionRect) (int, int, int, int, bool) {
	if !captionFinite(.left, .right, .front, .back) {
		return 0, 0, 0, 0, false
	}
	, , ,  := int(math.Floor(.left)), int(math.Floor(.right)), int(math.Floor(.front)), int(math.Floor(.back))
	,  := int64()-int64()+1, int64()-int64()+1
	return , , , ,  > 0 &&  > 0 &&  <= maxRouteCaptionCells &&  <= maxRouteCaptionCells && * <= maxRouteCaptionCells
}

func ( *routeCaptionPlacer) ( routeCaptionRect) {
	if len(.rects) >= maxRouteCaptionRects {
		return
	}
	 := len(.rects)
	.rects = append(.rects, )
	, , , ,  := captionCells()
	if ! || .refs+(-+1)*(-+1) > maxRouteCaptionGridRefs {
		.wide = append(.wide, )
		return
	}
	for  := ;  <= ; ++ {
		for  := ;  <= ; ++ {
			 := routeCaptionCell{, }
			.grid[] = append(.grid[], )
			.refs++
		}
	}
}

func ( *routeCaptionPlacer) ( routeCaptionRect) (float64, bool) {
	 := make(map[int]bool)
	 := 0.
	 := func( int) bool {
		if .work >= maxRouteCaptionWork {
			return false
		}
		.work++
		if [] {
			return true
		}
		[] = true
		 := .rects[]
		// Mesh silhouettes have more separating axes than rectangles. Charge
		// their actual narrow-phase size so detailed artwork cannot bypass
		// the shared placement work limit.
		if (len(.hull) > 0 || len(.hull) > 0) && .right > .left && .right > .left && .back > .front && .back > .front {
			 := max(4, len(.hull)) + max(4, len(.hull))
			 :=  * 
			if .work+ > maxRouteCaptionWork {
				return false
			}
			.work += 
		}
		if captionOverlap(, ) {
			 := 1.
			if .hard {
				 = 10000
			}
			 += 
		}
		return true
	}
	, , , ,  := captionCells()
	if ! {
		for  := range .rects {
			if !() {
				return , false
			}
		}
		return , true
	}
	for ,  := range .wide {
		if !() {
			return , false
		}
	}
	for  := ;  <= ; ++ {
		for  := ;  <= ; ++ {
			if .work >= maxRouteCaptionWork {
				return , false
			}
			.work++
			for ,  := range .grid[routeCaptionCell{, }] {
				if !() {
					return , false
				}
			}
		}
	}
	return , true
}