__pragma__ ('skip')
window = Math = Date = 0	# Prevent complaints by optional static checker
__pragma__ ('noskip')

from com.fabricjs import fabric

orthoWidth = 1000
orthoHeight = 750
fieldHeight = 650

enter, esc, space = 13, 27, 32

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 reset (self):		# Restore starting positions or score, then commit to fabric
		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):	# 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 an image that fabric can display
		self.image = __new__ (fabric.Rect ({
			'width': self.game.scaleX (self.width), 'height': self.game.scaleY (self.height),
			'originX': 'center', 'originY': 'center', 'fill': 'white'
		}))
		
	__pragma__ ('kwargs')
	def reset (self, vX = 0, vY = 0, x = 0, y = 0):
		self.vX = vX		# Speed
		self.vY = vY
		
		self.x = x			# Predicted position, can be commit, no bouncing initially
		self.y = y
		
		Attribute.reset (self)
	__pragma__ ('nokwargs')
		
	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 fabric image for asynch draw
		self.image.left = self.game.orthoX (self.x)
		self.image.top = self.game.orthoY (self.y)
		
	def draw (self):
		self.game.canvas.add (self.image)
		 
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 // 2 - self.margin if self.index else -orthoWidth // 2 + self.margin,
			y = 0
		)
		
	def predict (self): # Let paddle react on keys
		self.vY = 0
	
		if self.index:							# Right player
			if ord ('K') in self.game.keySet:		# Letter k pressed
				self.vY = self.speed
			elif ord ('M') in self.game.keySet:
				self.vY = -self.speed
		else:			  						# Left player
			if ord ('A') in self.game.keySet:
				self.vY = self.speed
			elif ord ('Z') in self.game.keySet:
				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 = Math.max (self.height // 2 - fieldHeight // 2, Math.min (self.y, fieldHeight // 2 - 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
			self.game.ball.speedUp (self)
		
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
			+
			(1 if Math.random () > 0.5 else -1) * Math.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 < -orthoWidth // 2:	# If out on left side
			self.game.scored (1)		#	Right player scored
		elif self.x > orthoWidth // 2:
			self.game.scored (0)
			
		if self.y > fieldHeight // 2:	# If it hits top wall
			self.y = fieldHeight // 2	#	It may have gone too far already
			self.vY = -self.vY			#	Bounce
		elif self.y < -fieldHeight // 2:
			self.y = -fieldHeight // 2
			self.vY = -self.vY

	def speedUp (self, bat):
		factor = 1 + 0.15 * (1 - Math.abs (self.y - bat.y) / (bat.height // 2)) ** 2	# Speed will increase more if paddle hit near centre
		
		if Math.abs (self.vX) < 3 * self.speed:
			self.vX *= factor
			self.vY *= factor			

class Scoreboard (Attribute):
	nameShift = 75
	hintShift = 25
			
	def install (self): # Graphical representation of scoreboard are four labels and a separator line
		self.playerLabels = [__new__ (fabric.Text ('Player {}'.format (name), {
				'fill': 'white', 'fontFamily': 'arial', 'fontSize': '30',
				'left': self.game.orthoX (position * orthoWidth), 'top': self.game.orthoY (fieldHeight // 2 + self.nameShift)
		})) for name, position in (('AZ keys:', -7/16), ('KM keys:', 1/16))]
 		
		self.hintLabel = __new__ (fabric.Text ('[spacebar] starts game, [enter] resets score', {
				'fill': 'white', 'fontFamily': 'arial', 'fontSize': '12',
				'left': self.game.orthoX (-7/16 * orthoWidth), 'top': self.game.orthoY (fieldHeight // 2 + self.hintShift)
		}))
		
		self.image = __new__ (fabric.Line ([
				self.game.orthoX (-orthoWidth // 2), self.game.orthoY (fieldHeight // 2),
				self.game.orthoX (orthoWidth // 2), self.game.orthoY (fieldHeight // 2)
			],
			{'stroke': 'white'}
		))
				
	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
		self.scoreLabels = [__new__ (fabric.Text ('{}'.format (score), {
				'fill': 'white', 'fontFamily': 'arial', 'fontSize': '30',
				'left': self.game.orthoX (position * orthoWidth), 'top': self.game.orthoY (fieldHeight // 2 + self.nameShift)
		})) for score, position in zip (self.scores, (-2/16, 6/16))]

	def draw (self):
		for playerLabel, scoreLabel in zip (self.playerLabels, self.scoreLabels):
			self.game.canvas.add (playerLabel)
			self.game.canvas.add (scoreLabel)
			self.game.canvas.add (self.hintLabel)
		self.game.canvas.add (self.image)
		
class Game:
	def __init__ (self):
		self.serviceIndex = 1 if Math.random () > 0.5 else 0	# Index of player that has initial service
		self.pause = True							# Start game in paused state
		self.keySet = set ()
		
		self.canvas = __new__ (fabric.Canvas ('canvas', {'backgroundColor': 'black', 'originX': 'center', 'originY': 'center'}))
		self.canvas.onWindowResize = self.resize	# Install draw callback, will be called asynch
		self.canvas.onWindowDraw = self.draw		# Install resize callback, will be called if resized
		self.canvas.lineWidth = 2
		self.canvas.clear ()	

		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)		

		window.setInterval (self.update, 10)	# Install update callback, time in ms
		window.setInterval (self.draw, 20)		# Install draw callback, time in ms
		window.addEventListener ('keydown', self.keydown)
		window.addEventListener ('keyup', self.keyup)
		
		self.time = + __new__ (Date)
 
	def update (self):							# Note that update and draw are not synchronized
		oldTime = self.time
		self.time = + __new__ (Date)
		self.deltaT = (self.time - oldTime) / 1000.
		
		if self.pause:							# If in paused state
			if space in self.keySet:			#	If spacebar hit
				self.pause = False				#		  Start playing
			elif enter in self.keySet:		#	Else if enter hit
				self.scoreboard.reset ()		#		  Reset score
		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.canvas.clear ()
		for attribute in self.attributes:
			attribute.draw ()
			 
	def resize (self, width, height):
		pass
		
	def scaleX (self, x):
		return x * (self.canvas.width / orthoWidth)
			
	def scaleY (self, y):
		return y * (self.canvas.height / orthoHeight)	
		
	def orthoX (self, x):
		return self.scaleX (x + orthoWidth // 2)
		
	def orthoY (self, y):
		return self.scaleY (orthoHeight - fieldHeight // 2 - y)
				
	def keydown (self, event):
		self.keySet.add (event.keyCode)
		
	def keyup (self, event):
		self.keySet.remove (event.keyCode)
		
game = Game ()	# Create and run game
