package world

import (
	"errors"
	"fmt"
	"hash"
	"hash/fnv"
	"image/color"
	"math"
	"math/bits"
)

var (
	errInvalidCameraZoom = errors.New("invalid camera zoom")
)

const (
	// SCALE is the fixed-point multiplier used across the package.
	// A real value of 1.0 is represented as SCALE.
	SCALE = 1000

	// MIN_ZOOM and MAX_ZOOM define the supported zoom range in fixed-point form.
	// They are reserved for future validation/clamping logic.
	MIN_ZOOM = int(SCALE / 4)  // 0.25x
	MAX_ZOOM = int(SCALE * 32) // 32x

	// cellSizeMin and cellSizeMax bound the automatically selected grid cell size.
	cellSizeMin = 16 * SCALE
	cellSizeMax = 512 * SCALE
)

// Rect is a half-open rectangle in fixed-point world coordinates:
// [minX, maxX) x [minY, maxY).
type Rect struct {
	minX, maxX int
	minY, maxY int
}

// wrap maps value into the half-open interval [0, size).
// It supports negative input values and is used for torus coordinates.
func wrap(value, size int) int {
	r := value % size
	if r < 0 {
		r += size
	}
	return r
}

// clamp limits value to the closed interval [minValue, maxValue].
func clamp(value, minValue, maxValue int) int {
	if value < minValue {
		return minValue
	}
	if value > maxValue {
		return maxValue
	}
	return value
}

// ceilDiv returns ceil(a / b) for positive integers.
func ceilDiv(a, b int) int {
	return (a + b - 1) / b
}

// floorDiv returns floor(a / b) for b > 0 and supports negative a.
func floorDiv(a, b int) int {
	if b <= 0 {
		panic("floorDiv: non-positive divisor")
	}

	q := a / b
	r := a % b
	if r != 0 && a < 0 {
		q--
	}
	return q
}

// fixedPoint converts a real value into the package fixed-point representation
// using nearest-integer rounding.
func fixedPoint(v float64) int {
	return int(math.Round(v * SCALE))
}

// abs returns the absolute value of v.
func abs(v int) int {
	if v < 0 {
		return -v
	}
	return v
}

// viewportPxToWorldFixed converts a viewport size in pixels into the visible
// world size in fixed-point coordinates for the given fixed-point zoom.
func viewportPxToWorldFixed(viewportWidthPx, viewportHeightPx, cameraZoomFp int) (int, int) {
	return PixelSpanToWorldFixed(viewportWidthPx, cameraZoomFp),
		PixelSpanToWorldFixed(viewportHeightPx, cameraZoomFp)
}

// worldToCell maps a world coordinate to a grid cell index.
// The input coordinate is wrapped on the torus before the cell is computed.
// The function panics when the grid configuration is invalid.
func worldToCell(value, worldSize, cells, cellSize int) int {
	if cells <= 0 || cellSize <= 0 {
		panic(fmt.Sprintf("worldToCell: cells=%d cellSize=%d", cells, cellSize))
	}

	wrappedValue := wrap(value, worldSize)
	cell := wrappedValue / cellSize
	if cell >= cells {
		cell = cells - 1
	}
	return cell
}

// splitByWrap splits a half-open rectangle by torus wrap into 1..4 rectangles
// fully contained inside [0, W) x [0, H).
func splitByWrap(W, H, minX, maxX, minY, maxY int) []Rect {
	width := maxX - minX
	height := maxY - minY

	if width <= 0 || height <= 0 {
		return nil
	}

	if width >= W {
		minX = 0
		maxX = W
	}
	if height >= H {
		minY = 0
		maxY = H
	}

	type xPart struct {
		minX, maxX int
	}

	xParts := make([]xPart, 0, 2)

	if minX >= 0 && maxX <= W {
		xParts = append(xParts, xPart{minX: minX, maxX: maxX})
	} else {
		wrappedMinX := wrap(minX, W)
		wrappedMaxX := wrap(maxX, W)

		if wrappedMinX < wrappedMaxX {
			xParts = append(xParts, xPart{minX: wrappedMinX, maxX: wrappedMaxX})
		} else {
			xParts = append(xParts, xPart{minX: wrappedMinX, maxX: W})
			if wrappedMaxX > 0 {
				xParts = append(xParts, xPart{minX: 0, maxX: wrappedMaxX})
			}
		}
	}

	result := make([]Rect, 0, 4)

	for _, xp := range xParts {
		if minY >= 0 && maxY <= H {
			result = append(result, Rect{
				minX: xp.minX, maxX: xp.maxX,
				minY: minY, maxY: maxY,
			})
			continue
		}

		wrappedMinY := wrap(minY, H)
		wrappedMaxY := wrap(maxY, H)

		if wrappedMinY < wrappedMaxY {
			result = append(result, Rect{
				minX: xp.minX, maxX: xp.maxX,
				minY: wrappedMinY, maxY: wrappedMaxY,
			})
		} else {
			result = append(result, Rect{
				minX: xp.minX, maxX: xp.maxX,
				minY: wrappedMinY, maxY: H,
			})
			if wrappedMaxY > 0 {
				result = append(result, Rect{
					minX: xp.minX, maxX: xp.maxX,
					minY: 0, maxY: wrappedMaxY,
				})
			}
		}
	}

	return result
}

// PixelSpanToWorldFixed converts a span in pixels into a span in fixed-point
// world coordinates for the given fixed-point zoom.
func PixelSpanToWorldFixed(spanPx int, zoomFp int) int {
	return (spanPx * SCALE * SCALE) / zoomFp
}

// shortestWrappedDelta returns a canonical torus representation of the pair
// (from, to) along a single axis of length size.
//
// The resulting delta (b - a) is normalized into the half-open interval
// [-size/2, size/2). This makes the tie-case deterministic:
// when the points are exactly half a world apart, wrap is always applied in
// the direction that produces the negative delta.
func shortestWrappedDelta(from, to, size int) (a int, b int) {
	a, b = from, to

	delta := to - from
	half := size / 2

	if delta >= half {
		a += size
		return
	}
	if delta < -half {
		b += size
		return
	}

	return
}

// CameraZoomToWorldFixed converts a UI-facing zoom multiplier into the package
// fixed-point representation used by world-space calculations.
//
// The input zoom is expected to be a finite positive real value where 1.0 means
// the neutral zoom level. The result is rounded to the nearest fixed-point value.
//
// An error is returned when the input is invalid or when rounding would produce
// a non-positive fixed-point zoom.
func CameraZoomToWorldFixed(cameraZoom float64) (int, error) {
	if cameraZoom <= 0 || math.IsNaN(cameraZoom) || math.IsInf(cameraZoom, 0) {
		return 0, errInvalidCameraZoom
	}

	zoomFp := int(math.Round(cameraZoom * SCALE))
	if zoomFp <= 0 {
		return 0, errInvalidCameraZoom
	}

	return zoomFp, nil
}

// mustCameraZoomToWorldFixed is the panic-on-error variant of
// cameraZoomToWorldFixed. It is intended for internal code paths where invalid
// zoom is considered a programmer or integration error and must fail fast.
func mustCameraZoomToWorldFixed(cameraZoom float64) int {
	zoomFp, err := CameraZoomToWorldFixed(cameraZoom)
	if err != nil {
		panic(err)
	}
	return zoomFp
}

// ClampCameraNoWrapViewport clamps camera center so that the VIEWPORT world-rect
// stays within the bounded world [0..worldW) x [0..worldH), when possible.
//
// This is the correct clamp for user panning when wrap is disabled.
// Margins (expanded canvas) are intentionally ignored here; they may extend outside the world.
func ClampCameraNoWrapViewport(
	cameraXWorldFp, cameraYWorldFp int,
	viewportW, viewportH int,
	zoomFp int,
	worldW, worldH int,
) (int, int) {
	if zoomFp <= 0 {
		panic("ClampCameraNoWrapViewport: invalid zoom")
	}
	if viewportW < 0 || viewportH < 0 {
		panic("ClampCameraNoWrapViewport: negative viewport")
	}
	if worldW <= 0 || worldH <= 0 {
		panic("ClampCameraNoWrapViewport: invalid world size")
	}

	spanW := PixelSpanToWorldFixed(viewportW, zoomFp)
	spanH := PixelSpanToWorldFixed(viewportH, zoomFp)

	halfW := spanW / 2
	halfH := spanH / 2

	cameraXWorldFp = clampCameraAxis(cameraXWorldFp, worldW, halfW)
	cameraYWorldFp = clampCameraAxis(cameraYWorldFp, worldH, halfH)

	return cameraXWorldFp, cameraYWorldFp
}

// ClampCameraNoWrapExpanded clamps camera center so that the EXPANDED CANVAS world-rect
// (viewport + margins) stays within the bounded world, when possible.
//
// This is stricter than viewport-based clamp and can prevent panning when margins are large.
func ClampCameraNoWrapExpanded(
	cameraXWorldFp, cameraYWorldFp int,
	viewportW, viewportH int,
	marginX, marginY int,
	zoomFp int,
	worldW, worldH int,
) (int, int) {
	if zoomFp <= 0 {
		panic("ClampCameraNoWrapExpanded: invalid zoom")
	}
	if viewportW < 0 || viewportH < 0 || marginX < 0 || marginY < 0 {
		panic("ClampCameraNoWrapExpanded: negative sizes")
	}
	if worldW <= 0 || worldH <= 0 {
		panic("ClampCameraNoWrapExpanded: invalid world size")
	}

	canvasW := viewportW + 2*marginX
	canvasH := viewportH + 2*marginY

	spanW := PixelSpanToWorldFixed(canvasW, zoomFp)
	spanH := PixelSpanToWorldFixed(canvasH, zoomFp)

	halfW := spanW / 2
	halfH := spanH / 2

	cameraXWorldFp = clampCameraAxis(cameraXWorldFp, worldW, halfW)
	cameraYWorldFp = clampCameraAxis(cameraYWorldFp, worldH, halfH)

	return cameraXWorldFp, cameraYWorldFp
}

// clampCameraAxis clamps one camera axis for bounded-world rendering.
//
// If the visible span is larger than the world on that axis, the camera is
// forced to the world center to keep the result deterministic.
func clampCameraAxis(cam, worldSize, halfSpan int) int {
	// If viewport/span does not fit: force center.
	if 2*halfSpan > worldSize {
		return worldSize / 2
	}

	minCam := halfSpan
	maxCam := worldSize - halfSpan

	if cam < minCam {
		return minCam
	}
	if cam > maxCam {
		return maxCam
	}
	return cam
}

// ClampRenderParamsNoWrap clamps camera center in-place when wrap is disabled.
// It uses viewport-based clamp (NOT expanded) so panning remains possible even with margins.
func (w *World) ClampRenderParamsNoWrap(p *RenderParams) {
	if p == nil {
		return
	}
	allowWrap := true
	if p.Options != nil && p.Options.DisableWrapScroll {
		allowWrap = false
	}
	if allowWrap {
		return
	}

	zoomFp, err := p.CameraZoomFp()
	if err != nil || zoomFp <= 0 {
		return
	}

	cx, cy := ClampCameraNoWrapViewport(
		p.CameraXWorldFp, p.CameraYWorldFp,
		p.ViewportWidthPx, p.ViewportHeightPx,
		zoomFp,
		w.W, w.H,
	)
	p.CameraXWorldFp = cx
	p.CameraYWorldFp = cy
}

// PivotZoomCameraNoWrap adjusts camera center so that the world point under the cursor remains fixed
// when zoom changes from oldZoomFp to newZoomFp.
//
// Coordinate conventions:
// - CameraXWorldFp/YWorldFp is the center of the viewport in world-fixed units.
// - cursorXPx/cursorYPx are pixel coordinates relative to the top-left of the viewport.
// - viewportW/H are viewport size in pixels.
//
// This function does not clamp the result; caller should clamp for no-wrap mode using ClampCameraNoWrapViewport.
func PivotZoomCameraNoWrap(
	cameraXWorldFp, cameraYWorldFp int,
	viewportW, viewportH int,
	cursorXPx, cursorYPx int,
	oldZoomFp, newZoomFp int,
) (newCamX, newCamY int) {
	if oldZoomFp <= 0 || newZoomFp <= 0 {
		panic("PivotZoomCameraNoWrap: invalid zoom")
	}
	if viewportW <= 0 || viewportH <= 0 {
		panic("PivotZoomCameraNoWrap: invalid viewport")
	}

	// Offset of cursor from viewport center in pixels.
	offXPx := cursorXPx - viewportW/2
	offYPx := cursorYPx - viewportH/2

	// World-fixed per 1 pixel at each zoom.
	// (Conservative: integer arithmetic, consistent with PixelSpanToWorldFixed.)
	oldWorldPerPx := PixelSpanToWorldFixed(1, oldZoomFp)
	newWorldPerPx := PixelSpanToWorldFixed(1, newZoomFp)

	// World point under cursor before zoom:
	// world = camera + offsetPx * worldPerPx
	worldX := cameraXWorldFp + offXPx*oldWorldPerPx
	worldY := cameraYWorldFp + offYPx*oldWorldPerPx

	// Choose new camera so that the same world point stays under cursor:
	// camera' = world - offsetPx * newWorldPerPx
	newCamX = worldX - offXPx*newWorldPerPx
	newCamY = worldY - offYPx*newWorldPerPx
	return
}

// worldFixedToCameraZoom converts a fixed-point zoom value back into the
// UI-facing floating-point representation where 1.0 means neutral zoom.
func worldFixedToCameraZoom(zoomFp int) float64 {
	return float64(zoomFp) / float64(SCALE)
}

// requiredZoomToFitWorld returns the minimum fixed-point zoom needed so that
// a viewport span of viewportSpanPx pixels does not exceed a world span of
// worldSpanFp fixed-point units.
//
// The result is rounded up, not down, because the fit constraint must be
// satisfied conservatively: after correction, the visible world span must
// never be larger than the actual world span.
func requiredZoomToFitWorld(viewportSpanPx, worldSpanFp int) int {
	if viewportSpanPx < 0 {
		panic("requiredZoomToFitWorld: negative viewport span")
	}
	if worldSpanFp <= 0 {
		panic("requiredZoomToFitWorld: non-positive world span")
	}
	if viewportSpanPx == 0 {
		return 0
	}

	return ceilDiv(viewportSpanPx*SCALE*SCALE, worldSpanFp)
}

// correctCameraZoomFp corrects a fixed-point zoom value using two groups
// of constraints:
//
//  1. Fit-to-world constraints derived from viewport and world sizes.
//     These have the highest priority and prevent the viewport from becoming
//     larger than the world on any axis, which would otherwise expose wrap
//     on the visible user area.
//
//  2. Optional UI zoom bounds [minZoomFp, maxZoomFp].
//     A zero bound means "ignore this bound".
//     If fit-to-world requires a zoom larger than maxZoomFp, the fit constraint
//     wins and maxZoomFp is ignored for that case.
//
// The function returns either the corrected zoom or currentZoomFp unchanged
// when no correction is required.
func correctCameraZoomFp(
	currentZoomFp int,
	viewportWidthPx, viewportHeightPx int,
	worldWidthFp, worldHeightFp int,
	minZoomFp, maxZoomFp int,
) int {
	if currentZoomFp <= 0 {
		panic("correctCameraZoomFp: non-positive current zoom")
	}
	if viewportWidthPx < 0 || viewportHeightPx < 0 {
		panic("correctCameraZoomFp: negative viewport size")
	}
	if worldWidthFp <= 0 || worldHeightFp <= 0 {
		panic("correctCameraZoomFp: non-positive world size")
	}
	if minZoomFp < 0 || maxZoomFp < 0 {
		panic("correctCameraZoomFp: negative zoom bound")
	}
	if minZoomFp > 0 && maxZoomFp > 0 && minZoomFp > maxZoomFp {
		panic("correctCameraZoomFp: min zoom greater than max zoom")
	}

	// Start from the user zoom.
	result := currentZoomFp

	// Apply min bound first (only increases zoom, always valid).
	if minZoomFp > 0 && result < minZoomFp {
		result = minZoomFp
	}

	// Apply max bound tentatively. This can be overridden later by the anti-wrap constraint.
	if maxZoomFp > 0 && result > maxZoomFp {
		result = maxZoomFp
	}

	// If viewport is larger than the world on any axis at the current result zoom,
	// increase zoom to the minimum value that prevents wrap in the visible area.
	requiredFitX := requiredZoomToFitWorld(viewportWidthPx, worldWidthFp)
	requiredFitY := requiredZoomToFitWorld(viewportHeightPx, worldHeightFp)
	requiredFit := max(requiredFitX, requiredFitY)

	if requiredFit > 0 && result < requiredFit {
		result = requiredFit
	}

	// Re-apply max bound only if it does not conflict with the anti-wrap requirement.
	// If anti-wrap requires zoom > maxZoomFp, anti-wrap wins.
	if maxZoomFp > 0 && result > maxZoomFp && requiredFit <= maxZoomFp {
		result = maxZoomFp
	}

	return result
}

// CorrectCameraZoom adapts fixed-point zoom correction for UI code.
//
// currentZoom is the user-facing zoom multiplier in floating-point form.
// The result is returned in the same representation.
func (w *World) CorrectCameraZoom(
	currentZoom float64,
	viewportWidthPx int,
	viewportHeightPx int,
) float64 {
	currentZoomFp := mustCameraZoomToWorldFixed(currentZoom)
	correctedZoomFp := correctCameraZoomFp(
		currentZoomFp,
		viewportWidthPx,
		viewportHeightPx,
		w.W,
		w.H,
		MIN_ZOOM,
		MAX_ZOOM,
	)

	return worldFixedToCameraZoom(correctedZoomFp)
}

// u128 is an unsigned 128-bit integer for safe squared comparisons.
type u128 struct{ hi, lo uint64 }

// u128FromMul64 returns the full 128-bit product of two uint64 values.
func u128FromMul64(a, b uint64) u128 {
	hi, lo := bits.Mul64(a, b)
	return u128{hi: hi, lo: lo}
}

// u128Add returns the 128-bit sum a+b.
func u128Add(a, b u128) u128 {
	lo := a.lo + b.lo
	hi := a.hi + b.hi
	if lo < a.lo {
		hi++
	}
	return u128{hi: hi, lo: lo}
}

// u128Cmp compares two unsigned 128-bit values.
func u128Cmp(a, b u128) int {
	if a.hi < b.hi {
		return -1
	}
	if a.hi > b.hi {
		return 1
	}
	if a.lo < b.lo {
		return -1
	}
	if a.lo > b.lo {
		return 1
	}
	return 0
}

// abs64 returns the absolute value of x.
func abs64(x int64) int64 {
	if x < 0 {
		return -x
	}
	return x
}

// sqU128Int64 returns x*x as an unsigned 128-bit value.
func sqU128Int64(x int64) u128 {
	u := uint64(abs64(x))
	return u128FromMul64(u, u)
}

// distSqU128 returns dx*dx + dy*dy as an unsigned 128-bit value.
func distSqU128(dx, dy int64) u128 {
	return u128Add(sqU128Int64(dx), sqU128Int64(dy))
}

// shortestTorusDelta returns the shortest signed delta from a->b on a torus axis of size.
// It is deterministic in tie cases (size even, exactly half): chooses negative direction.
func shortestTorusDelta(a, b, size int) int64 {
	d := int64(b - a)
	s := int64(size)
	half := s / 2

	// Normalize d into (-s, s).
	d = d % s
	if d <= -half {
		d += s
	} else if d > half {
		d -= s
	}

	// Tie case when size even and d == +half: choose -half.
	if s%2 == 0 && d == half {
		d = -half
	}
	return d
}

// effectiveHitSlopPx resolves the per-primitive hit slop, falling back to def
// when the primitive does not override it explicitly.
func effectiveHitSlopPx(hitSlopPx int, def int) int {
	if hitSlopPx > 0 {
		return hitSlopPx
	}
	return def
}

// alphaNonZero reports whether c is non-nil and has a non-zero alpha channel.
func alphaNonZero(c color.Color) bool {
	if c == nil {
		return false
	}
	_, _, _, a := c.RGBA()
	return a != 0
}

// hitPoint performs point hit testing in world-fixed coordinates.
func hitPoint(p Point, cx, cy int, zoomFp int, allowWrap bool, worldW, worldH int) (Hit, bool) {
	slopPx := effectiveHitSlopPx(p.HitSlopPx, DefaultHitSlopPointPx)
	slopW := PixelSpanToWorldFixed(slopPx, zoomFp)

	var dx, dy int64
	if allowWrap {
		dx = shortestTorusDelta(p.X, cx, worldW)
		dy = shortestTorusDelta(p.Y, cy, worldH)
	} else {
		dx = int64(cx - p.X)
		dy = int64(cy - p.Y)
	}

	// Point is treated as a small disc: dist <= slop.
	ds := distSqU128(dx, dy)
	rs := sqU128Int64(int64(slopW))

	if u128Cmp(ds, rs) <= 0 {
		return Hit{
			ID:         p.Id,
			Kind:       KindPoint,
			Priority:   p.Priority,
			StyleID:    p.StyleID,
			DistanceSq: ds,
			X:          p.X,
			Y:          p.Y,
		}, true
	}
	return Hit{}, false
}

// hitCircle performs circle hit testing in world-fixed coordinates, including
// fill-vs-stroke semantics and minimum point-like radius handling.
func hitCircle(c Circle, effRadiusFp int, style Style, cx, cy int, zoomFp int, allowWrap bool, worldW, worldH int) (Hit, bool) {
	slopPx := effectiveHitSlopPx(c.HitSlopPx, DefaultHitSlopCirclePx)
	slopW := PixelSpanToWorldFixed(slopPx, zoomFp)

	fillVisible := alphaNonZero(style.FillColor)

	// Determine if circle is point-like at current zoom.
	// IMPORTANT: point-like disc behavior applies only for filled circles.
	rPx := worldSpanFixedToCanvasPx(effRadiusFp, zoomFp)
	pointLike := fillVisible && rPx < CirclePointLikeMinRadiusPx

	var dx, dy int64
	if allowWrap {
		dx = shortestTorusDelta(c.X, cx, worldW)
		dy = shortestTorusDelta(c.Y, cy, worldH)
	} else {
		dx = int64(cx - c.X)
		dy = int64(cy - c.Y)
	}

	ds := distSqU128(dx, dy)

	// Filled + point-like: treat as a disc with minimum visible radius + slop.
	if pointLike {
		// Treat as a disc with minimum visible radius in px.
		minRW := PixelSpanToWorldFixed(CirclePointLikeMinRadiusPx, zoomFp)
		effR := minRW
		if effRadiusFp > effR {
			effR = effRadiusFp
		}
		r := effR + slopW
		if u128Cmp(ds, sqU128Int64(int64(r))) <= 0 {
			return Hit{
				ID:         c.Id,
				Kind:       KindCircle,
				Priority:   c.Priority,
				StyleID:    c.StyleID,
				DistanceSq: ds,
				X:          c.X,
				Y:          c.Y,
				Radius:     effRadiusFp,
			}, true
		}
		return Hit{}, false
	}

	// Filled circle: hit-test by disc (surface).
	if fillVisible {
		r := effRadiusFp + slopW
		if u128Cmp(ds, sqU128Int64(int64(r))) <= 0 {
			return Hit{
				ID:         c.Id,
				Kind:       KindCircle,
				Priority:   c.Priority,
				StyleID:    c.StyleID,
				DistanceSq: ds,
				X:          c.X,
				Y:          c.Y,
				Radius:     effRadiusFp,
			}, true
		}
		return Hit{}, false
	}

	// Stroke-only circle: ring hit, but NEVER at exact center.
	// For very small circles, expand the effective radius to a minimum visible size
	// so that ring selection remains practical, while still excluding center.
	effR := effRadiusFp
	if rPx < CirclePointLikeMinRadiusPx {
		minRW := PixelSpanToWorldFixed(CirclePointLikeMinRadiusPx, zoomFp)
		if minRW > effR {
			effR = minRW
		}
	}

	low := effR - slopW
	// IMPORTANT: center must not hit for stroke-only circles.
	if low < 1 {
		low = 1
	}
	high := effR + slopW

	lowSq := sqU128Int64(int64(low))
	highSq := sqU128Int64(int64(high))

	if u128Cmp(ds, lowSq) >= 0 && u128Cmp(ds, highSq) <= 0 {
		return Hit{
			ID:         c.Id,
			Kind:       KindCircle,
			Priority:   c.Priority,
			StyleID:    c.StyleID,
			DistanceSq: ds,
			X:          c.X,
			Y:          c.Y,
			Radius:     effRadiusFp,
		}, true
	}
	return Hit{}, false
}

// hitLine performs line hit testing against the torus-shortest segment set used
// by the renderer.
func hitLine(l Line, cx, cy int, zoomFp int, allowWrap bool, worldW, worldH int) (Hit, bool) {
	slopPx := effectiveHitSlopPx(l.HitSlopPx, DefaultHitSlopLinePx)
	slopW := PixelSpanToWorldFixed(slopPx, zoomFp)

	// For wrap: compare against torus-shortest representation (same as rendering).
	// We test all segments produced by torusShortestLineSegments and take the best (min distance).
	segs := []lineSeg{{x1: l.X1, y1: l.Y1, x2: l.X2, y2: l.Y2}}
	if allowWrap {
		segs = torusShortestLineSegments(l, worldW, worldH)
	}

	best := Hit{}
	found := false

	for _, s := range segs {
		ds := distSqPointToSegmentU128(int64(cx), int64(cy), int64(s.x1), int64(s.y1), int64(s.x2), int64(s.y2))

		// Check ds <= slopW^2
		if u128Cmp(ds, sqU128Int64(int64(slopW))) <= 0 {
			h := Hit{
				ID:         l.Id,
				Kind:       KindLine,
				Priority:   l.Priority,
				StyleID:    l.StyleID,
				DistanceSq: ds,
				X1:         l.X1,
				Y1:         l.Y1,
				X2:         l.X2,
				Y2:         l.Y2,
			}
			if !found || hitLess(h, best) {
				best = h
				found = true
			}
		}
	}

	return best, found
}

// distSqPointToSegmentU128 computes squared distance from point P to segment AB using safe 128-bit comparisons.
func distSqPointToSegmentU128(px, py, ax, ay, bx, by int64) u128 {
	abx := bx - ax
	aby := by - ay
	apx := px - ax
	apy := py - ay

	// Degenerate segment => distance to point A.
	if abx == 0 && aby == 0 {
		return distSqU128(apx, apy)
	}

	dot := apx*abx + apy*aby
	if dot <= 0 {
		return distSqU128(apx, apy)
	}

	abLen2 := abx*abx + aby*aby
	if dot >= abLen2 {
		bpx := px - bx
		bpy := py - by
		return distSqU128(bpx, bpy)
	}

	// Perpendicular distance: dist^2 = cross^2 / |AB|^2, compare in 128 if needed by callers.
	// Here we actually return an exact rational? We return floor(cross^2 / abLen2) in integer domain
	// would lose precision. Instead, for HitTest we only compare dist^2 <= slop^2, but we also use
	// dist^2 for tie-breaking. We'll compute an approximate using integer division in 128/64.
	//
	// cross = AP x AB
	cross := apx*aby - apy*abx

	// cross^2 fits in u128, abLen2 fits in int64.
	c2 := sqU128Int64(cross)
	return u128DivByU64(c2, uint64(abLen2))
}

// u128DivByU64 returns floor(a / d) where d>0, producing u128 result.
// Here we only need it for tie-breaking (monotonic).
func u128DivByU64(a u128, d uint64) u128 {
	if d == 0 {
		panic("u128DivByU64: divide by zero")
	}
	// Simple long division for 128/64 -> 128 quotient (but high part will be small here).
	// We compute using two-step: divide high then combine.
	qHi := a.hi / d
	rHi := a.hi % d

	// Combine remainder with low as 128-bit number (rHi<<64 + lo) divided by d.
	// Use bits.Div64 for (hi, lo)/d.
	qLo, _ := bits.Div64(rHi, a.lo, d)

	return u128{hi: qHi, lo: qLo}
}

// torusShortestLineSegmentsInto converts a Line primitive into 1..4 canonical segments
// inside [0..worldW) x [0..worldH) that represent the torus-shortest polyline.
//
// It appends results into dst using tmp as an intermediate buffer.
// No allocations occur if dst/tmp have sufficient capacity (>=4).
func torusShortestLineSegmentsInto(dst, tmp []lineSeg, l Line, worldW, worldH int) ([]lineSeg, []lineSeg) {
	dst = dst[:0]
	tmp = tmp[:0]

	// Step 1: choose the torus-shortest representation in unwrapped space.
	ax, bx := shortestWrappedDelta(l.X1, l.X2, worldW)
	ay, by := shortestWrappedDelta(l.Y1, l.Y2, worldH)

	// Step 2: shift so that A is inside canonical [0..W) x [0..H).
	shiftX := floorDiv(ax, worldW) * worldW
	shiftY := floorDiv(ay, worldH) * worldH

	ax -= shiftX
	bx -= shiftX
	ay -= shiftY
	by -= shiftY

	dst = append(dst, lineSeg{x1: ax, y1: ay, x2: bx, y2: by})

	// Step 3: split by X boundary if needed (jump-aware).
	tmp = splitSegmentsByXInto(tmp, dst, worldW)

	// Step 4: split by Y boundary if needed (jump-aware).
	dst = splitSegmentsByYInto(dst, tmp, worldH)

	return dst, tmp
}

// torusShortestLineSegments is a compatibility wrapper that allocates.
// Prefer torusShortestLineSegmentsInto in hot paths.
func torusShortestLineSegments(l Line, worldW, worldH int) []lineSeg {
	dst := make([]lineSeg, 0, 4)
	tmp := make([]lineSeg, 0, 4)
	dst, _ = torusShortestLineSegmentsInto(dst, tmp, l, worldW, worldH)
	return dst
}

// splitSegmentsByXInto appends 1..2 segments for each input segment into out, without allocating.
// out is reset to length 0 by this function.
func splitSegmentsByXInto(out []lineSeg, segs []lineSeg, worldW int) []lineSeg {
	out = out[:0]

	for _, s := range segs {
		x1, y1, x2, y2 := s.x1, s.y1, s.x2, s.y2

		// After normalization, x1 is expected inside [0..worldW). Only x2 may be outside.
		if x2 >= 0 && x2 < worldW {
			out = append(out, s)
			continue
		}

		dx := x2 - x1
		dy := y2 - y1
		if dx == 0 {
			out = append(out, s)
			continue
		}

		if x2 >= worldW {
			// Crosses the right boundary at x=worldW, then reappears at x=0.
			bx := worldW
			num := bx - x1
			iy := y1 + (dy*num)/dx

			s1 := lineSeg{x1: x1, y1: y1, x2: worldW, y2: iy}
			s2 := lineSeg{x1: 0, y1: iy, x2: x2 - worldW, y2: y2}
			out = append(out, s1, s2)
			continue
		}

		// x2 < 0: crosses the left boundary at x=0, then reappears at x=worldW.
		bx := 0
		num := bx - x1
		iy := y1 + (dy*num)/dx

		s1 := lineSeg{x1: x1, y1: y1, x2: 0, y2: iy}
		s2 := lineSeg{x1: worldW, y1: iy, x2: x2 + worldW, y2: y2}
		out = append(out, s1, s2)
	}

	return out
}

// splitSegmentsByYInto appends 1..2 segments for each input segment into out, without allocating.
// out is reset to length 0 by this function.
func splitSegmentsByYInto(out []lineSeg, segs []lineSeg, worldH int) []lineSeg {
	out = out[:0]

	for _, s := range segs {
		x1, y1, x2, y2 := s.x1, s.y1, s.x2, s.y2

		// After normalization, y1 is expected inside [0..worldH). Only y2 may be outside.
		if y2 >= 0 && y2 < worldH {
			out = append(out, s)
			continue
		}

		dx := x2 - x1
		dy := y2 - y1
		if dy == 0 {
			out = append(out, s)
			continue
		}

		if y2 >= worldH {
			// Crosses the top boundary at y=worldH, then reappears at y=0.
			by := worldH
			num := by - y1
			ix := x1 + (dx*num)/dy

			s1 := lineSeg{x1: x1, y1: y1, x2: ix, y2: worldH}
			s2 := lineSeg{x1: ix, y1: 0, x2: x2, y2: y2 - worldH}
			out = append(out, s1, s2)
			continue
		}

		// y2 < 0: crosses the bottom boundary at y=0, then reappears at y=worldH.
		by := 0
		num := by - y1
		ix := x1 + (dx*num)/dy

		s1 := lineSeg{x1: x1, y1: y1, x2: ix, y2: 0}
		s2 := lineSeg{x1: ix, y1: worldH, x2: x2, y2: y2 + worldH}
		out = append(out, s1, s2)
	}

	return out
}

// mergeOverrides applies userOv on top of classOv, preserving the "nil means
// not specified" semantics used by StyleOverride fields.
func mergeOverrides(classOv, userOv StyleOverride) StyleOverride {
	out := classOv

	// Colors: nil means "unset"
	if userOv.FillColor != nil {
		out.FillColor = userOv.FillColor
	}
	if userOv.StrokeColor != nil {
		out.StrokeColor = userOv.StrokeColor
	}

	// Pointers: nil means "unset"
	if userOv.StrokeWidthPx != nil {
		out.StrokeWidthPx = userOv.StrokeWidthPx
	}
	if userOv.StrokeDashes != nil {
		out.StrokeDashes = userOv.StrokeDashes
	}
	if userOv.StrokeDashOffset != nil {
		out.StrokeDashOffset = userOv.StrokeDashOffset
	}
	if userOv.PointRadiusPx != nil {
		out.PointRadiusPx = userOv.PointRadiusPx
	}

	return out
}

// hashU64 writes v to the hash in little-endian form.
// We keep it manual to avoid extra allocations and dependencies.
func hashU64(h hash.Hash64, v uint64) {
	var b [8]byte
	b[0] = byte(v)
	b[1] = byte(v >> 8)
	b[2] = byte(v >> 16)
	b[3] = byte(v >> 24)
	b[4] = byte(v >> 32)
	b[5] = byte(v >> 40)
	b[6] = byte(v >> 48)
	b[7] = byte(v >> 56)
	_, _ = h.Write(b[:])
}

// hashBool writes a boolean value to the fingerprint stream.
func hashBool(h hash.Hash64, v bool) {
	if v {
		hashU64(h, 1)
	} else {
		hashU64(h, 0)
	}
}

// hashColor writes a color value to the fingerprint stream.
func hashColor(h hash.Hash64, c color.Color) {
	if c == nil {
		hashU64(h, 0)
		return
	}
	r, g, b, a := c.RGBA()
	hashU64(h, uint64(r))
	hashU64(h, uint64(g))
	hashU64(h, uint64(b))
	hashU64(h, uint64(a))
}

// fingerprint returns a stable hash of the override content.
//
// Notes on semantics:
//   - FillColor / StrokeColor: nil means "unset" (do not override). Transparent override is represented
//     by a non-nil color with alpha=0.
//   - Pointer fields (*float64, *[]float64) encode presence via nil/non-nil.
//   - StrokeDashes: nil pointer means "unset"; non-nil pointer to nil slice means "set to nil".
func (o StyleOverride) fingerprint() uint64 {
	h := fnv.New64a() // returns hash.Hash64

	// FillColor / StrokeColor
	hashBool(h, o.FillColor != nil)
	hashColor(h, o.FillColor)

	hashBool(h, o.StrokeColor != nil)
	hashColor(h, o.StrokeColor)

	// StrokeWidthPx
	hashBool(h, o.StrokeWidthPx != nil)
	if o.StrokeWidthPx != nil {
		hashU64(h, math.Float64bits(*o.StrokeWidthPx))
	}

	// StrokeDashes
	hashBool(h, o.StrokeDashes != nil)
	if o.StrokeDashes != nil {
		ds := *o.StrokeDashes
		if ds == nil {
			// Explicitly set to nil slice
			hashU64(h, 0xffffffffffffffff)
		} else {
			hashU64(h, uint64(len(ds)))
			for _, v := range ds {
				hashU64(h, math.Float64bits(v))
			}
		}
	}

	// StrokeDashOffset
	hashBool(h, o.StrokeDashOffset != nil)
	if o.StrokeDashOffset != nil {
		hashU64(h, math.Float64bits(*o.StrokeDashOffset))
	}

	// PointRadiusPx
	hashBool(h, o.PointRadiusPx != nil)
	if o.PointRadiusPx != nil {
		hashU64(h, math.Float64bits(*o.PointRadiusPx))
	}

	return h.Sum64()
}

// drawPointsFromPlan keeps backward compatibility for older tests/helpers.
func drawPointsFromPlan(drawer PrimitiveDrawer, plan RenderPlan, allowWrap bool) {
	// Default world sizes are unknown here, so this wrapper is no longer suitable for wrap-aware points.
	// Keep it for historical call sites only if they pass through Render().
	// Prefer calling drawPointsFromPlanWithRadius with world sizes.
	drawPointsFromPlanWithRadius(drawer, plan, 0, 0, DefaultRenderStyle().PointRadiusPx, allowWrap)
}

// drawPointsFromPlanWithRadius executes a points-only draw from an already built render plan,
// using the provided screen-space radius. If worldW/worldH are zero, wrap copies are disabled.
func drawPointsFromPlanWithRadius(drawer PrimitiveDrawer, plan RenderPlan, worldW, worldH int, radiusPx float64, allowWrap bool) {
	// Convert screen radius to world-fixed conservatively (ceil), so wrap copies are not missed.
	rPxInt := int(math.Ceil(radiusPx))
	if rPxInt < 0 {
		rPxInt = 0
	}
	rWorldFp := 0
	if rPxInt > 0 {
		rWorldFp = PixelSpanToWorldFixed(rPxInt, plan.ZoomFp)
	}

	for _, td := range plan.Tiles {
		if td.ClipW <= 0 || td.ClipH <= 0 {
			continue
		}

		points := make([]Point, 0, len(td.Candidates))
		for _, it := range td.Candidates {
			p, ok := it.(Point)
			if !ok {
				continue
			}
			points = append(points, p)
		}
		if len(points) == 0 {
			continue
		}

		type pointCopy struct {
			p  Point
			dx int
			dy int
		}
		copiesToDraw := make([]pointCopy, 0, len(points))

		for _, p := range points {
			var shifts []wrapShift
			if allowWrap {
				shifts = pointWrapShifts(p, rWorldFp, worldW, worldH)
			} else {
				shifts = []wrapShift{{dx: 0, dy: 0}}
			}
			for _, s := range shifts {
				if pointCopyIntersectsTile(p, rWorldFp, s.dx, s.dy, td.Tile) {
					copiesToDraw = append(copiesToDraw, pointCopy{p: p, dx: s.dx, dy: s.dy})
				}
			}
		}

		if len(copiesToDraw) == 0 {
			continue
		}

		drawer.Save()
		drawer.ClipRect(float64(td.ClipX), float64(td.ClipY), float64(td.ClipW), float64(td.ClipH))

		for _, pc := range copiesToDraw {
			p := pc.p

			px := worldSpanFixedToCanvasPx((p.X+td.Tile.OffsetX+pc.dx)-plan.WorldRect.minX, plan.ZoomFp)
			py := worldSpanFixedToCanvasPx((p.Y+td.Tile.OffsetY+pc.dy)-plan.WorldRect.minY, plan.ZoomFp)

			drawer.AddPoint(float64(px), float64(py), radiusPx)
		}

		drawer.Fill()
		drawer.Restore()
	}
}

// pointWrapShifts returns the torus-copy offsets required for a point marker
// whose visible disc may cross world edges.
func pointWrapShifts(p Point, rWorldFp, worldW, worldH int) []wrapShift {
	// If world sizes are unknown, do not generate wrap copies.
	if worldW <= 0 || worldH <= 0 {
		return []wrapShift{{dx: 0, dy: 0}}
	}

	xShifts := []int{0}
	yShifts := []int{0}

	if p.X+rWorldFp >= worldW {
		xShifts = append(xShifts, -worldW)
	}
	if p.X-rWorldFp < 0 {
		xShifts = append(xShifts, worldW)
	}

	if p.Y+rWorldFp >= worldH {
		yShifts = append(yShifts, -worldH)
	}
	if p.Y-rWorldFp < 0 {
		yShifts = append(yShifts, worldH)
	}

	out := make([]wrapShift, 0, len(xShifts)*len(yShifts))
	for _, dx := range xShifts {
		for _, dy := range yShifts {
			out = append(out, wrapShift{dx: dx, dy: dy})
		}
	}
	return out
}

// pointCopyIntersectsTile reports whether a particular wrapped point copy can
// contribute pixels inside the given world tile.
func pointCopyIntersectsTile(p Point, rWorldFp, dx, dy int, tile WorldTile) bool {
	segMinX := tile.OffsetX + tile.Rect.minX
	segMaxX := tile.OffsetX + tile.Rect.maxX
	segMinY := tile.OffsetY + tile.Rect.minY
	segMaxY := tile.OffsetY + tile.Rect.maxY

	px := p.X + tile.OffsetX + dx
	py := p.Y + tile.OffsetY + dy

	minX := px - rWorldFp
	maxX := px + rWorldFp
	minY := py - rWorldFp
	maxY := py + rWorldFp

	if maxX <= segMinX || minX >= segMaxX || maxY <= segMinY || minY >= segMaxY {
		return false
	}
	return true
}

// drawCirclesFromPlan executes a circles-only draw from an already built render plan.
func drawCirclesFromPlan(drawer PrimitiveDrawer, plan RenderPlan, worldW, worldH int, allowWrap bool, circleRadiusScaleFp int) {
	for _, td := range plan.Tiles {
		if td.ClipW <= 0 || td.ClipH <= 0 {
			continue
		}

		// Filter only circles; skip tiles that have no circles.
		circles := make([]Circle, 0, len(td.Candidates))
		for _, it := range td.Candidates {
			c, ok := it.(Circle)
			if !ok {
				continue
			}
			circles = append(circles, c)
		}
		if len(circles) == 0 {
			continue
		}

		// Determine which circle copies actually intersect this tile segment.
		type circleCopy struct {
			c  Circle
			dx int
			dy int
		}
		copiesToDraw := make([]circleCopy, 0, len(circles))

		for _, c := range circles {
			var shifts []wrapShift
			effRadius := circleRadiusEffFp(c.Radius, circleRadiusScaleFp)
			if allowWrap {
				shifts = circleWrapShifts(c.X, c.Y, effRadius, worldW, worldH)
			} else {
				shifts = []wrapShift{{dx: 0, dy: 0}}
			}
			for _, s := range shifts {
				if circleCopyIntersectsTile(c.X, c.Y, effRadius, s.dx, s.dy, td.Tile, worldW, worldH) {
					copiesToDraw = append(copiesToDraw, circleCopy{c: c, dx: s.dx, dy: s.dy})
				}
			}
		}

		if len(copiesToDraw) == 0 {
			continue
		}

		drawer.Save()
		drawer.ClipRect(float64(td.ClipX), float64(td.ClipY), float64(td.ClipW), float64(td.ClipH))

		for _, cc := range copiesToDraw {
			c := cc.c

			// Project the circle center for this tile copy (tile offset + wrap shift).
			cxPx := worldSpanFixedToCanvasPx((c.X+td.Tile.OffsetX+cc.dx)-plan.WorldRect.minX, plan.ZoomFp)
			cyPx := worldSpanFixedToCanvasPx((c.Y+td.Tile.OffsetY+cc.dy)-plan.WorldRect.minY, plan.ZoomFp)

			// Radius is a world span.
			rPx := worldSpanFixedToCanvasPx(c.Radius, plan.ZoomFp)

			drawer.AddCircle(float64(cxPx), float64(cyPx), float64(rPx))
		}

		drawer.Fill()
		drawer.Restore()
	}
}

// wrapShift stores one torus-copy offset in world-fixed coordinates.
type wrapShift struct {
	dx int
	dy int
}

// circleWrapShiftsInto appends required torus-copy shifts for a circle into dst and returns the resulting slice.
// It never allocates if dst has enough capacity.
//
// The 0-shift is always included. Additional copies are included when the circle's bbox crosses world edges.
func circleWrapShiftsInto(dst []wrapShift, cx, cy, radiusFp, worldW, worldH int) []wrapShift {
	dst = dst[:0]

	// Always include the original.
	dst = append(dst, wrapShift{dx: 0, dy: 0})

	if radiusFp <= 0 {
		return dst
	}

	minX := cx - radiusFp
	maxX := cx + radiusFp
	minY := cy - radiusFp
	maxY := cy + radiusFp

	needLeft := minX < 0
	needRight := maxX > worldW
	needTop := minY < 0
	needBottom := maxY > worldH

	// X-only copies.
	if needLeft {
		dst = append(dst, wrapShift{dx: +worldW, dy: 0})
	}
	if needRight {
		dst = append(dst, wrapShift{dx: -worldW, dy: 0})
	}

	// Y-only copies.
	if needTop {
		dst = append(dst, wrapShift{dx: 0, dy: +worldH})
	}
	if needBottom {
		dst = append(dst, wrapShift{dx: 0, dy: -worldH})
	}

	// Corner copies (combine X and Y).
	if (needLeft || needRight) && (needTop || needBottom) {
		var dxs [2]int
		dxn := 0
		if needLeft {
			dxs[dxn] = +worldW
			dxn++
		}
		if needRight {
			dxs[dxn] = -worldW
			dxn++
		}

		var dys [2]int
		dyn := 0
		if needTop {
			dys[dyn] = +worldH
			dyn++
		}
		if needBottom {
			dys[dyn] = -worldH
			dyn++
		}

		for i := 0; i < dxn; i++ {
			for j := 0; j < dyn; j++ {
				dst = append(dst, wrapShift{dx: dxs[i], dy: dys[j]})
			}
		}
	}

	return dst
}

// circleWrapShifts is a compatibility wrapper that allocates.
// Prefer circleWrapShiftsInto in hot paths.
func circleWrapShifts(cx, cy, radiusFp, worldW, worldH int) []wrapShift {
	var dst []wrapShift
	return circleWrapShiftsInto(dst, cx, cy, radiusFp, worldW, worldH)
}

// circleCopyIntersectsTile checks whether the circle copy (shifted by dx/dy) intersects the tile segment.
// We use the tile's unwrapped segment bounds: [offset+rect.min, offset+rect.max) per axis.
func circleCopyIntersectsTile(cx, cy, radiusFp, dx, dy int, tile WorldTile, worldW, worldH int) bool {
	// Unwrapped tile segment bounds.
	segMinX := tile.OffsetX + tile.Rect.minX
	segMaxX := tile.OffsetX + tile.Rect.maxX
	segMinY := tile.OffsetY + tile.Rect.minY
	segMaxY := tile.OffsetY + tile.Rect.maxY

	// Circle bbox in the same unwrapped space (apply shift + tile offset).
	cx = cx + tile.OffsetX + dx
	cy = cy + tile.OffsetY + dy

	minX := cx - radiusFp
	maxX := cx + radiusFp
	minY := cy - radiusFp
	maxY := cy + radiusFp

	// Treat bbox as half-open for intersection checks.
	if maxX <= segMinX || minX >= segMaxX || maxY <= segMinY || minY >= segMaxY {
		return false
	}
	return true
}
