import math
import random
import inspect

import pyglet
from pyglet.gl import *
 
orthoWidth = 1000
orthoHeight = 750
fieldHeight = 650

class Attribute:	# Attribute in the gaming sense of the word, rather than of an object
	def __init__ (self, game):
		self.game = game					# Attribute knows game it's part of
		self.game.attributes.append (self)	# Game knows all its attributes
		self.install ()						# Put in place graphical representation of attribute
		self.reset ()						# Reset attribute to start position
		
	def install (self):
		pass
					
	def reset (self):		# Restore starting positions or score, then commit to Pyglet
		self.commit ()		# Nothing to restore for the Attribute base class
				
	def predict (self):
		pass
				
	def interact (self):
		pass
		
	def commit (self):
		pass
	   
class Sprite (Attribute, pyglet.sprite.Sprite):	  # Here, a sprite is an attribute that can move
	def __init__ (self, game, width, height):
		self.width = width
		self.height = height
		Attribute.__init__ (self, game)
		
	def install (self):		# The sprite holds a pygletSprite, that pyglet can display
		image = pyglet.image.create (
			self.width,
			self.height,
			pyglet.image.SolidColorImagePattern ((255, 255, 255, 255))	# RGBA
		)
 
		image.anchor_x = self.width // 2	# Middle of image is reference point
		image.anchor_y = self.height // 2
		
		#self.pygletSprite = pyglet.sprite.Sprite (image, 0, 0, batch = self.game.batch)
		
	def reset (self, vX = 0, vY = 0, x = orthoWidth // 2, y = fieldHeight // 2):
		self.vX = vX		# Speed
		self.vY = vY
		
		self.x = x			# Predicted position, can be commit, no bouncing initially
		self.y = y
		
		Attribute.reset (self)
		
	def predict (self):		# Predict position, do not yet commit, bouncing may alter it
		self.x += self.vX * self.game.deltaT
		self.y += self.vY * self.game.deltaT

	def commit (self):		# Update pygletSprite for asynch draw
		self.pygletSprite.x = self.x
		self.pygletSprite.y = self.y
		 
class Paddle (Sprite):
	margin = 30 # Distance of paddles from walls
	width = 10
	height = 100
	speed = 400 # / s
	
	def __init__ (self, game, index):
		self.index = index	# Paddle knows its player index, 0 == left, 1 == right
		Sprite.__init__ (self, game, self.width, self.height)
		
	def reset (self):		# Put paddle in rest position, dependent on player index
		Sprite.reset (
			self,
			x = orthoWidth - self.margin if self.index else self.margin,
			y = fieldHeight // 2
		)
		
	def predict (self): # Let paddle react on keys
		self.vY = 0
		
		if self.index:	# Right player
			if self.game.keymap [pyglet.window.key.K]:	# Letter K pressed
				self.vY = self.speed
			elif self.game.keymap [pyglet.window.key.M]:
				self.vY = -self.speed
		else:			# Left player
			if self.game.keymap [pyglet.window.key.A]:
				self.vY = self.speed
			elif self.game.keymap [pyglet.window.key.Z]:
				self.vY = -self.speed
				
		Sprite.predict (self)	# Do not yet commit, paddle may bounce with walls

	def interact (self):		# Paddles and ball assumed infinitely thin
		# Paddle touches wall
		self.y = max (self.height / 2, min (self.y, fieldHeight - self.height / 2))
		
		# Paddle hits ball
		if (
			(self.y - self.height // 2) < self.game.ball.y < (self.y + self.height // 2)
			and (
				(self.index == 0 and self.game.ball.x < self.x) # On or behind left paddle
				or
				(self.index == 1 and self.game.ball.x > self.x) # On or behind right paddle
			)
		):
			self.game.ball.x = self.x				# Ball may have gone too far already
			self.game.ball.vX = -self.game.ball.vX	# Bounce on paddle
		
			speedUp = 1 + 0.5 * (1 - abs (self.game.ball.y - self.y) / (self.height // 2)) ** 2
			self.game.ball.vX *= speedUp			# Speed will increase more if paddle near centre
			self.game.ball.vY *= speedUp
			
		
class Ball (Sprite):
	side = 8
	speed = 300 # / s
	
	def __init__ (self, game):
		Sprite.__init__ (self, game, self.side, self.side)
 
	def reset (self):	# Launch according to service direction with random angle offset from horizontal
		angle =	 (
			self.game.serviceIndex * math.pi	# Service direction
			+
			random.choice ((-1, 1)) * random.random () * math.atan (fieldHeight / orthoWidth)
		)
		
		Sprite.reset (
			self,
			vX = self.speed * math.cos (angle),
			vY = self.speed * math.sin (angle)
		)
		
	def predict (self):
		Sprite.predict (self)		# Integrate velocity to position

		if self.x < 0:				# If out on left side
			self.game.scored (1)	#	Right player scored
		elif self.x > orthoWidth:
			self.game.scored (0)
			
		if self.y > fieldHeight:	# If it hit top wall
			self.y = fieldHeight	#	It may have gone too far already
			self.vY = -self.vY		#	Bounce
		elif self.y < 0:
			self.y = 0
			self.vY = -self.vY

class Scoreboard (Attribute, pytlet.text.Label):
	nameShift = 75
	scoreShift = 25
			
	def install (self): # Graphical representation of scoreboard are four labels and a separator line
		def defineLabel (text, x, y):
			return pyglet.text.Label (
				text,
				font_name = 'Arial', font_size = 24,
				x = x, y = y,
				anchor_x = 'center', anchor_y = 'center',
				batch = self.game.batch
			)
	
		defineLabel ('Speler AZ', 1 * orthoWidth // 4, fieldHeight + self.nameShift)	# Player name
		defineLabel ('Speler KM', 3 * orthoWidth // 4, fieldHeight + self.nameShift)
 
		self.playerLabels = (
			defineLabel ('000', 1 * orthoWidth // 4, fieldHeight + self.scoreShift),	# Player score
			defineLabel ('000', 3 * orthoWidth // 4, fieldHeight + self.scoreShift)
		)
 
		self.game.batch.add (2, GL_LINES, None, ('v2i', (0, fieldHeight, orthoWidth, fieldHeight))) # Line
		
	def increment (self, playerIndex):
		self.scores [playerIndex] += 1
	
	def reset (self):
		self.scores = [0, 0]
		Attribute.reset (self)	# Only does a commit here
		
	def commit (self):			# Committing labels is adapting their texts
		for playerLabel, score in zip (self.playerLabels, self.scores):
			playerLabel.text = '{}'.format (score)			
 
class Game (pyglet.window.Window, pyglet.graphipcs.Batch):
	def __init__ (self):
		# self.batch = pyglet.graphics.Batch ()	  # Graphical reprentations insert themselves for batch drawing

		self.deltaT = 0								# Elementary timestep of simulation
		self.serviceIndex = random.choice ((0, 1))	# Index of player that has initial service
		self.pause = True							# Start game in paused state
		
		self.attributes = []						# All attributes will insert themselves here
		self.paddles = [Paddle (self, index) for index in range (2)]	# Pass game as parameter self
		self.ball = Ball (self)
		self.scoreboard = Scoreboard (self)
						
		#self.window = pyglet.window.Window (				# Main window
		#	640, 480, resizable = True, visible = False, caption = "Pong"
		#)

		self.keymap = pyglet.window.key.KeyStateHandler ()	# Create keymap
		self.window.push_handlers (self.keymap)				# Install it as a handler
		
		self.window.on_draw = self.draw						# Install draw callback, will be called asynch
		self.window.on_resize = self.resize					# Install resize callback, will be called if resized
		
		self.window.set_location (							# Middle of the screen that it happens to be on
			(self.window.screen.width - self.window.width) // 2,
			(self.window.screen.height - self.window.height) // 2
		)
		
		self.window.clear ()
		self.window.flip ()									# Copy drawing buffer to window
		self.window.set_visible (True)						# Show window once its contents are OK
		
		pyglet.clock.schedule_interval (self.update, 1/60.) # Install update callback to be called 60 times per s
		pyglet.app.run ()									# Start pyglet engine
 
	def update (self, deltaT):								# Note that update and draw are not synchronized
		self.deltaT = deltaT								# Actual deltaT may vary, depending on processor load
	
		if self.pause:										# If in paused state
			if self.keymap [pyglet.window.key.SPACE]:		#	If SPACEBAR hit
				self.pause = False							#		Start playing
			elif self.keymap [pyglet.window.key.ENTER]:		#	Else if ENTER hit
				self.scoreboard.reset ()					#		Reset score
			elif self.keymap [pyglet.window.key.ESCAPE]:	#	Else if ESC hit
				self.exit ()								#		End game
				
		else:												# Else, so if in active state
			for attribute in self.attributes:				#	Compute predicted values
				attribute.predict ()
				
			for attribute in self.attributes:				#	Correct values for bouncing and scoring
				attribute.interact ()
				
			for attribute in self.attributes:				#	Commit them to pyglet for display
				attribute.commit ()
			
	def scored (self, playerIndex):				# Player has scored
		self.scoreboard.increment (playerIndex) # Increment player's points
		self.serviceIndex = 1 - playerIndex		# Grant service to the unlucky player
		
		for paddle in self.paddles:				# Put paddles in rest position
			paddle.reset ()

		self.ball.reset ()						# Put ball in rest position
		self.pause = True						# Wait for next round
		
	def draw (self):
		self.window.clear ()
		self.batch.draw ()		# All attributes added their graphical representation to the batch
		
	def resize (self, width, height):
		glViewport (0, 0, width, height)				# Tell openGL window size

		glMatrixMode (GL_PROJECTION)					# Work with projection matrix
		glLoadIdentity ()								# Start with identity matrix
		glOrtho (0, orthoWidth, 0, orthoHeight, -1, 1)	# Adapt it to orthographic projection
		
		glMatrixMode (GL_MODELVIEW)						# Work with model matrix
		glLoadIdentity ()								# No transforms
	 
		return pyglet.event.EVENT_HANDLED				# Block default event handler
		
game = Game ()	# Create and run game
