package game

import (
	"time"

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

type Game struct {
	Player       *types.Player
	OtherPlayers map[int32]*types.Player
	Camera       rl.Camera3D
	Models       []types.ModelAsset
	Music        rl.Music
	Chat         *Chat
}

func New() *Game {
	InitWorld()
	game := &Game{
		Player: &types.Player{
			PosActual:  rl.NewVector3(5*types.TileSize, 0, 5*types.TileSize),
			PosTile:    GetTile(5, 5),
			Speed:      50.0,
			TargetPath: []types.Tile{},
			UserData:   nil,
		},
		OtherPlayers: make(map[int32]*types.Player),
		Camera: rl.Camera3D{
			Position:   rl.NewVector3(0, 10, 10),
			Target:     rl.NewVector3(0, 0, 0),
			Up:         rl.NewVector3(0, 1, 0),
			Fovy:       45.0,
			Projection: rl.CameraPerspective,
		},
		Chat: NewChat(),
	}
	game.Player.UserData = game
	game.Chat.userData = game
	return game
}

func (g *Game) LoadAssets() error {
	var err error
	g.Models, err = assets.LoadModels()
	if err != nil {
		return err
	}

	g.Music, err = assets.LoadMusic("resources/audio/GoonScape2.mp3")
	if err != nil {
		return err
	}

	return nil
}

func (g *Game) Update(deltaTime float32) {
	if message, sent := g.Chat.Update(); sent {
		g.Player.Lock()
		g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
			Type:        pb.Action_CHAT,
			ChatMessage: message,
			PlayerId:    g.Player.ID,
		})
		g.Player.Unlock()
	}

	g.HandleInput()

	if len(g.Player.TargetPath) > 0 {
		g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid())
	}

	for _, other := range g.OtherPlayers {
		if len(other.TargetPath) > 0 {
			other.MoveTowards(other.TargetPath[0], deltaTime, GetMapGrid())
		}
	}

	UpdateCamera(&g.Camera, g.Player.PosActual, deltaTime)
}

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) {
	player.Lock()
	defer player.Unlock()

	grid := GetMapGrid()
	playerPos := rl.Vector3{
		X: player.PosActual.X,
		Y: grid[player.PosTile.X][player.PosTile.Y].Height*types.TileHeight + 16.0,
		Z: player.PosActual.Z,
	}

	rl.DrawModel(model, playerPos, 16, rl.White)

	if player.FloatingMessage != nil && time.Now().Before(player.FloatingMessage.ExpireTime) {
		screenPos := rl.GetWorldToScreen(rl.Vector3{
			X: playerPos.X,
			Y: playerPos.Y + 24.0,
			Z: playerPos.Z,
		}, g.Camera)

		player.FloatingMessage.ScreenPos = screenPos
	} else if player.FloatingMessage != nil {
		player.FloatingMessage = nil
	}

	if len(player.TargetPath) > 0 {
		targetTile := player.TargetPath[len(player.TargetPath)-1]
		targetPos := rl.Vector3{
			X: float32(targetTile.X * types.TileSize),
			Y: grid[targetTile.X][targetTile.Y].Height * types.TileHeight,
			Z: float32(targetTile.Y * types.TileSize),
		}
		rl.DrawCubeWires(targetPos, types.TileSize, types.TileHeight, types.TileSize, rl.Green)

		nextTile := player.TargetPath[0]
		nextPos := rl.Vector3{
			X: float32(nextTile.X * types.TileSize),
			Y: grid[nextTile.X][nextTile.Y].Height * types.TileHeight,
			Z: float32(nextTile.Y * types.TileSize),
		}
		rl.DrawCubeWires(nextPos, types.TileSize, types.TileHeight, types.TileSize, rl.Yellow)
	}
}

func (g *Game) Render() {
	rl.BeginDrawing()
	rl.ClearBackground(rl.RayWhite)

	rl.BeginMode3D(g.Camera)
	g.DrawMap()
	g.DrawPlayer(g.Player, g.Player.Model)
	for id, other := range g.OtherPlayers {
		g.DrawPlayer(other, g.Models[int(id)%len(g.Models)].Model)
	}
	rl.EndMode3D()

	drawFloatingMessage := func(msg *types.FloatingMessage) {
		if msg == nil || time.Now().After(msg.ExpireTime) {
			return
		}
		pos := msg.ScreenPos
		text := msg.Content
		textWidth := rl.MeasureText(text, 20)

		for offsetX := -2; offsetX <= 2; offsetX++ {
			for offsetY := -2; offsetY <= 2; offsetY++ {
				rl.DrawText(text,
					int32(pos.X)-textWidth/2+int32(offsetX),
					int32(pos.Y)+int32(offsetY),
					20,
					rl.Black)
			}
		}
		rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow)
	}

	if g.Player.FloatingMessage != nil {
		drawFloatingMessage(g.Player.FloatingMessage)
	}

	for _, other := range g.OtherPlayers {
		drawFloatingMessage(other.FloatingMessage)
	}

	rl.DrawFPS(10, 10)
	g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
	rl.EndDrawing()
}

func (g *Game) Cleanup() {
	assets.UnloadModels(g.Models)
	assets.UnloadMusic(g.Music)
}

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