package game

import (
	"log"
	"sync"
	"time"

	"gitea.boner.be/bdnugget/goonscape/assets"
	"gitea.boner.be/bdnugget/goonscape/network"
	"gitea.boner.be/bdnugget/goonscape/types"
	pb "gitea.boner.be/bdnugget/goonserver/actions"
	rl "github.com/gen2brain/raylib-go/raylib"
)

type Game struct {
	// Component-based architecture
	PlayerManager *PlayerManager
	AssetManager  *AssetManager
	UIManager     *UIManager

	// Core game state
	Camera       rl.Camera3D
	quitChan     chan struct{}
	cleanupOnce  sync.Once
	frameCounter int // For periodic logging
}

func New() *Game {
	// Create managers
	playerManager := NewPlayerManager()
	assetManager := NewAssetManager()
	uiManager := NewUIManager()

	g := &Game{
		PlayerManager: playerManager,
		AssetManager:  assetManager,
		UIManager:     uiManager,
		Camera: rl.Camera3D{
			Position:   rl.NewVector3(0.0, 20.0, 0.0),
			Target:     rl.NewVector3(0.0, 0.0, 0.0),
			Up:         rl.NewVector3(0.0, 1.0, 0.0),
			Fovy:       45.0,
			Projection: rl.CameraPerspective,
		},
		quitChan: make(chan struct{}),
	}

	// Set up inter-component references
	g.UIManager.Chat.userData = g // Pass game instance to chat for callbacks

	// Initialize world
	InitWorld()

	return g
}

func (g *Game) LoadAssets() error {
	return SafeExecute(func() error {
		// Load models
		var err error
		models, err := assets.LoadModels()
		if err != nil {
			log.Printf("Warning: Failed to load models: %v", err)
		}
		g.AssetManager.Models = models

		// Try to load music
		music, err := assets.LoadMusic("resources/audio/GoonScape1.mp3")
		if err != nil {
			log.Printf("Warning: Failed to load music: %v", err)
		} else {
			g.AssetManager.Music = music
		}

		return nil
	})
}

func (g *Game) Update(deltaTime float32) {
	// Handle login screen if not logged in
	if !g.UIManager.IsLoggedIn {
		// Handle login
		username, password, isRegistering, doAuth := g.UIManager.LoginScreen.Update()

		if doAuth {
			conn, playerID, err := network.ConnectToServer(username, password, isRegistering)
			if err != nil {
				g.UIManager.LoginScreen.SetError(err.Error())
				return
			}

			g.PlayerManager.LocalPlayer = &types.Player{
				Speed:       50.0,
				TargetPath:  []types.Tile{},
				ActionQueue: []*pb.Action{},
				QuitDone:    make(chan struct{}),
				ID:          playerID,
			}
			g.AssignModelToPlayer(g.PlayerManager.LocalPlayer)

			// Set user data to allow chat message handling
			g.PlayerManager.LocalPlayer.UserData = g

			go network.HandleServerCommunication(conn, playerID, g.PlayerManager.LocalPlayer, g.PlayerManager.OtherPlayers, g.quitChan)
			g.UIManager.IsLoggedIn = true
		}
		return
	}

	// Skip update logic if player is not initialized yet
	if g.PlayerManager.LocalPlayer == nil {
		log.Printf("Warning: LocalPlayer is nil during update, skipping")
		return
	}

	// Handle ESC for menu
	if rl.IsKeyPressed(rl.KeyEscape) {
		g.UIManager.MenuOpen = !g.UIManager.MenuOpen
		return
	}

	// Don't process other inputs if menu is open
	if g.UIManager.MenuOpen {
		return
	}

	// Handle chat updates
	if message, sent := g.UIManager.Chat.Update(); sent {
		g.PlayerManager.LocalPlayer.Lock()
		g.PlayerManager.LocalPlayer.ActionQueue = append(g.PlayerManager.LocalPlayer.ActionQueue, &pb.Action{
			Type:        pb.Action_CHAT,
			ChatMessage: message,
			PlayerId:    g.PlayerManager.LocalPlayer.ID,
		})
		g.PlayerManager.LocalPlayer.Unlock()
	}

	// Process player input
	g.HandleInput()

	// Update local player movement
	if g.PlayerManager.LocalPlayer.TargetPath != nil && len(g.PlayerManager.LocalPlayer.TargetPath) > 0 {
		g.PlayerManager.LocalPlayer.MoveTowards(g.PlayerManager.LocalPlayer.TargetPath[0], deltaTime, GetMapGrid())
	}

	// Periodically log information about other players
	g.frameCounter++
	if g.frameCounter%300 == 0 {
		rl.TraceLog(rl.LogInfo, "There are %d other players", len(g.PlayerManager.OtherPlayers))
		for id, other := range g.PlayerManager.OtherPlayers {
			if other != nil {
				// Calculate tile coordinates from absolute position
				tileX := int(other.PosActual.X / float32(types.TileSize))
				tileY := int(other.PosActual.Z / float32(types.TileSize))

				rl.TraceLog(rl.LogInfo, "Other player ID: %d, Position: (%f, %f, %f), Tile: (%d, %d), Has model: %v",
					id, other.PosActual.X, other.PosActual.Y, other.PosActual.Z,
					tileX, tileY, other.Model.Meshes != nil)
			} else {
				rl.TraceLog(rl.LogInfo, "Other player ID: %d is nil", id)
			}
		}
	}

	// Process other players
	for _, other := range g.PlayerManager.OtherPlayers {
		if other == nil {
			continue
		}

		if other.TargetPath != nil && len(other.TargetPath) > 0 {
			target := other.TargetPath[0]
			other.MoveTowards(target, deltaTime, GetMapGrid())
		}

		// Assign model if needed
		if other.Model.Meshes == nil {
			g.AssignModelToPlayer(other)
		}
	}

	// Update camera position
	UpdateCamera(&g.Camera, g.PlayerManager.LocalPlayer.PosActual, deltaTime)

	// Update music if available
	if g.AssetManager.Music.Stream.Buffer != nil {
		rl.UpdateMusicStream(g.AssetManager.Music)
	}
}

func (g *Game) DrawMap() {
	for x := 0; x < types.MapWidth; x++ {
		for y := 0; y < types.MapHeight; y++ {
			height := GetTileHeight(x, y)

			// Interpolate height for smoother landscape
			if x > 0 {
				height += GetTileHeight(x-1, y)
			}
			if y > 0 {
				height += GetTileHeight(x, y-1)
			}
			if x > 0 && y > 0 {
				height += GetTileHeight(x-1, y-1)
			}
			height /= 4.0

			tilePos := rl.Vector3{
				X: float32(x * types.TileSize),
				Y: height * types.TileHeight,
				Z: float32(y * types.TileSize),
			}
			color := rl.Color{R: uint8(height * 25), G: 100, B: 100, A: 64}
			rl.DrawCube(tilePos, types.TileSize, types.TileHeight, types.TileSize, color)
		}
	}
}

func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
	if player == nil {
		return
	}

	// Get necessary data
	modelIndex := int(player.ID) % len(g.AssetManager.Models)
	if modelIndex < 0 || modelIndex >= len(g.AssetManager.Models) {
		modelIndex = 0
	}
	modelAsset := g.AssetManager.Models[modelIndex]

	// Calculate position
	const defaultHeight = 8.0
	playerPos := rl.Vector3{
		X: player.PosActual.X,
		Y: player.PosActual.Y + defaultHeight + modelAsset.YOffset,
		Z: player.PosActual.Z,
	}

	// Simple drawing with scale parameter
	var drawColor rl.Color = rl.White
	if player.PlaceholderColor.A > 0 {
		drawColor = player.PlaceholderColor
	}

	// Draw the model at normal scale (16.0)
	rl.DrawModel(model, playerPos, 16.0, drawColor)

	// Update floating message position
	if player.FloatingMessage != nil {
		worldPos := rl.Vector3{
			X: playerPos.X,
			Y: playerPos.Y + 24.0, // Position above head
			Z: playerPos.Z,
		}
		player.FloatingMessage.ScreenPos = rl.GetWorldToScreen(worldPos, g.Camera)
	}
}

func (g *Game) DrawFloatingMessages() {
	var drawFloatingMessage = func(msg *types.FloatingMessage) {
		if msg == nil || time.Now().After(msg.ExpireTime) {
			return
		}

		// Draw the message with RuneScape-style coloring (black outline with yellow text)
		text := msg.Content
		textWidth := rl.MeasureText(text, 20)

		// Draw black outline by offsetting the text slightly in all directions
		for offsetX := -2; offsetX <= 2; offsetX++ {
			for offsetY := -2; offsetY <= 2; offsetY++ {
				rl.DrawText(text,
					int32(msg.ScreenPos.X)-textWidth/2+int32(offsetX),
					int32(msg.ScreenPos.Y)+int32(offsetY),
					20,
					rl.Black)
			}
		}

		// Draw the yellow text on top
		rl.DrawText(text, int32(msg.ScreenPos.X)-textWidth/2, int32(msg.ScreenPos.Y), 20, rl.Yellow)
	}

	if g.PlayerManager.LocalPlayer != nil && g.PlayerManager.LocalPlayer.FloatingMessage != nil {
		drawFloatingMessage(g.PlayerManager.LocalPlayer.FloatingMessage)
	}

	for _, other := range g.PlayerManager.OtherPlayers {
		if other != nil && other.FloatingMessage != nil {
			drawFloatingMessage(other.FloatingMessage)
		}
	}
}

func (g *Game) Render() {
	rl.BeginDrawing()
	defer rl.EndDrawing()

	rl.ClearBackground(rl.RayWhite)

	if !g.UIManager.IsLoggedIn {
		g.UIManager.LoginScreen.Draw()
		return
	}

	// Draw 3D elements
	rl.BeginMode3D(g.Camera)
	g.DrawMap()

	// Draw player only if valid
	if g.PlayerManager.LocalPlayer != nil && g.PlayerManager.LocalPlayer.Model.Meshes != nil {
		g.DrawPlayer(g.PlayerManager.LocalPlayer, g.PlayerManager.LocalPlayer.Model)
	}

	// Draw other players with defensive checks
	for _, other := range g.PlayerManager.OtherPlayers {
		if other == nil {
			continue
		}

		if other.Model.Meshes != nil {
			g.DrawPlayer(other, other.Model)
		}
	}
	rl.EndMode3D()

	// Draw floating messages with RuneScape style
	g.DrawFloatingMessages()

	// Draw menu if open
	if g.UIManager.MenuOpen {
		g.DrawMenu()
	}

	// Only draw chat if menu is not open
	if !g.UIManager.MenuOpen && g.UIManager.Chat != nil {
		g.UIManager.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
	}

	// Draw FPS counter
	rl.DrawFPS(10, 10)
}

func (g *Game) Cleanup() {
	g.cleanupOnce.Do(func() {
		// Cleanup models
		for _, model := range g.AssetManager.Models {
			rl.UnloadModel(model.Model)
			if model.Texture.ID > 0 {
				rl.UnloadTexture(model.Texture)
			}
		}

		// Unload music
		if g.AssetManager.Music.Stream.Buffer != nil {
			rl.UnloadMusicStream(g.AssetManager.Music)
		}

		// Only close the channel if it hasn't been closed yet
		select {
		case <-g.quitChan:
			// Channel already closed, do nothing
		default:
			close(g.quitChan)
		}
	})
}

func (g *Game) HandleInput() {
	clickedTile, clicked := g.GetTileAtMouse()
	if clicked {
		path := FindPath(GetTile(g.PlayerManager.LocalPlayer.PosTile.X, g.PlayerManager.LocalPlayer.PosTile.Y), clickedTile)
		if len(path) > 1 {
			g.PlayerManager.LocalPlayer.Lock()
			g.PlayerManager.LocalPlayer.TargetPath = path[1:]
			g.PlayerManager.LocalPlayer.ActionQueue = append(g.PlayerManager.LocalPlayer.ActionQueue, &pb.Action{
				Type:     pb.Action_MOVE,
				X:        int32(clickedTile.X),
				Y:        int32(clickedTile.Y),
				PlayerId: g.PlayerManager.LocalPlayer.ID,
			})
			g.PlayerManager.LocalPlayer.Unlock()
		}
	}
}

func (g *Game) DrawMenu() {
	screenWidth := float32(rl.GetScreenWidth())
	screenHeight := float32(rl.GetScreenHeight())

	// Draw semi-transparent background
	rl.DrawRectangle(0, 0, int32(screenWidth), int32(screenHeight), rl.ColorAlpha(rl.Black, 0.7))

	// Draw menu items
	menuItems := []string{"Resume", "Settings", "Quit"}
	menuY := screenHeight/2 - float32(len(menuItems)*40)/2

	for i, item := range menuItems {
		itemY := menuY + float32(i*40)
		mousePoint := rl.GetMousePosition()
		itemRect := rl.Rectangle{X: screenWidth/2 - 100, Y: itemY, Width: 200, Height: 36}

		// Check for hover
		isHover := rl.CheckCollisionPointRec(mousePoint, itemRect)

		// Draw button background
		if isHover {
			rl.DrawRectangleRec(itemRect, rl.ColorAlpha(rl.White, 0.3))
		} else {
			rl.DrawRectangleRec(itemRect, rl.ColorAlpha(rl.White, 0.1))
		}

		// Draw button text
		textWidth := rl.MeasureText(item, 20)
		rl.DrawText(item, int32(itemRect.X+(itemRect.Width-float32(textWidth))/2), int32(itemRect.Y+8), 20, rl.White)

		// Handle click
		if isHover && rl.IsMouseButtonReleased(rl.MouseLeftButton) {
			switch item {
			case "Resume":
				g.UIManager.MenuOpen = false
			case "Settings":
				// TODO: Implement settings
			case "Quit":
				g.Shutdown()
				rl.CloseWindow()
			}
		}
	}
}

func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
	// Check if Chat is properly initialized
	if g.UIManager != nil && g.UIManager.Chat != nil {
		g.UIManager.Chat.HandleServerMessages(messages)
	} else {
		log.Printf("Warning: Cannot handle server messages, Chat is not initialized")
	}
}

func (g *Game) QuitChan() <-chan struct{} {
	return g.quitChan
}

func (g *Game) Shutdown() {
	// Use the cleanup method which has channel-closing safety
	g.Cleanup()
}

func (g *Game) AssignModelToPlayer(player *types.Player) {
	if player == nil {
		return
	}

	modelAsset, found := g.AssetManager.GetModelForPlayer(player.ID)
	if !found {
		return
	}

	player.Model = modelAsset.Model
	player.PlaceholderColor = modelAsset.PlaceholderColor

	// Initialize animations if available
	if len(modelAsset.Animations.Idle) > 0 || len(modelAsset.Animations.Walk) > 0 {
		player.InitializeAnimations(modelAsset.Animations)
	}
}
