package main

import (
	"fmt"
	"math"

	rl "github.com/gen2brain/raylib-go/raylib"
)

const (
	MapWidth   = 50
	MapHeight  = 50
	TileSize   = 32
	TileHeight = 2.0
)

var (
	cameraDistance = float32(20.0)
	cameraYaw      = float32(145.0)
	cameraPitch    = float32(45.0) // Adjusted for a more overhead view
)

type Tile struct {
	X, Y     int
	Height   float32
	Walkable bool
}

type Player struct {
	PosActual  rl.Vector3
	PosTile    Tile
	TargetPath []Tile
	Speed      float32
}

// Initialize the map with some height data
func InitMap() [][]Tile {
	mapGrid := make([][]Tile, MapWidth)
	for x := 0; x < MapWidth; x++ {
		mapGrid[x] = make([]Tile, MapHeight)
		for y := 0; y < MapHeight; y++ {
			mapGrid[x][y] = Tile{
				X:        x,
				Y:        y,
				Height:   1.0 + float32(x%5), // Example height
				Walkable: true,               // Set to false for obstacles
			}
		}
	}
	return mapGrid
}

func DrawMap(mapGrid [][]Tile) {
	for x := 0; x < MapWidth; x++ {
		for y := 0; y < MapHeight; y++ {
			tile := mapGrid[x][y]
			// Interpolate height between adjacent tiles for a smoother landscape
			height := tile.Height
			if x > 0 {
				height += mapGrid[x-1][y].Height
			}
			if y > 0 {
				height += mapGrid[x][y-1].Height
			}
			if x > 0 && y > 0 {
				height += mapGrid[x-1][y-1].Height
			}
			height /= 4.0
			// Draw each tile as a 3D cube based on its height
			tilePos := rl.Vector3{
				X: float32(x * TileSize), // X-axis for horizontal position
				Y: height * TileHeight,   // Y-axis for height (Z in 3D is Y here)
				Z: float32(y * TileSize), // Z-axis for depth (Y in 3D is Z here)
			}
			color := rl.Color{R: uint8(height * 25), G: 100, B: 100, A: 64}
			rl.DrawCube(tilePos, TileSize, TileHeight, TileSize, color) // Draw a cube representing the tile
		}
	}
}

func DrawPlayer(player Player, model *rl.Model, mapGrid [][]Tile) {
	// Draw the player based on its actual position (PosActual) and current tile height
	playerPos := rl.Vector3{
		X: player.PosActual.X,
		Y: mapGrid[player.PosTile.X][player.PosTile.Y].Height*TileHeight + 16.0,
		Z: player.PosActual.Z,
	}
	// rl.DrawCube(playerPos, 16, 16, 16, rl.Green) // Draw player cube
	rl.DrawModel(*model, playerPos, 16, rl.White)

	// Draw highlight around target tile
	if len(player.TargetPath) > 0 {
		targetTile := player.TargetPath[len(player.TargetPath)-1] // last tile in the slice
		targetPos := rl.Vector3{
			X: float32(targetTile.X * TileSize),
			Y: mapGrid[targetTile.X][targetTile.Y].Height * TileHeight,
			Z: float32(targetTile.Y * TileSize),
		}
		rl.DrawCubeWires(targetPos, TileSize, TileHeight, TileSize, rl.Green)

		nextTile := player.TargetPath[0] // first tile in the slice
		nextPos := rl.Vector3{
			X: float32(nextTile.X * TileSize),
			Y: mapGrid[nextTile.X][nextTile.Y].Height * TileHeight,
			Z: float32(nextTile.Y * TileSize),
		}
		rl.DrawCubeWires(nextPos, TileSize, TileHeight, TileSize, rl.Yellow)
	}
}

// Helper function to test ray-box intersection (slab method)
func RayIntersectsBox(ray rl.Ray, boxMin, boxMax rl.Vector3) bool {
	tmin := (boxMin.X - ray.Position.X) / ray.Direction.X
	tmax := (boxMax.X - ray.Position.X) / ray.Direction.X

	if tmin > tmax {
		tmin, tmax = tmax, tmin
	}

	tymin := (boxMin.Z - ray.Position.Z) / ray.Direction.Z
	tymax := (boxMax.Z - ray.Position.Z) / ray.Direction.Z

	if tymin > tymax {
		tymin, tymax = tymax, tymin
	}

	if (tmin > tymax) || (tymin > tmax) {
		return false
	}

	if tymin > tmin {
		tmin = tymin
	}
	if tymax < tmax {
		tmax = tymax
	}

	tzmin := (boxMin.Y - ray.Position.Y) / ray.Direction.Y
	tzmax := (boxMax.Y - ray.Position.Y) / ray.Direction.Y

	if tzmin > tzmax {
		tzmin, tzmax = tzmax, tzmin
	}

	if (tmin > tzmax) || (tzmin > tmax) {
		return false
	}

	return true
}

func GetTileAtMouse(mapGrid [][]Tile, camera *rl.Camera3D) (Tile, bool) {
	if !rl.IsMouseButtonPressed(rl.MouseLeftButton) {
		return Tile{}, false
	}
	mouse := rl.GetMousePosition()
	ray := rl.GetMouseRay(mouse, *camera)

	for x := 0; x < MapWidth; x++ {
		for y := 0; y < MapHeight; y++ {
			tile := mapGrid[x][y]

			// Define the bounding box for each tile based on its position and height
			tilePos := rl.NewVector3(float32(x*TileSize), tile.Height*TileHeight, float32(y*TileSize))
			boxMin := rl.Vector3Subtract(tilePos, rl.NewVector3(TileSize/2, TileHeight/2, TileSize/2))
			boxMax := rl.Vector3Add(tilePos, rl.NewVector3(TileSize/2, TileHeight/2, TileSize/2))

			// Check if the ray intersects the bounding box
			if RayIntersectsBox(ray, boxMin, boxMax) {
				fmt.Println("Clicked:", tile.X, tile.Y)
				return tile, true
			}
		}
	}
	return Tile{}, false
}

func (player *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
	// Calculate the direction vector to the target tile
	targetPos := rl.Vector3{
		X: float32(target.X * TileSize),
		Y: mapGrid[target.X][target.Y].Height * TileHeight,
		Z: float32(target.Y * TileSize),
	}

	// Calculate direction and normalize it for smooth movement
	direction := rl.Vector3Subtract(targetPos, player.PosActual)
	distance := rl.Vector3Length(direction)
	if distance > 0 {
		direction = rl.Vector3Scale(direction, player.Speed*deltaTime/distance)
	}

	// Move the player towards the target tile
	if distance > 1.0 {
		player.PosActual = rl.Vector3Add(player.PosActual, direction)
	} else {
		// Snap to the target tile when close enough
		player.PosActual = targetPos
		player.PosTile = target                   // Update player's tile
		player.TargetPath = player.TargetPath[1:] // Move to next tile in path if any
	}
}

func HandleInput(player *Player, mapGrid [][]Tile, camera *rl.Camera) {
	clickedTile, clicked := GetTileAtMouse(mapGrid, camera)
	if clicked {
		path := FindPath(mapGrid, mapGrid[player.PosTile.X][player.PosTile.Y], clickedTile)
		if path != nil {
			// Exclude the first tile (current position)
			if len(path) > 1 {
				player.TargetPath = path[1:]
			}
		}
	}
}

func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
	// Update camera based on mouse wheel
	wheelMove := rl.GetMouseWheelMove()
	if wheelMove != 0 {
		cameraDistance += -wheelMove * 5
		if cameraDistance < 10 {
			cameraDistance = 10
		}
		if cameraDistance > 250 {
			cameraDistance = 250
		}
	}

	// Orbit camera around the player using arrow keys
	if rl.IsKeyDown(rl.KeyRight) {
		cameraYaw += 100 * deltaTime
	}
	if rl.IsKeyDown(rl.KeyLeft) {
		cameraYaw -= 100 * deltaTime
	}
	if rl.IsKeyDown(rl.KeyUp) {
		cameraPitch -= 50 * deltaTime
		if cameraPitch < 20 {
			cameraPitch = 20
		}
	}
	if rl.IsKeyDown(rl.KeyDown) {
		cameraPitch += 50 * deltaTime
		if cameraPitch > 85 {
			cameraPitch = 85
		}
	}

	// Calculate the new camera position using spherical coordinates
	cameraYawRad := float64(cameraYaw) * rl.Deg2rad
	cameraPitchRad := float64(cameraPitch) * rl.Deg2rad
	cameraPos := rl.Vector3{
		X: player.X + cameraDistance*float32(math.Cos(cameraYawRad))*float32(math.Cos(cameraPitchRad)),
		Y: player.Y + cameraDistance*float32(math.Sin(cameraPitchRad)),
		Z: player.Z + cameraDistance*float32(math.Sin(cameraYawRad))*float32(math.Cos(cameraPitchRad)),
	}

	// Update the camera's position and target
	camera.Position = cameraPos
	camera.Target = rl.NewVector3(player.X, player.Y, player.Z)
}

func main() {
	rl.InitWindow(1024, 768, "GoonScape")
	defer rl.CloseWindow()
	rl.InitAudioDevice()
	defer rl.CloseAudioDevice()

	playerModel := rl.LoadModel("resources/models/goonion.obj")
	defer rl.UnloadModel(playerModel)
	playerTexture := rl.LoadTexture("resources/models/goonion.png")
	defer rl.UnloadTexture(playerTexture)
	rl.SetMaterialTexture(playerModel.Materials, rl.MapDiffuse, playerTexture)

	coomerModel := rl.LoadModel("resources/models/coomer.obj")
	defer rl.UnloadModel(coomerModel)
	coomerTexture := rl.LoadTexture("resources/models/coomer.png")
	defer rl.UnloadTexture(coomerTexture)
	rl.SetMaterialTexture(coomerModel.Materials, rl.MapDiffuse, coomerTexture)

	mapGrid := InitMap()

	player := Player{
		PosActual:  rl.NewVector3(5*TileSize, 0, 5*TileSize),
		PosTile:    mapGrid[5][5],
		Speed:      50.0,
		TargetPath: []Tile{},
	}

	camera := rl.Camera3D{
		Position:   rl.NewVector3(0, 10, 10), // Will be updated every frame
		Target:     player.PosActual,
		Up:         rl.NewVector3(0, 1, 0), // Y is up in 3D
		Fovy:       45.0,
		Projection: rl.CameraPerspective,
	}

	rl.SetTargetFPS(60)

	// Music
	music := rl.LoadMusicStream("resources/audio/GoonScape2.mp3")
	rl.PlayMusicStream(music)
	rl.SetMusicVolume(music, 0.5)
	defer rl.UnloadMusicStream(music)

	for !rl.WindowShouldClose() {
		rl.UpdateMusicStream(music)

		// Time management
		deltaTime := rl.GetFrameTime()

		// Handle input
		HandleInput(&player, mapGrid, &camera)

		// Update player
		if len(player.TargetPath) > 0 {
			player.MoveTowards(player.TargetPath[0], deltaTime, mapGrid)
		}

		// Update camera
		UpdateCamera(&camera, player.PosActual, deltaTime)

		// Rendering
		rl.BeginDrawing()
		rl.ClearBackground(rl.RayWhite)
		rl.BeginMode3D(camera)
		DrawMap(mapGrid)
		DrawPlayer(player, &playerModel, mapGrid)

		rl.DrawModel(coomerModel, rl.NewVector3(5*TileSize+32, 32, 5*TileSize+32), 16, rl.White)

		rl.DrawFPS(10, 10)

		rl.EndMode3D()
		rl.EndDrawing()
	}
}

// pathfinding
type Node struct {
	Tile    Tile
	Parent  *Node
	G, H, F float32
}

func FindPath(mapGrid [][]Tile, start, end Tile) []Tile {
	openList := []*Node{}
	closedList := make(map[[2]int]bool)

	startNode := &Node{Tile: start, G: 0, H: heuristic(start, end)}
	startNode.F = startNode.G + startNode.H
	openList = append(openList, startNode)

	for len(openList) > 0 {
		// Find node with lowest F
		current := openList[0]
		currentIndex := 0
		for i, node := range openList {
			if node.F < current.F {
				current = node
				currentIndex = i
			}
		}

		// Move current to closed list
		openList = append(openList[:currentIndex], openList[currentIndex+1:]...)
		closedList[[2]int{current.Tile.X, current.Tile.Y}] = true

		// Check if reached the end
		if current.Tile.X == end.X && current.Tile.Y == end.Y {
			path := []Tile{}
			node := current
			for node != nil {
				path = append([]Tile{node.Tile}, path...)
				node = node.Parent
			}
			fmt.Printf("Path found: %v\n", path)
			return path
		}

		// Generate neighbors
		neighbors := GetNeighbors(mapGrid, current.Tile)
		for _, neighbor := range neighbors {
			if !neighbor.Walkable || closedList[[2]int{neighbor.X, neighbor.Y}] {
				continue
			}

			tentativeG := current.G + distance(current.Tile, neighbor)
			inOpen := false
			var existingNode *Node
			for _, node := range openList {
				if node.Tile.X == neighbor.X && node.Tile.Y == neighbor.Y {
					existingNode = node
					inOpen = true
					break
				}
			}

			if !inOpen || tentativeG < existingNode.G {
				newNode := &Node{
					Tile:   neighbor,
					Parent: current,
					G:      tentativeG,
					H:      heuristic(neighbor, end),
				}
				newNode.F = newNode.G + newNode.H
				if !inOpen {
					openList = append(openList, newNode)
				}
			}
		}
	}

	// No path found
	fmt.Println("No path found")
	return nil
}

func heuristic(a, b Tile) float32 {
	return float32(abs(a.X-b.X) + abs(a.Y-b.Y))
}

func distance(a, b Tile) float32 {
	_ = a
	_ = b
	return 1.0 //uniform cost for now
}

func GetNeighbors(mapGrid [][]Tile, tile Tile) []Tile {
	directions := [][2]int{
		{1, 0}, {-1, 0}, {0, 1}, {0, -1},
		{1, 1}, {-1, -1}, {1, -1}, {-1, 1},
	}
	neighbors := []Tile{}
	for _, dir := range directions {
		nx, ny := tile.X+dir[0], tile.Y+dir[1]
		if nx >= 0 && nx < MapWidth && ny >= 0 && ny < MapHeight {
			neighbors = append(neighbors, mapGrid[nx][ny])
		}
	}
	return neighbors
}

func abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}
