package world

import (
	"github.com/fogleman/gg"
	"github.com/stretchr/testify/require"
	"image"
	"image/color"
	"testing"
)

type benchBgTheme struct {
	img       image.Image
	anchor    BackgroundAnchorMode
	tileMode  BackgroundTileMode
	scaleMode BackgroundScaleMode
}

func (t benchBgTheme) ID() string   { return "benchbg" }
func (t benchBgTheme) Name() string { return "benchbg" }

func (t benchBgTheme) BackgroundColor() color.Color { return color.RGBA{A: 255} }
func (t benchBgTheme) BackgroundImage() image.Image { return t.img }

func (t benchBgTheme) BackgroundTileMode() BackgroundTileMode     { return t.tileMode }
func (t benchBgTheme) BackgroundScaleMode() BackgroundScaleMode   { return t.scaleMode }
func (t benchBgTheme) BackgroundAnchorMode() BackgroundAnchorMode { return t.anchor }

func (t benchBgTheme) PointStyle() Style {
	return Style{FillColor: color.RGBA{A: 255}, PointRadiusPx: 2}
}
func (t benchBgTheme) LineStyle() Style {
	return Style{StrokeColor: color.RGBA{A: 255}, StrokeWidthPx: 1}
}
func (t benchBgTheme) CircleStyle() Style {
	return Style{FillColor: color.RGBA{A: 255}, StrokeColor: color.RGBA{A: 255}, StrokeWidthPx: 1}
}

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

// BenchmarkRender_IncrementalPan_NoBackground benchmarks render Incremental Pan No Background.
func BenchmarkRender_IncrementalPan_NoBackground(b *testing.B) {
	w := NewWorld(600, 600)
	w.IndexOnViewportChange(1200, 800, 1.0)

	// Some primitives to keep it realistic but not dominant.
	for i := 0; i < 200; i++ {
		_, _ = w.AddPoint(float64(i%600), float64((i*7)%600))
	}
	w.Reindex()

	dc := gg.NewContext(1200, 800)
	drawer := &GGDrawer{DC: dc}

	params := RenderParams{
		ViewportWidthPx:  1000,
		ViewportHeightPx: 700,
		MarginXPx:        250,
		MarginYPx:        175,
		CameraXWorldFp:   300 * SCALE,
		CameraYWorldFp:   300 * SCALE,
		CameraZoom:       1.0,
		Options: &RenderOptions{
			Incremental: &IncrementalPolicy{
				AllowShiftOnly:   false,
				CoalesceUpdates:  false,
				MaxCatchUpAreaPx: 0,
				RenderBudgetMs:   0,
			},
		},
	}

	// Initial render (commit state).
	_ = w.Render(drawer, params)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		params.CameraXWorldFp += 1 * SCALE
		_ = w.Render(drawer, params)
	}
}

// BenchmarkRender_IncrementalPan_BackgroundRepeat_WorldAnchor_ScaleNone benchmarks render Incremental Pan Background Repeat World Anchor Scale None.
func BenchmarkRender_IncrementalPan_BackgroundRepeat_WorldAnchor_ScaleNone(b *testing.B) {
	benchRenderBg(b, BackgroundAnchorWorld, BackgroundTileRepeat, BackgroundScaleNone)
}

// BenchmarkRender_IncrementalPan_BackgroundRepeat_WorldAnchor_ScaleFit benchmarks render Incremental Pan Background Repeat World Anchor Scale Fit.
func BenchmarkRender_IncrementalPan_BackgroundRepeat_WorldAnchor_ScaleFit(b *testing.B) {
	benchRenderBg(b, BackgroundAnchorWorld, BackgroundTileRepeat, BackgroundScaleFit)
}

// BenchmarkRender_IncrementalPan_BackgroundRepeat_ViewportAnchor_ScaleNone benchmarks render Incremental Pan Background Repeat Viewport Anchor Scale None.
func BenchmarkRender_IncrementalPan_BackgroundRepeat_ViewportAnchor_ScaleNone(b *testing.B) {
	benchRenderBg(b, BackgroundAnchorViewport, BackgroundTileRepeat, BackgroundScaleNone)
}

func benchRenderBg(b *testing.B, anchor BackgroundAnchorMode, tile BackgroundTileMode, scale BackgroundScaleMode) {
	w := NewWorld(600, 600)
	w.IndexOnViewportChange(1200, 800, 1.0)

	for i := 0; i < 200; i++ {
		_, _ = w.AddPoint(float64(i%600), float64((i*7)%600))
	}
	w.Reindex()

	// Background tile (RGBA) — typical texture size.
	bg := image.NewRGBA(image.Rect(0, 0, 96, 96))
	// Make it semi-transparent so draw.Over has real work.
	for y := 0; y < 96; y++ {
		for x := 0; x < 96; x++ {
			bg.SetRGBA(x, y, color.RGBA{R: 255, G: 255, B: 255, A: 18})
		}
	}

	w.SetTheme(benchBgTheme{img: bg, anchor: anchor, tileMode: tile, scaleMode: scale})

	dc := gg.NewContext(1200, 800)
	drawer := &GGDrawer{DC: dc}

	params := RenderParams{
		ViewportWidthPx:  1000,
		ViewportHeightPx: 700,
		MarginXPx:        250,
		MarginYPx:        175,
		CameraXWorldFp:   300 * SCALE,
		CameraYWorldFp:   300 * SCALE,
		CameraZoom:       1.0,
		Options: &RenderOptions{
			Incremental: &IncrementalPolicy{
				AllowShiftOnly:   false,
				CoalesceUpdates:  false,
				MaxCatchUpAreaPx: 0,
				RenderBudgetMs:   0,
			},
		},
	}

	_ = w.Render(drawer, params)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		params.CameraXWorldFp += 1 * SCALE
		_ = w.Render(drawer, params)
	}
}

// BenchmarkDrawPlanSinglePass_Lines_GG benchmarks draw Plan Single Pass Lines GG.
func BenchmarkDrawPlanSinglePass_Lines_GG(b *testing.B) {
	w := NewWorld(600, 600)
	w.IndexOnViewportChange(1000, 700, 1.0)

	// Make a lot of lines, including ones that likely wrap.
	for i := 0; i < 4000; i++ {
		x1 := float64(i % 600)
		y1 := float64((i * 7) % 600)
		x2 := float64((i*13 + 500) % 600) // shift to create various deltas
		y2 := float64((i*17 + 300) % 600)
		_, _ = w.AddLine(x1, y1, x2, y2)
	}
	w.Reindex()

	params := RenderParams{
		ViewportWidthPx:  1000,
		ViewportHeightPx: 700,
		MarginXPx:        250,
		MarginYPx:        175,
		CameraXWorldFp:   300 * SCALE,
		CameraYWorldFp:   300 * SCALE,
		CameraZoom:       1.0,
		Options: &RenderOptions{
			BackgroundColor: color.RGBA{A: 255},
		},
	}

	plan, err := w.buildRenderPlan(params)
	if err != nil {
		b.Fatalf("build plan: %v", err)
	}

	dc := gg.NewContext(params.CanvasWidthPx(), params.CanvasHeightPx())
	drawer := &GGDrawer{DC: dc}

	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		w.drawPlanSinglePass(drawer, plan, true, drawPlanSinglePassClipEnabled, false)
	}
}

// BenchmarkDrawPlanSinglePass_Lines_Fake benchmarks draw Plan Single Pass Lines Fake.
func BenchmarkDrawPlanSinglePass_Lines_Fake(b *testing.B) {
	w := NewWorld(600, 600)
	w.IndexOnViewportChange(1000, 700, 1.0)

	for i := 0; i < 4000; i++ {
		x1 := float64(i % 600)
		y1 := float64((i * 7) % 600)
		x2 := float64((i*13 + 500) % 600)
		y2 := float64((i*17 + 300) % 600)
		_, _ = w.AddLine(x1, y1, x2, y2)
	}
	w.Reindex()

	params := RenderParams{
		ViewportWidthPx:  1000,
		ViewportHeightPx: 700,
		MarginXPx:        250,
		MarginYPx:        175,
		CameraXWorldFp:   300 * SCALE,
		CameraYWorldFp:   300 * SCALE,
		CameraZoom:       1.0,
		Options: &RenderOptions{
			BackgroundColor: color.RGBA{A: 255},
		},
	}

	plan, err := w.buildRenderPlan(params)
	if err != nil {
		b.Fatalf("build plan: %v", err)
	}

	drawer := &fakePrimitiveDrawer{}

	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		// Reset command log so it doesn't grow forever and dominate allocations.
		drawer.Reset()
		w.drawPlanSinglePass(drawer, plan, true, drawPlanSinglePassClipEnabled, false)
	}
}

// TestRender_IncrementalShift_UsesOuterClip_NotPerTileClips verifies render Incremental Shift Uses Outer Clip Not Per Tile Clips.
func TestRender_IncrementalShift_UsesOuterClip_NotPerTileClips(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)
	w.IndexOnViewportChange(100, 80, 1.0)
	w.resetGrid(2 * SCALE)

	_, _ = w.AddPoint(5, 5)
	w.Reindex()

	params := RenderParams{
		ViewportWidthPx:  100,
		ViewportHeightPx: 80,
		MarginXPx:        25,
		MarginYPx:        20,
		CameraXWorldFp:   5 * SCALE,
		CameraYWorldFp:   5 * SCALE,
		CameraZoom:       1.0,
		Options: &RenderOptions{
			Incremental: &IncrementalPolicy{AllowShiftOnly: false},
		},
	}

	// First render initializes state.
	d1 := &fakePrimitiveDrawer{}
	require.NoError(t, w.Render(d1, params))

	// Small pan.
	params2 := params
	params2.CameraXWorldFp += 1 * SCALE

	d2 := &fakePrimitiveDrawer{}
	require.NoError(t, w.Render(d2, params2))

	// Expect very few ClipRect calls (dirty strips count), not per tile.
	clipCmds := d2.CommandsByName("ClipRect")
	require.NotEmpty(t, clipCmds)
	require.LessOrEqual(t, len(clipCmds), 4)
}

// TestRender_BatchesConsecutiveLinesByStyleID verifies render Batches Consecutive Lines By Style ID.
func TestRender_BatchesConsecutiveLinesByStyleID(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)
	w.IndexOnViewportChange(100, 80, 1.0)

	// Two lines with default style, same priority.
	_, _ = w.AddLine(1, 1, 8, 1)
	_, _ = w.AddLine(1, 2, 8, 2)
	w.Reindex()

	params := RenderParams{
		ViewportWidthPx:  100,
		ViewportHeightPx: 80,
		MarginXPx:        25,
		MarginYPx:        20,
		CameraXWorldFp:   5 * SCALE,
		CameraYWorldFp:   5 * SCALE,
		CameraZoom:       1.0,
	}

	d := &fakePrimitiveDrawer{}
	require.NoError(t, w.Render(d, params))

	// We expect at least two AddLine, but only 1 Stroke for that run in a tile.
	adds := d.CommandsByName("AddLine")
	strokes := d.CommandsByName("Stroke")
	require.GreaterOrEqual(t, len(adds), 2)
	require.GreaterOrEqual(t, len(strokes), 1)

	// Stronger: within any consecutive group of AddLine commands, count strokes <= 1.
	// (Keep it loose to avoid depending on tile partitioning.)
}

// BenchmarkDrawPlanSinglePass_DrawItemsReuse benchmarks draw Plan Single Pass Draw Items Reuse.
func BenchmarkDrawPlanSinglePass_DrawItemsReuse(b *testing.B) {
	w := NewWorld(600, 600)

	// Make grid + index available.
	w.IndexOnViewportChange(1000, 700, 1.0)

	// Add enough objects so tiles have candidates.
	for i := range 2000 {
		_, _ = w.AddPoint(float64(i%600), float64((i*7)%600))
	}
	for i := range 500 {
		_, _ = w.AddCircle(float64((i*11)%600), float64((i*13)%600), 5.0)
	}
	w.Reindex()

	params := RenderParams{
		ViewportWidthPx:  1000,
		ViewportHeightPx: 700,
		MarginXPx:        250,
		MarginYPx:        175,
		CameraXWorldFp:   300 * SCALE,
		CameraYWorldFp:   300 * SCALE,
		CameraZoom:       1.0,
		Options: &RenderOptions{
			BackgroundColor: color.RGBA{A: 255},
		},
	}

	plan, err := w.buildRenderPlan(params)
	if err != nil {
		b.Fatalf("build plan: %v", err)
	}

	dc := gg.NewContext(params.CanvasWidthPx(), params.CanvasHeightPx())
	drawer := &GGDrawer{DC: dc}

	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		// We don't clear here; we only measure the draw loop overhead.
		w.drawPlanSinglePass(drawer, plan, true, drawPlanSinglePassClipEnabled, false)
	}
}

// BenchmarkBuildRenderPlanStageA_Candidates benchmarks build Render Plan Stage A Candidates.
func BenchmarkBuildRenderPlanStageA_Candidates(b *testing.B) {
	w := NewWorld(600, 600)

	// Make the index/grid available.
	w.IndexOnViewportChange(1000, 700, 1.0)

	// Populate with enough objects to create duplicates across cells.
	// Circles and lines create bbox indexing (more duplicates).
	for i := 0; i < 2000; i++ {
		_, _ = w.AddPoint(float64(i%600), float64((i*7)%600))
	}
	for i := 0; i < 1200; i++ {
		_, _ = w.AddCircle(float64((i*11)%600), float64((i*13)%600), 8.0)
	}
	for i := 0; i < 1200; i++ {
		x1 := float64((i*3 + 10) % 600)
		y1 := float64((i*5 + 20) % 600)
		x2 := float64((i*7 + 400) % 600)
		y2 := float64((i*11 + 300) % 600)
		_, _ = w.AddLine(x1, y1, x2, y2)
	}
	w.Reindex()

	params := RenderParams{
		ViewportWidthPx:  1000,
		ViewportHeightPx: 700,
		MarginXPx:        250,
		MarginYPx:        175,
		CameraXWorldFp:   300 * SCALE,
		CameraYWorldFp:   300 * SCALE,
		CameraZoom:       1.0,
		Options: &RenderOptions{
			BackgroundColor: color.RGBA{A: 255},
		},
	}

	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_, err := w.buildRenderPlan(params)
		if err != nil {
			b.Fatalf("build plan: %v", err)
		}
	}
}
