#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
智达GCMS设备驱动

支持智达GCMS设备的TCP通信协议，包括状态查询、方法获取、样品分析等功能。
通信协议版本：1.0.1
"""

import base64
import json
import socket
import time
import os
from pathlib import Path


class ZhidaClient:
    def __init__(self, host='192.168.3.184', port=5792, timeout=10.0):
        # 如果部署在智达GCMS上位机本地，可使用localhost: host='127.0.0.1'
        """
        初始化智达GCMS客户端
        
        Args:
            host (str): 设备IP地址，本地部署时可使用'127.0.0.1'
            port (int): 通信端口，默认5792
            timeout (float): 超时时间，单位秒
        """
        self.host = host
        self.port = port
        self.timeout = timeout
        self.sock = None
        self._ros_node = None  # ROS节点引用，由框架设置
    
    def post_init(self, ros_node):
        """
        ROS节点初始化后的回调方法，用于建立设备连接
        
        Args:
            ros_node: ROS节点实例
        """
        self._ros_node = ros_node
        try:
            self.connect()
            ros_node.lab_logger().info(f"智达GCMS设备连接成功: {self.host}:{self.port}")
        except Exception as e:
            ros_node.lab_logger().error(f"智达GCMS设备连接失败: {e}")
            # 不抛出异常，允许节点继续运行，后续可以重试连接

    def connect(self):
        """
        建立TCP连接到智达GCMS设备
        
        Raises:
            ConnectionError: 连接失败时抛出
        """
        try:
            self.sock = socket.create_connection((self.host, self.port), timeout=self.timeout)
            # 确保后续 recv/send 都会在 timeout 秒后抛 socket.timeout
            self.sock.settimeout(self.timeout)
        except Exception as e:
            raise ConnectionError(f"Failed to connect to {self.host}:{self.port} - {str(e)}")

    def close(self):
        """
        关闭与智达GCMS设备的TCP连接
        """
        if self.sock:
            try:
                self.sock.close()
            except Exception:
                pass  # 忽略关闭时的错误
            finally:
                self.sock = None

    def _send_command(self, cmd: dict) -> dict:
        """
        发送命令到智达GCMS设备并接收响应
        
        Args:
            cmd (dict): 要发送的命令字典
            
        Returns:
            dict: 设备响应的JSON数据
            
        Raises:
            ConnectionError: 连接错误
            TimeoutError: 超时错误
        """
        if not self.sock:
            raise ConnectionError("Not connected to device")

        try:
            # 发送JSON命令（UTF-8编码）
            payload = json.dumps(cmd, ensure_ascii=False).encode('utf-8')
            self.sock.sendall(payload)

            # 循环接收数据直到能成功解析完整JSON
            buffer = bytearray()
            start = time.time()
            while True:
                try:
                    chunk = self.sock.recv(4096)
                    if not chunk:
                        # 对端关闭连接，尝试解析已接收的数据
                        if buffer:
                            try:
                                text = buffer.decode('utf-8', errors='strict')
                                return json.loads(text)
                            except (UnicodeDecodeError, json.JSONDecodeError):
                                pass
                        break
                    buffer.extend(chunk)
                    
                    # 尝试解码和解析JSON
                    text = buffer.decode('utf-8', errors='strict')
                    try:
                        return json.loads(text)
                    except json.JSONDecodeError:
                        # JSON不完整，继续接收
                        pass
                        
                except socket.timeout:
                    # 超时时，尝试解析已接收的数据
                    if buffer:
                        try:
                            text = buffer.decode('utf-8', errors='strict')
                            return json.loads(text)
                        except (UnicodeDecodeError, json.JSONDecodeError):
                            pass
                    raise TimeoutError(f"recv() timed out after {self.timeout:.1f}s")
                
                # 防止死循环，总时长超过2倍超时时间就报错
                if time.time() - start > self.timeout * 2:
                    # 最后尝试解析已接收的数据
                    if buffer:
                        try:
                            text = buffer.decode('utf-8', errors='strict')
                            return json.loads(text)
                        except (UnicodeDecodeError, json.JSONDecodeError):
                            pass
                    raise TimeoutError(f"No complete JSON received after {time.time() - start:.1f}s")

            # 连接关闭，如果有数据则尝试解析
            if buffer:
                try:
                    text = buffer.decode('utf-8', errors='strict')
                    return json.loads(text)
                except (UnicodeDecodeError, json.JSONDecodeError):
                    pass
            
            raise ConnectionError("Connection closed before JSON could be parsed")
            
        except Exception as e:
            if isinstance(e, (ConnectionError, TimeoutError)):
                raise
            else:
                raise ConnectionError(f"Command send failed: {str(e)}")

    def get_status(self) -> str:
        """
        获取设备状态
        
        Returns:
            str: 设备状态 (Idle|Offline|Error|Busy|RunSample|Unknown)
        """
        if not self.sock:
            # 尝试重新连接
            try:
                self.connect()
                if self._ros_node:
                    self._ros_node.lab_logger().info("智达GCMS设备重新连接成功")
            except Exception as e:
                if self._ros_node:
                    self._ros_node.lab_logger().warning(f"智达GCMS设备连接失败: {e}")
                return "Offline"
        
        try:
            response = self._send_command({"command": "getstatus"})
            return response.get("result", "Unknown")
        except Exception as e:
            if self._ros_node:
                self._ros_node.lab_logger().warning(f"获取设备状态失败: {e}")
            return "Error"
    
    def get_methods(self) -> dict:
        """
        获取当前Project的方法列表
        
        Returns:
            dict: 包含方法列表的响应
        """
        if not self.sock:
            try:
                self.connect()
                if self._ros_node:
                    self._ros_node.lab_logger().info("智达GCMS设备重新连接成功")
            except Exception as e:
                if self._ros_node:
                    self._ros_node.lab_logger().warning(f"智达GCMS设备连接失败: {e}")
                return {"error": "Device not connected"}
        
        try:
            return self._send_command({"command": "getmethods"})
        except Exception as e:
            if self._ros_node:
                self._ros_node.lab_logger().warning(f"获取方法列表失败: {e}")
            return {"error": str(e)}


    
    def get_version(self) -> dict:
        """
        获取接口版本和InLabPAL固件版本
        
        Returns:
            dict: 响应格式 {"result": "OK|Error", "message": "Interface:x.x.x;FW:x.x.x.xxx"}
        """
        return self._send_command({"command": "version"})
    
    def put_tray(self) -> dict:
        """
        放盘操作，准备InLabPAL进样器
        
        注意：此功能仅在特殊场景下使用，例如：
        - 机械臂比较短，需要让开一个位置
        - 盘支架是可移动的，需要进样器配合做动作
        
        对于宜宾深势这套配置，空间足够，不需要这个额外的控制组件。
        
        Returns:
            dict: 响应格式 {"result": "OK|Error", "message": "ready_info|error_info"}
        """
        return self._send_command({"command": "puttray"})

    def start_with_csv_file(self, string: str = None, csv_file_path: str = None) -> dict:
        """
        使用CSV文件启动分析（支持ROS2动作调用）
        
        Args:
            string (str): CSV文件路径（ROS2参数名）
            csv_file_path (str): CSV文件路径（兼容旧接口）
            
        Returns:
            dict: ROS2动作结果格式 {"return_info": str, "success": bool}
            
        Raises:
            FileNotFoundError: CSV文件不存在
            Exception: 文件读取或通信错误
        """
        try:
            # 支持两种参数传递方式：ROS2的string参数和直接的csv_file_path参数
            file_path = string if string is not None else csv_file_path
            if file_path is None:
                error_msg = "未提供CSV文件路径参数"
                if self._ros_node:
                    self._ros_node.lab_logger().error(error_msg)
                return {"return_info": error_msg, "success": False}
            
            # 使用Path对象进行更健壮的文件处理
            csv_path = Path(file_path)
            if not csv_path.exists():
                error_msg = f"CSV文件不存在: {file_path}"
                if self._ros_node:
                    self._ros_node.lab_logger().error(error_msg)
                return {"return_info": error_msg, "success": False}
            
            # 读取CSV文件内容（UTF-8编码，替换未知字符）
            csv_content = csv_path.read_text(encoding="utf-8", errors="replace")
            
            # 转换为Base64编码
            b64_content = base64.b64encode(csv_content.encode('utf-8')).decode('ascii')
            
            if self._ros_node:
                self._ros_node.lab_logger().info(f"正在发送CSV文件到智达GCMS: {file_path}")
                self._ros_node.lab_logger().info(f"Base64编码长度: {len(b64_content)} 字符")
            
            # 发送start命令
            response = self._send_command({
                "command": "start",
                "message": b64_content
            })
            
            # 转换为ROS2动作结果格式
            if response.get("result") == "OK":
                success_msg = f"智达GCMS分析启动成功: {response.get('message', 'Unknown')}"
                if self._ros_node:
                    self._ros_node.lab_logger().info(success_msg)
                return {"return_info": success_msg, "success": True}
            else:
                error_msg = f"智达GCMS分析启动失败: {response.get('message', 'Unknown error')}"
                if self._ros_node:
                    self._ros_node.lab_logger().error(error_msg)
                return {"return_info": error_msg, "success": False}
            
        except Exception as e:
            error_msg = f"CSV文件处理失败: {str(e)}"
            if self._ros_node:
                self._ros_node.lab_logger().error(error_msg)
            return {"return_info": error_msg, "success": False}

    def start(self, string: str = None, text: str = None) -> dict:
        """
        使用Base64编码数据启动分析（支持ROS2动作调用）
        
        Args:
            string (str): Base64编码的CSV数据（ROS2参数名）
            text (str): Base64编码的CSV数据（兼容旧接口）
            
        Returns:
            dict: ROS2动作结果格式 {"return_info": str, "success": bool}
        """
        try:
            # 支持两种参数传递方式：ROS2的string参数和原有的text参数
            b64_content = string if string is not None else text
            if b64_content is None:
                error_msg = "未提供Base64编码数据参数"
                if self._ros_node:
                    self._ros_node.lab_logger().error(error_msg)
                return {"return_info": error_msg, "success": False}
            
            if self._ros_node:
                self._ros_node.lab_logger().info(f"正在发送Base64数据到智达GCMS")
                self._ros_node.lab_logger().info(f"Base64编码长度: {len(b64_content)} 字符")
            
            # 发送start命令
            response = self._send_command({
                "command": "start",
                "message": b64_content
            })
            
            # 转换为ROS2动作结果格式
            if response.get("result") == "OK":
                success_msg = f"智达GCMS分析启动成功: {response.get('message', 'Unknown')}"
                if self._ros_node:
                    self._ros_node.lab_logger().info(success_msg)
                return {"return_info": success_msg, "success": True}
            else:
                error_msg = f"智达GCMS分析启动失败: {response.get('message', 'Unknown error')}"
                if self._ros_node:
                    self._ros_node.lab_logger().error(error_msg)
                return {"return_info": error_msg, "success": False}
            
        except Exception as e:
            error_msg = f"Base64数据处理失败: {str(e)}"
            if self._ros_node:
                self._ros_node.lab_logger().error(error_msg)
            return {"return_info": error_msg, "success": False}

    def abort(self) -> dict:
        """
        停止当前运行的分析
        
        Returns:
            dict: 响应格式 {"result": "OK|Error", "message": "error_info"}
        """
        return self._send_command({"command": "abort"})


def test_zhida_client():
    """
    测试智达GCMS客户端功能
    """
    client = ZhidaClient()
    
    try:
        # 连接设备
        print("Connecting to Zhida GCMS...")
        client.connect()
        print("Connected successfully!")
        
        # 获取设备状态
        print(f"Device status: {client.status}")
        
        # 获取版本信息
        version_info = client.get_version()
        print(f"Version info: {version_info}")
        
        # 获取方法列表
        methods = client.get_methods()
        print(f"Available methods: {methods}")
        
        # 测试CSV文件发送（如果文件存在）
        csv_file = Path(__file__).parent / "zhida_gcms-test_1.csv"
        if csv_file.exists():
            print(f"Testing CSV file: {csv_file}")
            result = client.start_with_csv_file(str(csv_file))
            print(f"Start result: {result}")
        
    except Exception as e:
        print(f"Error: {str(e)}")
    
    finally:
        # 关闭连接
        client.close()
        print("Connection closed.")


if __name__ == "__main__":
    test_zhida_client()
