package world

import (
	"errors"
	"fmt"
)

var (
	errBadCoordinate = errors.New("invalid coordinates")
	errBadRadius     = errors.New("invalid radius")
	errNoSuchObject  = errors.New("no such object")
	errIDExhausted   = errors.New("primitive id exhausted")
)

// indexState stores the viewport-dependent parameters required to rebuild the
// spatial grid after object or style-affecting mutations.
type indexState struct {
	initialized bool
	viewportW   int
	viewportH   int
	zoomFp      int
}

// derivedStyleKey identifies one cached derived style by its base style and
// stable override fingerprint.
type derivedStyleKey struct {
	base StyleID
	fp   uint64
}

// World stores torus world dimensions, all registered objects,
// and the grid-based spatial index built for the current viewport settings.
type World struct {
	W, H       int // Fixed-point world size.
	grid       [][][]MapItem
	cellSize   int
	rows, cols int
	objects    map[PrimitiveID]MapItem

	styles                    *StyleTable
	theme                     StyleTheme
	themeDefaultLineStyleID   StyleID
	themeDefaultCircleStyleID StyleID
	themeDefaultPointStyleID  StyleID

	circleRadiusScaleFp int // fixed-point, 1.0 == SCALE

	// PrimitiveID allocator state.
	nextID  PrimitiveID
	freeIDs []PrimitiveID

	// Index dirty flag for add/remove updates.
	indexDirty bool
	index      indexState

	renderState  rendererIncrementalState
	derivedCache map[derivedStyleKey]StyleID

	// scratch buffers for hot render path (single goroutine assumption).
	scratchDrawItems   []drawItem
	scratchWrapShifts  []wrapShift
	scratchLineSegs    []lineSeg
	scratchLineSegsTmp []lineSeg

	// candidate dedupe scratch (hot path for plan building).
	candStamp         []uint32
	candEpoch         uint32
	scratchCandidates []MapItem
}

// NewWorld constructs a new world with the given real dimensions.
// The dimensions are converted to fixed-point and must be positive.
func NewWorld(width, height int) *World {
	if width <= 0 || height <= 0 {
		panic("invalid width or height")
	}
	return &World{
		W:        width * SCALE,
		H:        height * SCALE,
		cellSize: 1,
		objects:  make(map[PrimitiveID]MapItem),
		styles:   NewStyleTable(),
		theme:    DefaultTheme{},

		// At startup, "theme defaults" point to conservative built-ins.
		themeDefaultLineStyleID:   StyleIDDefaultLine,
		themeDefaultCircleStyleID: StyleIDDefaultCircle,
		themeDefaultPointStyleID:  StyleIDDefaultPoint,

		circleRadiusScaleFp: SCALE,

		derivedCache: make(map[derivedStyleKey]StyleID, 128),

		nextID: 1, // 0 is reserved as "invalid"
	}
}

// allocID allocates a new PrimitiveID using a free-list (reusable IDs) and a monotonic counter.
// It returns an error if the ID space is exhausted.
func (w *World) allocID() (PrimitiveID, error) {
	if n := len(w.freeIDs); n > 0 {
		id := w.freeIDs[n-1]
		w.freeIDs = w.freeIDs[:n-1]
		return id, nil
	}
	if w.nextID == PrimitiveID(^uint32(0)) {
		return 0, errIDExhausted
	}
	id := w.nextID
	w.nextID++
	return id, nil
}

// freeID returns an id back to the pool. It is safe to call only after the object is removed.
func (w *World) freeID(id PrimitiveID) {
	if id == 0 {
		return
	}
	w.freeIDs = append(w.freeIDs, id)
}

// checkCoordinate reports whether the fixed-point coordinate (xf, yf)
// lies inside the world bounds: [0, W) x [0, H).
func (w *World) checkCoordinate(xf, yf int) bool {
	if xf < 0 || xf >= w.W || yf < 0 || yf >= w.H {
		return false
	}
	return true
}

// AddStyleLine creates a new line style derived from the default line style.
func (w *World) AddStyleLine(override StyleOverride) StyleID {
	return w.styles.AddDerived(StyleIDDefaultLine, override)
}

// AddStyleCircle creates a new circle style derived from the default circle style.
func (w *World) AddStyleCircle(override StyleOverride) StyleID {
	return w.styles.AddDerived(StyleIDDefaultCircle, override)
}

// AddStylePoint creates a new point style derived from the default point style.
func (w *World) AddStylePoint(override StyleOverride) StyleID {
	return w.styles.AddDerived(StyleIDDefaultPoint, override)
}

// Theme returns the current theme. It is never nil.
func (w *World) Theme() StyleTheme {
	if w.theme == nil {
		return DefaultTheme{}
	}
	return w.theme
}

// SetTheme updates the world's current theme.
func (w *World) SetTheme(theme StyleTheme) {
	if theme == nil {
		theme = DefaultTheme{}
	}
	w.theme = theme

	// Drop derived cache when theme changes to avoid unbounded growth.
	for k := range w.derivedCache {
		delete(w.derivedCache, k)
	}

	// Materialize theme base styles as new IDs.
	w.themeDefaultLineStyleID = w.styles.AddStyle(theme.LineStyle())
	w.themeDefaultCircleStyleID = w.styles.AddStyle(theme.CircleStyle())
	w.themeDefaultPointStyleID = w.styles.AddStyle(theme.PointStyle())

	w.refreshThemeManagedStyles()

	// Full redraw to apply new background and base styles.
	w.renderState.Reset()
	w.ForceFullRedrawNext()
}

func (w *World) themeBaseStyleID(base styleBase) StyleID {
	switch base {
	case styleBaseThemeLine:
		return w.themeDefaultLineStyleID
	case styleBaseThemeCircle:
		return w.themeDefaultCircleStyleID
	case styleBaseThemePoint:
		return w.themeDefaultPointStyleID
	default:
		return StyleIDInvalid
	}
}

// refreshThemeManagedStyles recomputes resolved StyleID values for primitives
// that track the active theme rather than a fixed explicit style.
func (w *World) refreshThemeManagedStyles() {
	th := w.Theme()

	for id, it := range w.objects {
		switch v := it.(type) {
		case Point:
			if v.Base == styleBaseFixed {
				continue
			}
			baseID := w.themeBaseStyleID(v.Base)
			if baseID == StyleIDInvalid {
				continue
			}

			classOv := StyleOverride{}
			if ov, ok := th.PointClassOverride(v.Class); ok {
				classOv = ov
			}
			merged := mergeOverrides(classOv, v.Override)
			v.StyleID = w.derivedStyleID(baseID, merged)

			w.objects[id] = v

		case Circle:
			if v.Base == styleBaseFixed {
				continue
			}
			baseID := w.themeBaseStyleID(v.Base)
			if baseID == StyleIDInvalid {
				continue
			}

			classOv := StyleOverride{}
			if ov, ok := th.CircleClassOverride(v.Class); ok {
				classOv = ov
			}
			merged := mergeOverrides(classOv, v.Override)
			v.StyleID = w.derivedStyleID(baseID, merged)

			w.objects[id] = v

		case Line:
			if v.Base == styleBaseFixed {
				continue
			}
			baseID := w.themeBaseStyleID(v.Base)
			if baseID == StyleIDInvalid {
				continue
			}

			classOv := StyleOverride{}
			if ov, ok := th.LineClassOverride(v.Class); ok {
				classOv = ov
			}
			merged := mergeOverrides(classOv, v.Override)
			v.StyleID = w.derivedStyleID(baseID, merged)

			w.objects[id] = v

		default:
			panic("refreshThemeManagedStyles: unknown item type")
		}
	}

	w.ForceFullRedrawNext()
}

// derivedStyleID resolves a derived style, reusing the per-world cache when an
// identical base style and override combination has already been materialized.
func (w *World) derivedStyleID(base StyleID, ov StyleOverride) StyleID {
	if ov.IsZero() {
		return base
	}
	k := derivedStyleKey{base: base, fp: ov.fingerprint()}
	if id, ok := w.derivedCache[k]; ok {
		return id
	}
	id := w.styles.AddDerived(base, ov)
	w.derivedCache[k] = id
	return id
}

// Remove deletes an object by id. It returns errNoSuchObject if the id is unknown.
// It marks the spatial index dirty and triggers an autonomous rebuild if possible.
func (w *World) Remove(id PrimitiveID) error {
	if _, ok := w.objects[id]; !ok {
		return errNoSuchObject
	}
	delete(w.objects, id)
	w.freeID(id)

	w.indexDirty = true
	w.rebuildIndexFromLastState()
	return nil
}

// Reindex forces rebuilding the spatial index (grid) if the renderer has enough last-state
// information to choose a grid cell size. If not enough info exists yet, it keeps indexDirty=true.
func (w *World) Reindex() {
	w.indexDirty = true
	w.rebuildIndexFromLastState()
}

// rebuildIndexFromLastState rebuilds the index using last known viewport sizes and zoomFp
// from renderer state. If that state is not initialized, it does nothing.
func (w *World) rebuildIndexFromLastState() {
	if !w.indexDirty {
		return
	}
	if !w.index.initialized {
		return
	}
	if w.index.viewportW <= 0 || w.index.viewportH <= 0 || w.index.zoomFp <= 0 {
		return
	}

	w.rebuildIndexForViewportZoomFp(w.index.viewportW, w.index.viewportH, w.index.zoomFp)
	w.indexDirty = false
}

// AddPoint validates and stores a point primitive in the world.
// The input coordinates are given in real world units and are converted
// to fixed-point before validation.
func (w *World) AddPoint(x, y float64, opts ...PointOpt) (PrimitiveID, error) {
	xf := fixedPoint(x)
	yf := fixedPoint(y)
	if ok := w.checkCoordinate(xf, yf); !ok {
		return 0, errBadCoordinate
	}

	o := defaultPointOptions()
	for _, opt := range opts {
		if opt != nil {
			opt(&o)
		}
	}
	// styleID := g.resolvePointStyleID(o)

	id, err := w.allocID()
	if err != nil {
		return 0, err
	}

	obj := Point{
		Id:       id,
		X:        xf,
		Y:        yf,
		Priority: o.Priority,
		// StyleID:   styleID,
		HitSlopPx: o.HitSlopPx,
	}

	obj.Class = o.Class
	if o.hasStyleID {
		obj.Base = styleBaseFixed
		obj.StyleID = o.StyleID
		obj.Override = StyleOverride{}
	} else {
		obj.Base = styleBaseThemePoint
		baseID := w.themeDefaultPointStyleID

		// class override from current theme (may be absent)
		classOv := StyleOverride{}
		if th := w.Theme(); th != nil {
			if ov, ok := th.PointClassOverride(obj.Class); ok {
				classOv = ov
			}
		}

		merged := mergeOverrides(classOv, o.Override)
		obj.Override = o.Override // store only user override; class override comes from theme
		obj.StyleID = w.derivedStyleID(baseID, merged)
	}

	w.objects[id] = obj
	w.indexDirty = true
	w.rebuildIndexFromLastState()

	return id, nil
}

// AddCircle validates and stores a circle primitive in the world.
// The center and radius are given in real world units and are converted
// to fixed-point before validation. A zero radius is allowed.
func (w *World) AddCircle(x, y, r float64, opts ...CircleOpt) (PrimitiveID, error) {
	xf := fixedPoint(x)
	yf := fixedPoint(y)

	if ok := w.checkCoordinate(xf, yf); !ok {
		return 0, errBadCoordinate
	}
	if r < 0 {
		return 0, errBadRadius
	}

	o := defaultCircleOptions()
	for _, opt := range opts {
		if opt != nil {
			opt(&o)
		}
	}

	id, err := w.allocID()
	if err != nil {
		return 0, err
	}

	obj := Circle{
		Id:        id,
		X:         xf,
		Y:         yf,
		Radius:    fixedPoint(r),
		Priority:  o.Priority,
		HitSlopPx: o.HitSlopPx,
	}

	obj.Class = o.Class
	if o.hasStyleID {
		obj.Base = styleBaseFixed
		obj.StyleID = o.StyleID
		obj.Override = StyleOverride{}
	} else {
		obj.Base = styleBaseThemeCircle
		baseID := w.themeDefaultCircleStyleID

		// class override from current theme (may be absent)
		classOv := StyleOverride{}
		if th := w.Theme(); th != nil {
			if ov, ok := th.CircleClassOverride(obj.Class); ok {
				classOv = ov
			}
		}

		merged := mergeOverrides(classOv, o.Override)
		obj.Override = o.Override // store only user override; class override comes from theme
		obj.StyleID = w.derivedStyleID(baseID, merged)
	}

	w.objects[id] = obj
	w.indexDirty = true
	w.rebuildIndexFromLastState()

	return id, nil
}

// AddLine validates and stores a line primitive in the world.
// The endpoints are given in real world units and are converted
// to fixed-point before validation.
func (w *World) AddLine(x1, y1, x2, y2 float64, opts ...LineOpt) (PrimitiveID, error) {
	x1f := fixedPoint(x1)
	y1f := fixedPoint(y1)
	x2f := fixedPoint(x2)
	y2f := fixedPoint(y2)

	if ok := w.checkCoordinate(x1f, y1f); !ok {
		return 0, errBadCoordinate
	}
	if ok := w.checkCoordinate(x2f, y2f); !ok {
		return 0, errBadCoordinate
	}

	o := defaultLineOptions()
	for _, opt := range opts {
		if opt != nil {
			opt(&o)
		}
	}
	// styleID := g.resolveLineStyleID(o)

	id, err := w.allocID()
	if err != nil {
		return 0, err
	}

	obj := Line{
		Id:       id,
		X1:       x1f,
		Y1:       y1f,
		X2:       x2f,
		Y2:       y2f,
		Priority: o.Priority,
		// StyleID:   styleID,
		HitSlopPx: o.HitSlopPx,
	}

	obj.Class = o.Class
	if o.hasStyleID {
		obj.Base = styleBaseFixed
		obj.StyleID = o.StyleID
		obj.Override = StyleOverride{}
	} else {
		obj.Base = styleBaseThemeLine
		baseID := w.themeDefaultLineStyleID

		// class override from current theme (may be absent)
		classOv := StyleOverride{}
		if th := w.Theme(); th != nil {
			if ov, ok := th.LineClassOverride(obj.Class); ok {
				classOv = ov
			}
		}

		merged := mergeOverrides(classOv, o.Override)
		obj.Override = o.Override // store only user override; class override comes from theme
		obj.StyleID = w.derivedStyleID(baseID, merged)
	}

	w.objects[id] = obj
	w.indexDirty = true
	w.rebuildIndexFromLastState()

	return id, nil
}

// worldToCellX converts a fixed-point X coordinate to a grid column index.
func (w *World) worldToCellX(x int) int {
	return worldToCell(x, w.W, w.cols, w.cellSize)
}

// worldToCellY converts a fixed-point Y coordinate to a grid row index.
func (w *World) worldToCellY(y int) int {
	return worldToCell(y, w.H, w.rows, w.cellSize)
}

// resetGrid recreates the spatial grid with the given cell size
// and clears all previous indexing state.
func (w *World) resetGrid(cellSize int) {
	if cellSize <= 0 {
		panic("resetGrid: invalid cell size")
	}

	w.cellSize = cellSize
	w.cols = ceilDiv(w.W, w.cellSize)
	w.rows = ceilDiv(w.H, w.cellSize)

	w.grid = make([][][]MapItem, w.rows)
	for row := range w.grid {
		w.grid[row] = make([][]MapItem, w.cols)
	}
}

// indexObject inserts a single object into all grid cells touched by its
// indexing representation. Points are inserted into one cell, while circles
// and lines are inserted by their torus-aware bbox coverage.
func (w *World) indexObject(o MapItem) {
	switch mapItem := o.(type) {
	case Point:
		col := w.worldToCellX(mapItem.X)
		row := w.worldToCellY(mapItem.Y)
		w.grid[row][col] = append(w.grid[row][col], mapItem)

	case Line:
		x1 := mapItem.X1
		y1 := mapItem.Y1
		x2 := mapItem.X2
		y2 := mapItem.Y2

		x1, x2 = shortestWrappedDelta(x1, x2, w.W)
		y1, y2 = shortestWrappedDelta(y1, y2, w.H)

		minX := min(x1, x2)
		maxX := max(x1, x2)
		minY := min(y1, y2)
		maxY := max(y1, y2)

		if minX == maxX {
			maxX++
		}
		if minY == maxY {
			maxY++
		}

		w.indexBBox(mapItem, minX, maxX, minY, maxY)

	case Circle:
		rEff := circleRadiusEffFp(mapItem.Radius, w.circleRadiusScaleFp)
		w.indexBBox(mapItem,
			mapItem.X-rEff, mapItem.X+rEff,
			mapItem.Y-rEff, mapItem.Y+rEff,
		)

	default:
		panic(fmt.Sprintf("indexing: unknown element %T", mapItem))
	}
}

// indexBBox indexes an object by a half-open fixed-point bbox that may cross
// torus boundaries. The bbox is split into wrapped in-world rectangles first,
// then all covered grid cells are populated.
func (w *World) indexBBox(o MapItem, minX, maxX, minY, maxY int) {
	rects := splitByWrap(w.W, w.H, minX, maxX, minY, maxY)
	for _, r := range rects {
		colStart := w.worldToCellX(r.minX)
		colEnd := w.worldToCellX(r.maxX - 1)

		rowStart := w.worldToCellY(r.minY)
		rowEnd := w.worldToCellY(r.maxY - 1)

		for col := colStart; col <= colEnd; col++ {
			for row := rowStart; row <= rowEnd; row++ {
				w.grid[row][col] = append(w.grid[row][col], o)
			}
		}
	}
}

// IndexOnViewportChange is called when UI window sizes are changed.
// cameraZoom is float64, converted inside world to fixed-point.
func (w *World) IndexOnViewportChange(viewportWidthPx, viewportHeightPx int, cameraZoom float64) {
	zoomFp := mustCameraZoomToWorldFixed(cameraZoom) // must-version is ok here, matches your existing code

	// Remember params for autonomous reindex after Add/Remove.
	w.index.initialized = true
	w.index.viewportW = viewportWidthPx
	w.index.viewportH = viewportHeightPx
	w.index.zoomFp = zoomFp

	w.rebuildIndexForViewportZoomFp(viewportWidthPx, viewportHeightPx, zoomFp)
	w.indexDirty = false
}

// rebuildIndexForViewportZoomFp rebuilds the spatial grid for a particular
// viewport size and fixed-point zoom.
//
// The chosen cell size is derived from the currently visible world span and
// then clamped into the package-wide cell-size bounds.
func (w *World) rebuildIndexForViewportZoomFp(viewportWidthPx, viewportHeightPx int, zoomFp int) {
	worldWidth, worldHeight := viewportPxToWorldFixed(viewportWidthPx, viewportHeightPx, zoomFp)

	cellsAcrossMin := 8
	visibleMin := min(worldWidth, worldHeight)

	cellSize := visibleMin / cellsAcrossMin
	cellSize = clamp(cellSize, cellSizeMin, cellSizeMax)

	w.resetGrid(cellSize)

	for _, o := range w.objects {
		w.indexObject(o)
	}
}

// CircleRadiusScaleFp returns the current circle radius scale (fixed-point).
func (w *World) CircleRadiusScaleFp() int {
	return w.circleRadiusScaleFp
}

// SetCircleRadiusScaleFp sets the circle radius scale (fixed-point).
// scaleFp must be > 0. This affects indexing, rendering and hit-testing,
// so it forces a full redraw and triggers reindex when possible.
func (w *World) SetCircleRadiusScaleFp(scaleFp int) error {
	if scaleFp <= 0 {
		return errors.New("invalid circle radius scale")
	}
	if scaleFp == w.circleRadiusScaleFp {
		return nil
	}
	w.circleRadiusScaleFp = scaleFp

	// Radius scale affects circle bbox => spatial index must be rebuilt.
	w.indexDirty = true
	w.rebuildIndexFromLastState()

	// Visual change => full redraw.
	w.ForceFullRedrawNext()
	return nil
}

// circleRadiusEffFp converts a raw circle radius (world-fixed) into effective radius (world-fixed)
// using g.circleRadiusScaleFp.
func circleRadiusEffFp(rawRadiusFp, circleRadiusScaleFp int) int {
	// Use int64 to avoid overflow.
	v := (int64(rawRadiusFp) * int64(circleRadiusScaleFp)) / int64(SCALE)
	if v < 0 {
		return 0
	}
	if v > int64(^uint(0)>>1) {
		// Defensive; should never happen with sane inputs on 64-bit.
		return int(^uint(0) >> 1)
	}
	return int(v)
}

// PointClassID classifies Point primitives for theme-level style overrides.
//
// Themes may use the class to derive a final style from the point base style
// without changing the primitive geometry itself.
type PointClassID uint8

const (
	// PointClassDefault selects the theme's default point styling.
	PointClassDefault PointClassID = iota
	// PointClassTrackUnknown marks a point as an unknown track marker.
	PointClassTrackUnknown
	// PointClassTrackIncoming marks a point as an incoming track marker.
	PointClassTrackIncoming
	// PointClassTrackOutgoing marks a point as an outgoing track marker.
	PointClassTrackOutgoing
	// PointClassUnidentifiedPlanet marks an unidentified planet without visivle size.
	PointClassUnidentifiedPlanet
)

// LineClassID classifies Line primitives for theme-level style overrides.
type LineClassID uint8

const (
	// LineClassDefault selects the theme's default line styling.
	LineClassDefault LineClassID = iota
	// LineClassTrackIncoming marks a line as an incoming track.
	LineClassTrackIncoming
	// LineCLassTrackOutgoing marks a line as an outgoing track.
	// The unusual spelling is preserved for backward compatibility.
	LineCLassTrackOutgoing
	// LineClassMeasurement marks a line as a measurement helper.
	LineClassMeasurement
)

// CircleClassID classifies Circle primitives for theme-level style overrides.
type CircleClassID uint8

const (
	// CircleClassDefault selects the theme's default circle styling.
	CircleClassDefault CircleClassID = iota
	// CircleClassLocalPlanet marks a circle as a player-owned planet.
	CircleClassLocalPlanet
	// CircleClassOthersPlanet marks a circle as an occupied planet.
	CircleClassOthersPlanet
	// CircleClassFreePlanet marks a circle as a free planet.
	CircleClassFreePlanet
)

// PrimitiveID is a compact stable identifier for primitives stored in the World.
// It is allocated by the World and may be reused after deletion (free-list).
type PrimitiveID uint32

// MapItem is the common interface implemented by all world primitives.
type MapItem interface {
	ID() PrimitiveID
}

// styleBase describes how a primitive resolves its base style across theme changes.
type styleBase uint8

const (
	styleBaseFixed styleBase = iota
	styleBaseThemeLine
	styleBaseThemeCircle
	styleBaseThemePoint
)

// Point is a point primitive in fixed-point world coordinates.
type Point struct {
	Id   PrimitiveID
	X, Y int

	// Priority controls per-object draw ordering. Smaller draws earlier.
	Priority int
	// StyleID references a resolved style in the world's style table.
	StyleID StyleID
	// Theme style binding. If Base==styleBaseFixed => StyleID stays as-is across theme changes.
	Base styleBase
	// Override is applied relative to current theme base style (only when Base is theme* and Override is non-zero).
	Override StyleOverride
	Class    PointClassID

	// HitSlopPx expands hit-test radius in screen pixels (per-object override).
	// 0 means "use primitive default".
	HitSlopPx int
}

// Line is a line segment primitive in fixed-point world coordinates.
type Line struct {
	Id     PrimitiveID
	X1, Y1 int
	X2, Y2 int

	// Priority controls per-object draw ordering. Smaller draws earlier.
	Priority int
	// StyleID references a resolved style in the world's style table.
	StyleID StyleID
	// Theme style binding. If Base==styleBaseFixed => StyleID stays as-is across theme changes.
	Base styleBase
	// Override is applied relative to current theme base style (only when Base is theme* and Override is non-zero).
	Override StyleOverride
	Class    LineClassID

	// HitSlopPx expands hit-test radius in screen pixels (per-object override).
	// 0 means "use primitive default".
	HitSlopPx int
}

// Circle is a circle primitive in fixed-point world coordinates.
type Circle struct {
	Id     PrimitiveID
	X, Y   int
	Radius int

	// Priority controls per-object draw ordering. Smaller draws earlier.
	Priority int
	// StyleID references a resolved style in the world's style table.
	StyleID StyleID
	// Theme style binding. If Base==styleBaseFixed => StyleID stays as-is across theme changes.
	Base styleBase
	// Override is applied relative to current theme base style (only when Base is theme* and Override is non-zero).
	Override StyleOverride
	Class    CircleClassID

	// HitSlopPx expands hit-test radius in screen pixels (per-object override).
	// 0 means "use primitive default".
	HitSlopPx int
}

// ID returns the point identifier.
func (p Point) ID() PrimitiveID { return p.Id }

// ID returns the line identifier.
func (l Line) ID() PrimitiveID { return l.Id }

// ID returns the circle identifier.
func (c Circle) ID() PrimitiveID { return c.Id }

// MinX returns the minimum X endpoint coordinate of the line.
func (l Line) MinX() int { return min(l.X1, l.X2) }

// MaxX returns the maximum X endpoint coordinate of the line.
func (l Line) MaxX() int { return max(l.X1, l.X2) }

// MinY returns the minimum Y endpoint coordinate of the line.
func (l Line) MinY() int { return min(l.Y1, l.Y2) }

// MaxY returns the maximum Y endpoint coordinate of the line.
func (l Line) MaxY() int { return max(l.Y1, l.Y2) }

// MinX returns the minimum X coordinate of the circle bbox.
func (c Circle) MinX() int { return c.X - c.Radius }

// MaxX returns the maximum X coordinate of the circle bbox.
func (c Circle) MaxX() int { return c.X + c.Radius }

// MinY returns the minimum Y coordinate of the circle bbox.
func (c Circle) MinY() int { return c.Y - c.Radius }

// MaxY returns the maximum Y coordinate of the circle bbox.
func (c Circle) MaxY() int { return c.Y + c.Radius }

// PointOpt applies optional point-construction parameters to PointOptions.
type PointOpt func(*PointOptions)

// PointOptions stores optional arguments accepted by World.AddPoint.
//
// Defaults are resolved before applying user-provided PointOpt values.
type PointOptions struct {
	Priority int
	StyleID  StyleID
	Override StyleOverride
	Class    PointClassID

	HitSlopPx int

	hasStyleID bool
}

// defaultPointOptions returns the default option set used by World.AddPoint.
func defaultPointOptions() PointOptions {
	return PointOptions{
		Priority: DefaultPriorityPoint,
		StyleID:  StyleIDDefaultPoint,
		Class:    PointClassDefault,
	}
}

// PointWithPriority sets point draw priority.
//
// Lower priorities render earlier within the same tile.
func PointWithPriority(p int) PointOpt {
	return func(o *PointOptions) {
		o.Priority = p
	}
}

// PointWithStyleID forces the point to use a pre-registered style.
func PointWithStyleID(id StyleID) PointOpt {
	return func(o *PointOptions) {
		o.StyleID = id
		o.hasStyleID = true
		// Explicit style ID wins over overrides.
		o.Override = StyleOverride{}
	}
}

// PointWithClass selects the theme class used for point style resolution.
func PointWithClass(c PointClassID) PointOpt {
	return func(o *PointOptions) { o.Class = c }
}

// PointWithStyleOverride applies a user override on top of the resolved point base style.
//
// If PointWithStyleID is also supplied, the explicit style ID wins.
func PointWithStyleOverride(ov StyleOverride) PointOpt {
	return func(o *PointOptions) {
		o.Override = ov
	}
}

// PointWithHitSlopPx overrides the default point hit slop in screen pixels.
func PointWithHitSlopPx(px int) PointOpt {
	return func(o *PointOptions) { o.HitSlopPx = px }
}

// CircleOpt applies optional circle-construction parameters to CircleOptions.
type CircleOpt func(*CircleOptions)

// CircleOptions stores optional arguments accepted by World.AddCircle.
type CircleOptions struct {
	Priority int
	StyleID  StyleID
	Override StyleOverride
	Class    CircleClassID

	HitSlopPx int

	hasStyleID bool
}

// defaultCircleOptions returns the default option set used by World.AddCircle.
func defaultCircleOptions() CircleOptions {
	return CircleOptions{
		Priority: DefaultPriorityCircle,
		StyleID:  StyleIDDefaultCircle,
		Class:    CircleClassDefault,
	}
}

// CircleWithPriority sets circle draw priority.
func CircleWithPriority(p int) CircleOpt {
	return func(o *CircleOptions) {
		o.Priority = p
	}
}

// CircleWithStyleID forces the circle to use a pre-registered style.
func CircleWithStyleID(id StyleID) CircleOpt {
	return func(o *CircleOptions) {
		o.StyleID = id
		o.hasStyleID = true
		o.Override = StyleOverride{}
	}
}

// CircleWithClass selects the theme class used for circle style resolution.
func CircleWithClass(c CircleClassID) CircleOpt {
	return func(o *CircleOptions) { o.Class = c }
}

// CircleWithStyleOverride applies a user override on top of the resolved circle base style.
func CircleWithStyleOverride(ov StyleOverride) CircleOpt {
	return func(o *CircleOptions) {
		o.Override = ov
	}
}

// CircleWithHitSlopPx overrides the default circle hit slop in screen pixels.
func CircleWithHitSlopPx(px int) CircleOpt {
	return func(o *CircleOptions) { o.HitSlopPx = px }
}

// LineOpt applies optional line-construction parameters to LineOptions.
type LineOpt func(*LineOptions)

// LineOptions stores optional arguments accepted by World.AddLine.
type LineOptions struct {
	Priority int
	StyleID  StyleID
	Override StyleOverride
	Class    LineClassID

	HitSlopPx int

	hasStyleID bool
}

// defaultLineOptions returns the default option set used by World.AddLine.
func defaultLineOptions() LineOptions {
	return LineOptions{
		Priority: DefaultPriorityLine,
		StyleID:  StyleIDDefaultLine,
		Class:    LineClassDefault,
	}
}

// LineWithPriority sets line draw priority.
func LineWithPriority(p int) LineOpt {
	return func(o *LineOptions) {
		o.Priority = p
	}
}

// LineWithStyleID forces the line to use a pre-registered style.
func LineWithStyleID(id StyleID) LineOpt {
	return func(o *LineOptions) {
		o.StyleID = id
		o.hasStyleID = true
		o.Override = StyleOverride{}
	}
}

// LineWithClass selects the theme class used for line style resolution.
func LineWithClass(c LineClassID) LineOpt {
	return func(o *LineOptions) { o.Class = c }
}

// LineWithStyleOverride applies a user override on top of the resolved line base style.
func LineWithStyleOverride(ov StyleOverride) LineOpt {
	return func(o *LineOptions) {
		o.Override = ov
	}
}

// LineWithHitSlopPx overrides the default line hit slop in screen pixels.
func LineWithHitSlopPx(px int) LineOpt {
	return func(o *LineOptions) { o.HitSlopPx = px }
}
