import pygame
from pygame.locals import *
from pygame import mixer
import pickle
from os import path

pygame.mixer.pre_init(44100, -16, 2, 512)
mixer.init()
pygame.init()

clock = pygame.time.Clock()
fps = 60

screen_width = 32*28
screen_height = 32*28

screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption('Platformer')


#define font
font = pygame.font.SysFont('Bauhaus 93', 70)
font_score = pygame.font.SysFont('Bauhaus 93', 30)


#define game variables
tile_size = 32
game_over = 0
main_menu = True
level = 1
max_levels = 10
score = 0


#define colours
white = (255, 255, 255)
blue = (0, 0, 255)


#load images
sun_img = pygame.image.load('img/sun.png')
bg_img = pygame.image.load('img/sky.png')
restart_img = pygame.image.load('img/restart_btn.png')
start_img = pygame.image.load('img/start_btn.png')
exit_img = pygame.image.load('img/exit_btn.png')

#load sounds
pygame.mixer.music.load('img/music.wav')
pygame.mixer.music.play(-1, 0.0, 5000)
coin_fx = pygame.mixer.Sound('img/coin.wav')
coin_fx.set_volume(0.5)
jump_fx = pygame.mixer.Sound('img/jump.wav')
jump_fx.set_volume(0.5)
game_over_fx = pygame.mixer.Sound('img/game_over.wav')
game_over_fx.set_volume(0.5)


def draw_text(text, font, text_col, x, y):
	img = font.render(text, True, text_col)
	screen.blit(img, (x, y))


#function to reset level
def reset_level(level):
	player.reset(100, screen_height - 130)
	platform_group.empty()
	coin_group.empty()
	lava_group.empty()
	door_group.empty()
	exit_group.empty()

	#load in level data and create world
	if path.exists(f'level{level}_data'):
		pickle_in = open(f'level{level}_data', 'rb')
		world_data = pickle.load(pickle_in)
	world = World(world_data)
	#create dummy coin for showing the score
	score_coin = Coin(tile_size // 2, tile_size // 2)
	coin_group.add(score_coin)
	return world


class Button():
	def __init__(self, x, y, image):
		self.image = image
		self.rect = self.image.get_rect()
		self.rect.x = x
		self.rect.y = y
		self.clicked = False

	def draw(self):
		action = False

		#get mouse position
		pos = pygame.mouse.get_pos()

		#check mouseover and clicked conditions
		if self.rect.collidepoint(pos):
			if pygame.mouse.get_pressed()[0] == 1 and self.clicked == False:
				action = True
				self.clicked = True

		if pygame.mouse.get_pressed()[0] == 0:
			self.clicked = False


		#draw button
		screen.blit(self.image, self.rect)

		return action


class Player():
	def __init__(self, x, y):
		self.reset(x, y)

	def update(self, game_over):
		dx = 0
		dy = 0
		walk_cooldown = 5
		col_thresh = 20

		if game_over == 0:
			#get keypresses
			key = pygame.key.get_pressed()
			if (key[pygame.K_SPACE] or key[pygame.K_w]) and self.jumped == False and self.in_air == False:
				jump_fx.play()
				self.vel_y = -15
				self.jumped = True
			if key[pygame.K_SPACE] and key[pygame.K_w] == False:
				self.jumped = False
			if key[pygame.K_a]:
				dx -= 5
				self.counter += 1
				self.direction = -1
			if key[pygame.K_d]:
				dx += 5
				self.counter += 1
				self.direction = 1
			if key[pygame.K_a] == False and key[pygame.K_d] == False:
				self.counter = 0
				self.index = 0
				if self.direction == 1:
					self.image = self.images_right[self.index]
				if self.direction == -1:
					self.image = self.images_left[self.index]


			#handle animation
			if self.counter > walk_cooldown:
				self.counter = 0	
				self.index += 1
				if self.index >= len(self.images_right):
					self.index = 0
				if self.direction == 1:
					self.image = self.images_right[self.index]
				if self.direction == -1:
					self.image = self.images_left[self.index]


			#add gravity
			self.vel_y += 1
			if self.vel_y > 10:
				self.vel_y = 10
			dy += self.vel_y

			#check for collision
			self.in_air = True
			for tile in world.tile_list:
				#check for collision in x direction
				if tile[1].colliderect(self.rect.x + dx, self.rect.y, self.width, self.height):
					dx = 0
				#check for collision in y direction
				if tile[1].colliderect(self.rect.x, self.rect.y + dy, self.width, self.height):
					#check if below the ground i.e. jumping
					if self.vel_y < 0:
						dy = tile[1].bottom - self.rect.top
						self.vel_y = 0
					#check if above the ground i.e. falling
					elif self.vel_y >= 0:
						dy = tile[1].top - self.rect.bottom
						self.vel_y = 0
						self.in_air = False

			#check for collision with lava
			if pygame.sprite.spritecollide(self, lava_group, False):
				game_over = -1
				game_over_fx.play()

			#check for collision with exit
			if pygame.sprite.spritecollide(self, exit_group, False):
				game_over = 1


			#check for collision with platforms
			for platform in platform_group:
				#collision in the x direction
				if platform.rect.colliderect(self.rect.x + dx, self.rect.y, self.width, self.height):
					dx = 0
				#collision in the y direction
				if platform.rect.colliderect(self.rect.x, self.rect.y + dy, self.width, self.height):
					#check if below platform
					if abs((self.rect.top + dy) - platform.rect.bottom) < col_thresh:
						self.vel_y = 0
						dy = platform.rect.bottom - self.rect.top
					#check if above platform
					elif abs((self.rect.bottom + dy) - platform.rect.top) < col_thresh:
						self.rect.bottom = platform.rect.top - 1
						self.in_air = False
						dy = 0
					#move sideways with the platform
					if platform.move_x != 0:
						self.rect.x += platform.move_direction


			# Check for collision with doors
			for door in door_group:
				if door.rect.colliderect(self.rect.x + dx, self.rect.y, self.width, self.height):
					# Adjust player's horizontal position to prevent overlap with the door
					if dx > 0:
						self.rect.right = door.rect.left
					elif dx < 0:
						self.rect.left = door.rect.right



			#update player coordinates
			self.rect.x += dx
			self.rect.y += dy


		elif game_over == -1:
			self.image = self.dead_image
			draw_text('GAME OVER!', font, blue, (screen_width // 2) - 200, screen_height // 2)
			if self.rect.y > 200:
				self.rect.y -= 5

		#draw player onto screen
		screen.blit(self.image, self.rect)

		return game_over


	def reset(self, x, y):
		self.images_right = []
		self.images_left = []
		self.index = 0
		self.counter = 0
		for num in range(1, 5):
			img_right = pygame.image.load(f'img/guy{num}.png')
			img_right = pygame.transform.scale(img_right, (40, 80))
			img_left = pygame.transform.flip(img_right, True, False)
			self.images_right.append(img_right)
			self.images_left.append(img_left)
		self.dead_image = pygame.image.load('img/ghost.png')
		self.image = self.images_right[self.index]
		self.rect = self.image.get_rect()
		self.rect.x = x
		self.rect.y = y
		self.width = self.image.get_width()
		self.height = self.image.get_height()
		self.vel_y = 0
		self.jumped = False
		self.direction = 0
		self.in_air = True



class World():
	def __init__(self, data):
		self.tile_list = []

		#load images
		dirt_img = pygame.image.load('img/dirt.png')
		grass_img = pygame.image.load('img/grass.png')

		row_count = 0
		for row in data:
			col_count = 0
			for tile in row:
				if tile == 1:# BASE PLATFORMS
					img = pygame.transform.scale(dirt_img, (tile_size, tile_size))
					img_rect = img.get_rect()
					img_rect.x = col_count * tile_size
					img_rect.y = row_count * tile_size
					tile = (img, img_rect)
					self.tile_list.append(tile)
				if tile == 2:
					img = pygame.transform.scale(grass_img, (tile_size, tile_size))
					img_rect = img.get_rect()
					img_rect.x = col_count * tile_size
					img_rect.y = row_count * tile_size
					tile = (img, img_rect)
					self.tile_list.append(tile)
				if tile == 3: # x moving platforms
					platform = Platform(col_count * tile_size, row_count * tile_size, 1, 0)
					platform_group.add(platform)
				if tile == 4: # y moving platforms
					platform = Platform(col_count * tile_size, row_count * tile_size, 0, 1)
					platform_group.add(platform)
				if tile == 5: # lava
					lava = Lava(col_count * tile_size, row_count * tile_size + (tile_size // 2))
					lava_group.add(lava)
				if tile == 6: # coins
					coin = Coin(col_count * tile_size + (tile_size // 2), row_count * tile_size + (tile_size // 2))
					coin_group.add(coin)
				if tile == 7: # exit
					exit = Exit(col_count * tile_size, row_count * tile_size - (tile_size // 2))
					exit_group.add(exit)
				if tile == 8: # doors - red
					door = Door(col_count * tile_size, row_count * tile_size, "r")
					door_group.add(door)
				if tile == 9: # blue
					door = Door(col_count * tile_size, row_count * tile_size, "b")
					door_group.add(door)
				if tile == 10: # green
					door = Door(col_count * tile_size, row_count * tile_size, "g")
					door_group.add(door)
				if tile == 11: # pressure plates - red
					pressure = Pressure(col_count * tile_size, row_count * tile_size, "r")
					pressure_group.add(pressure)
				if tile == 12: # blue
					pressure = Pressure(col_count * tile_size, row_count * tile_size, "b")
					pressure_group.add(pressure)
				if tile == 13: # green
					pressure = Pressure(col_count * tile_size, row_count * tile_size, "g")
					pressure_group.add(pressure)
				col_count += 1
			row_count += 1


	def draw(self):
		for tile in self.tile_list:
			screen.blit(tile[0], tile[1])

# Door class

class Door(pygame.sprite.Sprite):
    def __init__(self, x, y, col):
        super().__init__()
        
        self.images = []
        self.index = 0
        self.count = 0
        self.col = col
        self.open = False
        self.original_y = y - tile_size * 3  # Store the original y position
        
        # Load and scale images for the door animation
        for num in range(1, 7):
            img = pygame.image.load(f'img/door{num}{col}.png')
            img = pygame.transform.scale(img, (tile_size, tile_size * 4))
            self.images.append(img)

        # Set the initial image and rect attributes
        self.image = self.images[self.index]
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = self.original_y

    def update(self):
        # If the door is open and not fully opened, increment the count
        if self.open and self.count < 30:
            self.count += 1
            # Scale the door smaller vertically and move downwards
            scale_factor = (1 + (1/4 - 1) * self.count / 30)
            new_height = int(self.image.get_height() * scale_factor)
            self.image = pygame.transform.scale(self.image, (self.rect.width, new_height))
            self.rect.y = self.original_y + (self.image.get_height() - new_height)
        # If the door is closed and not fully closed, decrement the count
        elif not self.open and self.count > 0:
            self.count -= 1
            # Scale the door bigger vertically and move upwards
            scale_factor = (1 + (1/4 - 1) * self.count / 30)
            new_height = int(self.image.get_height() * scale_factor)
            self.image = pygame.transform.scale(self.image, (self.rect.width, new_height))
            self.rect.y = self.original_y + (self.image.get_height() - new_height)

        # Update the animation frame based on the count
        self.index = self.count // 5
        self.image = self.images[self.index]
		

# Pressure plate class
class Pressure(pygame.sprite.Sprite):
    def __init__(self, x, y, col):
        super().__init__()
        self.images = []
        self.index = 0
        self.count = 0
        self.col = col

        # Load and scale images for the pressure plate animation
        for col_variant in col:
            for num in range(1, 5):
                img = pygame.image.load(f'img/pressure{num}{col_variant}.png')
                img = pygame.transform.scale(img, (tile_size, tile_size))
                self.images.append(img)

        # Set the initial image and rect attributes
        self.image = self.images[self.index]
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y

    def update(self):
        # Check for collision with the player
        if pygame.sprite.spritecollide(self, player, False):
            if self.count < 30:
                self.count += 1
            # Open the doors that match the pressure plate's color when count reaches 30
            for door in door_group:
                if door.col == self.col and self.count == 30:
                    door.open = True
        else:
            if self.count > 0:
                self.count -= 1
            # Close the doors that match the pressure plate's color
            for door in door_group:
                if door.col == self.col:
                    door.open = False

        # Update the animation frame based on the count
        self.index = self.count // 5
        self.image = self.images[self.index]


#lever class

class Lever(pygame.sprite.Sprite):
    def __init__(self, x, y, col):
        super().__init__()
        self.images = []
        # Load and scale images for the lever animation
        for num in range(1, 5):
            img = pygame.image.load(f'img/lever{num}{col}.png')
            img = pygame.transform.scale(img, (tile_size, tile_size))
            self.images.append(img)
        # Set the initial image and rect attributes
        self.image = self.images[0]
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.col = col
        self.switched = False
        self.count = 0
        self.index = 0
        self.prev_e_state = False  # To track the previous state of the "E" key

    def update(self, player, door_group, draw_text, font, screen, screen_width, screen_height):
        # Get the current state of all keys
        key = pygame.key.get_pressed()
        current_e_state = key[pygame.K_e]

        # Check for collision with the player
        if pygame.sprite.spritecollide(self, player, False):
            # Draw the interaction prompt at the center of the screen
            draw_text("Press E To Open", font, screen, screen_width // 2, screen_height // 2)
            # Toggle the switch state when "E" key is pressed
            if current_e_state and not self.prev_e_state:
                self.switched = not self.switched

        # Update the previous "E" key state
        self.prev_e_state = current_e_state

        # Manage the animation and door state based on the switch state
        if self.switched:
            if self.count < 30:
                self.count += 1
            # Open the doors that match the lever's color when count reaches 30
            for door in door_group:
                if door.col == self.col and self.count == 30:
                    door.open = True
        else:
            if self.count > 0:
                self.count -= 1
            # Close the doors that match the lever's color
            for door in door_group:
                if door.col == self.col:
                    door.open = False

        # Update the animation frame based on the count
        self.index = self.count // 5
        self.image = self.images[self.index]


class Platform(pygame.sprite.Sprite):
	def __init__(self, x, y, move_x, move_y):
		pygame.sprite.Sprite.__init__(self)
		img = pygame.image.load('img/platform.png')
		self.image = pygame.transform.scale(img, (tile_size, tile_size // 2))
		self.rect = self.image.get_rect()
		self.rect.x = x
		self.rect.y = y
		self.move_counter = 0
		self.move_direction = 1
		self.move_x = move_x
		self.move_y = move_y


	def update(self):
		self.rect.x += self.move_direction * self.move_x
		self.rect.y += self.move_direction * self.move_y
		self.move_counter += 1
		if abs(self.move_counter) > 50:
			self.move_direction *= -1
			self.move_counter *= -1





class Lava(pygame.sprite.Sprite):
	def __init__(self, x, y):
		pygame.sprite.Sprite.__init__(self)
		img = pygame.image.load('img/lava.png')
		self.image = pygame.transform.scale(img, (tile_size, tile_size // 2))
		self.rect = self.image.get_rect()
		self.rect.x = x
		self.rect.y = y


class Coin(pygame.sprite.Sprite):
	def __init__(self, x, y):
		pygame.sprite.Sprite.__init__(self)
		img = pygame.image.load('img/coin.png')
		self.image = pygame.transform.scale(img, (tile_size // 2, tile_size // 2))
		self.rect = self.image.get_rect()
		self.rect.center = (x, y)


class Exit(pygame.sprite.Sprite):
	def __init__(self, x, y):
		pygame.sprite.Sprite.__init__(self)
		img = pygame.image.load('img/exit.png')
		self.image = pygame.transform.scale(img, (tile_size, int(tile_size * 1.5)))
		self.rect = self.image.get_rect()
		self.rect.x = x
		self.rect.y = y



player = Player(100, screen_height - 130)

door_group = pygame.sprite.Group()
pressure_group = pygame.sprite.Group()
platform_group = pygame.sprite.Group()
lava_group = pygame.sprite.Group()
coin_group = pygame.sprite.Group()
exit_group = pygame.sprite.Group()

#create dummy coin for showing the score
score_coin = Coin(tile_size // 2, tile_size // 2)
coin_group.add(score_coin)

#load in level data and create world
if path.exists(f'level{level}_data'):
	pickle_in = open(f'level{level}_data', 'rb')
	world_data = pickle.load(pickle_in)

world = World(world_data)


#create buttons
restart_button = Button(screen_width // 2 - 50, screen_height // 2 + 100, restart_img)
start_button = Button(screen_width // 2 - 350, screen_height // 2, start_img)
exit_button = Button(screen_width // 2 + 150, screen_height // 2, exit_img)


run = True
while run:

	clock.tick(fps)

	screen.blit(bg_img, (0, 0))
	screen.blit(sun_img, (100, 100))

	if main_menu == True:
		if exit_button.draw():
			run = False
		if start_button.draw():
			main_menu = False
	else:
		world.draw()

		if game_over == 0:
			platform_group.update()
			#update score
			#check if a coin has been collected
			if pygame.sprite.spritecollide(player, coin_group, True):
				score += 1
				coin_fx.play()
			draw_text('X ' + str(score), font_score, white, tile_size - 10, 10)
		
		platform_group.draw(screen)
		lava_group.draw(screen)
		coin_group.draw(screen)
		exit_group.draw(screen)
		door_group.draw(screen)

		game_over = player.update(game_over)

		#if player has died
		if game_over == -1:
			if restart_button.draw():
				world_data = []
				world = reset_level(level)
				game_over = 0
				score = 0

		#if player has completed the level
		if game_over == 1:
			#reset game and go to next level
			level += 1
			if level <= max_levels:
				#reset level
				world_data = []
				world = reset_level(level)
				game_over = 0
			else:
				draw_text('YOU WIN!', font, blue, (screen_width // 2) - 140, screen_height // 2)
				if restart_button.draw():
					level = 1
					#reset level
					world_data = []
					world = reset_level(level)
					game_over = 0
					score = 0

	for event in pygame.event.get():
		if event.type == pygame.QUIT:
			run = False

	pygame.display.update()

pygame.quit()