"""
Created on Nov 5, 2022

@author: YOUR NAME HERE!!

This module represents a template for making grid-based Arcade games
with different screens for the instructions and game over messages.
"""
import random
import arcade


# Choose a name for your game to appear in the title bar of the game window
GAME_NAME = 'TO BE DETERMINED'
# Choose the size of your game board
BOARD_WIDTH = 800
BOARD_HEIGHT = 800
# Choose placement of game text
TEXT_OFFSET = 100
SCORE_MARGIN = 80
SCORE_X_OFFSET = 10
SCORE_Y_OFFSET = BOARD_HEIGHT + SCORE_X_OFFSET - 10
SCORE_COLOR = arcade.color.HELIOTROPE
# Choose the size of your game window
SCREEN_WIDTH = BOARD_WIDTH
SCREEN_HEIGHT = BOARD_HEIGHT + SCORE_MARGIN

# Choose values that determine game play
BOARD_SIZE = 10
TOO_MANY_MOVES = 3 * BOARD_SIZE
EMPTY_MOVE_TIME = 3
# Choose piece colors
COLOR1 = arcade.color.DUKE_BLUE
COLOR2 = arcade.color.CYBER_YELLOW


###
# Game mechanics related classes to help simplify the main Game class.
###
class Grid:
    """
    This class represents the 2D data structure that represents the state of the game board:
    - the values of each piece
    - the location of the empty space
    """
    def __init__(self, size):
        # start with a valid, empty list
        self.spaces = []
        # a list of two elements (x, y) - start with an invalid location that will be corrected during reset method
        self.empty_space = [ -1, -1 ]
        self.num_rows = size
        self.num_columns = size

    def reset(self):
        """
        Reset the grid space values back to their starting, alternating pattern.
        """
        # remove all the current values
        self.spaces.clear()
        # nested loop to make lists containing each column piece value within each row
        for row in range(self.num_rows):
            # create the new list to represent a row
            self.spaces.append([])
            for col in range(self.num_columns):
                # add either a 1 or a 2 to represent the two different piece values
                self.spaces[row].append((row + col) % 2 + 1)

        # remove one space at random and remember which space it is
        emptyRow, emptyCol = self.randomSpace()
        self.spaces[emptyRow][emptyCol] = 0
        self.empty_space = [emptyRow, emptyCol]
        print(emptyRow, emptyCol)

    def numPieces(self, pieceNum):
        """
        Count how many of the given piece are on the game board.
        """
        total = 0
        for listOfSpaces in self.spaces:
            for space in listOfSpaces:
                if space == pieceNum:
                    total += 1
        return total

    def flip(self, row, col):
        """
        Change a space's value to the other value.
        """
        if self.spaces[row][col] != 0:
            self.spaces[row][col] = 2 if self.spaces[row][col] == 1 else 1

    def swapWithEmptySpace(self, pieceRow, pieceCol):
        """
        Swap the given piece's value with the empty space.
        """
        emptyRow, emptyCol = self.empty_space
        self.spaces[emptyRow][emptyCol] = self.spaces[pieceRow][pieceCol]
        self.spaces[pieceRow][pieceCol] = 0
        self.empty_space = [pieceRow, pieceCol]

    def inBounds(self, row, col):
        """
        Return True if the given (row, col) location is in the game board (use to avoid index out of bounds exceptions).
        """
        return 0 <= row < self.num_rows and 0 <= col < self.num_columns

    def randomSpace(self):
        """
        Return a random non-empty space on the game board.
        """
        row = random.randrange(self.num_rows)
        col = random.randrange(self.num_columns)
        if row == self.empty_space[0] and col == self.empty_space[1]:
            row = (row + 1) % self.num_rows
        return [ row, col ]

    def print(self):
        """
        Print the 2D grid one row per line with values evenly spaced across the row.
        """
        for listOfSpaces in self.spaces:
            for space in listOfSpaces:
                # align the output by formatting the value to always take up 4 spaces
                print(f"{space:4}", end='')
            print()
        print()


class Piece(arcade.SpriteCircle):
    """
    This class represents a piece on the grid game board as a Sprite that knows its piece value and game board location.
    """
    def __init__(self, row, column, pieceNum, size, x, y):
        # oddly, to be able to change the color properly later it must first be set to WHITE
        super().__init__(size // 2, arcade.color.WHITE)
        self.center_x = x
        self.center_y = y
        self.row = row
        self.column = column
        self.pieceNum = pieceNum

    def draw(self):
        """
        Draw as a Circle but select the proper color first.
        """
        self.color = COLOR1 if self.pieceNum == 1 else COLOR2
        super().draw()

    def flipNeighbors(self, board):
        """
        Flip the color of the neighbors immediately around this piece.
        """
        # loop over the neighbors that are (-1, 0, and 1) row and column away (nine total including itself (0, 0))
        for row_offset in range(-1, 2):
            for col_offset in range(-1, 2):
                neighborRow = self.row + row_offset
                neighborCol = self.column + col_offset
                # make sure the piece to flip is on the board and a different value
                if board.inBounds(neighborRow, neighborCol) and board.spaces[neighborRow][neighborCol] != self.pieceNum:
                    board.flip(neighborRow, neighborCol)

    def swapWithEmptySpace(self, board):
        """
        Swap this piece with the empty space by updating its value and moving its location on the game board.
        """
        # calculate difference in rows and columns and use to move both its logical and visual game board position
        row_offset = board.empty_space[0] - self.row
        col_offset = board.empty_space[1] - self.column
        # swap the underlying grid values
        board.swapWithEmptySpace(self.row, self.column)
        # update the piece information to match
        self.center_x += col_offset * BOARD_HEIGHT // board.num_columns
        self.center_y += -row_offset * BOARD_WIDTH // board.num_rows
        self.row += row_offset
        self.column += col_offset

    def isNeighborEmpty(self, board):
        """
        Return True if this piece is next to the empty space.
        """
        return abs(board.empty_space[0] - self.row) <= 1 and abs(board.empty_space[1] - self.column) <= 1


###
# Game view related classes to show text messages without worrying about interacting with the game.
#
# Views are similar to the Window class (you will override the same methods) but allow you to switch
# between them to make it easy to keep different Sprite collections, updates, and responses to input.
#
# These are a simple as possible, just displaying text until the user interacts with it.
###
class InstructionsView(arcade.View):
    """
    This class represents a screen that displays text for the game instructions until the user clicks the mouse.
    Overrides typical methods:
    - setting up any objects to appear in game
    - drawing them (in method on_draw)
    - responding to mouse input (in method on_mouse_press)
    """
    def __init__(self):
        super().__init__()
        # game instructions, could be read from a file instead
        self.instructions = '''
Click on a piece to flip its neighbors colors
But watch out for the empty space!
It swaps your piece and costs you a move
        '''
        # set background color just once
        arcade.set_background_color(arcade.color.BABY_BLUE_EYES)

    def on_draw(self):
        """
        Draw the instructions on the screen.
        """
        # DO NOT CHANGE -- always clear the screen as the FIRST step
        self.clear()
        # draw the instruction text from the top to bottom (higher to lower Y coordinate)
        for i, line in enumerate(self.instructions.split('\n')):
            arcade.draw_text(line, BOARD_WIDTH // 2, (3 - i) * TEXT_OFFSET + BOARD_HEIGHT // 2,
                             arcade.color.DUKE_BLUE, font_size=28, anchor_x='center')

        # tell player how to start the game
        arcade.draw_text('Click to start', BOARD_WIDTH // 2, -TEXT_OFFSET + BOARD_HEIGHT / 2,
                         arcade.color.DUKE_BLUE, font_size=36, anchor_x='center')

    def on_mouse_press(self, x, y, button, modifiers):
        """
        Called whenever the mouse is pressed --- anywhere is fine.
        """
        # create and show a new instance of the game to get it started
        self.window.show_view(GameView())


class GameOverView(arcade.View):
    """
    This class represents a screen that displays the score, winning or losing message, and instructions for restarting.
    Overrides typical methods:
    - setting up any objects to appear in game
    - drawing them (in method on_draw)
    - responding to key input (in method on_key_press)
    """
    def __init__(self, pieceNum, numMoves):
        super().__init__()
        # save data from the game's end to determine which results to display
        self.pieceNum = pieceNum
        self.numMoves = numMoves
        # set background color just once
        arcade.set_background_color(arcade.color.BABY_BLUE_EYES)

    def on_draw(self):
        """
        Draw the results on the screen.
        """
        # DO NOT CHANGE -- always clear the screen as the FIRST step
        self.clear()
        # display the score
        arcade.draw_text(f"{self.numMoves} moves", BOARD_WIDTH // 2, 3 * TEXT_OFFSET + BOARD_HEIGHT // 2,
                         arcade.color.DUKE_BLUE, font_size=64, anchor_x='center')

        # display either the winning or losing message
        if self.pieceNum != 0:
            arcade.draw_text(f"Piece {self.pieceNum} Wins!", BOARD_WIDTH // 2, TEXT_OFFSET + BOARD_HEIGHT // 2,
                             arcade.color.DUKE_BLUE, font_size=48, anchor_x='center')
        else:
            arcade.draw_text('Too many moves -- NO WINNER!', BOARD_WIDTH // 2, TEXT_OFFSET + BOARD_HEIGHT // 2,
                             arcade.color.DUKE_BLUE, font_size=32, anchor_x='center')

        # tell player how to restart the game
        arcade.draw_text("Press any key to restart", BOARD_WIDTH // 2, -TEXT_OFFSET + BOARD_HEIGHT / 2,
                         arcade.color.DUKE_BLUE, font_size=32, anchor_x='center')

    def on_key_press(self, key, modifiers):
        """
        Called whenever a key is pressed -- anywhere is fine.
        """
        # create and show a new instance of the game to restart it
        self.window.show_view(GameView())


###
# Main game view (used to extend Window) but now extends View so it can be switched with other views.
#
# Functionally, it is the same as before and overrides the same methods.
###
class GameView(arcade.View):
    """
    This class represents the entire game:
    - setting up any objects to appear in game
    - updating their values (in method on_update)
    - drawing them (in method on_draw)
    - responding to mouse input (in method on_mouse_press)
    - responding to key input (in method on_key_press)
    """
    def __init__(self):
        # no need to pass size and title since this is just a view within the window
        super().__init__()
        # create the 2D data structure that represents the game board
        self.board = Grid(BOARD_SIZE)
        # note, this must be a 1D SpriteList to support mouse clicking automatically
        self.sprites = arcade.SpriteList()
        # score: keep track of the number of moves made
        self.numMoves = 0

    def on_show_view(self):
        """
        Called when the view is displayed, can be used to set up the game's initial state.
        """
        arcade.set_background_color(arcade.color.LIGHT_BLUE)
        self.setup()
        # ADD ONLY ONCE per View
        # when the given amount of time has passed, move the empty space by swapping it with another space
        arcade.schedule(self.swap_empty_space, EMPTY_MOVE_TIME)

    def on_hide_view(self):
        """
        Called when the view is displayed, can be used to stop any scheduled actions or save any data to a file.
        """
        arcade.unschedule(self.swap_empty_space)

    def setup(self):
        """
        Set up the initial state at the beginning of the game or to allow playing again after a win or loss
        """
        self.numMoves = 0
        self.board.reset()
        self.sprites.clear()
        self.create_grid_sprites()
        self.board.print()

    def create_grid_sprites(self):
        """
        Create the Sprites to represent the state of the game on the screen.
        """
        size = min(BOARD_WIDTH // (self.board.num_rows * 2), BOARD_HEIGHT // (self.board.num_columns * 2))
        # nested loop to calculate the (x, y) coordinate of each Sprite
        for y in range(self.board.num_rows):
            for x in range(self.board.num_columns):
                # do not create a Sprite for the empty space
                if self.board.spaces[y][x] != 0:
                    self.sprites.append(
                        # create a Piece with information about the game board first, then size and (x, y) position
                        Piece(y, x, self.board.spaces[y][x], size,
                              size + x * BOARD_WIDTH // self.board.num_columns,
                              BOARD_HEIGHT - (size + y * BOARD_HEIGHT // self.board.num_rows)))

    def update_grid_sprites(self):
        """
        Update each Sprite's value to match the game board's value in case it changed
        """
        for sprite in self.sprites:
            sprite.pieceNum = self.board.spaces[sprite.row][sprite.column]

    def swap_empty_space(self, dt):
        """
        Swap a random piece with the empty space so it moves around the game board periodically.
        Note, dt parameter comes from Arcade since this is a scheduled function.
        """
        randomRow, randomCol = self.board.randomSpace()
        for sprite in self.sprites:
            if sprite.row == randomRow and sprite.column == randomCol:
                sprite.swapWithEmptySpace(self.board)

    def is_winner(self, pieceNum):
        """
        Returns True if the given piece has covered the entire game board.
        """
        return self.board.numPieces(pieceNum) == self.board.num_rows * self.board.num_columns - 1

    def on_key_press(self, key, modifiers):
        """
        Called whenever a key is pressed.
        """
        if key == arcade.key.R:
            self.setup()

    def on_mouse_press(self, x, y, button, modifiers):
        """
        Called whenever a mouse button is pressed.
        Also checks for game ending conditions since input changes the game board values.
        """
        # get any Sprites that intersect with the mouse's (x, y) position -- note the position is given as a list
        picked = arcade.get_sprites_at_point([x, y], self.sprites)
        # if only one Sprite was picked
        if len(picked) == 1:
            print(picked[0].row, picked[0].column, picked[0].pieceNum)
            # swap it if it is next to the empty space, otherwise flip its neighbors
            if picked[0].isNeighborEmpty(self.board):
                picked[0].swapWithEmptySpace(self.board)
            else:
                picked[0].flipNeighbors(self.board)
            # update the Sprites to reflect any changes made
            self.update_grid_sprites()
            # count it as a move
            self.numMoves += 1
            self.board.print()

        # if either piece has taken over the entire game board, create and show the next view
        if self.is_winner(1):
            self.window.show_view(GameOverView(1, self.numMoves))
        elif self.is_winner(2):
            self.window.show_view(GameOverView(2, self.numMoves))

        # if too many moves have been made, create and show the next view
        if self.numMoves > TOO_MANY_MOVES:
            self.window.show_view(GameOverView(0, self.numMoves))

    def on_draw(self):
        """
        Draw all the game objects on the screen.
        """
        # DO NOT CHANGE -- always clear the screen as the FIRST step
        self.clear()
        # draw the Sprite pieces, note that draw MUST be called directly to get the overriden version in the Piece class
        for sprite in self.sprites:
            sprite.draw()
        # draw score values on top
        arcade.draw_text(self.board.numPieces(1), SCORE_X_OFFSET, SCORE_Y_OFFSET,
                         COLOR1, font_size=64, anchor_x='left')
        arcade.draw_text(self.board.numPieces(2), BOARD_WIDTH - SCORE_X_OFFSET, SCORE_Y_OFFSET,
                         COLOR2, font_size=64, anchor_x='right')
        arcade.draw_text(self.numMoves, BOARD_WIDTH // 2, SCORE_Y_OFFSET,
                         SCORE_COLOR, font_size=64, anchor_x='center')

    def on_update(self, dt):
        """
        Handle game "rules" for every step (i.e., frame or "moment"):
        """
        pass


# Create the game window (with its size and title)
window = arcade.Window(BOARD_WIDTH, BOARD_HEIGHT + SCORE_MARGIN, GAME_NAME)
# Create and show the initial view
window.show_view(InstructionsView())
# Play the game forever
arcade.run()
