package client

/*
Fyne-friendly latest-wins coalescing for canvas.NewRaster(draw func(w,h int) image.Image).

Key property:
- draw() renders at most once per invocation (never loops).
- if new requests arrived while drawing, we schedule exactly one extra Refresh.
*/

import (
	"image"
	"sync"
)

// UIExecutor posts a function to run on the UI/main thread.
type UIExecutor interface {
	Post(fn func())
}

// Refresher is the minimal interface we need from fyne.CanvasObject / Raster.
type Refresher interface {
	Refresh()
}

// RasterRenderer renders the latest params and returns an image.
// Must be called on the UI thread (inside draw callback).
type RasterRenderer[P any] func(wPx, hPx int, params P) image.Image

// RasterCoalescer implements latest-wins coalescing for raster rendering.
// It is designed specifically for toolkits like fyne where the system calls draw(w,h)
// and expects a returned image.
type RasterCoalescer[P any] struct {
	exec      UIExecutor
	refresher Refresher
	renderer  RasterRenderer[P]

	mu sync.Mutex

	// inDraw == true while Draw() is running on UI thread.
	inDraw bool

	// refreshQueued == true when we have already posted a Refresh() that has not yet
	// resulted in a Draw() call (or is expected to call Draw soon).
	refreshQueued bool

	// pending == true when new params arrived while inDraw==true.
	// Draw() will schedule exactly one follow-up Refresh after it returns.
	pending bool

	latest P
	have   bool
}

// NewRasterCoalescer creates a new coalescer.
// - exec.Post must run fn on UI thread.
// - refresher.Refresh will trigger the framework to call draw(w,h).
func NewRasterCoalescer[P any](exec UIExecutor, refresher Refresher, renderer RasterRenderer[P]) *RasterCoalescer[P] {
	if exec == nil {
		panic("RasterCoalescer: nil executor")
	}
	if refresher == nil {
		panic("RasterCoalescer: nil refresher")
	}
	if renderer == nil {
		panic("RasterCoalescer: nil renderer")
	}
	return &RasterCoalescer[P]{exec: exec, refresher: refresher, renderer: renderer}
}

// Request stores the latest params and schedules exactly one refresh (latest-wins).
// Can be called from any goroutine.
func (c *RasterCoalescer[P]) Request(params P) {
	c.mu.Lock()
	c.latest = params
	c.have = true

	// If we are currently inside Draw(), don't schedule refresh immediately.
	// Just mark pending; Draw() will schedule one follow-up refresh after it returns.
	if c.inDraw {
		c.pending = true
		c.mu.Unlock()
		return
	}

	// Not drawing. Schedule at most one refresh until the next Draw() happens.
	if c.refreshQueued {
		c.mu.Unlock()
		return
	}

	c.refreshQueued = true
	c.mu.Unlock()

	c.exec.Post(c.refresher.Refresh)
}

// Draw must be called from the raster draw callback on the UI thread.
// It renders exactly once with the latest snapshot.
// If more requests arrived while drawing, it schedules exactly one extra refresh.
func (c *RasterCoalescer[P]) Draw(wPx, hPx int) image.Image {
	c.mu.Lock()
	// A Draw call corresponds to a previously scheduled refresh being serviced.
	c.refreshQueued = false

	if !c.have {
		c.mu.Unlock()
		return image.NewRGBA(image.Rect(0, 0, wPx, hPx))
	}

	c.inDraw = true
	c.pending = false
	params := c.latest
	c.mu.Unlock()

	img := c.renderer(wPx, hPx, params)

	c.mu.Lock()
	needAnother := c.pending
	c.pending = false
	c.inDraw = false

	// If we need another frame, schedule exactly one refresh (if not already queued).
	if needAnother && !c.refreshQueued {
		c.refreshQueued = true
		c.mu.Unlock()
		c.exec.Post(c.refresher.Refresh)
		return img
	}

	c.mu.Unlock()
	return img
}
