#!/usr/bin/env python3

from time import sleep
from random import choice, randint

import gpiozero
import scrollphat

scrollphat.set_brightness(5)

BOARD_COLS = 5
BOARD_ROWS = 11

# Define blocks as a list of lists of lists.
BLOCKS = [
    # T
    [[True, True, True],
     [False, True, False]],

    # S left
    [[False, True, True],
     [True, True, False]],

    # S right
    [[True, True, False],
     [False, True, True]],

    # L left
    [[True, False, False],
     [True, True, True]],

    # L right
    [[False, False, True],
     [True, True, True]],

    # Line
    [[True,True,True,True]],

    # Square
    [[True, True],
     [True, True]]
]


class Tetris(object):

    def __init__(self):
        """
        Initialize the game and set up Raspberry Pi GPIO interface for buttons.
        """

        self.init_game()
        self.init_board()

        # Initialize the GPIO pins for our buttons.
        self.btn_left = gpiozero.Button(26)
        self.btn_right = gpiozero.Button(20)
        self.btn_rotate = gpiozero.Button(6)
        self.btn_drop = gpiozero.Button(12)

        # Set up the handlers, to push events into the event queue.
        self.btn_left.when_pressed = lambda: self.event_queue.append(self.move_left)
        self.btn_right.when_pressed = lambda: self.event_queue.append(self.move_right)
        self.btn_rotate.when_pressed = lambda: self.event_queue.append(self.rotate_block)
        self.btn_drop.when_pressed = lambda: self.event_queue.append(self.drop_block)

    def init_game(self):
        """
        Initialize game.

        Reset level, block and set initial positions. Unset the game over
        flag and clear the event queue.
        """

        self.level = 1.0

        self.block = None
        self.x = 0
        self.y = 0

        self.game_over = False
        self.event_queue = []

    def init_board(self):
        """
        Initialize and reset the board.
        """

        self.board = []
        for n in range(BOARD_ROWS):
            self.add_row()

    def current_block_at(self, x, y):
        """

        :param x:
        :param y:
        :return:
        """

        if self.block is None:
            return False

        # If outside blocks dimensions, return False
        if (x < self.x or
            y < self.y or
            x > self.x + len(self.block[0])-1 or
            y > self.y + len(self.block)-1):
            return False

        # If we're here, we're inside our block
        return self.block[y-self.y][x-self.x]

    def update_board(self):
        """
        Write the current state to the board.

        Update the display pixels to match the current
        state of the board, including the current active block.
        Reverse x,y and flip the x position for the rotation
        of the Scroll pHAT (portrait).
        """

        scrollphat.set_pixels(lambda y,x: (
                                self.board[y][BOARD_COLS-x-1] or
                                self.current_block_at(BOARD_COLS-x-1,y) )
                              )
        scrollphat.update()

    def fill_board(self):
        """
        Fill up the board for game over.

        Set all pixels on from the bottom to the top,
        one row at a time.
        """

        for y in range(BOARD_ROWS-1,-1,-1):
            for x in range(BOARD_COLS):
                scrollphat.set_pixel(y,x,True)

            scrollphat.update()
            sleep(0.1)

    def add_row(self):
        """
        Add a new row to the top of the board.
        """

        self.board.insert(0, [False for n in range(BOARD_COLS)])

    def remove_lines(self):
        """
        Check board for any full lines and remove remove them.
        """

        complete_rows = [n for n, row in enumerate(self.board)
                         if sum(row) == BOARD_COLS]
        for row in complete_rows:
            del self.board[row] # Delete the specific row from the board.
            self.add_row() # Add new row to the top of the board (will not affect indexes).
            self.level += 0.1

    def add_block(self):
        """
        Add a new block to the board.

        Selects new block at random from those in BLOCKS. Rotates it
        a random number of times from 0-3. The block is placed in
        the middle of the board, off the top.

        The new block is checked for collision: a collision while placing
        a block is the signal for game over.

        :return: `bool` `True` if placed block collides.
        """

        self.block = choice(BLOCKS)

        # Rotate the block 0-3 times
        for n in range(randint(0,3)):
            self.rotate_block()

        self.x = BOARD_COLS // 2 - len(self.block[0]) //2
        self.y = -len(self.block)

        return not self.check_collision(yo=1)

    def rotate_block(self):
        """
        Rotate the block (clockwise).

        Rotated block is checked for collision, if there is
        a collision following rotate, we roll it back.
        """

        prev_block = self.block

        self.block = [[ self.block[y][x]
                        for y in range(len(self.block)) ]
                        for x in range(len(self.block[0]) - 1, -1, -1) ]

        if self.check_collision():
            self.block = prev_block

    def check_collision(self, xo=0, yo=0):
        """
        Check for collision between the currently active block
        and existing blocks on the board (or the
        left/right/bottom of the board).

        An optional x and y offset is used to check whether a
        collision would occur when the block is shifted.

        Returns `True` if a collision is found.

        :param xo: `int` x-offset to check for collision.
        :param yo: `int` y-offset to check for collision.
        :return: `bool` `True` if collision found.
        """

        if self.block is None:
            # We can't collide if there is no block.
            return False

        if self.y+yo+len(self.block) > BOARD_ROWS:
            # If the block is off the end of the board, always collides.
            return True

        if self.x+xo < 0 or self.x+xo+len(self.block[0]) > BOARD_COLS:
            # If the block is off the left or right of the board, it collides.
            return True

        for y, row in enumerate(self.block):
            for x, state in enumerate(row):
                if (self.within_bounds(self.x+x+xo,self.y+y+yo) and
                    self.board[self.y+y+yo][self.x+x+xo] and
                    state):

                    return True

    def within_bounds(self, x, y):
        """
        Check if a particular x and y coordinate is within
        the bounds of the board.

        :param x: `int` x-coordinate
        :param y: `int` y-coordinate
        :return: `bool` `True` if within the bounds.
        """
        return not( x < 0 or x > BOARD_COLS-1 or y < 0 or y > BOARD_ROWS -1)

    def move_left(self):
        """
        Move the active block left.

        Move left, if the new position of the block does not
        collide with the current board.
        """

        if not self.check_collision(xo=-1):
            self.x -= 1

    def move_right(self):
        """
        Move the active block right.

        Move right, if the new position of the block does not
        collide with the current board.
        """

        if not self.check_collision(xo=+1):
            self.x += 1


    def move_down(self):
        """
        Move the active block down.

        Move left, if the new position of the block collides
        with the current board, place the block (add to the board)
        and set the block to `None`.
        """

        if self.check_collision(yo=+1):
            self.place_block()
            self.block = None
        else:
            self.y += 1

    def drop_block(self):
        """
        Drop the block to the bottom of the board.

        Moves the block down as far as it can fall without
        hitting a collision.
        """

        while self.block:
            self.move_down()

    def place_block(self):
        """
        Transfer the current block to the board.
        """

        for y, row in enumerate(self.block):
            for x, state in enumerate(row):
                if self.within_bounds(self.x+x,self.y+y):
                    self.board[ self.y + y ][ self.x + x ] |= state

    def start(self):
        """
        Start the game for the first time. Initialize the board,
        game and then start the main loop.
        """

        self.init_board()
        self.init_game()
        self.game()

    def end_game(self):
        """
        End game state.

        Set the game over flag, clear the event queue and fill
        the display board.
        """

        self.game_over = True
        self.event_queue = []
        self.fill_board()

    def handle_events(self):
        """
        Handle events from the event queue.

        Events are stored as methods which can be called to
        handle the event. Iterate and fire of each event
        in the order it was added to the queue.
        """

        while self.event_queue:
              fn = self.event_queue.pop()
              fn()

    def game(self):
        """
        The main game loop.

        Once initialized the game will remain in this loop until
        exiting.
        """

        while True:
            if not self.game_over:
                if not self.block:
                    # No current block, add one.
                    if self.add_block() == False:
                        # If we failed to add a block (board full)
                        # it's game over. Set param and restart the loop.
                        self.end_game()
                        continue

                self.handle_events()
                self.move_down()
                self.check_collision()
                self.remove_lines()
                self.update_board()

            else:
                # Game over. We sit in here waiting
                # for any event in the queue, which
                # triggers a restart.
                if self.event_queue:
                    self.init_board()
                    self.init_game()

            # Sleep depending on current level
            sleep(1.0/self.level)


if __name__ == '__main__':
    tetris = Tetris()
    tetris.start()

