package main

import (
	"fmt"
	"image/color"
	_ "image/jpeg" // JPEG-Decoder
	_ "image/png"  // PNG-Decoder
	"log"
	mrand "math/rand"
	"sort"
	"strings"
	"sync"
	"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"
)

// --- KONFIGURATION ---
const (
	ScreenWidth      = 1280
	ScreenHeight     = 720
	StateMenu        = 0
	StateLobby       = 1
	StateGame        = 2
	StateLeaderboard = 3
	StatePresentation = 4
	RefFloorY = 540 // Server-Welt Boden-Position (unveränderlich)
)

var (
	ColText      = color.White
	ColBtnNormal = color.RGBA{40, 44, 52, 255}
	ColSky       = color.RGBA{135, 206, 235, 255}
	ColGrass     = color.RGBA{34, 139, 34, 255}
	ColDirt      = color.RGBA{101, 67, 33, 255}
)

// trailPoint speichert eine Position für den Player-Trail
type trailPoint struct {
	X, Y float64
}

// InputState speichert einen einzelnen Input für Replay
type InputState struct {
	Sequence uint32
	Left     bool
	Right    bool
	Jump     bool
	Down     bool
	JoyX     float64 // Analoger Joystick-Wert (-1.0 bis 1.0)
}

type MovingPlatform struct {
	ChunkID    string
	ObjectIdx  int
	AssetID    string
	CurrentX   float64
	CurrentY   float64
	StartX     float64
	StartY     float64
	EndX       float64
	EndY       float64
	Speed      float64
	Direction  float64
	IsActive   bool
	HitboxW    float64
	HitboxH    float64
	DrawOffX   float64
	DrawOffY   float64
	HitboxOffX float64
	HitboxOffY float64
}

// --- GAME STRUCT ---
type Game struct {
	appState       int
	wsConn         *wsConn // WebSocket für WASM
	connGeneration int     // Erhöht bei jedem Disconnect; macht alte WS-Handler ungültig
	isConnecting   bool    // Guard gegen mehrfaches connectAndStart()
	gameState      game.GameState
	stateMutex     sync.Mutex
	connected      bool
	world        *game.World
	assetsImages map[string]*ebiten.Image

	// Spieler Info
	playerName  string
	playerCode  string // Eindeutiger UUID für Leaderboard
	roomID      string
	activeField string // "name" oder "room" oder "teamname"
	gameMode    string // "solo" oder "coop"
	isOffline   bool   // Läuft das Spiel lokal ohne Server?
	offlineMovingPlatforms []*MovingPlatform // Lokale bewegende Plattformen für Offline-Modus
	godModeEndTime    time.Time
	magnetEndTime     time.Time
	doubleJumpEndTime time.Time
	isHost      bool
	teamName    string // Team-Name für Coop beim Game Over

	// Leaderboard
	leaderboard      []game.LeaderboardEntry
	scoreSubmitted   bool
	showLeaderboard  bool
	leaderboardMutex sync.Mutex

	// Lobby State (für Change Detection)
	lastPlayerCount int
	lastStatus      string

	// Client Prediction
	predictedX              float64 // Vorhergesagte Position
	predictedY              float64
	predictedVX             float64
	predictedVY             float64
	predictedGround         bool
	predictedOnWall         bool
	predictedHasDoubleJump  bool // Lokale Kopie des Double-Jump-Powerups
	predictedDoubleJumpUsed bool // Wurde zweiter Sprung schon verbraucht?
	currentSpeed    float64               // Aktuelle Scroll-Geschwindigkeit vom Server
	inputSequence   uint32                // Sequenznummer für Inputs
	pendingInputs   map[uint32]InputState // Noch nicht bestätigte Inputs
	lastServerSeq   uint32                // Letzte vom Server bestätigte Sequenz
	predictionMutex sync.Mutex            // Mutex für pendingInputs
	lastRecvSeq     uint32                // Letzte empfangene Server-Sequenznummer (für Out-of-Order-Erkennung)
	lastInputTime   time.Time             // Letzter Input-Send (für 20 TPS Throttling)

	// Interpolation (60fps Draw ↔ 20hz Physics)
	prevPredictedX   float64   // Position vor letztem Physics-Step (für Interpolation)
	prevPredictedY   float64
	lastPhysicsTime  time.Time // Zeitpunkt des letzten Physics-Steps

	// Jump Buffer: Sprung kurz vor Landung speichern → löst beim Aufkommen aus
	jumpBufferFrames int // Countdown in Physics-Frames (bei 0: kein Buffer)

	// Coyote Time: Sprung kurz nach Abgang von Kante erlauben
	coyoteFrames int // Countdown in Physics-Frames

	// Smooth Correction (Debug-Info)
	correctionX float64 // Letzte Korrektur-Magnitude X
	correctionY float64 // Letzte Korrektur-Magnitude Y

	// Visueller Korrektur-Offset: Physics springt sofort, Display folgt sanft
	// display_pos = predicted + correctionOffset (blendet harte Korrekturen aus)
	correctionOffsetX float64
	correctionOffsetY float64

	// Screen Shake
	shakeFrames    int
	shakeIntensity float64
	shakeBuffer    *ebiten.Image

	// Particle System
	particles            []Particle
	particlesMutex       sync.Mutex
	lastGroundState      bool            // Für Landing-Detection
	lastCollectedCoins   map[string]bool // Für Coin-Partikel
	lastCollectedPowerups map[string]bool // Für Powerup-Partikel
	lastPlayerStates     map[string]game.PlayerState // Für Death-Partikel
	trail                []trailPoint                // Player Trail

	// Highscore
	localHighscore int
	roundStartTime time.Time // Startzeit der aktuellen Runde (für Solo)

	// Audio System
	audio *AudioSystem

	// Zitate / Sprüche
	teacherQuote      game.Quote
	teacherQuoteTime  time.Time
	milestoneQuote    game.Quote
	milestoneQuoteTime time.Time
	deathQuote        game.Quote
	lastMilestone     int

	// Presentation Mode
	presQuote         game.Quote
	presQuoteTime     time.Time
	presAssets        []presAssetInstance
	lastPresUpdate    time.Time
	presQRCode        *ebiten.Image

	// Kamera
	camX float64

	// Touch State
	joyBaseX, joyBaseY   float64
	joyStickX, joyStickY float64
	joyActive            bool
	joyTouchID           ebiten.TouchID
	joyRadius            float64 // Joystick-Radius (skaliert mit Screen)
	btnJumpActive        bool
	btnJumpPressed       bool    // Jump wird gerade gehalten (visuelles Feedback)
	btnDownActive        bool    // Down/FastFall-Button gedrückt
	keyboardUsed         bool    // Wurde Tastatur benutzt?
	lastCanvasHeight     int     // Cache der Canvas-Höhe für Touch-Input
	lastCanvasWidth      int     // Cache der Canvas-Breite für Touch-Input
	downBtnX, downBtnY   float64 // Zentrum des Down-Buttons
	downBtnR             float64 // Radius des Down-Buttons

	// Debug Stats
	showDebug         bool      // Debug-Overlay anzeigen (F3 zum Umschalten)
	fpsCounter        int       // Frame-Zähler
	fpsSampleTime     time.Time // Letzter FPS-Sample
	currentFPS        float64   // Aktuelle FPS
	lastUpdateTime    time.Time // Letzte Server-Update Zeit
	updateLatency     float64   // Latenz zum letzten Update (ms)
	correctionCount   int       // Anzahl der Korrekturen
	outOfOrderCount   int       // Anzahl verworfener Out-of-Order Pakete
	totalUpdates      int       // Gesamtzahl empfangener Updates
	pendingInputCount int       // Anzahl pending Inputs
}

func NewGame() *Game {
	g := &Game{
		appState:     StateMenu,
		world:        game.NewWorld(),
		assetsImages: make(map[string]*ebiten.Image),
		gameState:    game.GameState{Players: make(map[string]game.PlayerState)},

		playerName:    "Student",
		activeField:   "",
		gameMode:      "",
		pendingInputs: make(map[uint32]InputState),
		leaderboard:   make([]game.LeaderboardEntry, 0),

		// Particle tracking
		lastCollectedCoins:    make(map[string]bool),
		lastCollectedPowerups: make(map[string]bool),
		lastPlayerStates:      make(map[string]game.PlayerState),

		// Audio System
		audio: NewAudioSystem(),

		// Debug Stats
		fpsSampleTime:  time.Now(),
		lastUpdateTime: time.Now(),

		// Interpolation
		lastPhysicsTime: time.Now(),

		joyBaseX: 150, joyBaseY: ScreenHeight - 150,
		joyStickX: 150, joyStickY: ScreenHeight - 150,
	}
	g.loadAssets()
	g.loadOrCreatePlayerCode()
	g.localHighscore = g.loadHighscore()

	// Gespeicherten Namen laden
	savedName := g.loadPlayerName()
	if savedName != "" {
		g.playerName = savedName
	}

	return g
}

// loadAssets() ist jetzt in assets_wasm.go und assets_native.go definiert

// --- UPDATE ---
func (g *Game) Update() error {
	// FPS Tracking
	g.fpsCounter++
	if time.Since(g.fpsSampleTime) >= time.Second {
		g.currentFPS = float64(g.fpsCounter) / time.Since(g.fpsSampleTime).Seconds()
		g.fpsCounter = 0
		g.fpsSampleTime = time.Now()
	}

	// Debug Toggle (F3)
	if inpututil.IsKeyJustPressed(ebiten.KeyF3) {
		g.showDebug = !g.showDebug
	}

	// Presentation Toggle (F1)
	if inpututil.IsKeyJustPressed(ebiten.KeyF1) {
		if g.appState == StatePresentation {
			g.appState = StateMenu
			g.disconnectFromServer()
		} else {
			g.appState = StatePresentation
			g.presAssets = nil // Reset assets
			g.presQuoteTime = time.Now() // Force immediate first quote
			
			// Setup Server Connection for Presentation Mode
			g.gameMode = "coop" // Use coop logic on server
			g.isHost = true
			g.roomID = "PRES" + generateRoomCode()
			g.playerName = "PRESENTATION"
			
			// Start connection process in background
			go g.connectAndStart()
			
			// Generate QR Code URL
			joinURL := "https://escape-from-school.de/?room=" + g.roomID
			g.presQRCode = generateQRCode(joinURL)

			// WASM: Notify JS
			g.notifyPresentationStarted_Platform(g.roomID)
		}
	}

	// Pending Inputs zählen für Debug
	g.predictionMutex.Lock()
	g.pendingInputCount = len(g.pendingInputs)
	g.predictionMutex.Unlock()

	// Aktuellen Status einmalig (thread-safe) lesen
	g.stateMutex.Lock()
	currentStatus := g.gameState.Status
	g.stateMutex.Unlock()

	// Game Over Handling
	if g.appState == StateGame && currentStatus == "GAMEOVER" {
		backBtnW, backBtnH := 120, 40
		if isHit(20, 20, backBtnW, backBtnH) {
			g.returnToMenu()
			log.Println("🔙 Zurück zum Menü (Back Button)")
			return nil
		}
		if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
			g.returnToMenu()
			log.Println("🔙 Zurück zum Menü (ESC)")
			return nil
		}
		if g.isHost {
			g.handleGameOverInput()
		}
	}

	// COUNTDOWN/RUNNING/PRESENTATION-Übergang: AppState auf StateGame setzen + JS benachrichtigen
	if (currentStatus == "COUNTDOWN" || currentStatus == "RUNNING" || currentStatus == "PRESENTATION") && g.appState != StateGame && g.appState != StatePresentation {
		log.Printf("🎮 Spiel startet! Status: %s -> %s", g.lastStatus, currentStatus)
		g.appState = StateGame
		g.notifyGameStarted()
	}
	if currentStatus == "RUNNING" && g.lastStatus != "RUNNING" {
		g.audio.PlayMusic()
		g.roundStartTime = time.Now()
	}
	if currentStatus == "GAMEOVER" && g.lastStatus == "RUNNING" {
		g.audio.StopMusic()
		g.deathQuote = game.GetRandomQuote()
		if g.gameMode == "solo" {
			g.verifyRoundResult()
		}
	}
	g.lastStatus = currentStatus

	switch g.appState {
	case StateMenu:
		g.updateMenu()
	case StateLobby:
		g.updateLobby()
	case StateGame:
		g.UpdateGame()
	case StateLeaderboard:
		g.updateLeaderboard()
	case StatePresentation:
		g.updatePresentation()
		g.updatePresentationState_Platform()
	}

	return nil
	}

func (g *Game) updateMenu() {
	g.handleMenuInput()

	// Volume Sliders (unten links)
	volumeX := 20
	volumeY := ScreenHeight - 100
	sliderWidth := 200
	sliderHeight := 10

	// Music Volume Slider
	musicSliderY := volumeY + 10
	if isSliderHit(volumeX, musicSliderY, sliderWidth, sliderHeight) {
		newVolume := getSliderValue(volumeX, sliderWidth)
		g.audio.SetMusicVolume(newVolume)
		return
	}

	// SFX Volume Slider
	sfxSliderY := volumeY + 50
	if isSliderHit(volumeX, sfxSliderY, sliderWidth, sliderHeight) {
		newVolume := getSliderValue(volumeX, sliderWidth)
		g.audio.SetSFXVolume(newVolume)
		// Test-Sound abspielen
		g.audio.PlayCoin()
		return
	}

	// Leaderboard Button
	lbBtnW, lbBtnH := 200, 50
	lbBtnX := ScreenWidth - lbBtnW - 20
	lbBtnY := 20
	if isHit(lbBtnX, lbBtnY, lbBtnW, lbBtnH) {
		g.appState = StateLeaderboard
		if !g.connected {
			go g.connectForLeaderboard()
		}
		return
	}

	// Name-Feld
	fieldW, fieldH := 250, 40
	nameX := ScreenWidth/2 - fieldW/2
	nameY := ScreenHeight/2 - 150

	if isHit(nameX, nameY, fieldW, fieldH) {
		g.activeField = "name"
		return
	}

	// Mode-Buttons
	btnW, btnH := 200, 60
	soloX := ScreenWidth/2 - btnW - 20
	coopX := ScreenWidth/2 + 20
	btnY := ScreenHeight/2 - 20

	if isHit(soloX, btnY, btnW, btnH) {
		// SOLO MODE (Offline by default)
		if g.playerName == "" {
			g.playerName = "Player"
		}
		g.gameMode = "solo"
		g.isHost = true
		g.startOfflineGame()
	} else if isHit(coopX, btnY, btnW, btnH) {
		// CO-OP MODE
		if g.playerName == "" {
			g.playerName = "Player"
		}
		g.gameMode = "coop"
		g.roomID = generateRoomCode()
		g.isHost = true
		g.appState = StateLobby
		go g.connectAndStart()
	}

	// Join Button (unten)
	joinW, joinH := 300, 50
	joinX := ScreenWidth/2 - joinW/2
	joinY := ScreenHeight/2 + 100

	if isHit(joinX, joinY, joinW, joinH) {
		g.activeField = "room"
	}

	// Join Code Feld
	if g.activeField == "room" {
		roomFieldW := 200
		roomFieldX := ScreenWidth/2 - roomFieldW/2
		roomFieldY := ScreenHeight/2 + 160

		if isHit(roomFieldX, roomFieldY, roomFieldW, 40) {
			// Stay in room field
		} else if isHit(roomFieldX+roomFieldW+20, roomFieldY, 100, 40) {
			// JOIN button next to code field
			if g.roomID != "" && g.playerName != "" {
				g.gameMode = "coop"
				g.isHost = false
				g.appState = StateLobby
				go g.connectAndStart()
			}
		}
	}
}

func (g *Game) updateLobby() {
	// Start Button (nur für Host)
	if g.isHost {
		btnW, btnH := 200, 60
		btnX := ScreenWidth/2 - btnW/2
		btnY := ScreenHeight - 150

		if isHit(btnX, btnY, btnW, btnH) {
			// START GAME
			g.sendStartRequest()
		}
	}

	// Zurück Button
	if isHit(50, 50, 100, 40) {
		g.disconnectFromServer()
		g.appState = StateMenu
		g.connected = false
		g.stateMutex.Lock()
		g.gameState = game.GameState{Players: make(map[string]game.PlayerState)}
		g.stateMutex.Unlock()
	}

	// Lobby State Change Detection (für HTML-Updates)
	g.stateMutex.Lock()
	currentPlayerCount := len(g.gameState.Players)
	g.stateMutex.Unlock()

	if currentPlayerCount != g.lastPlayerCount {
		g.lastPlayerCount = currentPlayerCount
		g.sendLobbyUpdateToJS()
	}

}

// --- DRAW ---
func (g *Game) Draw(screen *ebiten.Image) {
	// In WASM: Nur das Spiel zeichnen, kein Menü/Lobby (HTML übernimmt das)
	// In Native: Alles zeichnen
	g.draw(screen)
}

// draw ist die plattform-übergreifende Zeichenfunktion
func (g *Game) draw(screen *ebiten.Image) {
	switch g.appState {
	case StateMenu:
		g.drawMenu(screen)
	case StateLobby:
		g.drawLobby(screen)
	case StateGame:
		g.DrawGame(screen)
	case StateLeaderboard:
		g.drawLeaderboard(screen)
	case StatePresentation:
		g.drawPresentation(screen)
	}
}

// drawMenu, drawLobby, drawLeaderboard sind in draw_wasm.go und draw_native.go definiert

func (g *Game) DrawMenu(screen *ebiten.Image) {
	screen.Fill(color.RGBA{20, 20, 30, 255})

	// Titel
	title := "ESCAPE FROM TEACHER"
	text.Draw(screen, title, basicfont.Face7x13, ScreenWidth/2-80, 100, ColText)

	if g.localHighscore > 0 {
		hsText := fmt.Sprintf("Persönlicher Highscore: %d", g.localHighscore)
		text.Draw(screen, hsText, basicfont.Face7x13, ScreenWidth/2-70, 120, color.RGBA{255, 215, 0, 255})
	}

	// Name-Feld
	fieldW := 250
	nameX := ScreenWidth/2 - fieldW/2
	nameY := ScreenHeight/2 - 150

	col := color.RGBA{50, 50, 60, 255}
	if g.activeField == "name" {
		col = color.RGBA{70, 70, 80, 255}
	}
	vector.DrawFilledRect(screen, float32(nameX), float32(nameY), float32(fieldW), 40, col, false)
	vector.StrokeRect(screen, float32(nameX), float32(nameY), float32(fieldW), 40, 1, color.White, false)

	display := g.playerName
	if g.activeField == "name" && (time.Now().UnixMilli()/500)%2 == 0 {
		display += "|"
	}
	text.Draw(screen, "Name: "+display, basicfont.Face7x13, nameX+10, nameY+25, ColText)

	// Mode Selection
	text.Draw(screen, "Select Game Mode:", basicfont.Face7x13, ScreenWidth/2-60, ScreenHeight/2-60, ColText)

	// SOLO Button
	btnW, btnH := 200, 60
	soloX := ScreenWidth/2 - btnW - 20
	btnY := ScreenHeight/2 - 20
	vector.DrawFilledRect(screen, float32(soloX), float32(btnY), float32(btnW), float32(btnH), ColBtnNormal, false)
	vector.StrokeRect(screen, float32(soloX), float32(btnY), float32(btnW), float32(btnH), 2, color.White, false)
	text.Draw(screen, "SOLO", basicfont.Face7x13, soloX+80, btnY+35, ColText)

	// CO-OP Button
	coopX := ScreenWidth/2 + 20
	vector.DrawFilledRect(screen, float32(coopX), float32(btnY), float32(btnW), float32(btnH), ColBtnNormal, false)
	vector.StrokeRect(screen, float32(coopX), float32(btnY), float32(btnW), float32(btnH), 2, color.White, false)
	text.Draw(screen, "CO-OP (Host)", basicfont.Face7x13, coopX+45, btnY+30, ColText)
	text.Draw(screen, "(EXPERIMENTAL)", basicfont.Face7x13, coopX+45, btnY+45, color.RGBA{255, 68, 68, 255})

	// Join Section
	joinY := ScreenHeight/2 + 100
	text.Draw(screen, "Or join a room:", basicfont.Face7x13, ScreenWidth/2-60, joinY, color.Gray{200})

	if g.activeField == "room" {
		roomFieldW := 200
		roomFieldX := ScreenWidth/2 - roomFieldW/2
		roomFieldY := ScreenHeight/2 + 160

		col := color.RGBA{70, 70, 80, 255}
		vector.DrawFilledRect(screen, float32(roomFieldX), float32(roomFieldY), float32(roomFieldW), 40, col, false)
		vector.StrokeRect(screen, float32(roomFieldX), float32(roomFieldY), float32(roomFieldW), 40, 1, color.White, false)

		display := g.roomID
		if (time.Now().UnixMilli()/500)%2 == 0 {
			display += "|"
		}
		text.Draw(screen, display, basicfont.Face7x13, roomFieldX+10, roomFieldY+25, ColText)

		// Join Button
		joinBtnX := roomFieldX + roomFieldW + 20
		vector.DrawFilledRect(screen, float32(joinBtnX), float32(roomFieldY), 100, 40, color.RGBA{0, 150, 0, 255}, false)
		vector.StrokeRect(screen, float32(joinBtnX), float32(roomFieldY), 100, 40, 2, color.White, false)
		text.Draw(screen, "JOIN", basicfont.Face7x13, joinBtnX+30, roomFieldY+25, ColText)
	} else {
		joinBtnW := 300
		joinBtnX := ScreenWidth/2 - joinBtnW/2
		joinBtnY := ScreenHeight/2 + 120
		vector.DrawFilledRect(screen, float32(joinBtnX), float32(joinBtnY), float32(joinBtnW), 50, ColBtnNormal, false)
		vector.StrokeRect(screen, float32(joinBtnX), float32(joinBtnY), float32(joinBtnW), 50, 2, color.White, false)
		text.Draw(screen, "Join with Code", basicfont.Face7x13, joinBtnX+90, joinBtnY+30, ColText)
	}

	// Leaderboard Button
	lbBtnW := 200
	lbBtnX := ScreenWidth - lbBtnW - 20
	lbBtnY := 20
	vector.DrawFilledRect(screen, float32(lbBtnX), float32(lbBtnY), float32(lbBtnW), 50, ColBtnNormal, false)
	vector.StrokeRect(screen, float32(lbBtnX), float32(lbBtnY), float32(lbBtnW), 50, 2, color.RGBA{255, 215, 0, 255}, false)
	text.Draw(screen, "🏆 LEADERBOARD", basicfont.Face7x13, lbBtnX+35, lbBtnY+30, color.RGBA{255, 215, 0, 255})

	// Volume Controls (unten links)
	volumeX := 20
	volumeY := ScreenHeight - 100

	// Music Volume
	text.Draw(screen, "Music Volume:", basicfont.Face7x13, volumeX, volumeY, ColText)
	g.drawVolumeSlider(screen, volumeX, volumeY+10, 200, g.audio.GetMusicVolume())

	// SFX Volume
	text.Draw(screen, "SFX Volume:", basicfont.Face7x13, volumeX, volumeY+40, ColText)
	g.drawVolumeSlider(screen, volumeX, volumeY+50, 200, g.audio.GetSFXVolume())

	text.Draw(screen, "WASD / Arrows - SPACE to Jump - M to Mute", basicfont.Face7x13, ScreenWidth/2-130, ScreenHeight-15, color.Gray{150})
}

func (g *Game) DrawLobby(screen *ebiten.Image) {
	screen.Fill(color.RGBA{20, 20, 30, 255})

	// Titel
	text.Draw(screen, "LOBBY (EXPERIMENTAL)", basicfont.Face7x13, ScreenWidth/2-80, 80, color.RGBA{255, 68, 68, 255})

	// Room Code (groß anzeigen)
	text.Draw(screen, "Room Code:", basicfont.Face7x13, ScreenWidth/2-40, 150, color.Gray{200})
	codeBoxW, codeBoxH := 300, 60
	codeBoxX := ScreenWidth/2 - codeBoxW/2
	codeBoxY := 170
	vector.DrawFilledRect(screen, float32(codeBoxX), float32(codeBoxY), float32(codeBoxW), float32(codeBoxH), color.RGBA{50, 50, 60, 255}, false)
	vector.StrokeRect(screen, float32(codeBoxX), float32(codeBoxY), float32(codeBoxW), float32(codeBoxH), 3, color.RGBA{100, 200, 100, 255}, false)
	text.Draw(screen, g.roomID, basicfont.Face7x13, codeBoxX+100, codeBoxY+35, color.RGBA{100, 255, 100, 255})

	// Spieler-Liste
	g.stateMutex.Lock()
	playerCount := len(g.gameState.Players)

	// Spieler in sortierte Liste konvertieren (damit sie nicht flackern)
	type PlayerEntry struct {
		ID     string
		Name   string
		IsHost bool
	}
	players := make([]PlayerEntry, 0, playerCount)
	hostID := g.gameState.HostID

	for id, p := range g.gameState.Players {
		name := p.Name
		if name == "" {
			name = id
		}
		isHost := (id == hostID)
		players = append(players, PlayerEntry{
			ID:     id,
			Name:   name,
			IsHost: isHost,
		})

	}
	g.stateMutex.Unlock()

	// Sortieren: Host zuerst, dann alphabetisch nach Name
	sort.SliceStable(players, func(i, j int) bool {
		if players[i].IsHost {
			return true
		}
		if players[j].IsHost {
			return false
		}
		return players[i].Name < players[j].Name
	})

	text.Draw(screen, fmt.Sprintf("Players (%d/16):", playerCount), basicfont.Face7x13, 100, 280, ColText)

	y := 310
	for _, p := range players {
		name := p.Name
		// Host markieren
		if p.IsHost {
			name += " [HOST]"
		}

		text.Draw(screen, "• "+name, basicfont.Face7x13, 120, y, ColText)
		y += 25
		if y > ScreenHeight-200 {
			break
		}
	}

	// Status
	statusY := ScreenHeight - 180
	statusText := "Waiting for host to start..."
	statusCol := color.RGBA{200, 200, 0, 255}
	if g.gameState.Status == "COUNTDOWN" {
		statusText = fmt.Sprintf("Starting in %d...", g.gameState.TimeLeft)
		statusCol = color.RGBA{255, 150, 0, 255}
	}
	text.Draw(screen, statusText, basicfont.Face7x13, ScreenWidth/2-80, statusY, statusCol)

	// Start Button (nur für Host)
	if g.isHost && g.gameState.Status == "LOBBY" {
		btnW, btnH := 200, 60
		btnX := ScreenWidth/2 - btnW/2
		btnY := ScreenHeight - 150
		vector.DrawFilledRect(screen, float32(btnX), float32(btnY), float32(btnW), float32(btnH), color.RGBA{0, 180, 0, 255}, false)
		vector.StrokeRect(screen, float32(btnX), float32(btnY), float32(btnW), float32(btnH), 2, color.White, false)
		text.Draw(screen, "START GAME", basicfont.Face7x13, btnX+60, btnY+35, ColText)
	}

	// Zurück Button
	vector.DrawFilledRect(screen, 50, 50, 100, 40, color.RGBA{150, 0, 0, 255}, false)
	vector.StrokeRect(screen, 50, 50, 100, 40, 2, color.White, false)
	text.Draw(screen, "< Back", basicfont.Face7x13, 65, 75, ColText)
}

func (g *Game) Layout(w, h int) (int, int) {
	// Nutze die echte Window-Größe (Mobile: ~360px Höhe, Desktop: 720px+ Höhe)
	// Das erlaubt dynamische Anpassung an verschiedene Bildschirmgrößen
	return w, h
}

// --- HELPER ---

// GetFloorY gibt die Y-Position des Bodens basierend auf der aktuellen Bildschirmhöhe zurück
// WICHTIG: Kann nicht direkt aufgerufen werden, braucht Screen-Höhe als Parameter!
func GetFloorYFromHeight(screenHeight int) float64 {
	h := screenHeight
	if h == 0 {
		// Fallback wenn keine Höhe verfügbar
		h = ScreenHeight // 720
		log.Printf("⚠️ GetFloorY: Screen height is 0, using fallback: %d", h)
	}

	// Ziel: Gameplay füllt den Bildschirm optimal aus
	// Erde-Tiefe: ~100px (kompakt, damit mehr Gameplay-Raum bleibt)
	dirtDepth := 100.0

	// Berechne Boden-Position: möglichst weit unten
	floorY := float64(h) - dirtDepth

	// Minimum-Check: Bei sehr kleinen Bildschirmen (< 300px) mindestens 70% Höhe
	minFloorY := float64(h) * 0.7
	if floorY < minFloorY {
		floorY = minFloorY
	}

	return floorY
}

// GetScale gibt den Scale-Faktor zurück um die Spielwelt an den Bildschirm anzupassen.
// Die Scale ist proportional zur Bildschirmhöhe relativ zur Referenz-Auflösung (720p),
// damit auf jedem Gerät dieselbe Menge Spielwelt sichtbar ist wie auf dem PC.
func GetScale() float64 {
	_, h := ebiten.WindowSize()
	if h == 0 {
		h = ScreenHeight
	}
	return GetScaleFromHeight(h)
}

// GetScaleFromHeight - Scale proportional zur Bildschirmhöhe.
// scale = screenHeight / 720 (Referenz), maximal 1.0.
func GetScaleFromHeight(screenHeight int) float64 {
	h := screenHeight
	if h == 0 {
		h = ScreenHeight
	}
	scale := float64(h) / float64(ScreenHeight)
	if scale > 1.0 {
		scale = 1.0
	}
	return scale
}

// WorldToScreenYWithHeight konvertiert Welt-Y zu Bildschirm-Y.
// Skaliert die Y-Achse genauso wie die X-Achse, damit das Seitenverhältnis
// der Spielwelt auf allen Geräten gleich bleibt.
func WorldToScreenYWithHeight(worldY float64, screenHeight int) float64 {
	viewScale := GetScaleFromHeight(screenHeight)
	// Boden (Y=540) soll am unteren Bildschirmrand erscheinen (canvasH - 100).
	floorScreenY := float64(screenHeight) - 100.0
	return (worldY-float64(RefFloorY))*viewScale + floorScreenY
}

func isHit(x, y, w, h int) bool {
	if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
		mx, my := ebiten.CursorPosition()
		if mx >= x && mx <= x+w && my >= y && my <= y+h {
			return true
		}
	}
	for _, id := range inpututil.JustPressedTouchIDs() {
		tx, ty := ebiten.TouchPosition(id)
		if tx >= x && tx <= x+w && ty >= y && ty <= y+h {
			return true
		}
	}
	return false
}

func (g *Game) handleMenuInput() {
	if g.activeField == "" {
		return
	}

	// Text Eingabe
	var target *string
	if g.activeField == "name" {
		target = &g.playerName
	} else if g.activeField == "room" {
		target = &g.roomID
	}

	if target == nil {
		return
	}

	if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
		// Namen speichern wenn geändert
		if g.activeField == "name" && g.playerName != "" {
			g.savePlayerName(g.playerName)
		}
		g.activeField = ""
	} else if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) {
		if len(*target) > 0 {
			*target = (*target)[:len(*target)-1]
		}
	} else {
		// Für Room Code: Nur Großbuchstaben und Zahlen
		chars := string(ebiten.InputChars())
		if g.activeField == "room" {
			chars = strings.ToUpper(chars)
		}
		*target += chars
	}
}

func (g *Game) handleGameOverInput() {
	// Team-Name Feld
	fieldW := 300
	fieldX := ScreenWidth/2 - fieldW/2
	fieldY := ScreenHeight - 140

	// Click auf Team-Name Feld?
	if isHit(fieldX, fieldY, fieldW, 40) {
		g.activeField = "teamname"
		return
	}

	// Submit Button
	submitBtnW := 200
	submitBtnX := ScreenWidth/2 - submitBtnW/2
	submitBtnY := ScreenHeight - 85

	if isHit(submitBtnX, submitBtnY, submitBtnW, 40) {
		if g.teamName != "" {
			g.submitScore() // submitScore behandelt jetzt beide Modi
		}
		return
	}

	// Tastatur-Eingabe für Team-Name
	if g.activeField == "teamname" {
		if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
			if g.teamName != "" {
				g.submitScore() // submitScore behandelt jetzt beide Modi
			}
			g.activeField = ""
		} else if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) {
			if len(g.teamName) > 0 {
				g.teamName = g.teamName[:len(g.teamName)-1]
			}
		} else {
			chars := string(ebiten.InputChars())
			if len(g.teamName) < 30 { // Max 30 Zeichen
				g.teamName += chars
			}
		}
	}
}

// returnToMenu trennt die Verbindung und setzt den App-State zurück auf das Hauptmenü.
func (g *Game) returnToMenu() {
	g.disconnectFromServer()
	g.appState = StateMenu
	g.connected = false
	g.scoreSubmitted = false
	g.teamName = ""
	g.activeField = ""
	g.stateMutex.Lock()
	g.gameState = game.GameState{Players: make(map[string]game.PlayerState)}
	g.stateMutex.Unlock()
}

func generateRoomCode() string {
	mrand.Seed(time.Now().UnixNano())
	chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	code := make([]byte, 6)
	for i := range code {
		code[i] = chars[mrand.Intn(len(chars))]
	}
	return string(code)
}

// resetForNewGame setzt den gesamten Spiel-State zurück ohne die Seite neu zu laden.
// Muss vor jeder neuen Verbindung aufgerufen werden.
func (g *Game) resetForNewGame() {
	// Alte Verbindung sauber trennen
	g.disconnectFromServer()

	// Prediction-State zurücksetzen
	g.predictionMutex.Lock()
	g.pendingInputs = make(map[uint32]InputState)
	g.inputSequence = 0
	g.lastServerSeq = 0
	g.predictedX = 100
	g.predictedY = 200
	g.predictedVX = 0
	g.predictedVY = 0
	g.predictedGround = false
	g.predictedOnWall = false
	g.currentSpeed = 0
	g.correctionOffsetX = 0
	g.correctionOffsetY = 0
	g.predictionMutex.Unlock()

	// KRITISCH: lastRecvSeq zurücksetzen!
	// Ohne diesen Reset ignoriert die Out-of-Order-Prüfung alle Nachrichten
	// des neuen Spiels (neue Sequenzen < alter lastRecvSeq).
	g.lastRecvSeq = 0

	// Spieler-State zurücksetzen
	g.isOffline = false
	g.godModeEndTime = time.Time{}
	g.magnetEndTime = time.Time{}
	g.doubleJumpEndTime = time.Time{}
	g.scoreSubmitted = false
	g.lastStatus = ""
	g.correctionCount = 0
	g.outOfOrderCount = 0
	g.totalUpdates = 0

	// GameState leeren
	g.stateMutex.Lock()
	g.gameState = game.GameState{Players: make(map[string]game.PlayerState)}
	g.stateMutex.Unlock()

	// Leaderboard leeren
	g.leaderboardMutex.Lock()
	g.leaderboard = make([]game.LeaderboardEntry, 0)
	g.leaderboardMutex.Unlock()

	// Partikel leeren
	g.particlesMutex.Lock()
	g.particles = nil
	g.particlesMutex.Unlock()
	g.lastCollectedCoins = make(map[string]bool)
	g.lastCollectedPowerups = make(map[string]bool)
	g.lastPlayerStates = make(map[string]game.PlayerState)
}

func (g *Game) connectAndStart() {
	// Guard: verhindert mehrfaches gleichzeitiges Verbinden
	if g.isConnecting {
		log.Println("⚠️ connectAndStart bereits aktiv, ignoriere doppelten Aufruf")
		return
	}
	g.isConnecting = true
	defer func() { g.isConnecting = false }()

	g.resetForNewGame()

	// Verbindung über plattformspezifische Implementierung
	g.connectToServer()
}

func (g *Game) SendCommand(cmdType string) {
	if !g.connected {
		return
	}
	myID := g.getMyPlayerID()
	g.publishInput(game.ClientInput{PlayerID: myID, Type: cmdType})
}

func (g *Game) SendInputWithSequence(input InputState) {
	if !g.connected {
		// Im Offline-Modus den Jump-Sound trotzdem lokal abspielen
		if input.Jump && g.isOffline {
			g.audio.PlayJump()
		}
		return
	}

	myID := g.getMyPlayerID()

	// Kompletten Input-State in einer einzigen Nachricht senden.
	// Vorteile gegenüber mehreren Event-Nachrichten:
	// - Kein stuck-Input durch verlorene oder falsch sortierte Pakete
	// - Server hat immer den vollständigen Zustand nach einem Paket
	// - Weniger Nachrichten pro Frame
	g.publishInput(game.ClientInput{
		PlayerID:   myID,
		Type:       "STATE",
		Sequence:   input.Sequence,
		InputLeft:  input.Left,
		InputRight: input.Right,
		InputJump:  input.Jump,
		InputDown:  input.Down,
		InputJoyX:  input.JoyX,
	})

	// Jump-Sound lokal abspielen
	if input.Jump {
		g.audio.PlayJump()
	}
}

func (g *Game) getMyPlayerID() string {
	g.stateMutex.Lock()
	defer g.stateMutex.Unlock()

	for id, p := range g.gameState.Players {
		if p.Name == g.playerName {
			return id
		}
	}
	return g.playerName
}

// submitScore, requestLeaderboard, connectForLeaderboard
// sind in connection_native.go und connection_wasm.go definiert

func (g *Game) updateLeaderboard() {
	// Back Button (oben links) - Touch Support
	backBtnW, backBtnH := 120, 40
	backBtnX, backBtnY := 20, 20
	if isHit(backBtnX, backBtnY, backBtnW, backBtnH) {
		g.appState = StateMenu
		return
	}

	// ESC = zurück zum Menü
	if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
		g.appState = StateMenu
		return
	}
}

func (g *Game) DrawLeaderboard(screen *ebiten.Image) {
	screen.Fill(color.RGBA{20, 20, 30, 255})

	// Titel
	text.Draw(screen, "=== TOP 10 LEADERBOARD ===", basicfont.Face7x13, ScreenWidth/2-100, 80, color.RGBA{255, 215, 0, 255})

	// Leaderboard abrufen wenn leer (prüfen ohne Lock, dann ggf. nachladen)
	g.leaderboardMutex.Lock()
	empty := len(g.leaderboard) == 0
	g.leaderboardMutex.Unlock()

	if empty && g.connected {
		g.requestLeaderboard()
	}

	g.leaderboardMutex.Lock()

	y := 150
	if len(g.leaderboard) == 0 {
		text.Draw(screen, "Noch keine Einträge...", basicfont.Face7x13, ScreenWidth/2-80, y, color.Gray{150})
	} else {
		for i, entry := range g.leaderboard {
			if i >= 10 {
				break
			}

			// Eigenen Eintrag markieren
			var col color.Color = color.White
			marker := ""
			if entry.PlayerCode == g.playerCode {
				col = color.RGBA{0, 255, 0, 255}
				marker = " ← DU"
			}

			// Medaillen
			medal := ""
			if i == 0 {
				medal = "🥇 "
			} else if i == 1 {
				medal = "🥈 "
			} else if i == 2 {
				medal = "🥉 "
			}

			leaderMsg := fmt.Sprintf("%d. %s%s: %d pts%s", i+1, medal, entry.PlayerName, entry.Score, marker)
			text.Draw(screen, leaderMsg, basicfont.Face7x13, ScreenWidth/2-150, y, col)
			y += 30
		}
	}
	g.leaderboardMutex.Unlock()

	// Back Button (oben links)
	backBtnW, backBtnH := 120, 40
	backBtnX, backBtnY := 20, 20
	vector.DrawFilledRect(screen, float32(backBtnX), float32(backBtnY), float32(backBtnW), float32(backBtnH), color.RGBA{150, 0, 0, 255}, false)
	vector.StrokeRect(screen, float32(backBtnX), float32(backBtnY), float32(backBtnW), float32(backBtnH), 2, color.White, false)
	text.Draw(screen, "< ZURÜCK", basicfont.Face7x13, backBtnX+20, backBtnY+25, color.White)

	// Zurück-Button Anleitung
	text.Draw(screen, "ESC oder ZURÜCK-Button = Menü", basicfont.Face7x13, ScreenWidth/2-110, ScreenHeight-40, color.Gray{150})
}

// main() ist jetzt in main_wasm.go und main_native.go definiert

// drawVolumeSlider zeichnet einen Volume-Slider
func (g *Game) drawVolumeSlider(screen *ebiten.Image, x, y, width int, volume float64) {
	// Hintergrund
	vector.DrawFilledRect(screen, float32(x), float32(y), float32(width), 10, color.RGBA{40, 40, 50, 255}, false)
	vector.StrokeRect(screen, float32(x), float32(y), float32(width), 10, 1, color.White, false)

	// Füllstand
	fillWidth := int(float64(width) * volume)
	vector.DrawFilledRect(screen, float32(x), float32(y), float32(fillWidth), 10, color.RGBA{0, 200, 100, 255}, false)

	// Prozent-Anzeige
	pct := fmt.Sprintf("%.0f%%", volume*100)
	text.Draw(screen, pct, basicfont.Face7x13, x+width+10, y+10, ColText)
}

// isSliderHit prüft, ob auf einen Slider geklickt wurde
func isSliderHit(x, y, width, height int) bool {
	// Erweitere den Klickbereich vertikal für bessere Touch-Support
	return isHit(x, y-10, width, height+20)
}

// getSliderValue berechnet den Slider-Wert basierend auf Mausposition
func getSliderValue(sliderX, sliderWidth int) float64 {
	mx, _ := ebiten.CursorPosition()
	// Bei Touch: Ersten Touch nutzen
	touches := ebiten.TouchIDs()
	if len(touches) > 0 {
		mx, _ = ebiten.TouchPosition(touches[0])
	}

	// Berechne relative Position im Slider
	relX := float64(mx - sliderX)
	if relX < 0 {
		relX = 0
	}
	if relX > float64(sliderWidth) {
		relX = float64(sliderWidth)
	}

	return relX / float64(sliderWidth)
}
