package world

import (
	"github.com/fogleman/gg"
	"image"
	"image/color"
	"image/draw"
	"reflect"
)

// PrimitiveDrawer is a low-level drawing backend used by the world renderer.
//
// The renderer is responsible for all torus logic, viewport/margin logic,
// coordinate projection, and primitive duplication. This interface only accepts
// final canvas pixel coordinates and exposes the minimum drawing operations
// needed to build and render paths.
//
// AddPoint, AddLine, and AddCircle append geometry to the current path.
// They do not render by themselves. The caller must finalize the path by
// calling Stroke or Fill.
//
// Save and Restore are intended for temporary local state changes such as
// clipping, colors, line width, or dash settings. After Restore, the outer
// drawing state must be visible again.
type PrimitiveDrawer interface {
	// Save stores the current drawing state.
	Save()

	// Restore restores the most recently saved drawing state.
	Restore()

	// ResetClip clears the current clipping region completely.
	ResetClip()

	// ClipRect intersects the current clipping region with the given rectangle
	// in canvas pixel coordinates.
	ClipRect(x, y, w, h float64)

	// SetStrokeColor sets the color used by Stroke.
	SetStrokeColor(c color.Color)

	// SetFillColor sets the color used by Fill.
	SetFillColor(c color.Color)

	// SetLineWidth sets the line width used by Stroke.
	SetLineWidth(width float64)

	// SetDash sets the dash pattern used by Stroke.
	// Passing no values clears the current dash pattern.
	SetDash(dashes ...float64)

	// SetDashOffset sets the dash phase used by Stroke.
	SetDashOffset(offset float64)

	// AddPoint appends a point marker centered at (x, y) with radius r
	// to the current path in canvas pixel coordinates.
	AddPoint(x, y, r float64)

	// AddLine appends a line segment to the current path in canvas pixel coordinates.
	AddLine(x1, y1, x2, y2 float64)

	// AddCircle appends a circle to the current path in canvas pixel coordinates.
	AddCircle(cx, cy, r float64)

	// Stroke renders the current path using the current stroke state.
	Stroke()

	// Fill renders the current path using the current fill state.
	Fill()

	// CopyShift shifts backing pixels by (dx,dy). Newly exposed areas become transparent/undefined;
	// caller is expected to ClearRectTo() the dirty areas before drawing.
	CopyShift(dx, dy int)

	// Clear operations must NOT change clip state.
	ClearAllTo(bg color.Color)
	ClearRectTo(x, y, w, h int, bg color.Color)

	DrawImage(img image.Image, x, y int)

	DrawImageScaled(img image.Image, x, y, w, h int)
}

// ggClipRect stores one clip rectangle in canvas pixel coordinates.
// GGDrawer replays these rectangles on Restore because gg.Context Push/Pop
// do not restore clip masks the way this package expects.
type ggClipRect struct {
	x, y float64
	w, h float64
}

// GGDrawer is a PrimitiveDrawer implementation backed by gg.Context.
//
// It intentionally does not perform any world logic. It only forwards already
// projected canvas coordinates to gg while additionally maintaining a clip stack
// compatible with this package's Save/Restore contract.
type GGDrawer struct {
	DC *gg.Context

	clips     []ggClipRect
	clipStack [][]ggClipRect

	// scratch is a reusable buffer for CopyShift to avoid allocations.
	scratch *image.RGBA

	bgCache bgTileCache
}

// Save stores the current gg state and the current logical clip stack.
func (d *GGDrawer) Save() {
	d.DC.Push()

	snapshot := append([]ggClipRect(nil), d.clips...)
	d.clipStack = append(d.clipStack, snapshot)
}

// Restore restores the previous gg state and rebuilds the outer clip state.
//
// gg.Context.Pop restores most state from the stack, but its clip mask handling
// does not match this package's expected Save/Restore semantics. To preserve the
// contract, GGDrawer explicitly resets the clip and replays the previously saved
// clip rectangles after Pop.
func (d *GGDrawer) Restore() {
	if len(d.clipStack) == 0 {
		panic("GGDrawer: Restore without matching Save")
	}

	snapshot := d.clipStack[len(d.clipStack)-1]
	d.clipStack = d.clipStack[:len(d.clipStack)-1]

	d.DC.Pop()

	d.clips = append([]ggClipRect(nil), snapshot...)
	d.DC.ResetClip()
	for _, clip := range d.clips {
		d.DC.DrawRectangle(clip.x, clip.y, clip.w, clip.h)
		d.DC.Clip()
	}
}

// ResetClip clears the current clipping region and the logical clip stack
// for the active state frame.
func (d *GGDrawer) ResetClip() {
	d.DC.ResetClip()
	d.clips = nil
}

// ClipRect intersects the current clipping region with the given rectangle
// and records it so the clip can be reconstructed after Restore.
func (d *GGDrawer) ClipRect(x, y, w, h float64) {
	d.DC.DrawRectangle(x, y, w, h)
	d.DC.Clip()

	d.clips = append(d.clips, ggClipRect{x: x, y: y, w: w, h: h})
}

// SetStrokeColor sets the stroke color by installing a solid stroke pattern.
func (d *GGDrawer) SetStrokeColor(c color.Color) {
	d.DC.SetStrokeStyle(gg.NewSolidPattern(c))
}

// SetFillColor sets the fill color by installing a solid fill pattern.
func (d *GGDrawer) SetFillColor(c color.Color) {
	d.DC.SetFillStyle(gg.NewSolidPattern(c))
}

// SetLineWidth sets the line width used for stroking.
func (d *GGDrawer) SetLineWidth(width float64) {
	d.DC.SetLineWidth(width)
}

// SetDash sets the dash pattern used for stroking.
func (d *GGDrawer) SetDash(dashes ...float64) {
	d.DC.SetDash(dashes...)
}

// SetDashOffset sets the dash phase used for stroking.
func (d *GGDrawer) SetDashOffset(offset float64) {
	d.DC.SetDashOffset(offset)
}

// AddPoint appends a point marker to the current path.
func (d *GGDrawer) AddPoint(x, y, r float64) {
	d.DC.DrawPoint(x, y, r)
}

// AddLine appends a line segment to the current path.
func (d *GGDrawer) AddLine(x1, y1, x2, y2 float64) {
	d.DC.DrawLine(x1, y1, x2, y2)
}

// AddCircle appends a circle to the current path.
func (d *GGDrawer) AddCircle(cx, cy, r float64) {
	d.DC.DrawCircle(cx, cy, r)
}

// Stroke renders the current path using the current stroke state.
func (d *GGDrawer) Stroke() {
	d.DC.Stroke()
}

// Fill renders the current path using the current fill state.
func (d *GGDrawer) Fill() {
	d.DC.Fill()
}

// CopyShift shifts the backing RGBA image by (dx, dy) pixels.
// It clears newly exposed areas to transparent.
func (d *GGDrawer) CopyShift(dx, dy int) {
	if dx == 0 && dy == 0 {
		return
	}

	img, ok := d.DC.Image().(*image.RGBA)
	if !ok || img == nil {
		panic("GGDrawer.CopyShift: backing image is not *image.RGBA")
	}

	b := img.Bounds()
	w := b.Dx()
	h := b.Dy()
	if w <= 0 || h <= 0 {
		return
	}

	adx := abs(dx)
	ady := abs(dy)
	if adx >= w || ady >= h {
		// Everything shifts out of bounds => just clear.
		for i := range img.Pix {
			img.Pix[i] = 0
		}
		return
	}

	// Prepare scratch with the same bounds.
	if d.scratch == nil || d.scratch.Bounds().Dx() != w || d.scratch.Bounds().Dy() != h {
		d.scratch = image.NewRGBA(b)
	} else {
		// Clear scratch to transparent.
		for i := range d.scratch.Pix {
			d.scratch.Pix[i] = 0
		}
	}

	// Compute source/destination rectangles.
	dstX0 := 0
	dstY0 := 0
	srcX0 := 0
	srcY0 := 0
	if dx > 0 {
		dstX0 = dx
	} else {
		srcX0 = -dx
	}
	if dy > 0 {
		dstY0 = dy
	} else {
		srcY0 = -dy
	}

	copyW := w - max(dstX0, srcX0)
	copyH := h - max(dstY0, srcY0)
	if copyW <= 0 || copyH <= 0 {
		for i := range img.Pix {
			img.Pix[i] = 0
		}
		return
	}

	// Copy row-by-row (RGBA, 4 bytes per pixel).
	for row := 0; row < copyH; row++ {
		srcY := srcY0 + row
		dstY := dstY0 + row

		srcOff := srcY*img.Stride + srcX0*4
		dstOff := dstY*d.scratch.Stride + dstX0*4
		n := copyW * 4

		copy(d.scratch.Pix[dstOff:dstOff+n], img.Pix[srcOff:srcOff+n])
	}

	// Swap buffers by copying scratch into img.
	// (We keep img pointer stable for gg.Context.)
	copy(img.Pix, d.scratch.Pix)
}

func (d *GGDrawer) ClearAllTo(bg color.Color) {
	img, ok := d.DC.Image().(*image.RGBA)
	if !ok || img == nil {
		panic("GGDrawer.ClearAllTo: backing image is not *image.RGBA")
	}

	R, G, B, A := rgba8(bg)

	// Prepare one full scanline once.
	w := img.Bounds().Dx()
	if w <= 0 {
		return
	}
	line := make([]byte, w*4)
	for i := 0; i < len(line); i += 4 {
		line[i+0] = R
		line[i+1] = G
		line[i+2] = B
		line[i+3] = A
	}

	// Copy scanline into each row (fast memmove).
	h := img.Bounds().Dy()
	for y := 0; y < h; y++ {
		off := y * img.Stride
		copy(img.Pix[off:off+w*4], line)
	}
}

func (d *GGDrawer) ClearRectTo(x, y, w, h int, bg color.Color) {
	if w <= 0 || h <= 0 {
		return
	}

	img, ok := d.DC.Image().(*image.RGBA)
	if !ok || img == nil {
		panic("GGDrawer.ClearRectTo: backing image is not *image.RGBA")
	}

	b := img.Bounds()
	x0 := max(x, b.Min.X)
	y0 := max(y, b.Min.Y)
	x1 := min(x+w, b.Max.X)
	y1 := min(y+h, b.Max.Y)
	if x0 >= x1 || y0 >= y1 {
		return
	}

	R, G, B, A := rgba8(bg)

	rowPx := x1 - x0
	rowBytes := rowPx * 4

	// Build one row once for this rect width.
	line := make([]byte, rowBytes)
	for i := 0; i < rowBytes; i += 4 {
		line[i+0] = R
		line[i+1] = G
		line[i+2] = B
		line[i+3] = A
	}

	for yy := y0; yy < y1; yy++ {
		off := yy*img.Stride + x0*4
		copy(img.Pix[off:off+rowBytes], line)
	}
}

// rgba8 converts any color.Color into 8-bit RGBA components.
func rgba8(c color.Color) (R, G, B, A byte) {
	r, g, b, a := c.RGBA()
	return byte(r >> 8), byte(g >> 8), byte(b >> 8), byte(a >> 8)
}

func (g *GGDrawer) DrawImage(img image.Image, x, y int) {
	g.DC.DrawImage(img, x, y)
}

func (g *GGDrawer) DrawImageScaled(img image.Image, x, y, w, h int) {
	if w <= 0 || h <= 0 {
		return
	}
	b := img.Bounds()
	srcW := b.Dx()
	srcH := b.Dy()
	if srcW <= 0 || srcH <= 0 {
		return
	}

	g.DC.Push()
	// Translate to destination top-left.
	g.DC.Translate(float64(x), float64(y))
	// Scale so that the source bounds map to (w,h).
	g.DC.Scale(float64(w)/float64(srcW), float64(h)/float64(srcH))
	// Draw at origin in the scaled coordinate system.
	g.DC.DrawImage(img, 0, 0)
	g.DC.Pop()
}

// bgTileCacheKey identifies one scaled background-tile variant cached by GGDrawer.
type bgTileCacheKey struct {
	imgPtr    uintptr
	scaleMode BackgroundScaleMode
	canvasW   int
	canvasH   int
	srcW      int
	srcH      int
}

// bgTileCache stores the most recently used scaled background tile.
type bgTileCache struct {
	key        bgTileCacheKey
	valid      bool
	scaledTile *image.RGBA
	tileW      int
	tileH      int
}

// drawBackgroundFast renders the background directly into the RGBA backing
// image, bypassing gg path construction when the drawer supports it.
func (g *GGDrawer) drawBackgroundFast(w *World, params RenderParams, rect RectPx) bool {
	th := w.Theme()
	bgImg := th.BackgroundImage()
	if bgImg == nil {
		return false
	}

	dst, ok := g.DC.Image().(*image.RGBA)
	if !ok || dst == nil {
		return false
	}

	canvasW := params.CanvasWidthPx()
	canvasH := params.CanvasHeightPx()

	// Clamp rect to canvas.
	if rect.W <= 0 || rect.H <= 0 {
		return true
	}
	if rect.X < 0 {
		rect.W += rect.X
		rect.X = 0
	}
	if rect.Y < 0 {
		rect.H += rect.Y
		rect.Y = 0
	}
	if rect.X+rect.W > canvasW {
		rect.W = canvasW - rect.X
	}
	if rect.Y+rect.H > canvasH {
		rect.H = canvasH - rect.Y
	}
	if rect.W <= 0 || rect.H <= 0 {
		return true
	}

	imgB := bgImg.Bounds()
	srcW := imgB.Dx()
	srcH := imgB.Dy()
	if srcW <= 0 || srcH <= 0 {
		return true
	}

	tileMode := th.BackgroundTileMode()
	anchor := th.BackgroundAnchorMode()
	scaleMode := th.BackgroundScaleMode()

	// Compute scaled tile size in pixels (scale depends on canvas size).
	tileW, tileH := backgroundScaledSize(srcW, srcH, canvasW, canvasH, scaleMode)
	if tileW <= 0 || tileH <= 0 {
		return true
	}

	// Prepare the tile image (possibly scaled) from cache.
	tile := bgImg
	if scaleMode != BackgroundScaleNone || tileW != srcW || tileH != srcH {
		rgbaTile := g.getOrBuildScaledTile(bgImg, srcW, srcH, tileW, tileH, scaleMode, canvasW, canvasH)
		if rgbaTile == nil {
			// Fallback to slow path if we cannot scale (non-RGBA weirdness).
			return false
		}
		tile = rgbaTile
	}

	offX, offY := w.backgroundAnchorOffsetPx(params, tileW, tileH, anchor)

	switch tileMode {
	case BackgroundTileNone:
		// Draw single image centered in full canvas, then clipped by rect.
		x := (canvasW-tileW)/2 + offX
		y := (canvasH-tileH)/2 + offY
		w.drawOneTileRGBA(dst, tile, rect, x, y)

	case BackgroundTileRepeat:
		originX := offX
		originY := offY

		startX := floorDiv(rect.X-originX, tileW)*tileW + originX
		startY := floorDiv(rect.Y-originY, tileH)*tileH + originY

		for yy := startY; yy < rect.Y+rect.H; yy += tileH {
			for xx := startX; xx < rect.X+rect.W; xx += tileW {
				w.drawOneTileRGBA(dst, tile, rect, xx, yy)
			}
		}

	default:
		// Treat unknown as none.
		x := (canvasW-tileW)/2 + offX
		y := (canvasH-tileH)/2 + offY
		w.drawOneTileRGBA(dst, tile, rect, x, y)
	}

	return true
}

// getOrBuildScaledTile returns the cached scaled tile image for the current
// background configuration, rebuilding it when the cache key changes.
func (g *GGDrawer) getOrBuildScaledTile(img image.Image, srcW, srcH, dstW, dstH int, mode BackgroundScaleMode, canvasW, canvasH int) *image.RGBA {
	// Identify image pointer (themes typically provide *image.RGBA).
	ptr := imagePointer(img)

	key := bgTileCacheKey{
		imgPtr:    ptr,
		scaleMode: mode,
		canvasW:   canvasW,
		canvasH:   canvasH,
		srcW:      srcW,
		srcH:      srcH,
	}
	if g.bgCache.valid && g.bgCache.key == key && g.bgCache.scaledTile != nil &&
		g.bgCache.tileW == dstW && g.bgCache.tileH == dstH {
		return g.bgCache.scaledTile
	}

	// Scale only from *image.RGBA fast; otherwise, try a generic slow path.
	var scaled *image.RGBA
	if srcRGBA, ok := img.(*image.RGBA); ok {
		scaled = scaleNearestRGBA(srcRGBA, dstW, dstH)
	} else {
		scaled = scaleNearestGeneric(img, dstW, dstH)
	}

	g.bgCache.key = key
	g.bgCache.valid = true
	g.bgCache.scaledTile = scaled
	g.bgCache.tileW = dstW
	g.bgCache.tileH = dstH

	return scaled
}

// imagePointer returns a stable pointer identity for pointer-backed images.
// Non-pointer image values return 0, which disables cache reuse but remains correct.
func imagePointer(img image.Image) uintptr {
	// Works well when img is a pointer type (e.g. *image.RGBA).
	// If not pointer, Pointer() returns 0; cache will be less effective but still correct.
	v := reflect.ValueOf(img)
	if v.Kind() == reflect.Pointer || v.Kind() == reflect.UnsafePointer {
		return v.Pointer()
	}
	return 0
}

// scaleNearestRGBA scales src -> dst with nearest-neighbor sampling.
// This is intended for background textures; performance > quality.
func scaleNearestRGBA(src *image.RGBA, dstW, dstH int) *image.RGBA {
	if dstW <= 0 || dstH <= 0 {
		return nil
	}
	sb := src.Bounds()
	sw := sb.Dx()
	sh := sb.Dy()
	if sw <= 0 || sh <= 0 {
		return nil
	}

	dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH))

	for y := 0; y < dstH; y++ {
		sy := (y * sh) / dstH
		srcOff := (sy+sb.Min.Y)*src.Stride + sb.Min.X*4
		dstOff := y * dst.Stride
		for x := 0; x < dstW; x++ {
			sx := (x * sw) / dstW
			si := srcOff + sx*4
			di := dstOff + x*4
			dst.Pix[di+0] = src.Pix[si+0]
			dst.Pix[di+1] = src.Pix[si+1]
			dst.Pix[di+2] = src.Pix[si+2]
			dst.Pix[di+3] = src.Pix[si+3]
		}
	}

	return dst
}

// scaleNearestGeneric scales an arbitrary image.Image with nearest-neighbor sampling.
func scaleNearestGeneric(src image.Image, dstW, dstH int) *image.RGBA {
	if dstW <= 0 || dstH <= 0 {
		return nil
	}
	sb := src.Bounds()
	sw := sb.Dx()
	sh := sb.Dy()
	if sw <= 0 || sh <= 0 {
		return nil
	}

	dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH))
	for y := 0; y < dstH; y++ {
		sy := sb.Min.Y + (y*sh)/dstH
		for x := 0; x < dstW; x++ {
			sx := sb.Min.X + (x*sw)/dstW
			dst.Set(x, y, src.At(sx, sy))
		}
	}
	return dst
}

// drawOneTileRGBA draws tile at (x,y) into dst, but only the portion that intersects rect.
// Uses draw.Over (alpha compositing), assuming caller already cleared rect to background color.
func (w *World) drawOneTileRGBA(dst *image.RGBA, tile image.Image, rect RectPx, x, y int) {
	tileB := tile.Bounds()
	tw := tileB.Dx()
	th := tileB.Dy()
	if tw <= 0 || th <= 0 {
		return
	}

	// Intersection of tile rect and target rect.
	tx0 := x
	ty0 := y
	tx1 := x + tw
	ty1 := y + th

	rx0 := rect.X
	ry0 := rect.Y
	rx1 := rect.X + rect.W
	ry1 := rect.Y + rect.H

	ix0 := max(tx0, rx0)
	iy0 := max(ty0, ry0)
	ix1 := min(tx1, rx1)
	iy1 := min(ty1, ry1)
	if ix0 >= ix1 || iy0 >= iy1 {
		return
	}

	dstR := image.Rect(ix0, iy0, ix1, iy1)
	srcPt := image.Point{X: tileB.Min.X + (ix0 - tx0), Y: tileB.Min.Y + (iy0 - ty0)}

	draw.Draw(dst, dstR, tile, srcPt, draw.Over)
}
