package main

import (
	"fmt"
	"image/color"
	"log"
	"math"
	"math/rand"
	"strings"
	"time"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/inpututil"
	"github.com/hajimehoshi/ebiten/v2/text"
	"github.com/hajimehoshi/ebiten/v2/vector"
	"golang.org/x/image/font/basicfont"

	"git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game"
)

// --- KONSTANTEN ---

const (
	physicsStep      = 50 * time.Millisecond // Server-Physics-Rate (20 TPS)
	jumpBufferFrames = 6                     // Sprung-Buffer: ~300ms bei 20 TPS
	coyoteTimeFrames = 4                     // Coyote Time: ~200ms bei 20 TPS
	inputCap         = 60                    // Max unbestätigte Inputs (~3s bei 20 TPS)

	joystickDeadzone = 0.15 // Minimale Joystick-Auslenkung
	joyDownThreshold = 0.5  // Joystick-Down-Schwellenwert (Anteil am Radius)
	correctionDecay  = 0.85 // Visueller Korrektur-Decay pro Frame

	// Touch-Control Größen (relativ zur kleineren Screendimension)
	joyRadiusScale = 0.13 // Joystick-Außenring
	joyKnobRatio   = 0.48 // Knob ~halb so groß wie Ring
	jumpBtnScale   = 0.11 // Sprung-Button
	downBtnScale   = 0.08 // Down-Button

	// Touch-Control Positionen (X als Anteil der Canvas-Breite)
	joyDefaultXRatio = 0.18
	jumpBtnXRatio    = 0.82
	downBtnXRatio    = 0.62

	// Culling-Puffer: Objekte die ±800px außerhalb des Canvas liegen werden übersprungen
	cullingBuffer = 800.0
)

// --- RENDER SNAPSHOT ---

// renderSnapshot hält eine Momentaufnahme aller für DrawGame benötigten Daten.
// Beide Mutexe werden kurz gehalten, um den Snapshot zu befüllen, und dann
// sofort freigegeben – so gibt es keine Lock-Verschachtelung beim Zeichnen.
type renderSnapshot struct {
	// Canvas
	canvasW, canvasH int
	viewScale        float64

	// Lokaler Spieler
	isDead  bool
	myScore int

	// Spielzustand (aus stateMutex)
	status           string
	timeLeft         int
	difficultyFactor float64
	chunks           []game.ActiveChunk
	movingPlatforms  []game.MovingPlatformSync
	collectedCoins   map[string]bool
	collectedPowerups map[string]bool
	players          map[string]game.PlayerState

	// Powerup Timer HUD
	myHasDoubleJump       bool
	myDoubleJumpRemaining float64
	myDoubleJumpUsed      bool
	myHasGodMode          bool
	myGodModeRemaining    float64
	myHasMagnet           bool
	myMagnetRemaining     float64

	// Client-Prediction (aus predictionMutex)
	prevPredX, prevPredY float64
	predX, predY         float64
	offsetX, offsetY     float64
	physicsTime          time.Time
}

// takeRenderSnapshot liest alle benötigten Daten unter den jeweiligen Mutexen
// und gibt einen lock-freien Snapshot zurück.
func (g *Game) takeRenderSnapshot(screen *ebiten.Image) renderSnapshot {
	canvasW, canvasH := screen.Size()
	snap := renderSnapshot{
		canvasW:   canvasW,
		canvasH:   canvasH,
		viewScale: GetScaleFromHeight(canvasH),
	}

	// Prediction-Daten (kurzer Lock)
	g.predictionMutex.Lock()
	snap.prevPredX = g.prevPredictedX
	snap.prevPredY = g.prevPredictedY
	snap.predX = g.predictedX
	snap.predY = g.predictedY
	snap.offsetX = g.correctionOffsetX
	snap.offsetY = g.correctionOffsetY
	snap.physicsTime = g.lastPhysicsTime
	g.predictionMutex.Unlock()

	// Spielzustand (kurzer Lock)
	g.stateMutex.Lock()
	snap.status = g.gameState.Status
	snap.timeLeft = g.gameState.TimeLeft
	snap.difficultyFactor = g.gameState.DifficultyFactor
	snap.chunks = g.gameState.WorldChunks
	snap.movingPlatforms = g.gameState.MovingPlatforms
	snap.collectedCoins = g.gameState.CollectedCoins
	snap.collectedPowerups = g.gameState.CollectedPowerups
	snap.players = g.gameState.Players
	for _, p := range g.gameState.Players {
		if p.Name == g.playerName {
			snap.isDead = !p.IsAlive || p.IsSpectator
			snap.myScore = p.Score
			snap.myHasDoubleJump = p.HasDoubleJump
			snap.myDoubleJumpRemaining = p.DoubleJumpRemainingSeconds
			snap.myDoubleJumpUsed = p.DoubleJumpUsed
			snap.myHasGodMode = p.HasGodMode
			snap.myGodModeRemaining = p.GodModeRemainingSeconds
			snap.myHasMagnet = p.HasMagnet
			snap.myMagnetRemaining = p.MagnetRemainingSeconds
			break
		}
	}
	g.stateMutex.Unlock()

	return snap
}

// --- INPUT & UPDATE LOGIC ---

func (g *Game) UpdateGame() {
	// --- 1. MUTE TOGGLE ---
	if inpututil.IsKeyJustPressed(ebiten.KeyM) {
		g.audio.ToggleMute()
	}

	// --- 2. KEYBOARD INPUT ---
	keyLeft := ebiten.IsKeyPressed(ebiten.KeyA) || ebiten.IsKeyPressed(ebiten.KeyLeft)
	keyRight := ebiten.IsKeyPressed(ebiten.KeyD) || ebiten.IsKeyPressed(ebiten.KeyRight)
	keyDown := inpututil.IsKeyJustPressed(ebiten.KeyS) || inpututil.IsKeyJustPressed(ebiten.KeyDown)
	keyJump := inpututil.IsKeyJustPressed(ebiten.KeySpace) || inpututil.IsKeyJustPressed(ebiten.KeyW) || inpututil.IsKeyJustPressed(ebiten.KeyUp)

	if keyLeft || keyRight || keyDown || keyJump {
		g.keyboardUsed = true
	}

	// --- 3. TOUCH INPUT HANDLING ---
	g.handleTouchInput()

	// --- 4. JOYSTICK RICHTUNG BERECHNEN ---
	joyDir := 0.0
	if g.joyActive {
		maxDist := g.joyRadius
		if maxDist == 0 {
			maxDist = 60.0
		}
		joyDir = (g.joyStickX - g.joyBaseX) / maxDist
		if joyDir < -1.0 {
			joyDir = -1.0
		} else if joyDir > 1.0 {
			joyDir = 1.0
		}
		if joyDir > -joystickDeadzone && joyDir < joystickDeadzone {
			joyDir = 0
		}
	}

	isJoyDown := g.joyActive && (g.joyStickY-g.joyBaseY) > g.joyRadius*joyDownThreshold

	wantsJump := keyJump || g.btnJumpActive
	g.btnJumpActive = false
	g.btnDownActive = false

	// Jump Buffer: Sprung-Wunsch für bis zu jumpBufferFrames speichern
	if wantsJump {
		g.jumpBufferFrames = jumpBufferFrames
	}

	// --- 5. INPUT SENDEN (MIT CLIENT PREDICTION, 20 TPS) ---
	if (g.connected || g.isOffline) && time.Since(g.lastInputTime) >= physicsStep {
		g.lastInputTime = time.Now()

		// Offline: Update Scroll & World logic locally
		if g.isOffline {
			g.updateOfflineLoop()
		}

		g.predictionMutex.Lock()
		wasOnGround := g.predictedGround
		g.predictionMutex.Unlock()

		if g.coyoteFrames > 0 {
			g.coyoteFrames--
		}
		if g.jumpBufferFrames > 0 {
			g.jumpBufferFrames--
		}

		g.predictionMutex.Lock()
		onGround := g.predictedGround
		g.predictionMutex.Unlock()

		if wasOnGround && !onGround {
			g.coyoteFrames = coyoteTimeFrames
		}

		effectiveJump := wantsJump ||
			(g.jumpBufferFrames > 0 && onGround) ||
			(wantsJump && g.coyoteFrames > 0)

		if effectiveJump {
			g.jumpBufferFrames = 0
		}

		input := InputState{
			Sequence: g.inputSequence,
			Left:     keyLeft || joyDir < -0.1,
			Right:    keyRight || joyDir > 0.1,
			Jump:     effectiveJump,
			Down:     keyDown || isJoyDown,
			JoyX:     joyDir,
		}

		g.predictionMutex.Lock()
		g.prevPredictedX = g.predictedX
		g.prevPredictedY = g.predictedY
		g.lastPhysicsTime = time.Now()
		g.inputSequence++
		input.Sequence = g.inputSequence
		g.ApplyInput(input)
		g.pendingInputs[input.Sequence] = input

		if len(g.pendingInputs) > inputCap {
			oldest := g.inputSequence - inputCap
			for seq := range g.pendingInputs {
				if seq < oldest {
					delete(g.pendingInputs, seq)
				}
			}
		}
		g.predictionMutex.Unlock()

		g.SendInputWithSequence(input)

		// Solo: Lokale Prüfung der Runde (Tod/Score)
		if g.gameMode == "solo" {
			g.checkSoloRound()
		}

		// Trail: store predicted position every physics step
		g.trail = append(g.trail, trailPoint{X: g.predictedX, Y: g.predictedY})
		if len(g.trail) > 8 {
			g.trail = g.trail[1:]
		}

		// --- Zitate & Meilensteine ---
		g.updateQuotes()
	}

	// --- EMOTES ---
	if inpututil.IsKeyJustPressed(ebiten.Key1) {
		g.SendCommand("EMOTE_1")
	}
	if inpututil.IsKeyJustPressed(ebiten.Key2) {
		g.SendCommand("EMOTE_2")
	}
	if inpututil.IsKeyJustPressed(ebiten.Key3) {
		g.SendCommand("EMOTE_3")
	}
	if inpututil.IsKeyJustPressed(ebiten.Key4) {
		g.SendCommand("EMOTE_4")
	}

	// --- 6. KAMERA LOGIK (mit Smoothing) ---
	g.stateMutex.Lock()
	targetCam := g.gameState.ScrollX
	g.stateMutex.Unlock()

	if targetCam < 0 {
		targetCam = 0
	}
	g.camX += (targetCam - g.camX) * 0.2

	// --- 7. CORRECTION OFFSET ABKLINGEN ---
	g.predictionMutex.Lock()
	g.correctionOffsetX *= correctionDecay
	g.correctionOffsetY *= correctionDecay
	if g.correctionOffsetX*g.correctionOffsetX < 0.09 {
		g.correctionOffsetX = 0
	}
	if g.correctionOffsetY*g.correctionOffsetY < 0.09 {
		g.correctionOffsetY = 0
	}
	g.predictionMutex.Unlock()

	// --- 8. PARTIKEL ---
	g.UpdateParticles(1.0 / 60.0)
	g.DetectAndSpawnParticles()
}

// handleTouchInput verarbeitet Touch-Eingaben für Joystick und Buttons.
func (g *Game) handleTouchInput() {
	touches := ebiten.TouchIDs()

	halfW := float64(g.lastCanvasWidth) * 0.55
	if halfW == 0 {
		halfW = float64(ScreenWidth) * 0.55
	}

	joyRadius := g.joyRadius
	if joyRadius == 0 {
		joyRadius = 60.0
	}

	justPressed := inpututil.JustPressedTouchIDs()
	isJustPressed := func(id ebiten.TouchID) bool {
		for _, j := range justPressed {
			if id == j {
				return true
			}
		}
		return false
	}

	if len(touches) == 0 {
		g.joyActive = false
		g.btnJumpPressed = false
		g.joyStickX = g.joyBaseX
		g.joyStickY = g.joyBaseY
		return
	}

	joyFound := false

	for _, id := range touches {
		x, y := ebiten.TouchPosition(id)
		fx, fy := float64(x), float64(y)

		// ── EMOTES ───────────────────────────────────────────────────────────
		if fx >= float64(g.lastCanvasWidth)-80.0 && fy >= 40.0 && fy <= 250.0 && isJustPressed(id) {
			emoteIdx := int((fy - 50.0) / 50.0)
			if emoteIdx >= 0 && emoteIdx <= 3 {
				g.SendCommand(fmt.Sprintf("EMOTE_%d", emoteIdx+1))
			}
			continue
		}

		if fx >= halfW {
			// ── RECHTE SEITE: Jump und Down ──────────────────────────────────────
			g.btnJumpPressed = true

			if isJustPressed(id) {
				if g.downBtnR > 0 {
					dx := fx - g.downBtnX
					dy := fy - g.downBtnY
					if dx*dx+dy*dy < g.downBtnR*g.downBtnR*1.5 {
						g.btnDownActive = true
						continue
					}
				}
				g.btnJumpActive = true
			}
			continue
		}

		// ── LINKE SEITE: Floating Joystick ───────────────────────────────────
		if !g.joyActive {
			g.joyActive = true
			g.joyTouchID = id
			g.joyBaseX = fx
			g.joyBaseY = fy
			g.joyStickX = fx
			g.joyStickY = fy
		}

		if g.joyActive && id == g.joyTouchID {
			joyFound = true
			dx := fx - g.joyBaseX
			dy := fy - g.joyBaseY
			dist := math.Sqrt(dx*dx + dy*dy)
			if dist > joyRadius {
				scale := joyRadius / dist
				dx *= scale
				dy *= scale
			}
			g.joyStickX = g.joyBaseX + dx
			g.joyStickY = g.joyBaseY + dy
		}
	}

	if !joyFound {
		g.joyActive = false
		g.btnJumpPressed = false
		g.joyStickX = g.joyBaseX
		g.joyStickY = g.joyBaseY
	}
}

// --- RENDERING LOGIC ---

// DrawGame ist der zentrale Render-Einstiegspunkt für den Spielzustand.
// Es nimmt einen lock-freien Snapshot aller benötigten Daten und delegiert
// das Zeichnen an spezialisierte Sub-Funktionen.
func (g *Game) DrawGame(screen *ebiten.Image) {
	// GAMEOVER wird separat behandelt und beendet die Funktion früh.
	g.stateMutex.Lock()
	status := g.gameState.Status
	g.stateMutex.Unlock()

	if status == "GAMEOVER" {
		g.drawGameOver(screen)
		return
	}

	snap := g.takeRenderSnapshot(screen)

	// Screen Shake: draw to offscreen buffer when active
	target := screen
	if g.shakeFrames > 0 {
		w, h := screen.Size()
		if g.shakeBuffer == nil {
			g.shakeBuffer = ebiten.NewImage(w, h)
		} else if bw, bh := g.shakeBuffer.Size(); bw != w || bh != h {
			g.shakeBuffer = ebiten.NewImage(w, h)
		}
		g.shakeBuffer.Clear()
		target = g.shakeBuffer
		g.shakeFrames--
	}

	g.drawBackground(target, snap)
	g.RenderGround(target, g.camX, snap.viewScale)
	g.drawTeacher(target, snap)
	g.drawWorldObjects(target, snap)
	g.drawPlayers(target, snap)
	g.drawStatusUI(target, snap)
	g.drawDeathZoneLine(target, snap.canvasH)
	g.RenderParticles(target)

	if g.showDebug {
		g.drawDebugOverlay(target)
	}
	if !g.keyboardUsed {
		g.drawTouchControls(target)
	}

	// Blit shakeBuffer to screen with random offset
	if target != screen {
		ox := (rand.Float64()*2 - 1) * g.shakeIntensity
		oy := (rand.Float64()*2 - 1) * g.shakeIntensity
		if g.shakeFrames == 0 {
			g.shakeIntensity = 0
		}
		op := &ebiten.DrawImageOptions{}
		op.GeoM.Translate(ox, oy)
		screen.DrawImage(g.shakeBuffer, op)
	}
}

// drawGameOver behandelt den GAMEOVER-Bildschirm inkl. Score-Übermittlung.
func (g *Game) drawGameOver(screen *ebiten.Image) {
	g.stateMutex.Lock()
	myScore := 0
	for _, p := range g.gameState.Players {
		if p.Name == g.playerName {
			myScore = p.Score
			break
		}
	}
	g.stateMutex.Unlock()

	if !g.scoreSubmitted {
		g.submitScore()
		g.sendGameOverToJS(myScore)
	}

	g.drawGameOverScreen(screen, myScore)
}

// drawBackground zeichnet das Hintergrundbild (wechselt nach Score).
func (g *Game) drawBackground(screen *ebiten.Image, snap renderSnapshot) {
	bgID := "background"
	if snap.myScore >= 10000 {
		bgID = "background2"
	} else if snap.myScore >= 5000 {
		bgID = "background1"
	}

	bgImg, exists := g.assetsImages[bgID]
	if !exists || bgImg == nil {
		screen.Fill(ColSky)
		return
	}

	bgW, bgH := bgImg.Size()
	scaleX := float64(snap.canvasW) / float64(bgW)
	scaleY := float64(snap.canvasH) / float64(bgH)
	scale := math.Max(scaleX, scaleY)

	op := &ebiten.DrawImageOptions{}
	op.GeoM.Scale(scale, scale)
	op.GeoM.Translate(
		(float64(snap.canvasW)-float64(bgW)*scale)/2,
		(float64(snap.canvasH)-float64(bgH)*scale)/2,
	)
	screen.DrawImage(bgImg, op)
}

// drawWorldObjects zeichnet Chunk-Objekte und bewegende Plattformen.
func (g *Game) drawWorldObjects(screen *ebiten.Image, snap renderSnapshot) {
	for _, activeChunk := range snap.chunks {
		chunkDef, exists := g.world.ChunkLibrary[activeChunk.ChunkID]
		if !exists {
			log.Printf("⚠️ Chunk '%s' nicht in Library gefunden!", activeChunk.ChunkID)
			continue
		}
		for objIdx, obj := range chunkDef.Objects {
			if obj.MovingPlatform != nil {
				continue // Bewegende Plattformen separat gerendert
			}
			if assetDef, ok := g.world.Manifest.Assets[obj.AssetID]; ok {
				key := fmt.Sprintf("%s_%d", activeChunk.ChunkID, objIdx)
				if assetDef.Type == "coin" && snap.collectedCoins[key] {
					continue
				}
				if assetDef.Type == "powerup" && snap.collectedPowerups[key] {
					continue
				}
			}
			g.DrawAsset(screen, obj.AssetID, activeChunk.X+obj.X, WorldToScreenYWithHeight(obj.Y, snap.canvasH))
		}
	}

	for _, mp := range snap.movingPlatforms {
		g.DrawAsset(screen, mp.AssetID, mp.X, WorldToScreenYWithHeight(mp.Y, snap.canvasH))
	}

	// Debug: Boden-Collider visualisieren (GRÜN)
	if g.showDebug {
		vector.StrokeRect(screen,
			float32(-g.camX*snap.viewScale), float32(WorldToScreenYWithHeight(540, snap.canvasH)),
			10000, 5000, 2, color.RGBA{0, 255, 0, 255}, false)
	}
}

// drawPlayers zeichnet alle Spieler mit Sprites, Nametags und optionalen Hitboxen.
func (g *Game) drawPlayers(screen *ebiten.Image, snap renderSnapshot) {
	for id, p := range snap.players {
		posX, posY := p.X, p.Y

		if p.Name == g.playerName {
			// Lokaler Spieler: Interpolation zwischen Physics-Steps (20 TPS → 60 fps)
			alpha := float64(time.Since(snap.physicsTime)) / float64(physicsStep)
			if alpha > 1 {
				alpha = 1
			}
			posX = snap.prevPredX + (snap.predX-snap.prevPredX)*alpha + snap.offsetX
			posY = snap.prevPredY + (snap.predY-snap.prevPredY)*alpha + snap.offsetY
		}

		sprite := g.selectPlayerSprite(p.OnGround, p.VY)
		screenY := WorldToScreenYWithHeight(posY, snap.canvasH)

		if p.Name == g.playerName && len(g.trail) > 1 {
			for i, tp := range g.trail {
				ratio := float32(i+1) / float32(len(g.trail))
				alpha := uint8(ratio * 80)
				r := float32(8 * ratio)
				tx := float32((tp.X - g.camX) * snap.viewScale)
				ty := float32(WorldToScreenYWithHeight(tp.Y, snap.canvasH))
				vector.DrawFilledCircle(screen, tx, ty, r, color.RGBA{200, 220, 255, alpha}, false)
			}
		}

		g.DrawAsset(screen, sprite, posX, screenY)

		name := p.Name
		if name == "" {
			name = id
		}
		
		// In Presentation Mode normal players don't show names, only Host/PRESENTATION does (which is hidden anyway)
		if snap.status != "PRESENTATION" || name == g.playerName {
			text.Draw(screen, name, basicfont.Face7x13, int((posX-g.camX)*snap.viewScale), int(screenY-25), ColText)
		}

		// Draw Emote if active
		if p.State != "" && strings.HasPrefix(p.State, "EMOTE_") {
			emoteStr := p.State[6:] // e.g. EMOTE_1 -> "1"
			emoteMap := map[string]string{
				"1": "❤️",
				"2": "😂",
				"3": "😡",
				"4": "👍",
			}
			if emoji, ok := emoteMap[emoteStr]; ok {
				text.Draw(screen, emoji, basicfont.Face7x13, int((posX-g.camX)*snap.viewScale+15), int(screenY-40), color.White)
			}
		}

		if g.showDebug {
			g.drawPlayerHitbox(screen, posX, screenY, snap.viewScale)
		}
	}
}

// selectPlayerSprite gibt den Sprite-Namen basierend auf dem Bewegungszustand zurück.
func (g *Game) selectPlayerSprite(onGround bool, vy float64) string {
	if onGround || (vy >= -3.0 && vy <= 3.0) {
		return "player"
	}
	if vy < -5.0 {
		return "jump0" // Springt nach oben
	}
	return "jump1" // Fällt oder höchster Punkt
}

// drawPlayerHitbox visualisiert die Spieler-Hitbox im Debug-Modus.
func (g *Game) drawPlayerHitbox(screen *ebiten.Image, posX, screenY, viewScale float64) {
	def, ok := g.world.Manifest.Assets["player"]
	if !ok {
		return
	}
	hx := float32((posX + def.DrawOffX + def.Hitbox.OffsetX - g.camX) * viewScale)
	hy := float32(screenY + def.DrawOffY + def.Hitbox.OffsetY)
	vector.StrokeRect(screen, hx, hy, float32(def.Hitbox.W), float32(def.Hitbox.H), 3, color.RGBA{255, 0, 0, 255}, false)
	vector.DrawFilledCircle(screen, float32((posX-g.camX)*viewScale), float32(screenY), 5, color.RGBA{255, 255, 0, 255}, false)
}

// drawStatusUI zeichnet das spielzustandsabhängige UI (Countdown, Score, Spectator).
func (g *Game) drawStatusUI(screen *ebiten.Image, snap renderSnapshot) {
	switch snap.status {
	case "COUNTDOWN":
		msg := fmt.Sprintf("GO IN: %d", snap.timeLeft)
		text.Draw(screen, msg, basicfont.Face7x13, snap.canvasW/2-40, snap.canvasH/2, color.RGBA{255, 255, 0, 255})
	case "RUNNING":
		g.drawPowerupHUD(screen, snap)
		g.drawDangerOverlay(screen, snap)
		g.drawScoreBox(screen, snap)
		if snap.isDead {
			g.drawSpectatorOverlay(screen, snap)
		}

		// --- MEILENSTEIN-QUOTE ---
		if time.Now().Before(g.milestoneQuoteTime) {
			msg := fmt.Sprintf("🎉 %d PUNKTE! \"%s\"", g.lastMilestone, g.milestoneQuote.Text)
			tw := float32(len(msg) * 7)
			text.Draw(screen, msg, basicfont.Face7x13, int(float32(snap.canvasW)/2-tw/2), 60, color.RGBA{255, 255, 0, 255})
		}
	}
}

// drawDangerOverlay zeichnet einen roten Bildschirmrand wenn DifficultyFactor > 0.5.
func (g *Game) drawDangerOverlay(screen *ebiten.Image, snap renderSnapshot) {
	if snap.difficultyFactor <= 0.5 {
		return
	}
	alpha := uint8((snap.difficultyFactor - 0.5) * 2.0 * 60)
	col := color.RGBA{200, 0, 0, alpha}
	w, h := float32(snap.canvasW), float32(snap.canvasH)
	bw := float32(8)
	vector.DrawFilledRect(screen, 0, 0, w, bw, col, false)
	vector.DrawFilledRect(screen, 0, h-bw, w, bw, col, false)
	vector.DrawFilledRect(screen, 0, 0, bw, h, col, false)
	vector.DrawFilledRect(screen, w-bw, 0, bw, h, col, false)
}

// drawScoreBox zeichnet die Distanz- und Score-Anzeige oben rechts.
func (g *Game) drawScoreBox(screen *ebiten.Image, snap renderSnapshot) {
	dist := fmt.Sprintf("Distance: %.0f m", g.camX/64.0)
	scoreStr := fmt.Sprintf("Score: %d", snap.myScore)

	maxWidth := len(dist) * 7
	if sw := len(scoreStr) * 7; sw > maxWidth {
		maxWidth = sw
	}

	boxW := float32(maxWidth + 20)
	boxH := float32(50)
	boxX := float32(snap.canvasW) - boxW - 10
	boxY := float32(10)

	vector.DrawFilledRect(screen, boxX, boxY, boxW, boxH, color.RGBA{60, 60, 60, 200}, false)
	vector.StrokeRect(screen, boxX, boxY, boxW, boxH, 2, color.RGBA{100, 100, 100, 255}, false)
	textX := int(boxX) + 10
	text.Draw(screen, dist, basicfont.Face7x13, textX, int(boxY)+22, color.White)
	text.Draw(screen, scoreStr, basicfont.Face7x13, textX, int(boxY)+40, color.RGBA{255, 215, 0, 255})
}

// drawSpectatorOverlay zeigt das Spectator-Banner wenn der lokale Spieler tot ist.
func (g *Game) drawSpectatorOverlay(screen *ebiten.Image, snap renderSnapshot) {
	vector.DrawFilledRect(screen, 0, 0, float32(snap.canvasW), 80, color.RGBA{150, 0, 0, 180}, false)
	text.Draw(screen, "☠ DU BIST TOT - SPECTATOR MODE ☠", basicfont.Face7x13, snap.canvasW/2-140, 30, color.White)
	text.Draw(screen, fmt.Sprintf("Dein Final Score: %d", snap.myScore), basicfont.Face7x13, snap.canvasW/2-90, 55, color.RGBA{255, 255, 0, 255})
}

// drawPowerupHUD zeichnet Timer-Balken für aktive Powerups (oben links).
func (g *Game) drawPowerupHUD(screen *ebiten.Image, snap renderSnapshot) {
	type bar struct {
		label     string
		remaining float64
		maxTime   float64
		col       color.RGBA
		active    bool
		used      bool
	}
	bars := []bar{
		{"JUMP x2", snap.myDoubleJumpRemaining, 15.0, color.RGBA{100, 200, 255, 255}, snap.myHasDoubleJump, snap.myDoubleJumpUsed},
		{"GODMODE", snap.myGodModeRemaining, 10.0, color.RGBA{255, 215, 0, 255}, snap.myHasGodMode, false},
		{"MAGNET", snap.myMagnetRemaining, 8.0, color.RGBA{255, 80, 220, 255}, snap.myHasMagnet, false},
	}
	x := float32(10)
	y := float32(60)
	barW := float32(110)
	barH := float32(13)
	for _, b := range bars {
		if !b.active {
			continue
		}
		ratio := float32(b.remaining / b.maxTime)
		if ratio > 1 {
			ratio = 1
		}
		if ratio < 0 {
			ratio = 0
		}
		// Background
		vector.DrawFilledRect(screen, x, y, barW, barH, color.RGBA{30, 30, 30, 200}, false)
		// Fill — blinks red when < 30%
		fillCol := b.col
		if b.used {
			fillCol = color.RGBA{fillCol.R / 3, fillCol.G / 3, fillCol.B / 3, fillCol.A}
		}
		if ratio < 0.3 && (time.Now().UnixMilli()/250)%2 == 0 {
			fillCol = color.RGBA{255, 60, 60, 255}
		}
		vector.DrawFilledRect(screen, x, y, barW*ratio, barH, fillCol, false)
		vector.StrokeRect(screen, x, y, barW, barH, 1, color.RGBA{140, 140, 140, 180}, false)
		label := fmt.Sprintf("%s %.0fs", b.label, b.remaining)
		text.Draw(screen, label, basicfont.Face7x13, int(x)+2, int(y)+10, color.White)
		y += barH + 5
	}
}

// drawDeathZoneLine zeichnet die rote Todes-Linie am linken Bildschirmrand.
func (g *Game) drawDeathZoneLine(screen *ebiten.Image, canvasH int) {
	vector.StrokeLine(screen, 0, 0, 0, float32(canvasH), 10, color.RGBA{255, 0, 0, 128}, false)
	text.Draw(screen, "! DEATH ZONE !", basicfont.Face7x13, 10, canvasH/2, color.RGBA{255, 0, 0, 255})
}

// drawTouchControls zeichnet den virtuellen Joystick und die Touch-Buttons.
// Wird nur angezeigt wenn keine Tastatur benutzt wurde.
func (g *Game) drawTouchControls(screen *ebiten.Image) {
	tcW, tcH := screen.Size()

	// Canvas-Maße cachen (werden in handleTouchInput benötigt)
	g.lastCanvasHeight = tcH
	g.lastCanvasWidth = tcW

	floorY := GetFloorYFromHeight(tcH)
	refDim := math.Min(float64(tcW), float64(tcH))

	joyR := refDim * joyRadiusScale
	knobR := joyR * joyKnobRatio
	jumpR := refDim * jumpBtnScale
	downR := refDim * downBtnScale

	g.joyRadius = joyR

	// ── A) Floating Joystick (links) ─────────────────────────────────────────
	if !g.joyActive {
		g.joyBaseX = float64(tcW) * joyDefaultXRatio
		g.joyBaseY = floorY - joyR - 12
		g.joyStickX = g.joyBaseX
		g.joyStickY = g.joyBaseY
	}

	ringAlpha := uint8(40)
	if g.joyActive {
		ringAlpha = 70
	}
	vector.DrawFilledCircle(screen, float32(g.joyBaseX), float32(g.joyBaseY), float32(joyR), color.RGBA{80, 80, 80, ringAlpha}, false)
	vector.StrokeCircle(screen, float32(g.joyBaseX), float32(g.joyBaseY), float32(joyR), 2, color.RGBA{120, 120, 120, 90}, false)

	knobCol := color.RGBA{180, 180, 180, 100}
	if g.joyActive {
		knobCol = color.RGBA{80, 220, 80, 160}
	}
	vector.DrawFilledCircle(screen, float32(g.joyStickX), float32(g.joyStickY), float32(knobR), knobCol, false)

	// ── B) Jump Button (rechts) ───────────────────────────────────────────────
	jumpX := float64(tcW) * jumpBtnXRatio
	jumpY := floorY - jumpR - 12

	jumpFill := color.RGBA{220, 50, 50, 60}
	jumpStroke := color.RGBA{255, 80, 80, 130}
	if g.btnJumpPressed {
		jumpFill = color.RGBA{255, 80, 80, 130}
		jumpStroke = color.RGBA{255, 160, 160, 200}
	}
	vector.DrawFilledCircle(screen, float32(jumpX), float32(jumpY), float32(jumpR), jumpFill, false)
	vector.StrokeCircle(screen, float32(jumpX), float32(jumpY), float32(jumpR), 2.5, jumpStroke, false)
	text.Draw(screen, "JUMP", basicfont.Face7x13, int(jumpX)-14, int(jumpY)+5, color.RGBA{255, 255, 255, 180})

	// ── C) Down/FastFall Button (mitte-rechts) ────────────────────────────────
	downX := float64(tcW) * downBtnXRatio
	downY := floorY - downR - 12
	g.downBtnX = downX
	g.downBtnY = downY
	g.downBtnR = downR

	vector.DrawFilledCircle(screen, float32(downX), float32(downY), float32(downR), color.RGBA{50, 120, 220, 55}, false)
	vector.StrokeCircle(screen, float32(downX), float32(downY), float32(downR), 2, color.RGBA{80, 160, 255, 120}, false)
	text.Draw(screen, "▼", basicfont.Face7x13, int(downX)-4, int(downY)+5, color.RGBA{200, 220, 255, 180})

	// ── D) Emote Buttons (oben rechts) ─────────────────────────────────────────
	emoteY := 50.0
	emoteXBase := float64(tcW) - 60.0
	emoteSize := 40.0
	emotes := []string{"❤️", "😂", "😡", "👍"}
	
	for i, em := range emotes {
		x := emoteXBase
		y := emoteY + float64(i)*50.0
		vector.DrawFilledRect(screen, float32(x), float32(y), float32(emoteSize), float32(emoteSize), color.RGBA{0, 0, 0, 100}, false)
		vector.StrokeRect(screen, float32(x), float32(y), float32(emoteSize), float32(emoteSize), 2, color.RGBA{255, 255, 255, 100}, false)
		text.Draw(screen, em, basicfont.Face7x13, int(x)+10, int(y)+25, color.White)
	}
}

// TriggerShake aktiviert den Screen-Shake-Effekt.
func (g *Game) TriggerShake(frames int, intensity float64) {
	if frames > g.shakeFrames {
		g.shakeFrames = frames
	}
	if intensity > g.shakeIntensity {
		g.shakeIntensity = intensity
	}
}

// updateQuotes verarbeitet die Logik für zufällige Lehrer-Sprüche und Meilensteine.
func (g *Game) updateQuotes() {
	g.stateMutex.Lock()
	status := g.gameState.Status
	myScore := 0
	for _, p := range g.gameState.Players {
		if p.Name == g.playerName {
			myScore = p.Score
			break
		}
	}
	g.stateMutex.Unlock()

	if status != "RUNNING" {
		return
	}

	now := time.Now()

	// 1. Zufällige Lehrer-Sprüche (alle 10-25 Sekunden)
	if now.After(g.teacherQuoteTime) {
		g.teacherQuote = game.GetRandomQuote()
		// Nächster Spruch in 15-30 Sekunden
		g.teacherQuoteTime = now.Add(time.Duration(15+rand.Intn(15)) * time.Second)
	}

	// 2. Meilensteine (alle 1000 Punkte)
	milestone := (myScore / 1000) * 1000
	if milestone > 0 && milestone > g.lastMilestone {
		g.lastMilestone = milestone
		g.milestoneQuote = game.GetRandomQuote()
		g.milestoneQuoteTime = now.Add(4 * time.Second) // 4 Sekunden anzeigen
		log.Printf("🎉 Meilenstein erreicht: %d Punkte!", milestone)
	}
}

// drawSpeechBubble zeichnet eine einfache Sprechblase mit Text.
func (g *Game) drawSpeechBubble(screen *ebiten.Image, x, y float32, msg string) {
	// Text-Breite grob schätzen (Face7x13: ca 7px pro Zeichen)
	tw := float32(len(msg) * 7)
	th := float32(15)
	padding := float32(8)

	bx := x + 10
	by := y - th - padding*2
	bw := tw + padding*2
	bh := th + padding*2

	// Hintergrund
	vector.DrawFilledRect(screen, bx, by, bw, bh, color.RGBA{255, 255, 255, 220}, false)
	vector.StrokeRect(screen, bx, by, bw, bh, 2, color.Black, false)

	// Kleiner Pfeil
	vector.DrawFilledCircle(screen, bx, by+bh/2, 5, color.RGBA{255, 255, 255, 220}, false)

	text.Draw(screen, msg, basicfont.Face7x13, int(bx+padding), int(by+padding+10), color.Black)
}

// drawTeacher zeichnet den Lehrer-Charakter am linken Bildschirmrand.
func (g *Game) drawTeacher(screen *ebiten.Image, snap renderSnapshot) {
	if snap.status != "RUNNING" && snap.status != "COUNTDOWN" {
		return
	}

	danger := snap.difficultyFactor
	groundY := float32(GetFloorYFromHeight(snap.canvasH))

	// Teacher slides in from the left as danger increases
	// At danger=0: fully offscreen (-70). At danger=1: at X=5.
	teacherCX := float32(-70 + danger*75)

	bodyW := float32(28)
	bodyH := float32(55 + danger*15)
	headR := float32(14)

	bodyX := teacherCX - bodyW/2
	bodyY := groundY - bodyH

	alpha := uint8(40 + danger*215)

	// Shadow on left edge (red vignette)
	shadowW := int(20 + danger*40)
	for i := 0; i < shadowW; i++ {
		a := uint8(float64(70) * float64(shadowW-i) / float64(shadowW) * danger)
		vector.DrawFilledRect(screen, float32(i), 0, 1, float32(snap.canvasH), color.RGBA{200, 0, 0, a}, false)
	}

	// Only draw body when partially visible
	if teacherCX > -40 {
		// Body (dark suit)
		vector.DrawFilledRect(screen, bodyX, bodyY, bodyW, bodyH, color.RGBA{100, 10, 10, alpha}, false)
		// Tie
		vector.DrawFilledRect(screen, teacherCX-3, bodyY+4, 6, bodyH-8, color.RGBA{180, 0, 0, alpha}, false)
		// Head
		vector.DrawFilledCircle(screen, teacherCX, bodyY-headR, headR, color.RGBA{210, 160, 110, alpha}, false)
		// Angry eyes
		eyeA := uint8(200)
		vector.DrawFilledCircle(screen, teacherCX-5, bodyY-headR-1, 3, color.RGBA{255, 0, 0, eyeA}, false)
		vector.DrawFilledCircle(screen, teacherCX+5, bodyY-headR-1, 3, color.RGBA{255, 0, 0, eyeA}, false)
		// Legs
		vector.DrawFilledRect(screen, bodyX+1, groundY, bodyW/2-2, float32(snap.canvasH), color.RGBA{60, 10, 10, alpha}, false)
		vector.DrawFilledRect(screen, bodyX+bodyW/2+1, groundY, bodyW/2-2, float32(snap.canvasH), color.RGBA{60, 10, 10, alpha}, false)

		// --- SPRECHBLASE ---
		if time.Now().Before(g.teacherQuoteTime.Add(-10 * time.Second)) && g.teacherQuote.Text != "" {
			// Wir zeigen den Spruch für 5-10 Sekunden an
			g.drawSpeechBubble(screen, teacherCX+15, bodyY-20, g.teacherQuote.Text)
		}
		}
	// Warning text — blinks when close
	if danger > 0.55 {
		if (time.Now().UnixMilli()/300)%2 == 0 {
			warnX := int(teacherCX) - 20
			if warnX < 2 {
				warnX = 2
			}
			text.Draw(screen, "LEHRER!", basicfont.Face7x13, warnX, int(bodyY)-20, color.RGBA{255, 50, 50, 255})
		}
	}
}

// --- ASSET HELPER ---

// DrawAsset zeichnet ein Asset an einer Welt-Position auf den Screen.
// Objekte außerhalb des sichtbaren Bereichs (±cullingBuffer) werden übersprungen.
func (g *Game) DrawAsset(screen *ebiten.Image, assetID string, worldX, worldY float64) {
	def, ok := g.world.Manifest.Assets[assetID]
	if !ok {
		return
	}

	canvasW, canvasH := screen.Size()
	viewScale := GetScaleFromHeight(canvasH)
	screenX := (worldX - g.camX) * viewScale

	if screenX < -cullingBuffer || screenX > float64(canvasW)+cullingBuffer {
		return
	}

	img := g.assetsImages[assetID]
	if img != nil {
		op := &ebiten.DrawImageOptions{}
		op.Filter = ebiten.FilterLinear
		finalScale := def.Scale * viewScale
		op.GeoM.Scale(finalScale, finalScale)
		op.GeoM.Translate(
			screenX+(def.DrawOffX*viewScale),
			worldY+(def.DrawOffY*viewScale),
		)
		if def.Color.R != 0 || def.Color.G != 0 || def.Color.B != 0 || def.Color.A != 0 {
			op.ColorScale.ScaleWithColor(def.Color.ToRGBA())
		}
		screen.DrawImage(img, op)
	} else {
		// Fallback: farbiges Rechteck wenn Bild fehlt
		vector.DrawFilledRect(screen,
			float32(screenX+def.Hitbox.OffsetX),
			float32(worldY+def.Hitbox.OffsetY),
			float32(def.Hitbox.W),
			float32(def.Hitbox.H),
			def.Color.ToRGBA(),
			false,
		)
	}
}

// --- DEBUG OVERLAY ---

// drawDebugOverlay zeigt Performance- und Netzwerk-Stats (F3 zum Umschalten).
func (g *Game) drawDebugOverlay(screen *ebiten.Image) {
	vector.DrawFilledRect(screen, 10, 80, 350, 170, color.RGBA{0, 0, 0, 180}, false)
	vector.StrokeRect(screen, 10, 80, 350, 170, 2, color.RGBA{255, 255, 0, 255}, false)

	y := 95
	lh := 15 // line height

	text.Draw(screen, "=== DEBUG INFO (F3) ===", basicfont.Face7x13, 20, y, color.RGBA{255, 255, 0, 255})
	y += lh + 5

	fpsCol := color.RGBA{0, 255, 0, 255}
	if g.currentFPS < 15 {
		fpsCol = color.RGBA{255, 0, 0, 255}
	} else if g.currentFPS < 30 {
		fpsCol = color.RGBA{255, 165, 0, 255}
	}
	text.Draw(screen, fmt.Sprintf("FPS: %.1f", g.currentFPS), basicfont.Face7x13, 20, y, fpsCol)
	y += lh

	updateAge := time.Since(g.lastUpdateTime).Milliseconds()
	latencyCol := color.RGBA{0, 255, 0, 255}
	if updateAge > 200 {
		latencyCol = color.RGBA{255, 0, 0, 255}
	} else if updateAge > 100 {
		latencyCol = color.RGBA{255, 165, 0, 255}
	}
	text.Draw(screen, fmt.Sprintf("Update Age: %dms", updateAge), basicfont.Face7x13, 20, y, latencyCol)
	y += lh

	text.Draw(screen, fmt.Sprintf("Total Updates: %d", g.totalUpdates), basicfont.Face7x13, 20, y, color.White)
	y += lh

	oooCol := color.RGBA{0, 255, 0, 255}
	if g.outOfOrderCount > 50 {
		oooCol = color.RGBA{255, 0, 0, 255}
	} else if g.outOfOrderCount > 10 {
		oooCol = color.RGBA{255, 165, 0, 255}
	}
	text.Draw(screen, fmt.Sprintf("Out-of-Order: %d", g.outOfOrderCount), basicfont.Face7x13, 20, y, oooCol)
	y += lh

	if g.totalUpdates > 0 {
		lossRate := float64(g.outOfOrderCount) / float64(g.totalUpdates+g.outOfOrderCount) * 100
		lossCol := color.RGBA{0, 255, 0, 255}
		if lossRate > 10 {
			lossCol = color.RGBA{255, 0, 0, 255}
		} else if lossRate > 5 {
			lossCol = color.RGBA{255, 165, 0, 255}
		}
		text.Draw(screen, fmt.Sprintf("Loss Rate: %.1f%%", lossRate), basicfont.Face7x13, 20, y, lossCol)
		y += lh
	}

	text.Draw(screen, fmt.Sprintf("Pending Inputs: %d", g.pendingInputCount), basicfont.Face7x13, 20, y, color.White)
	y += lh

	corrCol := color.RGBA{0, 255, 0, 255}
	if g.correctionCount > 500 {
		corrCol = color.RGBA{255, 0, 0, 255}
	} else if g.correctionCount > 100 {
		corrCol = color.RGBA{255, 165, 0, 255}
	}
	text.Draw(screen, fmt.Sprintf("Corrections: %d", g.correctionCount), basicfont.Face7x13, 20, y, corrCol)
	y += lh

	corrMag := math.Sqrt(g.correctionX*g.correctionX + g.correctionY*g.correctionY)
	corrMagCol := color.RGBA{0, 255, 0, 255}
	if corrMag > 0.1 {
		corrMagCol = color.RGBA{255, 165, 0, 255}
	}
	text.Draw(screen, fmt.Sprintf("Corr Mag: %.1f", corrMag), basicfont.Face7x13, 20, y, corrMagCol)
	y += lh

	text.Draw(screen, fmt.Sprintf("Server Seq: %d", g.lastRecvSeq), basicfont.Face7x13, 20, y, color.White)
}
