package world

import (
	"image"
	"image/color"
	"sync"
)

// StyleID references a fully-materialized style stored in StyleTable.
type StyleID int

const (
	// StyleIDInvalid means "no style". It should not be used for rendering.
	StyleIDInvalid StyleID = 0

	// Built-in default styles (stable IDs).
	StyleIDDefaultLine   StyleID = 1
	StyleIDDefaultCircle StyleID = 2
	StyleIDDefaultPoint  StyleID = 3
)

// Default priorities (smaller draws earlier), step=100.
const (
	DefaultPriorityLine   = 100
	DefaultPriorityCircle = 200
	DefaultPriorityPoint  = 300
)

var (
	transparentColor color.Color = &color.RGBA{A: 0}
)

// TransparentFill returns a reusable fully transparent color value.
//
// It is intended for callers that want to explicitly disable fill while still
// setting a non-nil FillColor override.
func TransparentFill() color.Color { return transparentColor }

// Style is a fully resolved style used by the renderer.
// All fields are concrete values; no "optional" markers here.
// Optionality is handled by StyleOverride during style creation.
type Style struct {
	// FillColor is used for Fill() operations (points/circles typically).
	// If nil, the renderer may treat it as "do not fill" depending on primitive.
	FillColor color.Color

	// StrokeColor is used for Stroke() operations (lines typically).
	// If nil, the renderer may treat it as "do not stroke" depending on primitive.
	StrokeColor color.Color

	// StrokeWidthPx is a screen-space stroke width in pixels.
	StrokeWidthPx float64

	// StrokeDashes is the dash pattern in pixels. nil/empty means "solid".
	StrokeDashes []float64

	// StrokeDashOffset is the dash phase in pixels.
	StrokeDashOffset float64

	// PointRadiusPx is a screen-space radius for Point markers.
	PointRadiusPx float64
}

// StyleOverride describes partial modifications applied to a base Style.
// Fields set to nil mean "do not override".
type StyleOverride struct {
	FillColor        color.Color
	StrokeColor      color.Color
	StrokeWidthPx    *float64
	StrokeDashes     *[]float64
	StrokeDashOffset *float64
	PointRadiusPx    *float64
}

// IsZero reports whether override does not specify any fields.
func (o StyleOverride) IsZero() bool {
	return o.FillColor == nil &&
		o.StrokeColor == nil &&
		o.StrokeWidthPx == nil &&
		o.StrokeDashes == nil &&
		o.StrokeDashOffset == nil &&
		o.PointRadiusPx == nil
}

// Apply applies override to base style and returns a new fully resolved style.
// It copies slices defensively to avoid aliasing.
func (o StyleOverride) Apply(base Style) Style {
	out := base

	if o.FillColor != nil {
		out.FillColor = o.FillColor
	}
	if o.StrokeColor != nil {
		out.StrokeColor = o.StrokeColor
	}
	if o.StrokeWidthPx != nil {
		out.StrokeWidthPx = *o.StrokeWidthPx
	}
	if o.StrokeDashes != nil {
		// Copy to avoid future mutation by caller.
		src := *o.StrokeDashes
		if src == nil {
			out.StrokeDashes = nil
		} else {
			dst := make([]float64, len(src))
			copy(dst, src)
			out.StrokeDashes = dst
		}
	}
	if o.StrokeDashOffset != nil {
		out.StrokeDashOffset = *o.StrokeDashOffset
	}
	if o.PointRadiusPx != nil {
		out.PointRadiusPx = *o.PointRadiusPx
	}

	return out
}

// StyleTable stores fully resolved styles and provides stable lookups by StyleID.
// It also holds three built-in defaults for Line/Circle/Point.
type StyleTable struct {
	mu     sync.RWMutex
	nextID StyleID
	styles map[StyleID]Style
}

// NewStyleTable creates a new style table with built-in default styles.
// The default values are intentionally simple and stable.
func NewStyleTable() *StyleTable {
	t := &StyleTable{
		nextID: StyleIDDefaultPoint + 1,
		styles: make(map[StyleID]Style, 16),
	}

	// Defaults: conservative, deterministic.
	// Colors: opaque black. (Callers can override.)
	white := color.RGBA{R: 255, G: 255, B: 255, A: 255}

	t.styles[StyleIDDefaultLine] = Style{
		FillColor:        nil,
		StrokeColor:      white,
		StrokeWidthPx:    2.0,
		StrokeDashes:     nil,
		StrokeDashOffset: 0,
		PointRadiusPx:    0,
	}

	t.styles[StyleIDDefaultCircle] = Style{
		FillColor:        white,
		StrokeColor:      nil,
		StrokeWidthPx:    0,
		StrokeDashes:     nil,
		StrokeDashOffset: 0,
		PointRadiusPx:    0,
	}

	t.styles[StyleIDDefaultPoint] = Style{
		FillColor:        white,
		StrokeColor:      nil,
		StrokeWidthPx:    0,
		StrokeDashes:     nil,
		StrokeDashOffset: 0,
		PointRadiusPx:    2.0,
	}

	return t
}

// Get returns a style by id.
func (t *StyleTable) Get(id StyleID) (Style, bool) {
	t.mu.RLock()
	defer t.mu.RUnlock()
	s, ok := t.styles[id]
	if !ok {
		return Style{}, false
	}
	// Defensive copy of slices.
	if s.StrokeDashes != nil {
		cp := make([]float64, len(s.StrokeDashes))
		copy(cp, s.StrokeDashes)
		s.StrokeDashes = cp
	}
	return s, true
}

// AddDerived creates a new style based on baseID with an override applied.
// It returns the new style ID.
func (t *StyleTable) AddDerived(baseID StyleID, override StyleOverride) StyleID {
	t.mu.Lock()
	defer t.mu.Unlock()

	base, ok := t.styles[baseID]
	if !ok {
		panic("StyleTable.AddDerived: unknown base style ID")
	}

	derived := override.Apply(base)

	id := t.nextID
	t.nextID++

	// Defensive copy of slices on store.
	if derived.StrokeDashes != nil {
		cp := make([]float64, len(derived.StrokeDashes))
		copy(cp, derived.StrokeDashes)
		derived.StrokeDashes = cp
	}

	t.styles[id] = derived
	return id
}

// AddStyle stores a fully resolved style as a new StyleID.
// It defensively copies slice fields.
func (t *StyleTable) AddStyle(s Style) StyleID {
	t.mu.Lock()
	defer t.mu.Unlock()

	id := t.nextID
	t.nextID++

	if s.StrokeDashes != nil {
		cp := make([]float64, len(s.StrokeDashes))
		copy(cp, s.StrokeDashes)
		s.StrokeDashes = cp
	}

	t.styles[id] = s
	return id
}

// Count returns the number of styles stored in the table.
// Intended for tests/diagnostics.
func (t *StyleTable) Count() int {
	t.mu.RLock()
	defer t.mu.RUnlock()
	return len(t.styles)
}

// BackgroundTileMode defines how the background image is tiled.
type BackgroundTileMode uint8

const (
	BackgroundTileNone BackgroundTileMode = iota
	BackgroundTileRepeat
)

// BackgroundAnchorMode defines whether the background image scrolls with the world or stays fixed to viewport.
type BackgroundAnchorMode uint8

const (
	BackgroundAnchorWorld BackgroundAnchorMode = iota
	BackgroundAnchorViewport
)

// BackgroundScaleMode defines how the background image is scaled.
// (Step 1: defined for API completeness; used later when rendering background image.)
type BackgroundScaleMode uint8

const (
	BackgroundScaleNone BackgroundScaleMode = iota
	BackgroundScaleFit
	BackgroundScaleFill
)

// StyleTheme describes a cohesive style set (theme) for rendering.
// Step 1: we store it in World and use it for background and default base styles.
// Step 2+: theme-relative overrides and background image drawing.
type StyleTheme interface {
	ID() string
	Name() string

	BackgroundColor() color.Color
	BackgroundImage() image.Image

	BackgroundTileMode() BackgroundTileMode
	BackgroundScaleMode() BackgroundScaleMode
	BackgroundAnchorMode() BackgroundAnchorMode

	PointStyle() Style
	LineStyle() Style
	CircleStyle() Style

	// Class overrides (relative to base kind style).
	// Return (override, true) when class is supported; (zero, false) means "no override".
	PointClassOverride(class PointClassID) (StyleOverride, bool)
	LineClassOverride(class LineClassID) (StyleOverride, bool)
	CircleClassOverride(class CircleClassID) (StyleOverride, bool)
}

// DefaultTheme is a conservative theme matching built-in default styles.
type DefaultTheme struct{}

func (DefaultTheme) ID() string   { return "default" }
func (DefaultTheme) Name() string { return "Default" }

func (DefaultTheme) BackgroundColor() color.Color { return color.RGBA{A: 255} }
func (DefaultTheme) BackgroundImage() image.Image { return nil }

func (DefaultTheme) BackgroundTileMode() BackgroundTileMode     { return BackgroundTileNone }
func (DefaultTheme) BackgroundScaleMode() BackgroundScaleMode   { return BackgroundScaleNone }
func (DefaultTheme) BackgroundAnchorMode() BackgroundAnchorMode { return BackgroundAnchorWorld }

func (DefaultTheme) PointStyle() Style {
	s, _ := NewStyleTable().Get(StyleIDDefaultPoint)
	return s
}
func (DefaultTheme) LineStyle() Style {
	s, _ := NewStyleTable().Get(StyleIDDefaultLine)
	return s
}
func (DefaultTheme) CircleStyle() Style {
	s, _ := NewStyleTable().Get(StyleIDDefaultCircle)
	return s
}

func (DefaultTheme) PointClassOverride(PointClassID) (StyleOverride, bool) {
	return StyleOverride{}, false
}
func (DefaultTheme) LineClassOverride(LineClassID) (StyleOverride, bool) {
	return StyleOverride{}, false
}
func (DefaultTheme) CircleClassOverride(CircleClassID) (StyleOverride, bool) {
	return StyleOverride{}, false
}

// This file provides two sample themes for demos and UI integration:
// LightTheme uses only background color, while DarkTheme also carries a
// prebuilt tiled texture image.

var (
	// ThemeLight is the shared light theme instance used by the client package.
	ThemeLight = &LightTheme{}
	// ThemeDark is the shared dark theme instance used by the client package.
	ThemeDark = NewDarkTheme()
)

// -----------------------------
// Helpers
// -----------------------------

// cRGBA constructs an sRGB color from 8-bit RGBA channels.
func cRGBA(r, g, b, a uint8) color.Color { return color.RGBA{R: r, G: g, B: b, A: a} }

// -----------------------------
// Light Theme (color only)
// -----------------------------

// LightTheme is a soft high-contrast theme intended for bright backgrounds.
type LightTheme struct{}

func (LightTheme) ID() string   { return "theme.light.v1" }
func (LightTheme) Name() string { return "Light (Soft)" }

func (LightTheme) BackgroundColor() color.Color { return cRGBA(244, 246, 248, 255) } // #F4F6F8
func (LightTheme) BackgroundImage() image.Image { return nil }

func (LightTheme) BackgroundTileMode() BackgroundTileMode     { return BackgroundTileNone }
func (LightTheme) BackgroundScaleMode() BackgroundScaleMode   { return BackgroundScaleNone }
func (LightTheme) BackgroundAnchorMode() BackgroundAnchorMode { return BackgroundAnchorWorld }

// Base styles per primitive kind (full Style, not override).
func (LightTheme) PointStyle() Style {
	return Style{
		FillColor:     cRGBA(32, 161, 145, 255), // soft teal
		StrokeColor:   nil,
		StrokeWidthPx: 0,
		PointRadiusPx: 3.0,
	}
}

func (LightTheme) LineStyle() Style {
	return Style{
		FillColor:        nil,
		StrokeColor:      cRGBA(70, 108, 196, 220), // soft blue
		StrokeWidthPx:    2.0,
		StrokeDashes:     nil,
		StrokeDashOffset: 0,
	}
}

func (LightTheme) CircleStyle() Style {
	return Style{
		FillColor:     cRGBA(133, 110, 201, 60),  // soft purple with low alpha
		StrokeColor:   cRGBA(133, 110, 201, 200), // soft purple
		StrokeWidthPx: 2.0,
	}
}

// Point class overrides.
func (LightTheme) PointClassOverride(class PointClassID) (StyleOverride, bool) {
	switch class {
	case PointClassDefault:
		return StyleOverride{}, false

	case PointClassTrackUnknown:
		// muted gray-blue
		return StyleOverride{
			FillColor:     cRGBA(120, 135, 160, 230),
			PointRadiusPx: new(3.0),
		}, true

	case PointClassTrackIncoming:
		// soft green
		return StyleOverride{
			FillColor:     cRGBA(76, 171, 107, 240),
			PointRadiusPx: new(3.5),
		}, true

	case PointClassTrackOutgoing:
		// soft orange
		return StyleOverride{
			FillColor:     cRGBA(222, 142, 70, 240),
			PointRadiusPx: new(3.5),
		}, true

	case PointClassUnidentifiedPlanet:
		// soft orange
		return StyleOverride{
			FillColor:     cRGBA(192, 192, 192, 255),
			PointRadiusPx: new(2.5),
		}, true

	default:
		return StyleOverride{}, false
	}
}

func (LightTheme) LineClassOverride(class LineClassID) (StyleOverride, bool) {
	switch class {
	case LineClassDefault:
		return StyleOverride{}, false

	case LineClassTrackIncoming:
		return StyleOverride{
			StrokeColor:   cRGBA(76, 171, 107, 220),
			StrokeWidthPx: new(2.5),
		}, true

	case LineCLassTrackOutgoing:
		return StyleOverride{
			StrokeColor:   cRGBA(222, 142, 70, 220),
			StrokeWidthPx: new(2.5),
		}, true

	case LineClassMeasurement:
		// dashed neutral line
		d := []float64{6, 4}
		return StyleOverride{
			StrokeColor:      cRGBA(100, 110, 125, 200),
			StrokeWidthPx:    new(1.8),
			StrokeDashes:     &d,
			StrokeDashOffset: new(0.),
		}, true

	default:
		return StyleOverride{}, false
	}
}

func (LightTheme) CircleClassOverride(class CircleClassID) (StyleOverride, bool) {
	switch class {
	case CircleClassDefault:
		return StyleOverride{}, false

	case CircleClassLocalPlanet:
		// blue
		return StyleOverride{
			FillColor:     cRGBA(70, 108, 196, 45),
			StrokeColor:   cRGBA(70, 108, 196, 220),
			StrokeWidthPx: new(2.2),
		}, true

	case CircleClassOthersPlanet:
		// orange
		return StyleOverride{
			FillColor:     cRGBA(222, 142, 70, 50),
			StrokeColor:   cRGBA(222, 142, 70, 220),
			StrokeWidthPx: new(2.2),
		}, true

	case CircleClassFreePlanet:
		// green
		return StyleOverride{
			FillColor:     cRGBA(76, 171, 107, 45),
			StrokeColor:   cRGBA(76, 171, 107, 220),
			StrokeWidthPx: new(2.2),
		}, true

	default:
		return StyleOverride{}, false
	}
}

// -----------------------------
// Dark Theme (color + tiled image)
// -----------------------------

// DarkTheme is a dark theme with an optional reusable background tile.
type DarkTheme struct {
	bg image.Image
}

// NewDarkTheme constructs a DarkTheme with its immutable texture tile prepared.
func NewDarkTheme() *DarkTheme {
	return &DarkTheme{bg: makeDarkBackgroundTile(96, 96)}
}

func (*DarkTheme) ID() string   { return "theme.dark.v1" }
func (*DarkTheme) Name() string { return "Dark (Soft + Texture)" }

func (*DarkTheme) BackgroundColor() color.Color { return cRGBA(30, 32, 38, 255) } // #1E2026
func (t *DarkTheme) BackgroundImage() image.Image {
	return nil
	// This image is immutable after creation.
	// return t.bg
}

func (*DarkTheme) BackgroundTileMode() BackgroundTileMode     { return BackgroundTileRepeat }
func (*DarkTheme) BackgroundScaleMode() BackgroundScaleMode   { return BackgroundScaleNone }
func (*DarkTheme) BackgroundAnchorMode() BackgroundAnchorMode { return BackgroundAnchorViewport }

// Base styles for dark theme.
func (*DarkTheme) PointStyle() Style {
	return Style{
		FillColor:     cRGBA(120, 214, 198, 255),
		StrokeColor:   nil,
		StrokeWidthPx: 0,
		PointRadiusPx: 3.0,
	}
}

func (*DarkTheme) LineStyle() Style {
	return Style{
		FillColor:        nil,
		StrokeColor:      cRGBA(155, 175, 235, 255),
		StrokeWidthPx:    2.0,
		StrokeDashes:     nil,
		StrokeDashOffset: 0,
	}
}

func (*DarkTheme) CircleStyle() Style {
	return Style{
		FillColor:     nil, // cRGBA(186, 160, 255, 255),
		StrokeColor:   cRGBA(186, 160, 255, 255),
		StrokeWidthPx: 2.0,
	}
}

// Point class overrides.
func (*DarkTheme) PointClassOverride(class PointClassID) (StyleOverride, bool) {
	switch class {
	case PointClassDefault:
		return StyleOverride{}, false

	case PointClassTrackUnknown:
		return StyleOverride{
			FillColor:     cRGBA(150, 160, 175, 255),
			PointRadiusPx: new(3.0),
		}, true

	case PointClassTrackIncoming:
		return StyleOverride{
			FillColor:     cRGBA(132, 219, 162, 255),
			PointRadiusPx: new(3.5),
		}, true

	case PointClassTrackOutgoing:
		return StyleOverride{
			FillColor:     cRGBA(245, 178, 120, 255),
			PointRadiusPx: new(3.5),
		}, true

	case PointClassUnidentifiedPlanet:
		return StyleOverride{
			FillColor:     cRGBA(192, 192, 192, 255),
			PointRadiusPx: new(2.5),
		}, true

	default:
		return StyleOverride{}, false
	}
}

func (*DarkTheme) LineClassOverride(class LineClassID) (StyleOverride, bool) {
	switch class {
	case LineClassDefault:
		return StyleOverride{}, false

	case LineClassTrackIncoming:
		return StyleOverride{
			StrokeColor:   cRGBA(132, 219, 162, 255),
			StrokeWidthPx: new(2.5),
		}, true

	case LineCLassTrackOutgoing:
		return StyleOverride{
			StrokeColor:   cRGBA(245, 178, 120, 255),
			StrokeWidthPx: new(2.5),
		}, true

	case LineClassMeasurement:
		d := []float64{6, 4}
		return StyleOverride{
			StrokeColor:      cRGBA(170, 175, 190, 255),
			StrokeWidthPx:    new(1.8),
			StrokeDashes:     &d,
			StrokeDashOffset: new(0.),
		}, true

	default:
		return StyleOverride{}, false
	}
}

func (*DarkTheme) CircleClassOverride(class CircleClassID) (StyleOverride, bool) {
	switch class {
	case CircleClassDefault:
		return StyleOverride{}, false

	case CircleClassLocalPlanet:
		return StyleOverride{
			FillColor:     cRGBA(155, 175, 235, 255),
			StrokeColor:   cRGBA(155, 175, 235, 255),
			StrokeWidthPx: new(2.2),
		}, true

	case CircleClassOthersPlanet:
		return StyleOverride{
			FillColor:     cRGBA(245, 178, 120, 255),
			StrokeColor:   cRGBA(245, 178, 120, 255),
			StrokeWidthPx: new(2.2),
		}, true

	case CircleClassFreePlanet:
		return StyleOverride{
			FillColor:     cRGBA(132, 219, 162, 255),
			StrokeColor:   cRGBA(132, 219, 162, 255),
			StrokeWidthPx: new(2.2),
		}, true

	default:
		return StyleOverride{}, false
	}
}

// makeDarkBackgroundTile creates a subtle, low-contrast texture tile.
// It is intentionally simple: a faint grid + a few diagonal accents.
// The tile is meant to be repeated.
func makeDarkBackgroundTile(w, h int) image.Image {
	if w <= 0 || h <= 0 {
		return nil
	}
	img := image.NewRGBA(image.Rect(0, 0, w, h))

	// Base is transparent; background color is drawn separately.
	// We draw subtle strokes with low alpha.
	grid := color.RGBA{R: 255, G: 255, B: 255, A: 12} // very faint
	diag := color.RGBA{R: 255, G: 255, B: 255, A: 18} // slightly stronger
	dots := color.RGBA{R: 255, G: 255, B: 255, A: 10} // faint dots

	// Grid spacing (pixels).
	const step = 12

	// Vertical grid lines.
	for x := 0; x < w; x += step {
		for y := 0; y < h; y++ {
			img.SetRGBA(x, y, grid)
		}
	}
	// Horizontal grid lines.
	for y := 0; y < h; y += step {
		for x := 0; x < w; x++ {
			img.SetRGBA(x, y, grid)
		}
	}

	// Diagonal accents (sparse).
	for x := 0; x < w; x += step * 2 {
		for i := 0; i < step && x+i < w && i < h; i++ {
			img.SetRGBA(x+i, i, diag)
		}
	}

	// Small dot pattern.
	for y := step / 2; y < h; y += step {
		for x := step / 2; x < w; x += step {
			img.SetRGBA(x, y, dots)
		}
	}

	return img
}
