#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
生产级屏幕投影点击检测系统
适用于Qt嵌入的Panda3D环境
"""

from panda3d.core import Point3, Point2, Vec3
from typing import List, Optional, Tuple, Dict, Any
import math

class ScreenProjectionPicker:
    def __init__(self, camera, lens, window):
        """
        初始化屏幕投影拾取器
        
        Args:
            camera: Panda3D相机节点
            lens: 相机镜头
            window: 渲染窗口
        """
        self.camera = camera
        self.lens = lens
        self.window = window
        self.objects = []  # 可选中的对象列表
        
    def registerObject(self, node_path, name: str, priority: int = 0, 
                      custom_radius: Optional[float] = None):
        """
        注册可选中的对象
        
        Args:
            node_path: Panda3D节点路径
            name: 对象名称
            priority: 选择优先级（数值越大优先级越高）
            custom_radius: 自定义选择半径（世界坐标）
        """
        obj_info = {
            'node': node_path,
            'name': name,
            'priority': priority,
            'custom_radius': custom_radius,
            'world_pos': node_path.getPos(),
            'bounds': node_path.getBounds()
        }
        self.objects.append(obj_info)
        
    def unregisterObject(self, node_path):
        """取消注册对象"""
        self.objects = [obj for obj in self.objects if obj['node'] != node_path]
        
    def updateObjectPositions(self):
        """更新所有对象的世界位置（在对象移动后调用）"""
        for obj_info in self.objects:
            obj_info['world_pos'] = obj_info['node'].getPos()
            obj_info['bounds'] = obj_info['node'].getBounds()
    
    def worldToScreen(self, world_pos: Point3) -> Optional[Tuple[float, float]]:
        """
        将世界坐标转换为屏幕像素坐标
        
        Args:
            world_pos: 世界坐标位置
            
        Returns:
            (pixel_x, pixel_y) 或 None（如果不在屏幕内）
        """
        # 转换到相机坐标系
        cam_pos = self.camera.getRelativePoint(self.camera.getParent(), world_pos)
        
        # 投影到屏幕坐标
        screen_point = Point2()
        if self.lens.project(cam_pos, screen_point):
            # 转换为像素坐标
            win_width = self.window.getXSize()
            win_height = self.window.getYSize()
            
            pixel_x = (screen_point.getX() + 1.0) * win_width * 0.5
            pixel_y = (1.0 - screen_point.getY()) * win_height * 0.5
            
            return pixel_x, pixel_y
        
        return None
    
    def getObjectScreenRadius(self, obj_info: Dict) -> float:
        """
        计算对象在屏幕上的选择半径
        
        Args:
            obj_info: 对象信息字典
            
        Returns:
            屏幕像素半径
        """
        world_pos = obj_info['world_pos']
        
        # 如果有自定义半径，使用自定义半径
        if obj_info['custom_radius'] is not None:
            world_radius = obj_info['custom_radius']
        else:
            # 否则根据包围盒计算半径
            bounds = obj_info['bounds']
            if bounds.isEmpty():
                world_radius = 1.0  # 默认半径
            else:
                # 使用包围盒的最大尺寸作为半径
                min_point = bounds.getMin()
                max_point = bounds.getMax()
                size = max_point - min_point
                world_radius = max(size.getX(), size.getY(), size.getZ()) * 0.5
        
        # 计算屏幕半径
        center_screen = self.worldToScreen(world_pos)
        if not center_screen:
            return 0
            
        # 使用对象右侧边缘点计算屏幕半径
        edge_pos = world_pos + Vec3(world_radius, 0, 0)
        edge_screen = self.worldToScreen(edge_pos)
        
        if edge_screen:
            return abs(edge_screen[0] - center_screen[0])
        
        # 如果边缘点不可见，使用距离相机的距离估算
        cam_distance = (world_pos - self.camera.getPos()).length()
        if cam_distance > 0:
            # 基于透视投影的简单估算
            angular_size = world_radius / cam_distance
            fov = self.lens.getHfov()  # 水平视场角
            win_width = self.window.getXSize()
            screen_radius = angular_size * (win_width * 0.5) / math.tan(math.radians(fov * 0.5))
            return max(10.0, screen_radius)  # 最小10像素
        
        return 20.0  # 默认半径
    
    def pickObject(self, mouse_x: int, mouse_y: int, 
                   tolerance_multiplier: float = 1.0) -> Optional[Dict]:
        """
        在指定鼠标位置选择对象
        
        Args:
            mouse_x: 鼠标X坐标（像素）
            mouse_y: 鼠标Y坐标（像素）
            tolerance_multiplier: 容差倍数（增大可使选择更容易）
            
        Returns:
            选中的对象信息字典，或None
        """
        candidates = []
        
        for obj_info in self.objects:
            # 计算对象在屏幕上的位置
            screen_pos = self.worldToScreen(obj_info['world_pos'])
            if not screen_pos:
                continue  # 对象不在屏幕内
                
            screen_x, screen_y = screen_pos
            
            # 计算屏幕半径
            screen_radius = self.getObjectScreenRadius(obj_info)
            
            # 应用容差倍数
            effective_radius = screen_radius * tolerance_multiplier
            
            # 计算鼠标到对象中心的距离
            dx = mouse_x - screen_x
            dy = mouse_y - screen_y
            distance = math.sqrt(dx * dx + dy * dy)
            
            # 检查是否在选择范围内
            if distance <= effective_radius:
                # 计算选择分数（距离越近、优先级越高分数越高）
                distance_score = 1.0 - (distance / effective_radius)
                priority_score = obj_info['priority'] * 10.0
                total_score = distance_score + priority_score
                
                candidates.append((obj_info, distance, total_score))
        
        if not candidates:
            return None
        
        # 按分数排序，选择最高分的对象
        candidates.sort(key=lambda x: x[2], reverse=True)
        return candidates[0][0]
    
    def pickMultipleObjects(self, mouse_x: int, mouse_y: int,
                           tolerance_multiplier: float = 1.0) -> List[Dict]:
        """
        在指定位置选择所有可能的对象（用于上下文菜单等）
        
        Args:
            mouse_x: 鼠标X坐标
            mouse_y: 鼠标Y坐标
            tolerance_multiplier: 容差倍数
            
        Returns:
            按优先级排序的对象列表
        """
        candidates = []
        
        for obj_info in self.objects:
            screen_pos = self.worldToScreen(obj_info['world_pos'])
            if not screen_pos:
                continue
                
            screen_x, screen_y = screen_pos
            screen_radius = self.getObjectScreenRadius(obj_info)
            effective_radius = screen_radius * tolerance_multiplier
            
            dx = mouse_x - screen_x
            dy = mouse_y - screen_y
            distance = math.sqrt(dx * dx + dy * dy)
            
            if distance <= effective_radius:
                distance_score = 1.0 - (distance / effective_radius)
                priority_score = obj_info['priority'] * 10.0
                total_score = distance_score + priority_score
                
                candidates.append((obj_info, distance, total_score))
        
        # 按分数排序
        candidates.sort(key=lambda x: x[2], reverse=True)
        return [candidate[0] for candidate in candidates]
    
    def getDebugInfo(self, mouse_x: int, mouse_y: int) -> str:
        """获取调试信息"""
        debug_lines = [f"鼠标位置: ({mouse_x}, {mouse_y})"]
        debug_lines.append(f"注册对象数量: {len(self.objects)}")
        debug_lines.append("")
        
        for obj_info in self.objects:
            screen_pos = self.worldToScreen(obj_info['world_pos'])
            if screen_pos:
                screen_x, screen_y = screen_pos
                screen_radius = self.getObjectScreenRadius(obj_info)
                dx = mouse_x - screen_x
                dy = mouse_y - screen_y
                distance = math.sqrt(dx * dx + dy * dy)
                
                debug_lines.append(f"{obj_info['name']}:")
                debug_lines.append(f"  世界位置: {obj_info['world_pos']}")
                debug_lines.append(f"  屏幕位置: ({screen_x:.1f}, {screen_y:.1f})")
                debug_lines.append(f"  屏幕半径: {screen_radius:.1f}")
                debug_lines.append(f"  鼠标距离: {distance:.1f}")
                debug_lines.append(f"  可选中: {'是' if distance <= screen_radius else '否'}")
                debug_lines.append("")
            else:
                debug_lines.append(f"{obj_info['name']}: 不在屏幕内")
                debug_lines.append("")
        
        return "\n".join(debug_lines)

# 使用示例
def example_usage():
    """使用示例"""
    print("""
# 在您的应用中使用屏幕投影拾取器：

# 1. 初始化拾取器
picker = ScreenProjectionPicker(self.cam, self.cam.node().getLens(), self.win)

# 2. 注册可选中的对象
picker.registerObject(cube_node, "立方体", priority=1)
picker.registerObject(sphere_node, "球体", priority=2, custom_radius=3.0)

# 3. 在鼠标点击事件中使用
def on_mouse_click(mouse_x, mouse_y):
    selected_obj = picker.pickObject(mouse_x, mouse_y, tolerance_multiplier=1.2)
    if selected_obj:
        print(f"选中了: {selected_obj['name']}")
    else:
        print("没有选中任何对象")

# 4. 对象移动后更新位置
picker.updateObjectPositions()

# 5. 获取调试信息
debug_info = picker.getDebugInfo(mouse_x, mouse_y)
print(debug_info)
    """)

if __name__ == "__main__":
    example_usage() 