# Import libraries for random piece selection, terminal graphics, and timing
import random
import curses
import time

# Board dimensions: 10 columns wide, 20 rows tall (standard Tetris size)
W, H = 10, 20

# Terminal layout: Scale up cells for bigger blocks
# CELL_W: number of characters wide per block (4 = wider blocks)
# CELL_H: number of rows tall per block (2 = taller blocks)
CELL_W = 4
CELL_H = 2

# Function to calculate centered positioning in the terminal
# This reads the terminal size and returns padding values to center the game board
def get_centered_offsets(stdscr):
    """Calculate PAD_X and PAD_Y to center the game board in the terminal."""
    # Get terminal dimensions (max_y = height, max_x = width)
    max_y, max_x = stdscr.getmaxyx()
    # Calculate board dimensions in characters: width and height with borders
    board_width = W * CELL_W + 2
    board_height = H * CELL_H + 2
    # Total width includes board + side panel (score, next piece preview)
    total_width = board_width + 6 + 20
    # Center horizontally and vertically: divide remaining space by 2
    pad_x = max(1, (max_x - total_width) // 2)
    pad_y = max(1, (max_y - board_height) // 2)
    return pad_x, pad_y

# Default padding values (will be overridden by get_centered_offsets in main_curses)
# PAD_X: left padding (columns from terminal edge to board edge)
# PAD_Y: top padding (rows from terminal top to board edge)
# SIDE_X: column position of the side panel (score, next block)
PAD_X = 2
PAD_Y = 1
SIDE_X = PAD_X + W * CELL_W + 6
 
# 2D game board: 20 rows (height) × 10 columns (width)
# Each cell is either None (empty) or a color string (filled with a block)
board = [[None] * W for _ in range(H)]

# Function to draw the game board on screen
def draw_board_curses(win):
    # Draw the left and right borders (| characters)
    for y in range(H + 2):
        win.addstr(PAD_Y + y, PAD_X - 1, '|')
        win.addstr(PAD_Y + y, PAD_X + W * 2, '|')
    # Draw border that spans exactly from the top corner to the bottom corner
    top_row = PAD_Y - 1
    bottom_row = PAD_Y + H * CELL_H
    left_col = PAD_X - 1
    right_col = PAD_X + W * CELL_W
    # Draw vertical borders from top_row to bottom_row (inclusive)
    for row in range(top_row, bottom_row + 1):
        win.addstr(row, left_col, '|')
        win.addstr(row, right_col, '|')
    # Draw top and bottom border lines
    top = '+' + '-' * (W * CELL_W) + '+'
    win.addstr(top_row, left_col, top)
    win.addstr(bottom_row, left_col, top)
    # Draw each cell on the board (filled cells are shown as ██, empty as spaces)
    for i, row in enumerate(board[::-1]):  # board[::-1] reverses rows so row 0 (bottom) is at bottom
        for h in range(CELL_H):  # CELL_H is the height in rows each block takes
            screen_row = PAD_Y + 1 + i * CELL_H + h
            for j, color in enumerate(row):
                # Empty cell: spaces; filled cell: solid blocks (██)
                ch = ' ' * CELL_W
                attr = curses.color_pair(0)
                if color:
                    ch = '█' * CELL_W
                    attr = curses.color_pair(2)
                win.addstr(screen_row, PAD_X + j * CELL_W, ch, attr)

 
# Function to find rows that are completely filled (ready to be cleared)
# Yields (returns) each full row one at a time
def find_completed_rows():
    for row in board:
        # Check if all cells in the row have a color (no None values)
        if all(row):
            yield row
 
# Define all 7 Tetris piece shapes (I, O, T, S, Z, J, L)
# Each shape is stored as a list of (x, y) offsets from the piece's center
BLOCKS = [[] for _ in range(7)]
# Parse ASCII template: each 'o' represents a block, positions calculated from the string
for i, line in enumerate('''
     oo   o      o   oo  oo    o
oooo oo   ooo  ooo  oo    oo  ooo'''.split('\n')):
    for j, char in enumerate(line):
        if char == 'o':
            # Store block position relative to piece center
            BLOCKS[j // 5].append((j%5 - 1, -i + 2))
 
# Function to draw a tetromino (falling block) on screen
# Can draw on the main board or in the side panel (preview area)
def draw_block_curses(win, block, x0, y0, use_board=True, draw_x=None, draw_y=None):
    """Draw a block either on the main board (use_board=True) or in a side area.

    For board drawing, x0,y0 are board coordinates. For side drawing, set
    use_board=False and provide draw_x, draw_y as screen coordinates (columns, rows).
    """
    # Iterate through each block cell in the tetromino
    for dx, dy in block:
        # Calculate actual position by adding offsets to base position
        x, y = x0 + dx, y0 + dy
        if use_board:
            # Draw on main game board
            if 0 <= x < W and 0 <= y < H:
                # Convert board coordinates to screen coordinates
                # (accounting for CELL_W and CELL_H scaling)
                screen_row_base = PAD_Y + 1 + (H - 1 - y) * CELL_H
                screen_col = PAD_X + x * CELL_W
                # Draw the block with height CELL_H (fill multiple rows)
                for hh in range(CELL_H):
                    win.addstr(screen_row_base + hh, screen_col, '█' * CELL_W, curses.color_pair(2))
        else:
            # Draw in side panel (for next-piece preview)
            if draw_x is None or draw_y is None:
                continue
            # Calculate side panel position using scaled cell dimensions
            sx = draw_x + x * CELL_W
            sy = draw_y + (y * CELL_H)
            try:
                # Draw block with CELL_H rows
                for hh in range(CELL_H):
                    win.addstr(sy + hh, sx, '█' * CELL_W, curses.color_pair(2))
            except Exception:
                pass
 
# Function to check if a piece can be placed at a given position
# Returns True if the position is valid (no collisions), False otherwise
def can_place_block_clipped(block, x0, y0):
    # Check each cell of the piece
    for dx, dy in block:
        # Calculate actual position
        x, y = x0 + dx, y0 + dy
        # Check if position is out of bounds or collides with existing blocks
        if not (0 <= x < W and 0 <= y) or (y < H and board[y][x]):
            return False
    return True

 
# Function to lock a piece permanently onto the board
# Marks each cell of the piece as filled
def place_block(block, x0, y0):
    # Assume placement is complete initially
    complete = True
    # Place each block cell on the board
    for dx, dy in block:
        x, y = x0 + dx, y0 + dy
        # Only place if within board bounds (x and y must be valid)
        if 0 <= x < W and 0 <= y < H:
            board[y][x] = 'blue'  # Mark cell as filled with color 'blue'
        else:
            # If any part extends outside, placement is incomplete (game over)
            complete = False
    return complete

 
 
# Game state variables
# score: player's current score
score = 0
# t: game tick counter (increments each game loop iteration)
t = 0
# falling_period: how many ticks before a piece falls one row (decreases with level)
falling_period = 0
# state: current game state ('normal' = playing, 'game_over' = lost)
state = 'normal'
# block: currently falling piece (list of offsets), next_block: preview of next piece
block = next_block = None
# x, y: current position of the falling piece (x=column, y=row)
x, y = 0, 0
# falling_generator: iterator that controls piece falling animation
falling_generator = None
# deleting_rows_generator: iterator that controls line-clear animation
deleting_rows_generator = None

 
# Function to move the current falling piece left, right, or down
# dx: horizontal direction (-1=left, +1=right), dy: vertical direction (-1=down)
def move_block(dx, dy):
    global x, y
    # Calculate new position
    new_x, new_y = x + dx, y + dy
    # Check if new position is valid (no collisions)
    possible = can_place_block_clipped(block, new_x, new_y)
    # If valid, update piece position
    if possible:
        x, y = new_x, new_y
    return possible

 
# Function to rotate the current falling piece 90 degrees clockwise
# Rotation formula: (x, y) becomes (-y, x)
def rotate_block():
    global block
    # Calculate rotated positions
    new_block = [(-y, x) for x, y in block]
    # Check if rotated position is valid (no collisions)
    possible = can_place_block_clipped(new_block, x, y)
    # If valid, update piece with rotated version
    if possible:
        block = new_block
    return possible

 
# Function to spawn the next piece and start it falling
# Also calculates falling speed based on level (score)
def reset_block():
    global falling_period, block, next_block, x, y, state
    # Calculate falling period (how many ticks before piece falls): decreases with level
    # Higher t (more time) = higher level = faster falling
    falling_period = [10, 9, 8, 7, 6, 5, 4][min(t // 100, 6)]
    # Swap pieces: current next_block becomes new block, pick random next
    new_block = None
    while not new_block:
        new_block, next_block = next_block, BLOCKS[random.randrange(7)]
    # Spawn at center-top of board
    x, y = W // 2, H - 1
    # Check for game over: if new piece collides immediately, game is over
    if not can_place_block_clipped(new_block, x, y):
        state = 'game_over'
        return
    # Set active piece and start falling animation
    block = new_block
    begin_falling(falling_period)

 
# Function to create a falling animation generator for a piece
# The piece falls one row every 'period' ticks until it hits bottom
def begin_falling(period):
    global falling_generator
    def fall():
        global falling_generator
        # Main falling loop
        while True:
            # Wait 'period' ticks before falling next row
            for t in range(period):
                yield  # Pause here and resume on next tick
            # Try to move piece down one row
            if not move_block(0, -1):
                # Piece can't move down, so it has landed
                break
        # When piece lands, clear the falling generator and lock piece
        falling_generator = None
        place_block_and_begin_deleting()
        yield
    # Create and start the generator
    falling_generator = fall()

 
# Function to lock the current falling piece onto the board permanently
# Then start checking for completed rows to clear
def place_block_and_begin_deleting():
    global block, state
    # Save the current piece before clearing it
    old_block = block
    block = None  # Clear active piece
    # Place the piece on the board
    if not place_block(old_block, x, y):
        # If placement failed (piece extended above board), game over
        state = 'game_over'
        return
    # Start animation for clearing completed rows
    begin_deleting_rows()

 
# Function to animate and process line clears
# Completed rows flash and then disappear, with remaining blocks dropping down
def begin_deleting_rows():
    global deleting_rows_generator
    # Check if any rows are completed
    if any(find_completed_rows()):
        # Animation duration based on falling period (faster at higher levels)
        duration = falling_period
        def delete():
            global deleting_rows_generator, score
            # Animate the row clearing (flash between red and blue)
            for t in range(duration):
                if t in (0, duration // 2):
                    # Change color of completed rows (red or blue)
                    color = ('red', 'blue')[t // (duration//2)]
                    for row in find_completed_rows():
                        row[:] = [color] * W
                yield  # Pause to show animation
            # After animation, remove completed rows
            incomplete_rows = [row for row in board if not all(row)]
            n_deleted = H - len(incomplete_rows)
            # Shift remaining rows down and fill with empty rows
            board[:] = incomplete_rows + [[None] * W for _ in range(n_deleted)]
            # Award points: (number of rows cleared)^2 × 100
            score += (n_deleted**2) * 100
            # Spawn next piece
            reset_block()
            deleting_rows_generator = None
            yield
        # Create and start the generator
        deleting_rows_generator = delete()
    else:
        # No completed rows, just spawn next piece immediately
        reset_block()

 
# Function called once per game tick (100ms)
# Advances animations (falling piece, line clears) and increments the time counter
def update_stage():
    global t
    # Advance the falling animation generator (piece drops)
    if block and falling_generator:
        next(falling_generator)
    # Advance the line-clear animation generator
    if deleting_rows_generator:
        next(deleting_rows_generator)
    # Increment tick counter (used for level calculation)
    t += 1

 
# Function to render (draw) the current game state on the terminal screen
def render_stage_curses(win):
    # Clear the screen
    win.erase()
    # Draw the main game board with borders and filled cells
    draw_board_curses(win)
    # Display score in the side panel
    win.addstr(PAD_Y, SIDE_X, f'SCORE: {score}')
    # Display "NEXT:" label
    win.addstr(PAD_Y + 2, SIDE_X, 'NEXT:')
    # Draw the next piece preview in the side panel
    if next_block:
        # Normalize block coordinates so preview fits in a compact box
        minx = min(dx for dx, dy in next_block)
        miny = min(dy for dx, dy in next_block)
        # Position preview below the NEXT label
        preview_x = SIDE_X
        preview_y = PAD_Y + 6
        draw_block_curses(win, next_block, -minx, -miny, use_board=False, draw_x=preview_x, draw_y=preview_y)
    # Draw the currently falling piece on the main board
    if block:
        draw_block_curses(win, block, x, y)
    # Display GAME OVER message if the game has ended
    if state == 'game_over':
        win.addstr(PAD_Y + H//2, PAD_X + W - 4, 'GAME OVER', curses.color_pair(1))
    # Update the screen display
    win.refresh()

 
# Input handler: move piece left
# Called when 'a' or left arrow is pressed
def on_key_left():
    if block:
        move_block(-1, 0)

# Input handler: move piece right
# Called when 'd' or right arrow is pressed
def on_key_right():
    if block:
        move_block(1, 0)

# Input handler: soft drop (or lock piece if already at bottom)
# Called when 's' or down arrow is pressed
def on_key_down():
    if block:
        if not move_block(0, -1):
            place_block_and_begin_deleting()

# Input handler: hard drop (instant fall to bottom)
# Called when spacebar is pressed
def on_key_space():
    if block:
        begin_falling(period=0)  # period=0 means fall instantly

# Input handler: rotate piece 90 degrees
# Called when 'w' or up arrow is pressed
def on_key_up():
    if block:
        rotate_block()

 
# Main game function - runs the curses terminal game loop
def main_curses(stdscr):
    # Declare globals so we can modify them in this function
    global PAD_X, PAD_Y, SIDE_X
    
    # Curses initialization
    curses.curs_set(0)  # Hide the cursor
    stdscr.nodelay(True)  # Non-blocking input (don't wait for keypresses)
    stdscr.keypad(True)  # Enable special keys (arrow keys, etc.)
    curses.start_color()  # Enable color support
    curses.use_default_colors()  # Use terminal's default colors
    # Define color pairs: (ID, foreground color, background color)
    curses.init_pair(1, curses.COLOR_RED, -1)  # Red for GAME OVER text
    curses.init_pair(2, curses.COLOR_BLUE, -1)  # Blue for game blocks

    # Calculate centered board position based on terminal size
    PAD_X, PAD_Y = get_centered_offsets(stdscr)
    SIDE_X = PAD_X + W * CELL_W + 6

    # Initialize the game: spawn first piece
    reset_block()
    # Game tick rate: 100ms (0.1 seconds) between game updates
    tick = 0.1
    # Track time for tick timing
    last = time.time()
    
    # Main game loop
    while True:
        # Get current time
        now = time.time()
        # If enough time has passed, do a game update
        if now - last >= tick:
            # Advance game state (gravity, animations, etc.)
            update_stage()
            # Redraw the screen with updated state
            render_stage_curses(stdscr)
            # Reset timer for next tick
            last = now
        
        # Check for keyboard input (non-blocking)
        try:
            ch = stdscr.getch()
        except Exception:
            ch = -1
        
        # Process keyboard input
        if ch != -1:
            # Quit game
            if ch in (ord('q'), ord('Q')):
                break
            # Move left
            elif ch in (curses.KEY_LEFT, ord('a')):
                on_key_left()
            # Move right
            elif ch in (curses.KEY_RIGHT, ord('d')):
                on_key_right()
            # Soft drop
            elif ch in (curses.KEY_DOWN, ord('s')):
                on_key_down()
            # Rotate
            elif ch in (curses.KEY_UP, ord('w')):
                on_key_up()
            # Hard drop
            elif ch == ord(' '):
                on_key_space()
        
        # Small sleep to avoid consuming 100% CPU
        time.sleep(0.001)

# Entry point: run the game using curses wrapper (handles cleanup automatically)
if __name__ == '__main__':