package client

import (
	"image"
	"math"

	"galaxy/client/world"

	"fyne.io/fyne/v2"
	"github.com/fogleman/gg"
)

/*
Fyne integration notes:

- canvas.NewRaster calls draw(w,h) on the UI thread.
- We MUST keep draw() cheap and never loop re-rendering inside it.
- Coalescing must therefore schedule refreshes and render at most once per draw call.
- The world renderer expects:
  - RenderParams.ViewportWidthPx/HeightPx: the size of the visible viewport.
  - RenderParams.MarginXPx/MarginYPx: margins around viewport.
  - RenderParams.CameraXWorldFp/YWorldFp: camera center in world-fixed units.
  - RenderParams.CameraZoom: float zoom (converted inside world).
- world.Render draws on the full expanded canvas (viewport + 2*margins on each axis).

This adapter enforces:
- viewport sizes come from draw(w,h)
- margins are computed from viewport sizes (w/4 and h/4)
- gg context backing image is resized to the expanded canvas size
- IndexOnViewportChange is called when viewport sizes changed (you can also include zoom if desired)
*/

var (
	blankImage image.Image = image.NewRGBA(image.Rect(0, 0, 0, 0))
)

// FyneExecutor posts functions onto the Fyne UI thread.
type FyneExecutor struct{}

func (FyneExecutor) Post(fn func()) {
	fyne.Do(fn)
}

// GetParams returns a copy of current render params for external reads.
func (e *client) GetParams() world.RenderParams {
	e.mu.RLock()
	defer e.mu.RUnlock()
	return *e.wp
}

// UpdateParams applies a modification function to render params and schedules a refresh.
// This is a safe way to mutate camera/zoom from event handlers.
func (e *client) UpdateParams(fn func(p *world.RenderParams)) {
	e.mu.Lock()
	fn(e.wp)
	p := *e.wp
	e.mu.Unlock()

	e.co.Request(p)
}

// RequestRefresh schedules a refresh with the current params snapshot.
// Useful if you changed world objects and want to redraw.
func (e *client) RequestRefresh() {
	e.mu.RLock()
	p := *e.wp
	e.mu.RUnlock()
	e.co.Request(p)
}

// draw is the raster callback. It must be cheap and must not block on multiple re-renders.
// It delegates coalescing + rendering decision to RasterCoalescer.
func (e *client) draw(wPx, hPx int) image.Image {
	return e.co.Draw(wPx, hPx)
}

// renderRasterImage renders the expanded canvas into the GGDrawer backing image,
// then copies only the viewport ROI into a reusable viewport buffer and returns it.
func (e *client) renderRasterImage(viewportW, viewportH int, p world.RenderParams) image.Image {
	if e.world == nil {
		return image.NewRGBA(image.Rect(0, 0, 0, 0))
	}

	// Keep the incoming zoom snapshot so we can safely sync corrected zoom back
	// to base params only when no newer zoom was written concurrently.
	inputZoom := p.CameraZoom

	// Record current raster pixel size (used for event coordinate conversion).
	e.metaMu.Lock()
	e.lastRasterPxW = viewportW
	e.lastRasterPxH = viewportH
	e.metaMu.Unlock()

	// Fill viewport/margins derived from draw callback.
	p.ViewportWidthPx = viewportW
	p.ViewportHeightPx = viewportH
	p.MarginXPx = viewportW / 4
	p.MarginYPx = viewportH / 4

	// Correct zoom for viewport/world constraints, and clamp camera for no-wrap.
	correctedZoom := e.world.CorrectCameraZoom(inputZoom, viewportW, viewportH)
	p.CameraZoom = correctedZoom

	// Sync corrected zoom to the canonical UI-facing params snapshot.
	// Guard prevents stale render snapshots from overwriting a newer zoom value
	// that may have been set by another UI event.
	e.mu.Lock()
	if e.wp.CameraZoom == inputZoom {
		e.wp.CameraZoom = correctedZoom
	}
	e.mu.Unlock()

	// Ensure indexing is up-to-date when viewport size or zoom changes.
	zoomFp, err := p.CameraZoomFp()
	if err == nil {
		if viewportW != e.lastIndexedViewportW || viewportH != e.lastIndexedViewportH || zoomFp != e.lastIndexedZoomFp {
			e.world.IndexOnViewportChange(viewportW, viewportH, p.CameraZoom)
			e.lastIndexedViewportW = viewportW
			e.lastIndexedViewportH = viewportH
			e.lastIndexedZoomFp = zoomFp
		}
	}

	e.world.ClampRenderParamsNoWrap(&p)

	// Ensure backing expanded canvas (gg context) is sized properly.
	canvasW := p.CanvasWidthPx()
	canvasH := p.CanvasHeightPx()
	e.ensureDrawerCanvas(canvasW, canvasH)

	// Render into expanded canvas backing.
	_ = e.world.Render(e.drawer, p) // TODO: handle error

	// Save snapshot of params actually used for this render (for HitTest consistency).
	e.lastRenderedMu.Lock()
	e.lastRenderedParams = p
	e.lastRenderedMu.Unlock()

	// Copy viewport ROI into reusable viewport buffer and return it.
	e.ensureViewportBuffer(viewportW, viewportH)

	src, ok := e.drawer.DC.Image().(*image.RGBA)
	if !ok || src == nil {
		return image.NewRGBA(image.Rect(0, 0, viewportW, viewportH))
	}

	copyViewportRGBA(e.viewportImg, src, p.MarginXPx, p.MarginYPx, viewportW, viewportH)
	return e.viewportImg
}

// ensureDrawerCanvas ensures drawer has a gg.Context sized to canvasW x canvasH.
func (e *client) ensureDrawerCanvas(canvasW, canvasH int) {
	if e.drawer.DC != nil && e.lastCanvasW == canvasW && e.lastCanvasH == canvasH {
		return
	}
	// world.NewGGContextRGBA should return *gg.Context backed by *image.RGBA (gg.NewContext does).
	e.drawer.DC = NewGGContextRGBA(canvasW, canvasH)
	e.lastCanvasW = canvasW
	e.lastCanvasH = canvasH
}

func (e *client) ensureViewportBuffer(w, h int) {
	if e.viewportImg != nil && e.viewportW == w && e.viewportH == h {
		return
	}
	e.viewportImg = image.NewRGBA(image.Rect(0, 0, w, h))
	e.viewportW = w
	e.viewportH = h
}

func (e *client) getLastRenderedParams() world.RenderParams {
	e.lastRenderedMu.RLock()
	defer e.lastRenderedMu.RUnlock()
	return e.lastRenderedParams
}

// eventPosToPixel converts event logical coordinates (Fyne units) into pixel coordinates,
// using the last known raster logical size and the last draw callback pixel size.
//
// pixelX = floor(eventX * rasterPixelWidth / rasterLogicalWidth)
func (e *client) eventPosToPixel(eventX, eventY float32) (xPx, yPx int, ok bool) {
	e.metaMu.RLock()
	logW := e.lastRasterLogicW
	logH := e.lastRasterLogicH
	pxW := e.lastRasterPxW
	pxH := e.lastRasterPxH
	e.metaMu.RUnlock()

	if logW <= 0 || logH <= 0 || pxW <= 0 || pxH <= 0 {
		return 0, 0, false
	}

	x := int(math.Floor(float64(eventX) * float64(pxW) / float64(logW)))
	y := int(math.Floor(float64(eventY) * float64(pxH) / float64(logH)))

	// Clamp to viewport bounds.
	if x < 0 {
		x = 0
	} else if x > pxW {
		x = pxW
	}
	if y < 0 {
		y = 0
	} else if y > pxH {
		y = pxH
	}
	return x, y, true
}

func (e *client) CanvasScale() float32 {
	e.metaMu.RLock()
	defer e.metaMu.RUnlock()
	if e.lastCanvasScale <= 0 {
		return 1
	}
	return e.lastCanvasScale
}

func (e *client) ForceFullRedraw() {
	if e.world == nil {
		return
	}
	e.world.ForceFullRedrawNext()
}

func (e *client) onRasterWidgetLayout(fyne.Size) {
	e.updateSizes()
}

// updateSizes updates only metadata we need for event->pixel conversion and schedules a redraw.
// It must NOT try to compute pixel viewport sizes (those are known in raster draw callback).
func (e *client) updateSizes() {
	canvasObj := fyne.CurrentApp().Driver().CanvasForObject(e.raster)
	if canvasObj == nil {
		return
	}

	sz := e.raster.Size() // logical (Fyne units)
	scale := canvasObj.Scale()

	e.metaMu.Lock()
	e.lastRasterLogicW = sz.Width
	e.lastRasterLogicH = sz.Height
	e.lastCanvasScale = scale
	e.metaMu.Unlock()

	e.RequestRefresh()
}

func (e *client) onDragged(ev *fyne.DragEvent) {
	e.pan.Dragged(ev)
}

func (e *client) onDradEnd() {
	e.pan.DragEnd()
}

func (e *client) onScrolled(s *fyne.ScrollEvent) {
	if e.world == nil || s == nil {
		return
	}

	// Use last rendered viewport sizes (pixel) for zoom logic.
	e.metaMu.RLock()
	vw := e.lastRasterPxW
	vh := e.lastRasterPxH
	e.metaMu.RUnlock()
	if vw <= 0 || vh <= 0 {
		return
	}

	cxPx, cyPx, ok := e.eventPosToPixel(s.Position.X, s.Position.Y)
	if !ok {
		return
	}

	e.mu.Lock()
	oldZoom := e.wp.CameraZoom

	// Exponential zoom factor; tune later.
	const base = 1.005
	delta := float64(s.Scrolled.DY)
	newZoom := oldZoom * math.Pow(base, delta)

	newZoom = e.world.CorrectCameraZoom(newZoom, vw, vh)
	if newZoom == oldZoom {
		e.mu.Unlock()
		return
	}

	oldZoomFp, err := world.CameraZoomToWorldFixed(oldZoom)
	if err != nil {
		e.mu.Unlock()
		return
	}
	newZoomFp, err := world.CameraZoomToWorldFixed(newZoom)
	if err != nil {
		e.mu.Unlock()
		return
	}

	// Pivot zoom for no-wrap behavior.
	newCamX, newCamY := world.PivotZoomCameraNoWrap(
		e.wp.CameraXWorldFp, e.wp.CameraYWorldFp,
		vw, vh,
		cxPx, cyPx,
		oldZoomFp, newZoomFp,
	)

	e.wp.CameraZoom = newZoom
	e.wp.CameraXWorldFp = newCamX
	e.wp.CameraYWorldFp = newCamY
	e.mu.Unlock()

	// Any zoom change should rebuild index and force full redraw.
	e.world.ForceFullRedrawNext()
	e.RequestRefresh()
}

// copyViewportRGBA copies a viewport rectangle from src RGBA into dst RGBA.
// dst must be sized exactly (0,0)-(vw,vh). This is allocation-free.
// It avoids SubImage aliasing issues: dst becomes independent from src backing memory.
func copyViewportRGBA(dst, src *image.RGBA, marginX, marginY, vw, vh int) {
	for y := 0; y < vh; y++ {
		srcOff := (marginY+y)*src.Stride + marginX*4
		dstOff := y * dst.Stride
		n := vw * 4
		copy(dst.Pix[dstOff:dstOff+n], src.Pix[srcOff:srcOff+n])
	}
}

func NewGGContextRGBA(w, h int) *gg.Context {
	return gg.NewContext(w, h)
}
