import numpy as np
from typing import Tuple, List
import math

def generate_color_map(exempt_numbers: List[int]) -> np.ndarray:
    """Generate a random color mapping, preserving exempt numbers and correctly permuting the rest."""
    color_map = np.arange(10, dtype=np.int32)
    non_exempt = [i for i in range(10) if i not in exempt_numbers]
    
    if len(non_exempt) > 1:  # Only permute if there are at least 2 non-exempt numbers
        permutation = np.random.permutation(len(non_exempt))
        permuted_non_exempt = [non_exempt[i] for i in permutation]
        
        for original, permuted in zip(non_exempt, permuted_non_exempt):
            color_map[original] = permuted
    
    return color_map

def create_advanced_arc_input() -> np.ndarray:
    """
    Create a pattern based on a randomly selected theme and apply color permutation.
    """
    # TODO: work on getting this for varying shapes
    rows = 30
    cols = 30
    max_colors = 10
    
    rows = min(max(1, rows), 30)
    cols = min(max(1, cols), 30)
    grid = np.zeros((rows, cols), dtype=int)
    
    # Comprehensive list of all active pattern generation functions
    themes = [
        # Tessellating patterns
        create_repeating_tiles,
        create_rotating_tiles,
        create_mirrored_tiles,
        create_scaled_tiles,
        create_overlapping_tiles,
        
        # Agent-based and cluster patterns
        create_cellular_automaton,
        create_flocking_simulation,
        create_diffusion_limited_aggregation,
        create_voronoi_diagram,
        
        # Other patterns
        create_fireworks,
        create_school_of_fish,
        create_dna_helix,
        create_rain,
        create_butterfly,
        
        # Original patterns
        create_maze,
        create_spiral,
        create_concentric_circles,
        create_geometric_tessellation,
        create_mandala,
        create_wave_interference,
        create_tree_branches,
        create_falling_leaves,
        create_cityscape,
    ]
    
    # Select a random theme and print its number and name
    selected_theme = np.random.choice(themes)
    theme_index = themes.index(selected_theme)
    
    grid = selected_theme(grid, max_colors)
    
    # Generate color map, exempting 0 and 5
    color_map = generate_color_map([0, 5])
    
    # Apply color permutation
    permuted_grid = color_map[grid]
    
    return permuted_grid, {}

def create_maze(grid: np.ndarray, max_colors: int) -> np.ndarray:
    def carve_passage(x: int, y: int, color: int):
        directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
        np.random.shuffle(directions)
        
        for dx, dy in directions:
            nx, ny = x + dx*2, y + dy*2
            if 0 <= nx < grid.shape[1] and 0 <= ny < grid.shape[0] and grid[ny, nx] == 0:
                grid[y + dy, x + dx] = color
                grid[ny, nx] = color
                carve_passage(nx, ny, color)
    
    color = np.random.randint(1, max_colors)
    start_x, start_y = np.random.randint(0, grid.shape[1]), np.random.randint(0, grid.shape[0])
    carve_passage(start_x, start_y, color)
    return grid

def create_spiral(grid: np.ndarray, max_colors: int) -> np.ndarray:
    center = (grid.shape[0] // 2, grid.shape[1] // 2)
    max_radius = min(center[0], center[1])
    color = np.random.randint(1, max_colors)
    
    for r in range(max_radius):
        theta = np.linspace(0, 8*np.pi, 100*r)
        x = (r * theta * np.cos(theta) / (2*np.pi) + center[1]).astype(int)
        y = (r * theta * np.sin(theta) / (2*np.pi) + center[0]).astype(int)
        
        mask = (x >= 0) & (x < grid.shape[1]) & (y >= 0) & (y < grid.shape[0])
        grid[y[mask], x[mask]] = color
    
    return grid

def create_concentric_circles(grid: np.ndarray, max_colors: int) -> np.ndarray:
    center = (grid.shape[0] // 2, grid.shape[1] // 2)
    max_radius = min(center[0], center[1])
    
    for r in range(1, max_radius, 2):
        color = np.random.randint(1, max_colors)
        y, x = np.ogrid[-center[0]:grid.shape[0]-center[0], -center[1]:grid.shape[1]-center[1]]
        mask = (x*x + y*y <= r*r) & (x*x + y*y > (r-1)*(r-1))
        grid[mask] = color
    
    return grid

def create_mandala(grid: np.ndarray, max_colors: int) -> np.ndarray:
    center = (grid.shape[0] // 2, grid.shape[1] // 2)
    max_radius = min(center[0], center[1])
    
    for r in range(1, max_radius):
        color = np.random.randint(1, max_colors)
        num_points = np.random.randint(6, 13)
        theta = np.linspace(0, 2*np.pi, num_points, endpoint=False)
        x = (r * np.cos(theta) + center[1]).astype(int)
        y = (r * np.sin(theta) + center[0]).astype(int)
        
        mask = (x >= 0) & (x < grid.shape[1]) & (y >= 0) & (y < grid.shape[0])
        grid[y[mask], x[mask]] = color
        
        if r % 5 == 0:
            for i in range(num_points):
                draw_line(grid, (y[i], x[i]), center, color)
    
    return grid

def create_circuit_board(grid: np.ndarray, max_colors: int) -> np.ndarray:
    def draw_component(x: int, y: int, size: int, color: int):
        shapes = ['square', 'circle', 'cross']
        shape = np.random.choice(shapes)
        
        if shape == 'square':
            grid[y:y+size, x:x+size] = color
        elif shape == 'circle':
            yy, xx = np.ogrid[-size//2:size//2, -size//2:size//2]
            mask = xx*xx + yy*yy <= (size//2)**2
            grid[y:y+size, x:x+size][mask] = color
        else:  # cross
            mid = size // 2
            grid[y:y+size, x+mid-1:x+mid+1] = color
            grid[y+mid-1:y+mid+1, x:x+size] = color
    
    num_components = np.random.randint(5, 15)
    for _ in range(num_components):
        x = np.random.randint(0, grid.shape[1])
        y = np.random.randint(0, grid.shape[0])
        size = np.random.randint(3, 7)
        color = np.random.randint(1, max_colors)
        draw_component(x, y, size, color)
        
        # Draw connections
        for _ in range(2):
            end_x = np.random.randint(0, grid.shape[1])
            end_y = np.random.randint(0, grid.shape[0])
            draw_line(grid, (y, x), (end_y, end_x), color)
    
    return grid

def create_wave_interference(grid: np.ndarray, max_colors: int) -> np.ndarray:
    y, x = np.ogrid[:grid.shape[0], :grid.shape[1]]
    num_waves = np.random.randint(2, 5)
    
    for _ in range(num_waves):
        center = (np.random.randint(0, grid.shape[0]), np.random.randint(0, grid.shape[1]))
        frequency = np.random.uniform(0.1, 0.5)
        amplitude = np.random.uniform(0.5, 2.0)
        wave = amplitude * np.sin(frequency * ((x - center[1])**2 + (y - center[0])**2)**0.5)
        grid += (wave * (max_colors - 1)).astype(int)
    
    grid = np.clip(grid, 0, max_colors - 1)
    return grid

def create_tree_branches(grid: np.ndarray, max_colors: int) -> np.ndarray:
    def draw_branch(x: int, y: int, angle: float, length: int, color: int):
        if length < 2:
            return
        
        end_x = int(x + length * np.cos(angle))
        end_y = int(y + length * np.sin(angle))
        draw_line(grid, (y, x), (end_y, end_x), color)
        
        new_length = int(length * 0.7)
        draw_branch(end_x, end_y, angle + np.pi/4, new_length, color)
        draw_branch(end_x, end_y, angle - np.pi/4, new_length, color)
    
    color = np.random.randint(1, max_colors)
    start_x = grid.shape[1] // 2
    start_y = grid.shape[0] - 1
    draw_branch(start_x, start_y, -np.pi/2, min(grid.shape) // 2, color)
    
    return grid

       

def create_geometric_tessellation(grid: np.ndarray, max_colors: int) -> np.ndarray:
    def draw_triangle(x: int, y: int, size: int, color: int):
        for i in range(size):
            for j in range(i+1):
                if 0 <= y+i < grid.shape[0] and 0 <= x+j < grid.shape[1]:
                    grid[y+i, x+j] = color
    
    triangle_size = max(1, min(grid.shape) // 4)
    for y in range(0, grid.shape[0], triangle_size):
        for x in range(0, grid.shape[1], triangle_size):
            color = np.random.randint(1, max_colors)
            if (x // triangle_size + y // triangle_size) % 2 == 0:
                draw_triangle(x, y, triangle_size, color)
            else:
                draw_triangle(x+triangle_size, y, -triangle_size, color)
    
    return grid

def draw_line(grid: np.ndarray, start: Tuple[int, int], end: Tuple[int, int], color: int) -> np.ndarray:
    grid = grid.copy()
    y0, x0 = start
    y1, x1 = end
    dx = abs(x1 - x0)
    dy = abs(y1 - y0)
    sx = 1 if x0 < x1 else -1
    sy = 1 if y0 < y1 else -1
    err = dx - dy

    while True:
        if 0 <= y0 < grid.shape[0] and 0 <= x0 < grid.shape[1]:
            grid[y0, x0] = color
        if x0 == x1 and y0 == y1:
            break
        e2 = 2 * err
        if e2 > -dy:
            err -= dy
            x0 += sx
        if e2 < dx:
            err += dx
            y0 += sy

    return grid

def create_falling_leaves(grid: np.ndarray, max_colors: int) -> np.ndarray:
    def draw_leaf(x: int, y: int, size: int, color: int):
        # Draw leaf shape
        for i in range(size):
            for j in range(size):
                if (i - size//2)**2 + (j - size//2)**2 <= (size//2)**2:
                    if 0 <= y+i < grid.shape[0] and 0 <= x+j < grid.shape[1]:
                        grid[y+i, x+j] = color
        # Draw stem
        stem_length = size // 2
        for i in range(stem_length):
            if 0 <= y+size+i < grid.shape[0] and 0 <= x+size//2 < grid.shape[1]:
                grid[y+size+i, x+size//2] = color

    num_leaves = np.random.randint(5, 15)
    leaf_colors = [np.random.randint(1, max_colors) for _ in range(num_leaves)]
    
    for i in range(num_leaves):
        x = np.random.randint(0, grid.shape[1])
        y = np.random.randint(0, grid.shape[0])
        size = np.random.randint(3, max(4, min(grid.shape) // 5))
        draw_leaf(x, y, size, leaf_colors[i])
        
        # Draw "falling" trail
        trail_length = np.random.randint(size, size*3)
        for j in range(1, trail_length):
            if 0 <= y-j < grid.shape[0] and 0 <= x+j < grid.shape[1]:
                grid[y-j, x+j] = leaf_colors[i]
    
    return grid

def create_cityscape(grid: np.ndarray, max_colors: int) -> np.ndarray:
    def draw_building(x: int, width: int, height: int, color: int):
        grid[grid.shape[0]-height:, x:x+width] = color
        
        # Add windows
        window_color = np.random.randint(1, max_colors)
        for floor in range(1, height-1, 2):
            for window in range(1, width-1, 2):
                if np.random.random() > 0.3:  # 70% chance of a lit window
                    grid[grid.shape[0]-floor, x+window] = window_color

    num_buildings = np.random.randint(3, max(4, grid.shape[1] // 5))
    building_positions = sorted(np.random.choice(range(grid.shape[1]), num_buildings, replace=False))
    
    for i, x in enumerate(building_positions):
        width = min(np.random.randint(2, 6), 
                    (building_positions[i+1] - x if i < len(building_positions)-1 else grid.shape[1]) - x)
        height = np.random.randint(grid.shape[0] // 3, grid.shape[0])
        color = np.random.randint(1, max_colors)
        draw_building(x, width, height, color)
    
    # Add moon or sun
    celestial_body_color = np.random.randint(1, max_colors)
    celestial_body_radius = min(grid.shape) // 10
    celestial_body_x = np.random.randint(celestial_body_radius, grid.shape[1] - celestial_body_radius)
    celestial_body_y = np.random.randint(celestial_body_radius, grid.shape[0] // 3)
    
    for i in range(-celestial_body_radius, celestial_body_radius + 1):
        for j in range(-celestial_body_radius, celestial_body_radius + 1):
            if i*i + j*j <= celestial_body_radius*celestial_body_radius:
                if 0 <= celestial_body_y+i < grid.shape[0] and 0 <= celestial_body_x+j < grid.shape[1]:
                    grid[celestial_body_y+i, celestial_body_x+j] = celestial_body_color
    
    return grid

def create_fireworks(grid: np.ndarray, max_colors: int) -> np.ndarray:
    def draw_explosion(center_x: int, center_y: int, radius: int, color: int):
        for angle in np.linspace(0, 2*np.pi, 20):
            end_x = int(center_x + radius * np.cos(angle))
            end_y = int(center_y + radius * np.sin(angle))
            rr, cc = line(center_y, center_x, end_y, end_x)
            mask = (rr >= 0) & (rr < grid.shape[0]) & (cc >= 0) & (cc < grid.shape[1])
            grid[rr[mask], cc[mask]] = color

    num_fireworks = np.random.randint(3, 8)
    for _ in range(num_fireworks):
        color = np.random.randint(1, max_colors)
        center_x = np.random.randint(0, grid.shape[1])
        center_y = np.random.randint(0, grid.shape[0] // 2)
        radius = np.random.randint(grid.shape[0] // 10, grid.shape[0] // 4)
        draw_explosion(center_x, center_y, radius, color)

        # Draw trail
        start_y = grid.shape[0] - 1
        rr, cc = line(start_y, center_x, center_y, center_x)
        mask = (rr >= 0) & (rr < grid.shape[0]) & (cc >= 0) & (cc < grid.shape[1])
        grid[rr[mask], cc[mask]] = color

    return grid

def create_school_of_fish(grid: np.ndarray, max_colors: int) -> np.ndarray:
    def draw_fish(x: int, y: int, direction: int, color: int):
        # Fish body
        grid[y, x] = color
        if 0 <= x + direction < grid.shape[1]:
            grid[y, x + direction] = color
        
        # Fish tail
        if 0 <= y - 1 < grid.shape[0] and 0 <= x - direction < grid.shape[1]:
            grid[y - 1, x - direction] = color
        if 0 <= y + 1 < grid.shape[0] and 0 <= x - direction < grid.shape[1]:
            grid[y + 1, x - direction] = color

    num_fish = np.random.randint(10, 30)
    school_center_x = np.random.randint(grid.shape[1] // 4, 3 * grid.shape[1] // 4)
    school_center_y = np.random.randint(grid.shape[0] // 4, 3 * grid.shape[0] // 4)
    school_radius = min(grid.shape) // 4

    for _ in range(num_fish):
        angle = np.random.uniform(0, 2 * np.pi)
        r = np.random.uniform(0, school_radius)
        x = int(school_center_x + r * np.cos(angle))
        y = int(school_center_y + r * np.sin(angle))
        if 0 <= x < grid.shape[1] and 0 <= y < grid.shape[0]:
            color = np.random.randint(1, max_colors)
            direction = 1 if np.random.random() > 0.5 else -1
            draw_fish(x, y, direction, color)

    return grid

def create_dna_helix(grid: np.ndarray, max_colors: int) -> np.ndarray:
    def draw_base_pair(y: int, x1: int, x2: int, color1: int, color2: int):
        if 0 <= y < grid.shape[0]:
            if 0 <= x1 < grid.shape[1]:
                grid[y, x1] = color1
            if 0 <= x2 < grid.shape[1]:
                grid[y, x2] = color2

    center_x = grid.shape[1] // 2
    amplitude = min(grid.shape[1] // 4, 10)
    color1, color2 = np.random.randint(1, max_colors, size=2)

    for y in range(grid.shape[0]):
        x1 = center_x + int(amplitude * np.sin(y / 5))
        x2 = center_x + int(amplitude * np.sin((y / 5) + np.pi))
        draw_base_pair(y, x1, x2, color1, color2)

        # Draw connecting lines
        if y % 4 == 0:
            rr, cc = line(y, x1, y, x2)
            connect_color = np.random.randint(1, max_colors)
            for r, c in zip(rr, cc):
                if 0 <= r < grid.shape[0] and 0 <= c < grid.shape[1]:
                    grid[r, c] = connect_color

    return grid

def create_rain(grid: np.ndarray, max_colors: int) -> np.ndarray:
    rain_color = np.random.randint(1, max_colors)
    num_drops = np.random.randint(grid.shape[1] // 2, grid.shape[1] * 2)

    for _ in range(num_drops):
        x = np.random.randint(0, grid.shape[1])
        y = np.random.randint(0, grid.shape[0])
        length = np.random.randint(3, 8)
        
        for i in range(length):
            if 0 <= y + i < grid.shape[0]:
                grid[y + i, x] = rain_color

    # Add a puddle at the bottom
    puddle_color = np.random.randint(1, max_colors)
    puddle_height = np.random.randint(1, 4)
    grid[-puddle_height:, :] = puddle_color

    return grid

def create_butterfly(grid: np.ndarray, max_colors: int) -> np.ndarray:
    def draw_wing(center_x: int, center_y: int, size: int, color: int, flip: bool):
        for y in range(-size, size + 1):
            for x in range(-size, size + 1):
                if x**2 + y**2 <= size**2:
                    grid_x = center_x + (x if not flip else -x)
                    grid_y = center_y + y
                    if 0 <= grid_y < grid.shape[0] and 0 <= grid_x < grid.shape[1]:
                        grid[grid_y, grid_x] = color

    center_x = grid.shape[1] // 2
    center_y = grid.shape[0] // 2
    wing_size = min(grid.shape) // 4

    # Draw wings
    wing_color = np.random.randint(1, max_colors)
    draw_wing(center_x - wing_size // 2, center_y, wing_size, wing_color, False)
    draw_wing(center_x + wing_size // 2, center_y, wing_size, wing_color, True)

    # Draw body
    body_color = np.random.randint(1, max_colors)
    body_length = wing_size * 2
    for y in range(center_y - body_length // 2, center_y + body_length // 2):
        if 0 <= y < grid.shape[0]:
            grid[y, center_x] = body_color

    # Draw antennae
    antenna_color = np.random.randint(1, max_colors)
    antenna_length = wing_size // 2
    for i in range(antenna_length):
        y = center_y - body_length // 2 - i
        if y >= 0:
            if center_x - i >= 0:
                grid[y, center_x - i] = antenna_color
            if center_x + i < grid.shape[1]:
                grid[y, center_x + i] = antenna_color

    return grid

# Helper function for line drawing
def line(y0: int, x0: int, y1: int, x1: int) -> Tuple[np.ndarray, np.ndarray]:
    dx = abs(x1 - x0)
    dy = abs(y1 - y0)
    steep = dy > dx

    if steep:
        x0, y0 = y0, x0
        x1, y1 = y1, x1

    if x0 > x1:
        x0, x1 = x1, x0
        y0, y1 = y1, y0

    dx = x1 - x0
    dy = abs(y1 - y0)
    error = dx // 2
    ystep = 1 if y0 < y1 else -1

    y = y0
    points = []
    for x in range(x0, x1 + 1):
        coord = (y, x) if steep else (x, y)
        points.append(coord)
        error -= dy
        if error < 0:
            y += ystep
            error += dx

    return np.array(list(zip(*points)))

def create_cellular_automaton(grid: np.ndarray, max_colors: int) -> np.ndarray:
    def get_neighbors(x: int, y: int) -> int:
        count = 0
        for dx in [-1, 0, 1]:
            for dy in [-1, 0, 1]:
                if dx == 0 and dy == 0:
                    continue
                nx, ny = x + dx, y + dy
                if 0 <= nx < grid.shape[1] and 0 <= ny < grid.shape[0]:
                    count += grid[ny, nx] != 0
        return count

    # Initialize with random cells
    grid = np.random.choice([0, 1], size=grid.shape, p=[0.8, 0.2])
    
    # Run the automaton for a few generations
    for _ in range(3):
        new_grid = grid.copy()
        for y in range(grid.shape[0]):
            for x in range(grid.shape[1]):
                neighbors = get_neighbors(x, y)
                if grid[y, x] == 1:
                    if neighbors < 2 or neighbors > 3:
                        new_grid[y, x] = 0
                else:
                    if neighbors == 3:
                        new_grid[y, x] = np.random.randint(1, max_colors)
        grid = new_grid
    
    return grid

def create_flocking_simulation(grid: np.ndarray, max_colors: int) -> np.ndarray:
    num_boids = min(50, grid.shape[0] * grid.shape[1] // 10)
    boids = np.random.rand(num_boids, 2) * [grid.shape[1], grid.shape[0]]
    velocities = (np.random.rand(num_boids, 2) - 0.5) * 2
    
    for _ in range(20):  # Run simulation for 20 steps
        # Update positions
        boids += velocities
        
        # Wrap around edges
        boids %= [grid.shape[1], grid.shape[0]]
        
        # Simple flocking behavior
        for i in range(num_boids):
            center = boids.mean(axis=0)
            velocities[i] += (center - boids[i]) * 0.01
            
            # Limit velocity
            speed = np.linalg.norm(velocities[i])
            if speed > 1:
                velocities[i] /= speed
    
    # Draw final positions
    for boid in boids.astype(int):
        x, y = boid
        if 0 <= x < grid.shape[1] and 0 <= y < grid.shape[0]:
            grid[y, x] = np.random.randint(1, max_colors)
    
    return grid

def create_diffusion_limited_aggregation(grid: np.ndarray, max_colors: int) -> np.ndarray:
    # Start with a seed at the center
    center = (grid.shape[0] // 2, grid.shape[1] // 2)
    grid[center] = 1
    
    num_particles = min(1000, grid.shape[0] * grid.shape[1] // 4)
    for _ in range(num_particles):
        # Start a new particle at a random edge
        if np.random.rand() < 0.5:
            x, y = np.random.randint(0, grid.shape[1]), np.random.choice([0, grid.shape[0]-1])
        else:
            x, y = np.random.choice([0, grid.shape[1]-1]), np.random.randint(0, grid.shape[0])
        
        while True:
            # Move randomly
            dx, dy = np.random.choice([-1, 0, 1], size=2)
            new_x, new_y = x + dx, y + dy
            
            # Check if out of bounds
            if new_x < 0 or new_x >= grid.shape[1] or new_y < 0 or new_y >= grid.shape[0]:
                break
            
            # Check if next to an existing particle
            if np.any(grid[max(0, new_y-1):min(grid.shape[0], new_y+2),
                           max(0, new_x-1):min(grid.shape[1], new_x+2)] != 0):
                grid[y, x] = np.random.randint(1, max_colors)
                break
            
            x, y = new_x, new_y
    
    return grid

def create_voronoi_diagram(grid: np.ndarray, max_colors: int) -> np.ndarray:
    num_points = min(20, max(3, grid.shape[0] * grid.shape[1] // 100))
    points = np.random.rand(num_points, 2) * [grid.shape[1], grid.shape[0]]
    colors = np.random.randint(1, max_colors, size=num_points)
    
    for y in range(grid.shape[0]):
        for x in range(grid.shape[1]):
            distances = np.sum((points - [x, y])**2, axis=1)
            grid[y, x] = colors[np.argmin(distances)]
    
    return grid

def create_repeating_tiles(grid: np.ndarray, max_colors: int) -> np.ndarray:
    tile_size = min(5, min(grid.shape) // 2)
    tile = np.random.randint(0, max_colors, size=(tile_size, tile_size))
    
    for i in range(0, grid.shape[0], tile_size):
        for j in range(0, grid.shape[1], tile_size):
            grid[i:i+tile_size, j:j+tile_size] = tile[:min(tile_size, grid.shape[0]-i), 
                                                      :min(tile_size, grid.shape[1]-j)]
    return grid

def create_rotating_tiles(grid: np.ndarray, max_colors: int) -> np.ndarray:
    tile_size = min(5, min(grid.shape) // 2)
    tile = np.random.randint(0, max_colors, size=(tile_size, tile_size))
    
    for i in range(0, grid.shape[0], tile_size):
        for j in range(0, grid.shape[1], tile_size):
            rotated_tile = np.rot90(tile, k=((i+j)//tile_size)%4)
            grid[i:i+tile_size, j:j+tile_size] = rotated_tile[:min(tile_size, grid.shape[0]-i), 
                                                              :min(tile_size, grid.shape[1]-j)]
    return grid

def create_mirrored_tiles(grid: np.ndarray, max_colors: int) -> np.ndarray:
    tile_size = min(5, min(grid.shape) // 2)
    tile = np.random.randint(0, max_colors, size=(tile_size, tile_size))
    
    for i in range(0, grid.shape[0], tile_size):
        for j in range(0, grid.shape[1], tile_size):
            if (i//tile_size + j//tile_size) % 2 == 0:
                current_tile = tile
            else:
                current_tile = np.fliplr(tile)
            grid[i:i+tile_size, j:j+tile_size] = current_tile[:min(tile_size, grid.shape[0]-i), 
                                                              :min(tile_size, grid.shape[1]-j)]
    return grid

def create_scaled_tiles(grid: np.ndarray, max_colors: int) -> np.ndarray:
    base_size = min(3, min(grid.shape) // 2)
    base_tile = np.random.randint(0, max_colors, size=(base_size, base_size))
    
    for scale in range(1, 4):
        tile = np.kron(base_tile, np.ones((scale, scale), dtype=int))
        tile_size = base_size * scale
        
        for i in range(0, grid.shape[0], tile_size):
            for j in range(0, grid.shape[1], tile_size):
                grid[i:i+tile_size, j:j+tile_size] = tile[:min(tile_size, grid.shape[0]-i), 
                                                          :min(tile_size, grid.shape[1]-j)]
    return grid

def create_overlapping_tiles(grid: np.ndarray, max_colors: int) -> np.ndarray:
    tile_size = min(7, min(grid.shape) // 2)
    num_tiles = 5
    
    for _ in range(num_tiles):
        tile = np.random.randint(0, max_colors, size=(tile_size, tile_size))
        i = np.random.randint(0, grid.shape[0] - tile_size + 1)
        j = np.random.randint(0, grid.shape[1] - tile_size + 1)
        
        # Overlay the new tile on the existing grid
        mask = tile != 0
        grid[i:i+tile_size, j:j+tile_size][mask] = tile[mask]
    
    return grid

