package server

// delete me

import (
	"log"
	"sort"
	"sync"
	"time"

	"mcquay.me/bandwidth"
)

const maxPlayer = 128

// BotHealth is sent to all players so they know how other robots are
// doing.
type BotHealth struct {
	RobotId string `json:"robot_id"`
	Health  int    `json:"health"`
}

// Scanner contains a Robot/Projectile hash and is sent to the user to
// let them know which things they know about.
type Scanner struct {
	Id   string `json:"id"`
	Type string `json:"type"`
}

// BotStats is stats for a single Player's Robot.
type BotStats struct {
	Kills      int
	Deaths     int
	Suicides   int
	Shots      int
	DirectHits int
	Hits       int
	Wins       int
}

// PlayerStats is what you want many of. Contains a map of BotStats and total
// wins.
type PlayerStats struct {
	BotStats map[string]*BotStats
	Wins     int
}

// GameStats is a collection of PlayerStats for all players involved.
type GameStats struct {
	PlayerStats map[string]*PlayerStats
	sync.RWMutex
}

// Game is the main point of interest in this application. Embodies all info
// required to keep track of players, robots, stats, projectils, etc.
// Currently Controllers have a map of these.
type Game struct {
	id                string
	players           map[*Player]bool
	projectiles       map[*Projectile]bool
	splosions         map[*Splosion]bool
	defaultObstacles  []Obstacle
	obstacles         []Obstacle
	obstacleCount     int
	register          chan *Player
	unregister        chan *Player
	turn              int
	players_remaining int
	width, height     float64
	maxPoints         int
	spectators        map[*Spectator]bool
	sregister         chan *Spectator
	sunregister       chan *Spectator
	kill              chan bool
	repair_hp         int
	repair_rate       float64
	tick_duration     int
	stats             GameStats
	mode              GameMode
	bw                *bandwidth.Bandwidth
}

// This is the interface that different gametypes should implement.
type GameMode interface {
	setup(g *Game)
	tick(gg *Game, payload *Boardstate)
	gameOver(gg *Game) (bool, *GameOver)
}

// NewGame Poplulates a Game struct and starts the bandwidth calculator.
func NewGame(id string, width, height float64, tick, maxPoints int, mode string) (*Game, error) {
	bw, err := bandwidth.NewBandwidth(
		[]int{1, 10, 60},
		1*time.Second,
	)
	if err != nil {
		return nil, err
	}
	go bw.Run()
	g := &Game{
		id:                id,
		register:          make(chan *Player, maxPlayer),
		unregister:        make(chan *Player, maxPlayer),
		projectiles:       make(map[*Projectile]bool),
		splosions:         make(map[*Splosion]bool),
		players:           make(map[*Player]bool),
		turn:              0,
		width:             width,
		height:            height,
		maxPoints:         maxPoints,
		spectators:        make(map[*Spectator]bool),
		sregister:         make(chan *Spectator),
		sunregister:       make(chan *Spectator),
		kill:              make(chan bool),
		repair_hp:         5,
		repair_rate:       3.0,
		tick_duration:     tick,
		players_remaining: 2,
		stats:             GameStats{PlayerStats: make(map[string]*PlayerStats)},
		bw:                bw,
	}

	if mode == "melee" {
		g.mode = &melee{respawn: make(map[*Robot]float64)}
	} else {
		g.mode = &deathmatch{}
	}

	g.mode.setup(g)

	return g, nil
}

// tick is the method called every TICK ms.
func (g *Game) tick(payload *Boardstate) {
	g.players_remaining = 0

	// Update Players
	for p := range g.players {
		living_robots := 0

		for _, r := range p.Robots {
			if r.Health > 0 {
				living_robots++
				r.Tick(g)
			}

			if len(r.Message) > 0 {
				if len(r.Message) > 100 {
					r.Message = r.Message[0:99]
				}
				payload.Messages = append(payload.Messages, r.Message)
			}

			payload.OtherRobots = append(
				payload.OtherRobots,
				r.GetTruncatedDetails())

			payload.AllBots = append(
				payload.AllBots,
				BotHealth{RobotId: r.Id, Health: r.Health})
		}

		if living_robots > 0 {
			g.players_remaining++
		}
	}

	// Update Projectiles
	for pr := range g.projectiles {
		pr.Tick(g)
	}

	// We do this here, because the tick calls can alter g.projectiles
	for pr := range g.projectiles {
		payload.Projectiles = append(payload.Projectiles, *pr)
	}

	// Update Splosions
	for s := range g.splosions {
		s.Tick()
		if !s.Alive() {
			delete(g.splosions, s)
		}
		payload.Splosions = append(payload.Splosions, *s)
	}
}

// sendUpdate is what we use to determine what data goes out to each client;
// performs filtering and sorting of the data.
func (g *Game) sendUpdate(payload *Boardstate) {
	// Ensure that the robots are always sent in a consistent order
	sort.Sort(RobotSorter{Robots: payload.OtherRobots})
	sort.Sort(AllRobotSorter{Robots: payload.AllBots})

	for p := range g.players {

		// Copy the payload but only add the robots in scanner range
		player_payload := NewBoardstate()
		player_payload.Messages = payload.Messages
		player_payload.AllBots = payload.AllBots
		player_payload.Turn = payload.Turn

		for _, r := range p.Robots {
			player_payload.MyRobots = append(player_payload.MyRobots, *r)
		}

		player_payload.Obstacles = []Obstacle{}
		player_payload.Splosions = []Splosion{}
		player_payload.Projectiles = []Projectile{}
		living_robots := 0

		for _, r := range p.Robots {
			if r.Health > 0 {
				living_robots++

				// Filter robots by scanner
				for player := range g.players {
					for _, scan_entry := range r.Scanners {
						for _, r := range player.Robots {
							if r.Id == scan_entry.Id {
								player_payload.OtherRobots = append(
									player_payload.OtherRobots,
									r.GetTruncatedDetails())
							}
						}
					}
				}

				// Filter projectiles
				for proj := range g.projectiles {

					if proj.Owner == r {
						player_payload.Projectiles = append(
							player_payload.Projectiles,
							*proj)
					}

					for _, scan_entry := range r.Scanners {
						if proj.Id == scan_entry.Id {
							player_payload.Projectiles = append(
								player_payload.Projectiles,
								*proj)
						}
					}
				}

				// Filter splosions
				for splo := range g.splosions {
					for _, scan_entry := range r.Scanners {
						if splo.Id == scan_entry.Id {
							player_payload.Splosions = append(
								player_payload.Splosions,
								*splo)
						}
					}
				}

				// Filter objects
				for _, ob := range g.obstacles {
					if ob.distance_from_point(r.Position) < float64(r.Stats.ScannerRadius)+r.ScanCounter {
						player_payload.Obstacles = append(
							player_payload.Obstacles, ob)
					}
				}
			}
		}

		p.send <- player_payload
	}
	for s := range g.spectators {
		payload.Obstacles = g.obstacles
		s.send <- payload
	}

}

// run is the method that contians the main game loop.
func (g *Game) run() {
	ticker := time.NewTicker(time.Duration(g.tick_duration) * time.Millisecond)
	for {
		select {
		case <-g.kill:
			log.Printf("game %s: received kill signal, dying gracefully", g.id)
			g.bw.Quit <- true
			for player := range g.players {
				close(player.send)
			}
			return
		case p := <-g.register:
			log.Println("registering player:", p.Id)
			g.players[p] = true
			g.stats.PlayerStats[p.Id] = &PlayerStats{
				BotStats: make(map[string]*BotStats),
			}
			for _, r := range p.Robots {
				g.stats.PlayerStats[p.Id].BotStats[r.Name] = &BotStats{}
				r.gameStats = g.stats.PlayerStats[p.Id].BotStats[r.Name]
			}
		case p := <-g.unregister:
			log.Println("unregistering player:", p.Id)
			delete(g.players, p)
			close(p.send)
		case s := <-g.sregister:
			log.Println("registering spectator:", s.Id)
			g.spectators[s] = true
		case s := <-g.sunregister:
			log.Println("unregistering spectator:", s.Id)
			delete(g.spectators, s)
			close(s.send)
		case <-ticker.C:
			payload := NewBoardstate()

			g.turn++
			payload.Turn = g.turn

			// UPDATE GAME STATE
			if end, data := g.mode.gameOver(g); end {
				g.sendGameOver(data)
			}

			g.tick(payload)
			g.mode.tick(g, payload)

			// SEND THE UPDATE TO EACH PLAYER
			g.sendUpdate(payload)
		}
	}
}

// sendGameOver is a special method that sends a GameOver object to the clients
// instead of a normal Boardstate message.
func (g *Game) sendGameOver(eg *GameOver) {
	log.Printf("sending out game over message: %+v", eg)
	for p := range g.players {
		p.send <- eg
	}
	for s := range g.spectators {
		s.send <- eg
	}
}

// returns a GameParam object popuplated by info from the game. This is
// used during client/server initial negociation.
func (g *Game) gameParam() *GameParam {
	return &GameParam{
		BoardSize: BoardSize{
			Width:  g.width,
			Height: g.height,
		},
		MaxPoints: g.maxPoints,
		Type:      "gameparam",
	}
}
