"""
This file is part of the Tetris project.

Written 2023 by Maarten Tromp <maarten@geekabit.nl>


Website
-------
https://www.geekabit.nl/projects/tetris-window/


License
-------
CC0 (no copyright, public domain)
The person who associated a work with this deed has dedicated the work to the public domain by
waiving all of his or her rights to the work worldwide under copyright law, including all related
and neighbouring rights, to the extent allowed by law. You can copy, modify, distribute and perform
the work, even for commercial purposes, all without asking permission.
https://creativecommons.org/share-your-work/public-domain/cc0/
This file is part of the Tetris project.
"""


# imports
import random
import logging
import json


class Snake:
	"""
	Snake class. This is the thing that wiggles across your screen, not the game.
	"""

	# constants
	directions = ("left", "up", "right", "down")
	rotations = ("cw", "ccw")


	def __init__(self, coordinates, direction, colour):
		"""
		Initialize Snake.

		Parameters
		----------
		coordinates: tuple or list of 2 numbers
			Initial snake x and y coordinates. Default is origin.
		direction: string
			Snake direction of movement. Default is down.
		"""

		assert len(coordinates) == 2, f"coordinates: {coordinates}"

		# public variables
		self.body = [coordinates] # start coordinates
		self.colour = colour

		# private variables
		self._direction = None
		self._desired_length = 3

		# init
		self.set_direction(direction)


	def set_direction(self, desired_movement):
		"""
		Set snake direction of movement.

		Parameters
		----------
		desired_movement: string
			Snake desired direction of movement. One of: up, down, left, right, cw, ccw.

		Returns
		-------
		none
		"""

		assert desired_movement in self.directions + self.rotations, f"desired_movement: {desired_movement}"

		if desired_movement == "ccw":
			self._direction = self.directions[(self.directions.index(self._direction) - 1) % 4]
		elif desired_movement == "cw":
			self._direction = self.directions[(self.directions.index(self._direction) + 1) % 4]
		elif desired_movement in self.directions:
			self._direction = desired_movement


	def grow(self):
		"""
		Grow snake by adding segments.

		Parameters
		----------
		none

		Returns
		-------
		none
		"""

		self._desired_length += 3


	def behead(self):
		"""
		Behead snake after a collision, so body can be shown in the new position.

		Parameters
		----------
		none

		Returns
		-------
		none
		"""

		self.body.pop(0)


	def move(self):
		"""
		Move snake in set direction.

		Parameters
		----------
		none

		Returns
		-------
		none
		"""

		# check snake length
		if len(self.body) < self._desired_length:
			# grow snake
			self.body.append(())

		# move snake segments to new position, starting at the end
		for i in range(len(self.body) - 1, 0, -1):
			self.body[i] = self.body[i - 1]
		# every segment now is in the new position except the head

		# move head to new position
		(head_x, head_y) = self.body[0]
		if self._direction == "left":
			head_x -= 1
		elif self._direction == "up":
			head_y -= 1
		elif self._direction == "right":
			head_x += 1
		elif self._direction == "down":
			head_y += 1
		self.body[0] = (head_x, head_y)


class Gem:
	"""
	Gem class. This is what snakes eat.
	"""

	# constants
	colour = (255, 0, 0)


	def __init__(self, screen, random_seed):
		"""
		Initialize gem class.

		Parameters
		----------
		screen: object
		random_seed: int
			Value to seed to random function to have repeatable games.
		"""

		# public variables
		self.coordinates = None

		# private variables
		self._screen = screen
		self._random_seed = random_seed

		# init
		self.new()


	def new(self):
		"""
		Place gem at random location. This function is called at the start of the game and when a gem is
		collected.

		Parameters
		----------
		none

		Returns
		-------
		none
		"""

		random.seed(self._random_seed)
		self._random_seed += 1

		self.coordinates = (random.randrange(self._screen.width), random.randrange(self._screen.height))


class SnakeGame:
	"""
	Snake game class.
	"""

	# constants
	delay = 2
	sounds = ("gem",)


	def __init__(self, screen, players, random_seed):
		"""
		Initialize Snake game.

		Parameters
		----------
		screen: object
		players: tuple or list of 1 or 2 player objects

		Returns
		-------
		none
		"""

		assert 1 <= len(players) <= 2

		# public variables
		self.save = False

		# private variables
		self._screen = screen
		self._players = players
		self._snakes = []
		self._delay_counter = 0
		self._gem = Gem(self._screen, random_seed)
		self._points = 0

		# init
		self._screen.clear()
		if len(self._players) == 1:
			logging.debug("starting Snake game for player %s (%s)", self._players[0].name,
					self._players[0].id)
			self._snakes.append(Snake(coordinates = (2, 8), direction = "down", colour = (0, 255, 0)))
		else:
			logging.debug("starting Snake game for players %s and %s (%s and %s)", self._players[0].name,
					self._players[1].name, self._players[0].id, self._players[1].id)
			self._snakes.append(Snake(coordinates = (2, 8), direction = "down", colour = (127,127, 0)))
			self._snakes.append(Snake(coordinates = (7, 21), direction = "up", colour = (0, 0, 255)))

		for player in self._players:
			player.send(json.dumps({"sounds" : self.sounds}))


	def update(self):
		"""
		Update game state. This function is called at a regular interval. Handle timed actions and user
		actions.

		Parameters
		----------
		none

		Returns
		-------
		bool
			True if game should continue, False on game-over.
		"""

		finished = False

		# read controller, set snake direction
		for i, snake in enumerate(self._snakes):
			button_state = self._players[i].get_button_state()
			if button_state["left"] == 1:
				snake.set_direction("left")
			elif button_state["up"] == 1:
				snake.set_direction("up")
			elif button_state["right"] == 1:
				snake.set_direction("right")
			elif button_state["down"] == 1:
				snake.set_direction("down")
			elif button_state["a"] == 1:
				snake.set_direction("ccw")
			elif button_state["b"] == 1:
				snake.set_direction("cw")

		# slow down snake movement
		self._delay_counter += 1
		if not self._delay_counter > self.delay:
			return True # not finished
		self._delay_counter = 0

		for i, snake in enumerate(self._snakes):
			snake.move()

			# check for gem
			if snake.body[0] == self._gem.coordinates:
				self._players[i].send(json.dumps({"sound" : "gem"}))
				self._points += 1
				self._gem.new()
				snake.grow()

				if self._points >= 5:
					self.save = True
			else:
				# do not run these checks if snake got a gem, so it can travel through tail.
				# check for head hitting own tail
				for segment in snake.body[1:]: # all segments except head
					if snake.body[0] == segment:
						#self._players[i].send(json.dumps({"sound" : "crash"}))
						snake.behead()
						logging.debug("you ran into your own tail, game finished")
						finished = True

				# check for head hitting walls
				(head_x, head_y) = snake.body[0]
				if not (0 <= head_x < self._screen.width and 0 <= head_y < self._screen.height):
					#self._players[i].send(json.dumps({"sound" : "crash"}))
					snake.behead()
					logging.debug("you ran into a wall, game finished")
					finished = True

		# update screen
		self._update_screen()

		return not finished


	def _update_screen(self):
		"""
		Draw snake(s) and gem on screen.

		Parameters
		----------
		none

		Returns
		-------
		none
		"""

		self._screen.clear()

		# draw snake
		for i, snake in enumerate(self._snakes):
			for segment in snake.body:
				self._screen.set_pixel(segment, self._snakes[i].colour)

		# draw gem
		self._screen.set_pixel(self._gem.coordinates, self._gem.colour)


# It's only appropriate to write a Snake game in Python.
