"""
Tetris on the Card10.
By Peace-Maker
@jhartung10
"""
import buttons
import color
import display
import leds
import math
import urandom
import utime
import vibra

urandom.seed(utime.time_ms())

# 160x80 display. we use it vertically.
# 10 tetriminos per row -> 80 / 10 = 8 pixels per tetrimino
TETRIMINO_SIZE = 8

FIELD_WIDTH = 10
FIELD_HEIGHT = 20

# Move the tetrimino down every x ticks.
TETRIMINO_MOVE_DOWN_INTERVAL = 25
MAX_LEVEL = 11
LEVEL_SPEEDUP_STEP = 1

# Move left or right more often than down.
TETRIMINO_MOVE_HORIZONTAL_INTERVAL = 10

TETRIMINO_NONE = 0
TETRIMINO_SQUARE = 1
TETRIMINO_T = 2
TETRIMINO_Z = 3
TETRIMINO_S = 4
TETRIMINO_J = 5
TETRIMINO_L = 6
TETRIMINO_I = 7
NUMBER_OF_TERIMINOS = 7

TETRIMINO_COLORS = [
    (0, 0, 0),   # None
    (255, 255, 0), # Square
    (153, 0, 255), # T
    (255, 0, 0), # Z
    (0, 255, 0), # S
    (0, 0, 255), # J
    (255, 165, 0), # L
    (0, 255, 255), # I
]

ROTATION_0 = 0
ROTATION_90 = 1
ROTATION_180 = 2
ROTATION_270 = 3

TETRIMINO_GEOMETRY = [
    (), # None
    ( # Square
        # ==
        # ==
        ( # The same in all rotations..
            (1, 1),
            (1, 1),
        ),
        (
            (1, 1),
            (1, 1),
        ),
        (
            (1, 1),
            (1, 1),
        ),
        (
            (1, 1),
            (1, 1),
        ),
    ),
    ( # T
        # ===
        #  =
        ( # ROTATION_0
            (1, 1, 1),
            (0, 1, 0),
        ),
        #  =
        # ==
        #  =
        ( # ROTATION_90
            (0, 1),
            (1, 1),
            (0, 1),
        ),
        #  =
        # ===
        ( # ROTATION_180
            (0, 1, 0),
            (1, 1, 1),
        ),
        # =
        # ==
        # =
        ( # ROTATION_270
            (1, 0),
            (1, 1),
            (1, 0),
        ),
    ),
    ( # Z
        # ==
        #  ==
        ( # ROTATION_0
            (1, 1, 0),
            (0, 1, 1),
        ),
        #  =
        # ==
        # =
        ( # ROTATION_90
            (0, 1),
            (1, 1),
            (1, 0),
        ),
        # ==
        #  ==
        ( # ROTATION_180
            (1, 1, 0),
            (0, 1, 1),
        ),
        #  =
        # ==
        # =
        ( # ROTATION_270
            (0, 1),
            (1, 1),
            (1, 0),
        ),
    ),
    ( # S
        #  ==
        # ==
        ( # ROTATION_0
            (0, 1, 1),
            (1, 1, 0),
        ),
        # =
        # ==
        #  =
        ( # ROTATION_90
            (1, 0),
            (1, 1),
            (0, 1),
        ),
        #  ==
        # ==
        ( # ROTATION_180
            (0, 1, 1),
            (1, 1, 0),
        ),
        # =
        # ==
        #  =
        ( # ROTATION_270
            (1, 0),
            (1, 1),
            (0, 1),
        ),
    ),
    ( # J
        # ==
        # =
        # =
        ( # ROTATION_0
            (1, 1),
            (1, 0),
            (1, 0),
        ),
        # ===
        #   =
        ( # ROTATION_90
            (1, 1, 1),
            (0, 0, 1),
        ),
        #  =
        #  =
        # ==
        ( # ROTATION_180
            (0, 1),
            (0, 1),
            (1, 1),
        ),
        # =
        # ===
        ( # ROTATION_270
            (1, 0, 0),
            (1, 1, 1),
        ),
    ),
    ( # L
        # ==
        #  =
        #  =
        ( # ROTATION_0
            (1, 1),
            (0, 1),
            (0, 1),
        ),
        #   =
        # ===
        ( # ROTATION_90
            (0, 0, 1),
            (1, 1, 1),
        ),
        # =
        # =
        # ==
        ( # ROTATION_180
            (1, 0),
            (1, 0),
            (1, 1),
        ),
        # ===
        # =
        ( # ROTATION_270
            (1, 1, 1),
            (1, 0, 0),
        ),
    ),
    ( # I
        # =
        # =
        # =
        # =
        ( # ROTATION_0
            [1],
            [1],
            [1],
            [1],
        ),
        # ====
        ( # ROTATION_90
            (1, 1, 1, 1),
        ),
        # =
        # =
        # =
        # =
        ( # ROTATION_180
            [1],
            [1],
            [1],
            [1],
        ),
        # ====
        ( # ROTATION_270
            (1, 1, 1, 1),
        ),
    ),
]

def get_tetrimino_minmax(typ, rotation):
    if typ == TETRIMINO_SQUARE:
        return {'x': 2, 'y': 2}
    elif typ in [TETRIMINO_T, TETRIMINO_S, TETRIMINO_Z]:
        if rotation in [ROTATION_0, ROTATION_180]:
            return {'x': 3, 'y': 2}
        else:
            return {'x': 2, 'y': 3}
    elif typ in [TETRIMINO_L, TETRIMINO_J]:
        if rotation in [ROTATION_0, ROTATION_180]:
            return {'x': 2, 'y': 3}
        else:
            return {'x': 3, 'y': 2}
    elif typ == TETRIMINO_I:
        if rotation in [ROTATION_0, ROTATION_180]:
            return {'x': 1, 'y': 4}
        else:
            return {'x': 4, 'y': 1}
    return {'x': 0, 'y': 0}

def get_tetrimino_default_rotation(typ):
    if typ in [TETRIMINO_SQUARE, TETRIMINO_S, TETRIMINO_Z]:
        return ROTATION_0
    elif typ in [TETRIMINO_L, TETRIMINO_I]:
        return ROTATION_90
    elif typ == TETRIMINO_T:
        return ROTATION_180
    elif typ == TETRIMINO_J:
        return ROTATION_270

# def randombyte(min, max):
#     rand = os.urandom(2)
#     r = rand[0] | (rand[1] << 8) # | (rand[2] << 16) | (rand[3] << 24)
#     return math.ceil(float(r) / (float(32767) / float(max - min + 1))) + min - 1

class GameField:
    def __init__(self):
        self.gamefield = [[TETRIMINO_NONE] * FIELD_WIDTH for _ in range(FIELD_HEIGHT)]

    def field(self, x, y):
        return self.gamefield[y][x]

    def reset(self):
        for y in range(FIELD_HEIGHT):
            for x in range(FIELD_WIDTH):
                self.gamefield[y][x] = TETRIMINO_NONE

    def put_tetrimino(self, typ, rotation, position):
        shape = TETRIMINO_GEOMETRY[typ][rotation]
        for y in range(len(shape)):
            for x in range(len(shape[y])):
                # print(x, y, shape[y][x], y + position['y'], x + position['x'])
                if shape[y][x] == 1:
                    # Draw it like it's shown in the defining code above. Not upside down.
                    self.gamefield[((len(shape)-1) - y) + position['y']][x + position['x']] = typ

    def collides_with(self, other):
        for y in range(FIELD_HEIGHT):
            for x in range(FIELD_WIDTH):
                if self.gamefield[y][x] != TETRIMINO_NONE and other.gamefield[y][x] != TETRIMINO_NONE:
                    return True
        return False
    
    def remove_row(self, row):
        self.gamefield = self.gamefield[:row] + self.gamefield[row+1:]
        self.gamefield.append([TETRIMINO_NONE] * FIELD_WIDTH)

class Tetris:
    def __init__(self):
        self.new_game()

    def new_game(self):
        self.tick = 0
        self.gameover = False
        self.down_interval = TETRIMINO_MOVE_DOWN_INTERVAL
        # Do we need to redraw the gamefield?
        self.dirty = True
        # gamefield[y][x]
        self.gamefield = GameField()
        self.temp_gamefield = GameField()
        self.current_tetrimino = 0
        self.select_random_tetrimino()
        self.desired_direction = 0 # left = -1, none = 0, right = 1
        self.desire_rotation = False
        self.rotation_button_pressed = False

        # Scoring
        self.lines_cleared = 0
        self.score = 0
        self.level = 1
        self.combo = 0

        # Reset the leds.
        self.update_leds()

    def loop(self):
        with display.open() as disp:
            while True:
                # main loop
                while not self.gameover:
                    self.think(disp)
                    utime.sleep_ms(10)

                # Start a new game on button press.
                b = 0
                while b == 0:
                    b = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT)
                    utime.sleep_ms(50)

                self.new_game()

    def select_random_tetrimino(self):
        # randint doesn't seem to obey the range all the time.
        self.current_tetrimino = 0
        while self.current_tetrimino <= 0 or self.current_tetrimino > NUMBER_OF_TERIMINOS:
            self.current_tetrimino = urandom.randint(1, NUMBER_OF_TERIMINOS)

        self.current_rotation = get_tetrimino_default_rotation(self.current_tetrimino)

        self.cursor = {}
        minmax = get_tetrimino_minmax(self.current_tetrimino, self.current_rotation)
        self.cursor['x'] = (FIELD_WIDTH // 2) - (minmax['x'] - 1) // 2
        self.cursor['y'] = FIELD_HEIGHT - minmax['y']

    def move_current_tetrimino_leftright(self):
        temp_cursor = {}
        temp_cursor['x'] = self.cursor['x']
        temp_cursor['y'] = self.cursor['y']

        # Player wants to move left.
        if self.desired_direction == -1:
            temp_cursor['x'] -= 1
        else:
            temp_cursor['x'] += 1
        
        # Player has to press the button again in the next time window.
        self.desired_direction = 0
        
        # Still inside the gamefield?
        if temp_cursor['x'] < 0:
            return

        minmax = get_tetrimino_minmax(self.current_tetrimino, self.current_rotation)
        if temp_cursor['x'] + minmax['x'] - 1 >= FIELD_WIDTH:
            return

        # Something in the way?
        self.temp_gamefield.reset()
        self.temp_gamefield.put_tetrimino(self.current_tetrimino, self.current_rotation, temp_cursor)
        if self.temp_gamefield.collides_with(self.gamefield):
            return
        
        self.cursor['x'] = temp_cursor['x']
        self.dirty = True

    def rotate_tetrimino(self):
        temp_rotation = (self.current_rotation + 1) % 4

        temp_cursor = {}
        temp_cursor['x'] = self.cursor['x']
        temp_cursor['y'] = self.cursor['y']

        # Keep the cursor centered below the tetrimino.
        # based on the initial cursor position using minmax of tetrimino: 
        # 
        # Keep the center the same visually.
        # The tetrimino shape is always drawn in it's full 4x4 box,
        # so move the cursor in such a way, that the center
        # appears in the same square.
        # Like 
        # =x=
        # c=o
        #
        # Becomes after rotation:
        # o=o
        # =xo
        # c=o
        # So move the cursor one up.
        if self.current_tetrimino == TETRIMINO_T:
            # Center:
            # =c=
            #  =
            if temp_rotation == ROTATION_0:
                temp_cursor['x'] -= 1
            elif temp_rotation == ROTATION_180:
                temp_cursor['y'] += 1
            elif temp_rotation == ROTATION_270:
                temp_cursor['x'] += 1
                temp_cursor['y'] -= 1
        # ==
        #  c=
        elif self.current_tetrimino in [TETRIMINO_S, TETRIMINO_Z]:
            if temp_rotation in [ROTATION_0, ROTATION_180]:
                temp_cursor['x'] -= 1
                temp_cursor['y'] += 1
            elif temp_rotation in [ROTATION_90, ROTATION_270]:
                temp_cursor['x'] += 1
                temp_cursor['y'] -= 1
        # ==
        # c
        # = 
        if self.current_tetrimino == TETRIMINO_J:
            if temp_rotation == ROTATION_0:
                temp_cursor['x'] += 1
                temp_cursor['y'] -= 1
            elif temp_rotation == ROTATION_90:
                temp_cursor['x'] -= 1
            elif temp_rotation == ROTATION_270:
                temp_cursor['y'] += 1
        # ==
        #  c
        #  =
        if self.current_tetrimino == TETRIMINO_L:
            if temp_rotation == ROTATION_90:
                temp_cursor['y'] += 1
            elif temp_rotation == ROTATION_180:
                temp_cursor['x'] += 1
                temp_cursor['y'] -= 1
            elif temp_rotation == ROTATION_270:
                temp_cursor['x'] -= 1
        # = = = =
        #    c
        elif self.current_tetrimino == TETRIMINO_I:
            # o=oo
            # o=oo
            # o=oo
            # o=oo
            if temp_rotation == ROTATION_0:
                temp_cursor['x'] += 1
                temp_cursor['y'] -= 1
            # oooo
            # ====
            # oooo
            # oooo
            elif temp_rotation == ROTATION_90:
                temp_cursor['x'] -= 1
                temp_cursor['y'] += 2
            # oo=o
            # oo=o
            # oo=o
            # oo=o
            elif temp_rotation == ROTATION_180:
                temp_cursor['x'] += 2
                temp_cursor['y'] -= 2
            # oooo
            # oooo
            # ====
            # oooo
            elif temp_rotation == ROTATION_270:
                temp_cursor['x'] -= 2
                temp_cursor['y'] += 1

        # Out of bounds on the left.
        if temp_cursor['x'] < 0:
            return False
        
        # Out of bounds on the bottom.
        if temp_cursor['y'] < 0:
            return False
        
        # Check if there is enough space to rotate here.
        minmax = get_tetrimino_minmax(self.current_tetrimino, temp_rotation)

        # Out of bounds on the right.
        if temp_cursor['x'] + minmax['x'] - 1 >= FIELD_WIDTH:
            return False

        # Out of bounds on the top.
        if temp_cursor['y'] + minmax['y'] - 1 >= FIELD_HEIGHT:
            return False
        
        self.temp_gamefield.reset()
        self.temp_gamefield.put_tetrimino(self.current_tetrimino, temp_rotation, temp_cursor)
        # There's something in the way.
        if self.temp_gamefield.collides_with(self.gamefield):
            return False
        
        # Rotation successful.
        self.current_rotation = temp_rotation
        self.cursor['x'] = temp_cursor['x']
        self.cursor['y'] = temp_cursor['y']
        self.dirty = True
        return True

    def move_current_tetrimino_down(self):
        temp_cursor = {}
        temp_cursor['x'] = self.cursor['x']
        temp_cursor['y'] = self.cursor['y'] - 1
        self.temp_gamefield.reset()
        # We're on the ground already. Stay here now.
        if (temp_cursor['y'] < 0):
            self.save_current_tetrimino()

            # Is there space for the new terimino?
            self.temp_gamefield.reset()
            self.temp_gamefield.put_tetrimino(self.current_tetrimino, self.current_rotation, self.cursor)
            if self.temp_gamefield.collides_with(self.gamefield):
                self.gameover = True # Nope.
                self.dirty = True
            return False
        else:
            # Is there enough space below?
            self.temp_gamefield.put_tetrimino(self.current_tetrimino, self.current_rotation, temp_cursor)
            if self.temp_gamefield.collides_with(self.gamefield):
                # No. Save it here.
                self.save_current_tetrimino()

                # Is there space for the new terimino?
                self.temp_gamefield.reset()
                self.temp_gamefield.put_tetrimino(self.current_tetrimino, self.current_rotation, self.cursor)
                if self.temp_gamefield.collides_with(self.gamefield):
                    self.gameover = True # Nope.
                    self.dirty = True
                return False
            else:
                # Yes, there is. Move the object down for real.
                self.cursor['y'] -= 1
                self.dirty = True
                return True

    
    def save_current_tetrimino(self):
        # Just assume there is space.
        self.gamefield.put_tetrimino(self.current_tetrimino, self.current_rotation, self.cursor)
        self.dirty = True

        lines_cleared = self.remove_full_lines()

        # TODO: Display score
        # Handle combos
        if self.combo > 0 and lines_cleared == 0:
            self.score += self.combo * 50 * self.level
            self.combo = 0
        elif lines_cleared > 0:
            self.combo += 1
        
        # Handle scoring
        if lines_cleared == 1:
            self.score += 100 * self.level
            self.lines_cleared += 1
        elif lines_cleared == 2:
            self.score += 300 * self.level
            self.lines_cleared += 3
        elif lines_cleared == 3:
            self.score += 500 * self.level
            self.lines_cleared += 5
        elif lines_cleared == 4:
            self.score += 800 * self.level
            self.lines_cleared += 8
        
        # Speed up the game each level.
        while (self.level * 5) <= self.lines_cleared:
            self.level += 1
            if self.level < MAX_LEVEL:
                self.down_interval -= LEVEL_SPEEDUP_STEP
        
        # Show the current level on the leds.
        self.update_leds()

        # Give some feedback, that tetrimino is down.
        vibra.vibrate(lines_cleared * 15 + 10)

        self.select_random_tetrimino()
    
    def update_leds(self):
        led_colors = [color.CHAOSBLUE] * (MAX_LEVEL - self.level) + [color.RED] * self.level
        leds.set_all(led_colors)

    def remove_full_lines(self):
        removed_lines = 0
        # Check every row.
        y = 0
        while y < FIELD_HEIGHT:
            for x in range(FIELD_WIDTH):
                # Skip the line if there is a blank square in it.
                if self.gamefield.field(x, y) == TETRIMINO_NONE:
                    break
                
                # We're not at the end of line yet.
                if x != (FIELD_WIDTH - 1):
                    continue
                
                removed_lines += 1
                self.gamefield.remove_row(y)
                # Check this row again, since we moved it all one down.
                y -= 1
            y += 1
        return removed_lines

    def think(self, disp):
        self.tick += 1

        # Any controls pressed?
        current_presses = buttons.read(buttons.TOP_RIGHT | buttons.BOTTOM_RIGHT | buttons.BOTTOM_LEFT)
        if current_presses > 0:
            # Ignore if both buttons were pressed
            if (current_presses & (buttons.TOP_RIGHT | buttons.BOTTOM_RIGHT)) != (buttons.TOP_RIGHT | buttons.BOTTOM_RIGHT):
                if (current_presses & buttons.BOTTOM_RIGHT) == buttons.BOTTOM_RIGHT:
                    # Move right
                    self.desired_direction = 1
                elif (current_presses & buttons.TOP_RIGHT) == buttons.TOP_RIGHT:
                    # Move left
                    self.desired_direction = -1
            # Player wants to rotate?
            if (current_presses & buttons.BOTTOM_LEFT) == buttons.BOTTOM_LEFT:
                # Require them to let go of the button again before rotating again.
                if not self.rotation_button_pressed:
                    self.desire_rotation = True
                    self.rotation_button_pressed = True

        # Player let go of the rotate button.
        if (current_presses & buttons.BOTTOM_LEFT) == 0:
            self.rotation_button_pressed = False
        
        # Actuate the controls every x ticks.
        if (self.tick % TETRIMINO_MOVE_HORIZONTAL_INTERVAL) == 0:
            if self.desired_direction != 0:
                self.move_current_tetrimino_leftright()
            if self.desire_rotation:
                self.rotate_tetrimino()
                self.desire_rotation = False

        # The tetrimino falls slower than it can move sideways or rotate.
        if (self.tick % self.down_interval) == 0:
            self.move_current_tetrimino_down()
        
        # Check if we even have to redraw the gamefield.
        if not self.dirty:
           return
        
        # Clear the screen first if something changed.
        disp.clear()
    
        # Render all the changes.
        self.draw_gamefield(disp)

        if self.gameover:
            disp.print('Game Over', fg=(255, 255, 255), bg=(255, 0, 0), posx=14, posy=40)
            vibra.vibrate(500)

        # print('cursor: {} {}'.format(self.cursor['y'], self.cursor['x']))
        # disp.circ(self.cursor['y']*TETRIMINO_SIZE, self.cursor['x']*TETRIMINO_SIZE, 4, col=(255,255,255), filled=True)
        disp.update()
        self.dirty = False

    def draw_gamefield(self, disp):
        # print('Drawing game field')
        self.temp_gamefield.reset()
        self.temp_gamefield.put_tetrimino(self.current_tetrimino, self.current_rotation, self.cursor)

        for y in range(FIELD_HEIGHT):
            draw_y = y * TETRIMINO_SIZE
            for x in range(FIELD_WIDTH):
                draw_x = x * TETRIMINO_SIZE
                tetrimino_type = self.gamefield.field(x, y)
                if tetrimino_type != TETRIMINO_NONE:
                    # print('Drawing ({} {}) to ({} {})'.format(draw_y, draw_x, draw_y + TETRIMINO_SIZE, draw_x + TETRIMINO_SIZE))
                    disp.rect(draw_y, draw_x, draw_y + TETRIMINO_SIZE, draw_x + TETRIMINO_SIZE, col=TETRIMINO_COLORS[tetrimino_type], filled=True)
                else:
                    tetrimino_type = self.temp_gamefield.field(x, y)
                    if tetrimino_type != TETRIMINO_NONE:
                        # print('Drawing current ({} {}) to ({} {})'.format(draw_y, draw_x, draw_y + TETRIMINO_SIZE, draw_x + TETRIMINO_SIZE))
                        disp.rect(draw_y, draw_x, draw_y + TETRIMINO_SIZE, draw_x + TETRIMINO_SIZE, col=TETRIMINO_COLORS[tetrimino_type], filled=True)


tetris = Tetris()
tetris.loop()