import pygame
import pymunk
import pymunk.pygame_util
import math
import random
import sys
import time
from enum import Enum

# 粒子状态
class ParticleState(Enum):
    UPPER = "upper"
    FLOWING = "flowing"
    FALLING = "falling"
    LOWER = "lower"

# 物理常数
class PhysicsConstants:
    GRAVITY = 981.0
    SAND_DENSITY = 1.6
    FRICTION_STATIC = 0.001  # 接近零的摩擦力
    FRICTION_DYNAMIC = 0.001  # 接近零的摩擦力
    RESTITUTION = 0.001  # 减小弹性，避免反弹
    ANGLE_OF_REPOSE = 30
    MAX_VELOCITY = 500.0

# 配置
CONFIG = {
    'screen_width': 1200,
    'screen_height': 900,
    'fps': 60,
    'hourglass_pos': (600, 450),
    'hourglass_outer_width': 320,  # 增加上下部分宽度，让容器更大
    'hourglass_height': 600,
    'neck_width': 15,  # 缩小颈部宽度，让沙子流得更细更慢
    'glass_thickness': 5,
    'particle_radius': 1.5,
    'particle_mass': 0.05,
    'particle_count': 9000,  # 增加到9000个粒子（增加50%）
    'base_width': 360,  # 相应增加底座宽度
    'base_height': 40,
}

# 颜色定义
COLORS = {
    'bg': (45, 42, 40),
    'glass_edge': (200, 220, 210),
    'glass_highlight': (255, 255, 255, 200),
    'glass_shadow': (120, 140, 130, 100),
    'metal_base': (35, 31, 28),
    'metal_highlight': (165, 142, 120),
    'metal_accent': (225, 190, 150),
    'sand_dry': (238, 203, 143),
    'sand_flowing': (245, 215, 160),
    'sand_shadow': (195, 165, 115),
    'sand_highlight': (252, 235, 195),
    'wood_dark': (70, 50, 35),
    'wood_light': (120, 85, 60),
    'text': (240, 235, 230),
    'text_shadow': (40, 35, 30),
    'button': (100, 150, 200),
    'button_hover': (120, 170, 220),
}

class SandParticle:
    """沙粒"""
    def __init__(self, space, pos, radius, mass, particle_id):
        inertia = pymunk.moment_for_circle(mass, 0, radius, (0, 0))
        self.body = pymunk.Body(mass, inertia)
        self.body.position = pos
        
        self.shape = pymunk.Circle(self.body, radius)
        self.shape.friction = PhysicsConstants.FRICTION_DYNAMIC
        self.shape.elasticity = 0.001  # 极低弹性
        self.shape.collision_type = 2
        
        self.state = ParticleState.UPPER
        self.color = COLORS['sand_dry']
        self.particle_id = particle_id
        self.has_passed_neck = False
        self.release_time = None  # 记录释放时间
        
        space.add(self.body, self.shape)
    
    def update_state(self, neck_y, dt):
        """更新状态"""
        y = self.body.position.y
        velocity = math.sqrt(self.body.velocity.x**2 + self.body.velocity.y**2)
        
        if not self.has_passed_neck and y > neck_y:
            self.has_passed_neck = True
            self.state = ParticleState.FLOWING
        elif self.state == ParticleState.FLOWING and y > neck_y + 30:
            self.state = ParticleState.FALLING
        elif self.state == ParticleState.FALLING and velocity < 30:
            self.state = ParticleState.LOWER
        
        # 更新颜色
        if self.state == ParticleState.FLOWING:
            self.color = COLORS['sand_flowing']
        elif self.state == ParticleState.FALLING:
            factor = min(1.0, velocity / 200)
            self.color = tuple(
                int(COLORS['sand_dry'][i] * (1 - factor) + 
                    COLORS['sand_flowing'][i] * factor)
                for i in range(3)
            )
        else:
            self.color = COLORS['sand_dry']
    
    def reset_after_flip(self):
        """翻转后重置状态"""
        self.has_passed_neck = False
        self.release_time = None
        
        # 根据当前位置设置状态，而不是简单判断
        cy = CONFIG['hourglass_pos'][1]
        y = self.body.position.y
        
        # 使用更平滑的状态转换
        if y < cy - 20:  # 上半部分
            self.state = ParticleState.UPPER
        elif y > cy + 20:  # 下半部分
            self.state = ParticleState.LOWER
        else:  # 颈部附近
            # 颈部附近的粒子根据速度决定状态
            velocity = math.sqrt(self.body.velocity.x**2 + self.body.velocity.y**2)
            if velocity > 50:
                self.state = ParticleState.FLOWING
            else:
                self.state = ParticleState.UPPER
        
        self.color = COLORS['sand_dry']

class FlippableHourglass:
    """可翻转沙漏"""
    def __init__(self, space, config):
        self.space = space
        self.config = config
        self.pos = config['hourglass_pos']
        
        self.walls = []
        self.flip_angle = 0
        self.is_flipping = False
        self.flip_speed = 3
        
        self._create_physics_boundaries()
    
    def _create_physics_boundaries(self):
        """创建完整的物理边界"""
        cx, cy = self.pos
        w = self.config['hourglass_outer_width'] - 20
        h = self.config['hourglass_height']
        nw = self.config['neck_width']
        
        # 清理旧墙壁
        for wall in self.walls:
            try:
                self.space.remove(wall)
            except:
                pass
        self.walls = []
        
        wall_thickness = 5
        segments = 40
        
        # 上半部分 - 左侧
        for i in range(segments):
            t = i / segments
            t_next = (i + 1) / segments
            curve1 = (1 - math.cos(t * math.pi)) / 2
            curve2 = (1 - math.cos(t_next * math.pi)) / 2
            
            x1 = cx - (w/2 * (1 - curve1) + nw/2 * curve1)
            y1 = cy - h/2 + (h/2 - 10) * t
            x2 = cx - (w/2 * (1 - curve2) + nw/2 * curve2)
            y2 = cy - h/2 + (h/2 - 10) * t_next
            
            wall = pymunk.Segment(self.space.static_body, (x1, y1), (x2, y2), wall_thickness)
            wall.friction = 0.001
            wall.elasticity = 0.001
            wall.collision_type = 1
            self.walls.append(wall)
        
        # 上半部分 - 右侧
        for i in range(segments):
            t = i / segments
            t_next = (i + 1) / segments
            curve1 = (1 - math.cos(t * math.pi)) / 2
            curve2 = (1 - math.cos(t_next * math.pi)) / 2
            
            x1 = cx + (w/2 * (1 - curve1) + nw/2 * curve1)
            y1 = cy - h/2 + (h/2 - 10) * t
            x2 = cx + (w/2 * (1 - curve2) + nw/2 * curve2)
            y2 = cy - h/2 + (h/2 - 10) * t_next
            
            wall = pymunk.Segment(self.space.static_body, (x1, y1), (x2, y2), wall_thickness)
            wall.friction = 0.001
            wall.elasticity = 0.001
            wall.collision_type = 1
            self.walls.append(wall)
        
        # 颈部
        neck_segments = [
            ((cx - nw/2, cy - 15), (cx - nw/2, cy + 15)),
            ((cx + nw/2, cy - 15), (cx + nw/2, cy + 15))
        ]
        
        for start, end in neck_segments:
            wall = pymunk.Segment(self.space.static_body, start, end, wall_thickness/2)
            wall.friction = 0.0
            wall.elasticity = 0.0
            wall.collision_type = 1
            self.walls.append(wall)
        
        # 下半部分 - 左侧
        for i in range(segments):
            t = i / segments
            t_next = (i + 1) / segments
            curve1 = (1 - math.cos(t * math.pi)) / 2
            curve2 = (1 - math.cos(t_next * math.pi)) / 2
            
            x1 = cx - (nw/2 * (1 - curve1) + w/2 * curve1)
            y1 = cy + 10 + (h/2 - 15) * t
            x2 = cx - (nw/2 * (1 - curve2) + w/2 * curve2)
            y2 = cy + 10 + (h/2 - 15) * t_next
            
            wall = pymunk.Segment(self.space.static_body, (x1, y1), (x2, y2), wall_thickness)
            wall.friction = 0.001
            wall.elasticity = 0.001
            wall.collision_type = 1
            self.walls.append(wall)
        
        # 下半部分 - 右侧
        for i in range(segments):
            t = i / segments
            t_next = (i + 1) / segments
            curve1 = (1 - math.cos(t * math.pi)) / 2
            curve2 = (1 - math.cos(t_next * math.pi)) / 2
            
            x1 = cx + (nw/2 * (1 - curve1) + w/2 * curve1)
            y1 = cy + 10 + (h/2 - 15) * t
            x2 = cx + (nw/2 * (1 - curve2) + w/2 * curve2)
            y2 = cy + 10 + (h/2 - 15) * t_next
            
            wall = pymunk.Segment(self.space.static_body, (x1, y1), (x2, y2), wall_thickness)
            wall.friction = 0.001
            wall.elasticity = 0.001
            wall.collision_type = 1
            self.walls.append(wall)
        
        # 底部封闭
        bottom_y = cy + h/2 - 5
        bottom_wall = pymunk.Segment(self.space.static_body,
                                    (cx - w/2, bottom_y), (cx + w/2, bottom_y), wall_thickness)
        bottom_wall.friction = 0.001
        bottom_wall.elasticity = PhysicsConstants.RESTITUTION
        bottom_wall.collision_type = 1
        self.walls.append(bottom_wall)
        
        self.space.add(*self.walls)
    
    def start_flip(self):
        """开始翻转"""
        if not self.is_flipping:
            self.is_flipping = True
    
    def update_flip(self, dt):
        """更新翻转动画"""
        if self.is_flipping:
            self.flip_angle += self.flip_speed * dt
            
            if self.flip_angle >= math.pi:
                self.flip_angle = 0
                self.is_flipping = False
                return True
        return False
    
    def draw(self, screen):
        """绘制沙漏"""
        cx, cy = self.pos
        w = self.config['hourglass_outer_width']
        h = self.config['hourglass_height']
        nw = self.config['neck_width'] + self.config['glass_thickness'] * 2
        
        if self.is_flipping:
            temp_surface = pygame.Surface((w + 200, h + 200), pygame.SRCALPHA)
            temp_cx = (w + 200) // 2
            temp_cy = (h + 200) // 2
            
            self._draw_glass_shape(temp_surface, temp_cx, temp_cy, w, h, nw)
            
            angle_degrees = math.degrees(self.flip_angle)
            rotated_surface = pygame.transform.rotate(temp_surface, angle_degrees)
            
            rect = rotated_surface.get_rect(center=(cx, cy))
            screen.blit(rotated_surface, rect)
        else:
            self._draw_glass_shape(screen, cx, cy, w, h, nw)
    
    def _draw_glass_shape(self, surface, cx, cy, w, h, nw):
        """绘制玻璃形状"""
        segments = 30
        points = []
        
        visual_w = w - 20
        
        # 上半部分
        for i in range(segments + 1):
            t = i / segments
            curve = (1 - math.cos(t * math.pi)) / 2
            x = cx - (visual_w/2 * (1 - curve) + nw/2 * curve)
            y = cy - h/2 + (h/2 - 10) * t
            points.append((x, y))
        
        # 下半部分
        for i in range(segments + 1):
            t = i / segments
            curve = (1 - math.cos(t * math.pi)) / 2
            x = cx - (nw/2 * (1 - curve) + visual_w/2 * curve)
            y = cy + 10 + (h/2 - 15) * t
            points.append((x, y))
        
        # 右侧（镜像）
        right_points = [(2*cx - x, y) for x, y in reversed(points)]
        all_points = points + right_points
        
        # 绘制玻璃效果
        glass_surface = pygame.Surface((surface.get_width(), surface.get_height()), pygame.SRCALPHA)
        pygame.draw.polygon(glass_surface, (255, 255, 255, 15), all_points, 0)
        surface.blit(glass_surface, (0, 0))
        
        pygame.draw.lines(surface, COLORS['glass_edge'], True, all_points, 2)
        
        # 高光
        highlight_surface = pygame.Surface((surface.get_width(), surface.get_height()), pygame.SRCALPHA)
        for i in range(len(points)//4):
            if i < len(points) - 1:
                pygame.draw.line(highlight_surface, (255, 255, 255, 100),
                               points[i], points[i+1], 1)
        surface.blit(highlight_surface, (0, 0))

class SandManager:
    """沙子管理器"""
    def __init__(self, space, config):
        self.space = space
        self.config = config
        self.particles = []
        self.hourglass = None
        
        self.is_running = False
        self.is_paused = False
        self.start_time = 0
        self.target_time = 60
        self.time_before_flip = 0
        
        # PI控制器用于流量控制
        self.time_control_error_integral = 0.0
        
        self._create_particles()
    
    def set_hourglass(self, hourglass):
        self.hourglass = hourglass
    
    def _create_particles(self):
        """创建粒子，使其在下半部分稳定堆积"""
        cx, cy = self.config['hourglass_pos']
        w = self.config['hourglass_outer_width'] - 25
        h = self.config['hourglass_height']
        radius = self.config['particle_radius']
        nw = self.config['neck_width']
        
        particles_created = 0
        
        # 在下半部分创建粒子
        bottom_y_start = cy + 25
        bottom_y_end = cy + h/2 - 20
        
        # 填充几乎整个下半部分以最大化粒子数量
        layer_height = radius * 2.0  # 更密集的堆积
        current_y = bottom_y_start
        
        while current_y < bottom_y_end:
            t = (current_y - (cy + 10)) / (h/2 - 15)
            t = max(0.0, min(1.0, t))
            
            curve = (1 - math.cos(t * math.pi)) / 2
            layer_width = (nw/2 * (1 - curve) + w/2 * curve) * 0.9
            
            particles_this_layer = int(layer_width * 2 / layer_height)
            
            for i in range(particles_this_layer):
                offset = (layer_height / 4) if (int(current_y / layer_height) % 2 == 0) else 0
                x = cx - layer_width + (i + 0.5) * (layer_width * 2) / particles_this_layer + offset
                x += random.uniform(-radius * 0.1, radius * 0.1)
                y = current_y + random.uniform(-radius * 0.1, radius * 0.1)
                
                if abs(x-cx) < layer_width:
                    particle = SandParticle(
                        self.space,
                        (x, y),
                        radius,
                        self.config['particle_mass'],
                        particles_created
                    )
                    # 初始状态设置为LOWER
                    particle.state = ParticleState.LOWER
                    self.particles.append(particle)
                    particles_created += 1
            
            current_y += layer_height
        
        self.config['particle_count'] = particles_created
        print(f"创建了 {particles_created} 个粒子")

    def release_particle(self, particle, elapsed_time):
        """'Releases' a single particle by giving it a push towards the neck."""
        particle.has_passed_neck = True
        particle.state = ParticleState.FLOWING
        particle.release_time = elapsed_time
        
        # Give it a downward push to pass through the neck
        particle.body.velocity = (
            random.uniform(-10, 10),
            random.uniform(80, 120) # Increased velocity for more reliable passage
        )

    def flip_particles(self):
        """翻转粒子，通过精确的180度旋转保持其相对位置，并添加物理随机性"""
        cy = self.config['hourglass_pos'][1]
        cx = self.config['hourglass_pos'][0]
        h = self.config['hourglass_height']
        w = self.config['hourglass_outer_width'] - 25
        
        if self.is_running:
            self.time_before_flip = time.time() - self.start_time
        
        for particle in self.particles:
            x, y = particle.body.position
            
            # 围绕中心点 (cx, cy) 旋转180度
            new_x = 2 * cx - x
            new_y = 2 * cy - y
            
            # 确保新位置在沙漏边界内
            max_y = cy + h/2 - 20
            min_y = cy - h/2 + 20
            new_y = max(min_y, min(max_y, new_y))
            
            # 根据新的y坐标计算该高度的沙漏宽度，以约束x坐标
            if new_y < cy:  # 位于新的上半部分
                dist_from_end = new_y - (cy - h/2)
                total_dist = (h/2 - 10)
                t = dist_from_end / total_dist if total_dist > 0 else 0
            else:  # 位于新的下半部分
                dist_from_end = new_y - (cy + 10)
                total_dist = (h/2 - 15)
                t = dist_from_end / total_dist if total_dist > 0 else 0

            t = max(0.0, min(1.0, t))
            curve = (1 - math.cos(t * math.pi)) / 2

            if new_y < cy:
                max_x_offset = (w/2 * (1 - curve) + self.config['neck_width']/2 * curve)
            else:
                max_x_offset = (self.config['neck_width']/2 * (1 - curve) + w/2 * curve)
            
            # 将x坐标限制在计算出的宽度内
            new_x = max(cx - max_x_offset, min(cx + max_x_offset, new_x))

            # 添加随机偏移量，模拟真实翻转时的分散效果
            random_offset_x = random.uniform(-self.config['particle_radius'] * 1.5, self.config['particle_radius'] * 1.5)
            random_offset_y = random.uniform(-self.config['particle_radius'] * 1.5, self.config['particle_radius'] * 1.5)
            new_x += random_offset_x
            new_y += random_offset_y

            particle.body.position = (new_x, new_y)
            
            # 保持部分动量并添加向下重力，而不是完全重置速度
            # 计算翻转前的速度大小和方向
            vx, vy = particle.body.velocity
            speed = math.sqrt(vx**2 + vy**2)
            
            # 翻转后给予一个向下的初始速度，并保持部分水平动量
            new_vx = random.uniform(-speed * 0.3, speed * 0.3)
            new_vy = random.uniform(50, 100)  # 向下的初始速度
            
            particle.body.velocity = (new_vx, new_vy)
            
            particle.reset_after_flip()
        
        self.time_control_error_integral = 0.0
        
        if self.is_running:
            self.start_time = time.time() - self.time_before_flip
    
    def start(self, duration):
        """开始计时"""
        self.target_time = duration
        self.start_time = time.time()
        self.is_running = True
        self.is_paused = False
        self.time_before_flip = 0
        self.time_control_error_integral = 0.0
        
        # 重置所有粒子状态
        for p in self.particles:
            p.has_passed_neck = False
            p.state = ParticleState.UPPER
            p.release_time = None
            # 给粒子一些初始速度以帮助它们开始流动
            p.body.velocity = (random.uniform(-5, 5), random.uniform(5, 15))
        
        print(f"开始计时: {duration}秒，共{len(self.particles)}个粒子")
    
    def pause(self):
        """暂停"""
        if self.is_running and not self.is_paused:
            self.is_paused = True
            self.time_before_flip = time.time() - self.start_time
    
    def resume(self):
        """恢复"""
        if self.is_paused:
            self.is_paused = False
            self.start_time = time.time() - self.time_before_flip
    
    def reset(self):
        """重置"""
        for particle in self.particles:
            try:
                self.space.remove(particle.body, particle.shape)
            except:
                pass
        self.particles.clear()
        
        self._create_particles()
        
        self.is_running = False
        self.is_paused = False
        self.start_time = 0
        self.time_before_flip = 0
        self.time_control_error_integral = 0.0
    
    def update(self, dt):
        """更新"""
        if not self.is_running or self.is_paused:
            return
        
        if self.hourglass and self.hourglass.is_flipping:
            return
        
        neck_y = self.config['hourglass_pos'][1]
        cx = self.config['hourglass_pos'][0]
        
        # 更新粒子状态
        for particle in self.particles:
            particle.update_state(neck_y, dt)
            
            # 防止粒子速度过大
            vx, vy = particle.body.velocity
            max_v = PhysicsConstants.MAX_VELOCITY
            if abs(vx) > max_v or abs(vy) > max_v:
                particle.body.velocity = (
                    max(-max_v, min(max_v, vx)),
                    max(-max_v, min(max_v, vy))
                )
        
        # PI controller for precise flow rate
        elapsed = time.time() - self.start_time
        
        # If time is up, force all remaining particles down
        if elapsed >= self.target_time:
            upper_particles = [p for p in self.particles if p.state == ParticleState.UPPER and not p.has_passed_neck]
            for p in upper_particles:
                self.release_particle(p, elapsed)
            if not upper_particles and self.is_running: # all particles passed
                self.is_running = False
            return

        target_passed_count = (elapsed / self.target_time) * len(self.particles)
        actual_passed_count = sum(1 for p in self.particles if p.has_passed_neck)
        
        error = target_passed_count - actual_passed_count
        
        # Update integral term (with anti-windup)
        if abs(error) < len(self.particles) * 0.1: # Only integrate when error is not too large
            self.time_control_error_integral += error * dt
        
        # PI controller constants
        KP = 0.5  # Proportional gain
        KI = 0.05 # Integral gain
        
        # Number of particles to release based on controller output
        release_count = int(error * KP + self.time_control_error_integral * KI)
        
        # Add a cap to release_count to prevent bursts and ensure it's not negative
        max_release_per_frame = 10
        release_count = max(0, min(release_count, max_release_per_frame))
        
        if release_count > 0:
            # Find best candidates to release (lowest in the upper bulb)
            candidates = [
                p for p in self.particles
                if p.state == ParticleState.UPPER and not p.has_passed_neck
            ]
            
            if candidates:
                # Sort by y (descending) then by x distance from center (ascending)
                candidates.sort(key=lambda p: (-p.body.position.y, abs(p.body.position.x - cx)))
                
                for i in range(min(release_count, len(candidates))):
                    self.release_particle(candidates[i], elapsed)

    def draw(self, screen):
        """绘制粒子"""
        if self.hourglass and self.hourglass.is_flipping:
            cx, cy = self.config['hourglass_pos']
            
            w = self.config['hourglass_outer_width'] + 200
            h = self.config['hourglass_height'] + 200
            temp_surface = pygame.Surface((w, h), pygame.SRCALPHA)
            offset_x = w // 2 - cx
            offset_y = h // 2 - cy
            
            for particle in self.particles:
                x = int(particle.body.position.x + offset_x)
                y = int(particle.body.position.y + offset_y)
                radius = int(particle.shape.radius)
                
                if 0 <= x < w and 0 <= y < h:
                    pygame.draw.circle(temp_surface, particle.color, (x, y), radius)
            
            angle_degrees = math.degrees(self.hourglass.flip_angle)
            rotated_surface = pygame.transform.rotate(temp_surface, angle_degrees)
            
            rect = rotated_surface.get_rect(center=(cx, cy))
            screen.blit(rotated_surface, rect)
        else:
            for particle in self.particles:
                x, y = int(particle.body.position.x), int(particle.body.position.y)
                radius = max(1, int(particle.shape.radius))
                
                pygame.draw.circle(screen, COLORS['sand_shadow'],
                                 (x + 1, y + 1), radius)
                pygame.draw.circle(screen, particle.color, (x, y), radius)
                
                if radius > 1:
                    highlight_radius = max(1, radius//2)
                    pygame.draw.circle(screen, COLORS['sand_highlight'],
                                     (x - 1, y - 1), highlight_radius)

class HourglassController:
    """沙漏控制器 - 提供外部接口"""
    def __init__(self, simulator):
        self.simulator = simulator
    
    def start_timer(self, duration):
        """开始计时流程"""
        if not self.simulator.sand_manager.is_running and not self.simulator.hourglass.is_flipping:
            self.simulator.start_simulation(duration)
            return True
        return False
    
    def pause_timer(self):
        """暂停计时"""
        self.simulator.sand_manager.pause()
        return True
    
    def resume_timer(self):
        """恢复计时"""
        self.simulator.sand_manager.resume()
        return True
    
    def reset_hourglass(self):
        """重置沙漏"""
        self.simulator.reset_simulation()
        return True
    
    def flip_hourglass(self):
        """翻转沙漏"""
        if not self.simulator.hourglass.is_flipping:
            if self.simulator.sand_manager.is_running:
                self.simulator.sand_manager.pause()
                self.simulator.pending_start = False # 取消待处理的启动
            self.simulator.hourglass.start_flip()
            return True
        return False
    
    def get_status(self):
        """获取当前状态"""
        status = {
            'is_running': self.simulator.sand_manager.is_running,
            'is_paused': self.simulator.sand_manager.is_paused,
            'is_flipping': self.simulator.hourglass.is_flipping,
            'elapsed_time': 0,
            'remaining_time': 0,
            'total_particles': len(self.simulator.sand_manager.particles),
            'passed_particles': 0
        }
        
        if self.simulator.sand_manager.is_running:
            elapsed = time.time() - self.simulator.sand_manager.start_time
            status['elapsed_time'] = elapsed
            status['remaining_time'] = max(0, self.simulator.sand_manager.target_time - elapsed)
            status['passed_particles'] = sum(1 for p in self.simulator.sand_manager.particles 
                                            if p.has_passed_neck)
        
        return status

class FlippableSimulator:
    """可翻转沙漏模拟器"""
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((CONFIG['screen_width'], CONFIG['screen_height']), pygame.RESIZABLE)
        pygame.display.set_caption("Optimized Physics Hourglass")
        self.clock = pygame.time.Clock()
        
        self.game_surface = pygame.Surface((CONFIG['screen_width'], CONFIG['screen_height']))
        self.scale = 1.0
        self.blit_rect = self.game_surface.get_rect(center=self.screen.get_rect().center)

        self.space = pymunk.Space()
        self.space.gravity = (0, PhysicsConstants.GRAVITY)
        self.space.damping = 0.99
        self.space.iterations = 15
        self.space.collision_slop = 0.1
        
        self.hourglass = FlippableHourglass(self.space, CONFIG)
        self.sand_manager = SandManager(self.space, CONFIG)
        self.sand_manager.set_hourglass(self.hourglass)
        
        self.controller = HourglassController(self)
        
        self.input_time = 60
        self.input_active = False
        self.input_text = str(self.input_time)
        
        self.running = True
        self.pending_start = False
        self.stored_duration = 0
        
        self.font = None
        self.small_font = None
        
        font_paths = [
            "/System/Library/Fonts/STHeiti Light.ttc",
            "/System/Library/Fonts/Hiragino Sans GB.ttc",
            "/System/Library/Fonts/PingFang.ttc",
            "/System/Library/Fonts/STHeiti Medium.ttc",
        ]
        
        for font_path in font_paths:
            try:
                self.font = pygame.font.Font(font_path, 24)
                self.small_font = pygame.font.Font(font_path, 18)
                print(f"使用字体: {font_path}")
                break
            except:
                continue
        
        if not self.font:
            self.font = pygame.font.Font(None, 24)
            self.small_font = pygame.font.Font(None, 18)
            print("使用默认字体")
    
    def start_simulation(self, duration):
        """启动整个计时流程，从翻转开始"""
        self.stored_duration = duration
        self.pending_start = True
        self.hourglass.start_flip()

    def reset_simulation(self):
        """重置整个模拟器状态"""
        self.sand_manager.reset()
        self.pending_start = False
        self.stored_duration = 0
        # 确保物理引擎在重置后更新一次，让沙子稳定下来
        for _ in range(10):
            self.space.step(1/120)

    def _transform_mouse_pos(self, screen_pos):
        if self.scale == 0: return (-1, -1)
        return (
            (screen_pos[0] - self.blit_rect.left) / self.scale,
            (screen_pos[1] - self.blit_rect.top) / self.scale
        )

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            elif event.type == pygame.VIDEORESIZE:
                self.screen = pygame.display.set_mode((event.w, event.h), pygame.RESIZABLE)
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    if self.input_active:
                        self.input_active = False
                        self.input_text = str(self.input_time)
                    else:
                        self.running = False
                elif self.input_active:
                    if event.key == pygame.K_RETURN:
                        try:
                            new_time = int(self.input_text)
                            if 10 <= new_time <= 1800: self.input_time = new_time
                        except: pass
                        self.input_active = False
                        self.input_text = str(self.input_time)
                    elif event.key == pygame.K_BACKSPACE:
                        self.input_text = self.input_text[:-1]
                    elif event.unicode.isdigit() and len(self.input_text) < 4:
                        self.input_text += event.unicode
                else:
                    if event.key == pygame.K_SPACE:
                        if not self.sand_manager.is_running and not self.hourglass.is_flipping:
                            self.controller.start_timer(self.input_time)
                        elif self.sand_manager.is_running:
                            if self.sand_manager.is_paused:
                                self.controller.resume_timer()
                            else:
                                self.controller.pause_timer()
                    elif event.key == pygame.K_t:
                        self.input_active = True
                        self.input_text = ""
                    elif event.key == pygame.K_f:
                        self.controller.flip_hourglass()
                    elif event.key == pygame.K_r:
                        self.controller.reset_hourglass()
            elif event.type == pygame.MOUSEBUTTONDOWN:
                if not self.input_active:
                    game_pos = self._transform_mouse_pos(event.pos)
                    button_rect = pygame.Rect(CONFIG['screen_width']//2 - 60, 100, 120, 40)
                    if button_rect.collidepoint(game_pos) and not self.hourglass.is_flipping:
                        if not self.sand_manager.is_running:
                            self.controller.start_timer(self.input_time)
                        else:
                            self.controller.flip_hourglass()
    
    def draw_ui(self, surface):
        mouse_pos = self._transform_mouse_pos(pygame.mouse.get_pos())

        if self.input_active:
            prompt_text = f"Set Time (s): {self.input_text}_"
            prompt_surface = self.font.render(prompt_text, True, COLORS['text'])
            prompt_rect = prompt_surface.get_rect(center=(CONFIG['screen_width'] // 2, 45))
            surface.blit(prompt_surface, prompt_rect)
            
            hint_text = "Enter: confirm, ESC: cancel"
            hint_surface = self.small_font.render(hint_text, True, COLORS['text_shadow'])
            hint_rect = hint_surface.get_rect(center=(CONFIG['screen_width'] // 2, 70))
            surface.blit(hint_surface, hint_rect)
        else:
            timer_text = f"Timer: {self.input_time}s (Press 'T' to change)"
            timer_surface = self.font.render(timer_text, True, COLORS['text'])
            timer_rect = timer_surface.get_rect(center=(CONFIG['screen_width'] // 2, 45))
            surface.blit(timer_surface, timer_rect)

        button_rect = pygame.Rect(CONFIG['screen_width']//2 - 60, 100, 120, 40)
        is_hover = button_rect.collidepoint(mouse_pos)
        
        color = COLORS['button_hover'] if is_hover else COLORS['button']
        if self.hourglass.is_flipping: color = COLORS['metal_base']
        
        pygame.draw.rect(surface, color, button_rect, border_radius=20)
        pygame.draw.rect(surface, COLORS['glass_edge'], button_rect, 2, border_radius=20)
        
        text = "Flipping..." if self.hourglass.is_flipping else ("Start" if not self.sand_manager.is_running else "Flip (F)")
        button_text = self.font.render(text, True, COLORS['text'])
        text_rect = button_text.get_rect(center=button_rect.center)
        surface.blit(button_text, text_rect)
        
        info_texts = [
            "SPACE - Start timer (flip & count)",
            "F - Manual flip",
            "T - Set Timer",
            "R - Reset",
            "ESC - Exit",
        ]
        
        y = 20
        for text in info_texts:
            info_surface = self.small_font.render(text, True, COLORS['text'])
            surface.blit(info_surface, (20, y))
            y += 25
        
        status = self.controller.get_status()
        
        if status['is_running']:
            time_text = f"Time: {status['remaining_time']:.1f}s / {self.sand_manager.target_time}s"
            text_surface = self.font.render(time_text, True, COLORS['metal_accent'])
            text_rect = text_surface.get_rect(center=(CONFIG['screen_width'] // 2, 160))
            surface.blit(text_surface, text_rect)
            
            total_particles = status['total_particles']
            if total_particles > 0:
                progress = (status['passed_particles'] / total_particles) * 100
                expected_progress = min(100, (status['elapsed_time'] / self.sand_manager.target_time) * 100)
                progress_text = f"Progress: {progress:.1f}% (Target: {expected_progress:.1f}%)"
                text_surface = self.small_font.render(progress_text, True, COLORS['text_shadow'])
                text_rect = text_surface.get_rect(center=(CONFIG['screen_width'] // 2, 190))
                surface.blit(text_surface, text_rect)
            
            if status['is_paused']:
                pause_text = "PAUSED"
                text_surface = self.font.render(pause_text, True, COLORS['button'])
                text_rect = text_surface.get_rect(center=(CONFIG['screen_width'] // 2, 220))
                surface.blit(text_surface, text_rect)
    
    def run(self):
        dt_accumulator = 0
        physics_dt = 1/120
        last_time = time.time()
        
        while self.running:
            current_time = time.time()
            frame_dt = min(current_time - last_time, 0.05)
            last_time = current_time
            
            self.handle_events()
            
            if self.hourglass.update_flip(frame_dt):
                self.sand_manager.flip_particles()
                if self.pending_start:
                    self.sand_manager.start(self.stored_duration)
                    self.pending_start = False
                else:
                    self.sand_manager.resume()
            
            dt_accumulator += frame_dt
            while dt_accumulator >= physics_dt:
                if not self.hourglass.is_flipping and not self.sand_manager.is_paused:
                    self.space.step(physics_dt)
                if not self.hourglass.is_flipping:
                    self.sand_manager.update(physics_dt)
                dt_accumulator -= physics_dt
            
            self.game_surface.fill(COLORS['bg'])
            self.hourglass.draw(self.game_surface)
            self.sand_manager.draw(self.game_surface)
            self.draw_ui(self.game_surface)
            
            self.screen.fill(COLORS['bg'])
            screen_w, screen_h = self.screen.get_size()
            game_w, game_h = self.game_surface.get_size()
            
            self.scale = min(screen_w / game_w, screen_h / game_h)
            
            if self.scale > 0:
                scaled_w, scaled_h = int(game_w * self.scale), int(game_h * self.scale)
                scaled_surface = pygame.transform.smoothscale(self.game_surface, (scaled_w, scaled_h))
                self.blit_rect = scaled_surface.get_rect(center=self.screen.get_rect().center)
                self.screen.blit(scaled_surface, self.blit_rect)

            pygame.display.flip()
            self.clock.tick(CONFIG['fps'])
        
        pygame.quit()
        sys.exit()

if __name__ == "__main__":
    simulator = FlippableSimulator()
    simulator.run()