//! FoE (File over EtherCAT) 实例
//!
//! 对应 C# Slave/FoE.cs
//! 提供文件上传/下载操作。
//!
//! # 异步使用 (对齐 C# FoEInstance.DownloadAsync / UploadAsync)
//!
//! FoE 固件升级常耗时数分钟, 强烈建议在异步上下文中执行以避免阻塞其它协议。
//! 本 crate 提供**双轨**异步 API:
//!
//! ## 轨道 1: std::thread (无依赖, 默认启用)
//!
//! ```ignore
//! use ethercat::slave::foe::FoEInstance;
//!
//! let handle = FoEInstance::download_blocking(0, 1, "firmware.bin".into(), None, Some(60_000));
//! let firmware = handle.join().unwrap()?;
//!
//! let handle = FoEInstance::upload_blocking(0, 1, "firmware.bin".into(), vec![/* data */], None, Some(60_000));
//! handle.join().unwrap()?;
//! ```
//!
//! ## 轨道 2: tokio async (可选 feature `async-tokio`)
//!
//! ```ignore
//! let foe = slave.foe();
//! let firmware = foe.download_async("firmware.bin".into(), None, Some(60_000)).await?;
//! foe.upload_async("firmware.bin".into(), firmware_bytes, None, Some(60_000)).await?;
//!
//! // tokio timeout 包装
//! let result = tokio::time::timeout(
//!     std::time::Duration::from_secs(120),
//!     foe.upload_async("firmware.bin".into(), bytes, None, Some(60_000))
//! ).await??;
//! ```
//!
//! ## 注意事项
//!
//! - 多个从站固件**并行**升级: 在 tokio 中用 `JoinSet` 管理多个 spawn_blocking task
//! - 进度回调 (`FoEProgressHook`) 跨线程安全, 但回调在 DLL 线程触发,
//!   若需推送到 async 通道, 使用 `tokio::sync::mpsc` 或 `std::sync::mpsc`
//! - 一个 tokio runtime 默认 blocking pool 有 512 线程上限, 足够固件升级用量

use crate::utils::ffi;
use crate::data::error::{DarraError, FoEErrorCode, Result};
use std::ffi::{CStr, CString};
use std::sync::{Arc, Mutex, OnceLock};

/// FoE 实例
///
/// 对应 C# FoEInstance
pub struct FoEInstance {
    master_index: u16,
    slave_index: u16,
    /// 默认超时时间（毫秒）
    pub default_timeout_ms: i32,
    /// 默认密码
    pub default_password: u32,
    /// 最近一次 FoE 错误码
    last_error: Option<FoEErrorCode>,
}

impl FoEInstance {
    /// 创建新的 FoE 实例
    pub fn new(master_index: u16, slave_index: u16) -> Self {
        Self {
            master_index,
            slave_index,
            default_timeout_ms: 5000,
            default_password: 0,
            last_error: None,
        }
    }

    /// 获取最近一次 FoE 错误码
    pub fn last_error(&self) -> Option<FoEErrorCode> {
        self.last_error
    }

    /// 从站是否支持 FoE 邮箱协议 (对齐 C# FoEInstance.IsSupported).
    ///
    /// 读取 SII mbx_proto bit 3 (ECT_MBXPROT_FOE = 0x08) 判断支持情况.
    /// DLL 调用失败时保守返回 true (未知能力视为支持).
    pub fn is_supported(&self) -> bool {
        let proto = unsafe { ffi::GetSlaveMailboxProto(self.master_index, self.slave_index) };
        (proto & 0x08) != 0
    }

    /// 从从站设备下载（读取）文件
    ///
    /// 对应 C# FoEInstance.Download
    pub fn download(
        &self,
        filename: &str,
        password: Option<u32>,
        timeout_ms: Option<i32>,
    ) -> Result<Vec<u8>> {
        let c_filename = CString::new(filename)
            .map_err(|_| DarraError::InvalidParameter("文件名包含空字节".into()))?;

        let pwd = password.unwrap_or(self.default_password);
        let timeout_us = (timeout_ms.unwrap_or(self.default_timeout_ms)) * 1000;

        let mut data_ptr = std::ptr::null_mut();
        let mut file_size: i32 = 0;

        let result = unsafe {
            ffi::FOERead(
                self.master_index,
                self.slave_index,
                c_filename.as_ptr(),
                pwd,
                &mut data_ptr,
                &mut file_size,
                timeout_us,
            )
        };

        if result == 0 || data_ptr.is_null() || file_size <= 0 {
            if !data_ptr.is_null() {
                unsafe { ffi::FreeMemory(data_ptr as *mut std::os::raw::c_void); }
            }
            return Err(DarraError::FoeFailed(format!(
                "FoE 下载失败: 从站={}, 文件={}",
                self.slave_index, filename
            )));
        }

        let data = unsafe {
            let slice = std::slice::from_raw_parts(data_ptr as *const u8, file_size as usize);
            let vec = slice.to_vec();
            ffi::FreeMemory(data_ptr as *mut std::os::raw::c_void);
            vec
        };

        Ok(data)
    }

    /// 上传（写入）文件到从站设备
    ///
    /// 对应 C# FoEInstance.Upload
    pub fn upload(
        &self,
        filename: &str,
        file_data: &[u8],
        password: Option<u32>,
        timeout_ms: Option<i32>,
    ) -> Result<()> {
        let c_filename = CString::new(filename)
            .map_err(|_| DarraError::InvalidParameter("文件名包含空字节".into()))?;

        if file_data.is_empty() {
            return Err(DarraError::InvalidParameter("文件数据不能为空".into()));
        }

        let pwd = password.unwrap_or(self.default_password);
        let timeout_us = (timeout_ms.unwrap_or(self.default_timeout_ms)) * 1000;

        let result = unsafe {
            ffi::FOEWrite(
                self.master_index,
                self.slave_index,
                c_filename.as_ptr(),
                pwd,
                file_data.as_ptr() as *const std::os::raw::c_void,
                file_data.len() as i32,
                timeout_us,
            )
        };

        if result == 0 {
            return Err(DarraError::FoeFailed(format!(
                "FoE 上传失败: 从站={}, 文件={}",
                self.slave_index, filename
            )));
        }

        Ok(())
    }

    /// 设置 FoE 进度回调
    pub fn set_progress_hook(&self, callback: ffi::FoEProgressCallback) -> bool {
        unsafe { ffi::FOESetProgressHook(self.master_index, Some(callback)) != 0 }
    }

    /// 清除 FoE 进度回调
    pub fn clear_progress_hook(&self) -> bool {
        unsafe { ffi::FOEClearProgressHook(self.master_index) != 0 }
    }

    // ===================== FoE 取消 / BUSY 回调 (对齐 C# FoE.cs) =====================

    /// 请求取消当前 FoE 传输 (对齐 C# `DLL.FOERequestCancel`).
    ///
    /// 透传 ec_foe.c 的取消标志, 下一次传输循环迭代起生效. 固件烧写中途取消
    /// 可能导致从站 Flash 部分写入, 需重新上传完整固件.
    ///
    /// 返回: true=DLL 已接受取消请求.
    pub fn cancel(&self) -> bool {
        unsafe { ffi::FOERequestCancel(self.master_index, self.slave_index) != 0 }
    }

    /// 清除 FoE 取消标志 (对齐 C# `DLL.FOEClearCancel`).
    ///
    /// Read / Write 入口会自动清零, 仅异常复位用.
    pub fn clear_cancel(&self) -> bool {
        unsafe { ffi::FOEClearCancel(self.master_index, self.slave_index) != 0 }
    }

    /// 设置 BUSY 回调 (ETG.1000.6 Table 93 Done/Entire/BusyText).
    ///
    /// 从站 Flash 烧写慢时周期返回 BUSY 帧, 携带已完成量 `done` / 总量 `entire` /
    /// 可选文本 `text`. 对齐 C# `FoEInstance.EnableProgressHook` 中 `FOESetBusyHook`.
    ///
    /// # 设计
    /// 由于 DLL 层按主站索引注册**单个** C FFI 回调, Rust 端用全局
    /// `OnceLock<Arc<Mutex<Vec<FoEBusyCallback>>>>` 维护回调列表, C trampoline
    /// 快照后遍历调用, 支持多订阅. 对齐 Z-4 `FsoeDataExchangeDispatcher` 模式.
    pub fn set_busy_hook(&self, cb: FoEBusyCallback) {
        add_busy_hook(self.master_index, cb);
    }

    /// 清空本主站所有 BUSY 回调, 并通知 DLL 解绑.
    pub fn clear_busy_hooks(&self) {
        clear_busy_hooks(self.master_index);
    }

    /// 估算文件传输所需的数据包数量
    pub fn estimate_packet_count(file_size: usize, mailbox_size: usize) -> usize {
        if file_size == 0 || mailbox_size == 0 {
            return 0;
        }
        let data_per_packet = if mailbox_size > 6 { mailbox_size - 6 } else { 128 };
        (file_size + data_per_packet - 1) / data_per_packet
    }

    // ===================== 默认超时/密码属性 =====================

    /// 获取默认超时时间 (毫秒)
    pub fn default_timeout_ms(&self) -> i32 {
        self.default_timeout_ms
    }

    /// 设置默认超时时间 (毫秒)
    pub fn set_default_timeout_ms(&mut self, ms: i32) {
        self.default_timeout_ms = ms;
    }

    /// 获取默认密码
    pub fn default_password(&self) -> u32 {
        self.default_password
    }

    /// 设置默认密码
    pub fn set_default_password(&mut self, pw: u32) {
        self.default_password = pw;
    }

    // ===================== CRC 增强传输 =====================

    /// 从从站设备下载（读取）文件，启用 CRC 校验
    ///
    /// 对应 C# FoEInstance.DownloadWithCRC / FOEReadEx
    pub fn download_with_crc(
        &mut self,
        filename: &str,
        password: Option<u32>,
        timeout_ms: Option<i32>,
        enable_crc: bool,
    ) -> Result<Vec<u8>> {
        let c_filename = CString::new(filename)
            .map_err(|_| DarraError::InvalidParameter("文件名包含空字节".into()))?;

        let pwd = password.unwrap_or(self.default_password);
        let timeout_us = (timeout_ms.unwrap_or(self.default_timeout_ms)) * 1000;

        let mut data_ptr = std::ptr::null_mut();
        let mut file_size: i32 = 0;

        // 构建 FoE 扩展选项
        // strict_mode: CRC 启用时设为 1 (对齐 C# FoEInstance.DownloadWithCRC)
        let mut options = ffi::FoEOptions {
            enable_crc: if enable_crc { 1 } else { 0 },
            strict_mode: if enable_crc { 1 } else { 0 },
            auto_append_crc: if enable_crc { 1 } else { 0 },
            expected_crc: 0,
            crc_progress_callback: None,
            crc_callback_userdata: std::ptr::null_mut(),
            reserved: [0u32; 8],
        };

        let result = unsafe {
            ffi::FOEReadEx(
                self.master_index,
                self.slave_index,
                c_filename.as_ptr(),
                pwd,
                &mut data_ptr,
                &mut file_size,
                timeout_us,
                &mut options,
            )
        };

        if result == 0 || data_ptr.is_null() || file_size <= 0 {
            if !data_ptr.is_null() {
                unsafe { ffi::FreeMemory(data_ptr as *mut std::os::raw::c_void); }
            }
            self.last_error = Some(FoEErrorCode::NotDefined);
            return Err(DarraError::FoeFailed(format!(
                "FoE CRC下载失败: 从站={}, 文件={}",
                self.slave_index, filename
            )));
        }

        let data = unsafe {
            let slice = std::slice::from_raw_parts(data_ptr as *const u8, file_size as usize);
            let vec = slice.to_vec();
            ffi::FreeMemory(data_ptr as *mut std::os::raw::c_void);
            vec
        };

        self.last_error = None;
        Ok(data)
    }

    /// 上传（写入）文件到从站设备，启用 CRC 校验
    ///
    /// 对应 C# FoEInstance.UploadWithCRC / FOEWriteEx
    pub fn upload_with_crc(
        &mut self,
        filename: &str,
        file_data: &[u8],
        password: Option<u32>,
        timeout_ms: Option<i32>,
        enable_crc: bool,
    ) -> Result<()> {
        let c_filename = CString::new(filename)
            .map_err(|_| DarraError::InvalidParameter("文件名包含空字节".into()))?;

        if file_data.is_empty() {
            return Err(DarraError::InvalidParameter("文件数据不能为空".into()));
        }

        let pwd = password.unwrap_or(self.default_password);
        let timeout_us = (timeout_ms.unwrap_or(self.default_timeout_ms)) * 1000;

        // 构建 FoE 扩展选项
        // strict_mode: CRC 启用时设为 1 (对齐 C# FoEInstance.UploadWithCRC)
        let mut options = ffi::FoEOptions {
            enable_crc: if enable_crc { 1 } else { 0 },
            strict_mode: if enable_crc { 1 } else { 0 },
            auto_append_crc: if enable_crc { 1 } else { 0 },
            expected_crc: 0,
            crc_progress_callback: None,
            crc_callback_userdata: std::ptr::null_mut(),
            reserved: [0u32; 8],
        };

        let result = unsafe {
            ffi::FOEWriteEx(
                self.master_index,
                self.slave_index,
                c_filename.as_ptr(),
                pwd,
                file_data.as_ptr() as *const std::os::raw::c_void,
                file_data.len() as i32,
                timeout_us,
                &mut options,
            )
        };

        if result == 0 {
            self.last_error = Some(FoEErrorCode::NotDefined);
            return Err(DarraError::FoeFailed(format!(
                "FoE CRC上传失败: 从站={}, 文件={}",
                self.slave_index, filename
            )));
        }

        self.last_error = None;
        Ok(())
    }

    /// 获取 FoE 错误代码的中文描述文本 (对齐 C# GetErrorDescription)
    pub fn get_error_description(error_code: FoEErrorCode) -> &'static str {
        match error_code {
            FoEErrorCode::NotDefined       => "未定义错误",
            FoEErrorCode::NotFound         => "文件未找到",
            FoEErrorCode::AccessDenied     => "访问被拒绝",
            FoEErrorCode::DiskFull         => "磁盘已满",
            FoEErrorCode::Illegal          => "非法操作",
            FoEErrorCode::PacketNumberWrong => "数据包序号错误",
            FoEErrorCode::AlreadyExists    => "文件已存在",
            FoEErrorCode::NoUser           => "无此用户",
            FoEErrorCode::BootstrapOnly    => "仅 Bootstrap 模式可用",
            FoEErrorCode::NotBootstrap     => "不在 Bootstrap 模式",
            FoEErrorCode::NoRights         => "权限不足",
            FoEErrorCode::ProgramError     => "程序错误",
        }
    }
}

// ===================== 异步 API (轨道 1: std::thread, 无依赖) =====================

impl FoEInstance {
    /// FoE 下载 (从从站读取文件, std::thread 异步包装)
    ///
    /// 返回 `JoinHandle`, 用户通过 `.join()` 等待结果。
    pub fn download_blocking(
        master_index: u16,
        slave_index: u16,
        filename: String,
        password: Option<u32>,
        timeout_ms: Option<i32>,
    ) -> std::thread::JoinHandle<Result<Vec<u8>>> {
        std::thread::spawn(move || {
            let foe = FoEInstance::new(master_index, slave_index);
            foe.download(&filename, password, timeout_ms)
        })
    }

    /// FoE 上传 (写入文件到从站, std::thread 异步包装)
    pub fn upload_blocking(
        master_index: u16,
        slave_index: u16,
        filename: String,
        file_data: Vec<u8>,
        password: Option<u32>,
        timeout_ms: Option<i32>,
    ) -> std::thread::JoinHandle<Result<()>> {
        std::thread::spawn(move || {
            let foe = FoEInstance::new(master_index, slave_index);
            foe.upload(&filename, &file_data, password, timeout_ms)
        })
    }
}

// ===================== 异步 API (轨道 2: tokio, feature 门控) =====================

#[cfg(feature = "async-tokio")]
impl FoEInstance {
    /// FoE 下载 (tokio async, 使用 spawn_blocking)
    pub async fn download_async(
        &self,
        filename: String,
        password: Option<u32>,
        timeout_ms: Option<i32>,
    ) -> Result<Vec<u8>> {
        let master = self.master_index;
        let slave = self.slave_index;
        let default_pwd = self.default_password;
        let default_to = self.default_timeout_ms;
        tokio::task::spawn_blocking(move || {
            let mut foe = FoEInstance::new(master, slave);
            foe.default_password = default_pwd;
            foe.default_timeout_ms = default_to;
            foe.download(&filename, password, timeout_ms)
        })
        .await
        .map_err(|e| DarraError::Other(format!("tokio join error: {}", e)))?
    }

    /// FoE 上传 (tokio async, 使用 spawn_blocking)
    pub async fn upload_async(
        &self,
        filename: String,
        file_data: Vec<u8>,
        password: Option<u32>,
        timeout_ms: Option<i32>,
    ) -> Result<()> {
        let master = self.master_index;
        let slave = self.slave_index;
        let default_pwd = self.default_password;
        let default_to = self.default_timeout_ms;
        tokio::task::spawn_blocking(move || {
            let mut foe = FoEInstance::new(master, slave);
            foe.default_password = default_pwd;
            foe.default_timeout_ms = default_to;
            foe.upload(&filename, &file_data, password, timeout_ms)
        })
        .await
        .map_err(|e| DarraError::Other(format!("tokio join error: {}", e)))?
    }
}

// ===================== IMailboxProtocol trait impl (对齐 C# FoEInstance) =====================

impl crate::abstractions::MailboxProtocol for FoEInstance {
    fn protocol_type(&self) -> u8 { 0x04 }
    fn protocol_name(&self) -> &'static str { "FoE" }

    fn is_supported(&self) -> bool {
        FoEInstance::is_supported(self)
    }

    fn last_error_code(&self) -> u32 {
        match self.last_error {
            Some(e) => e as u32,
            None => 0,
        }
    }

    fn statistics(&self) -> crate::abstractions::MailboxStatistics {
        let mut stats = ffi::EcMbxStatsC::default();
        let rc = unsafe {
            ffi::mbx_get_stats_by_master(
                self.master_index, self.slave_index, 0x04, &mut stats,
            )
        };
        if rc == 1 {
            stats.into()
        } else {
            crate::abstractions::MailboxStatistics::empty()
        }
    }

    fn reset_statistics(&self) {
        unsafe {
            ffi::mbx_reset_stats_by_master(self.master_index, self.slave_index, 0x04);
        }
    }
}

// ===================== FoE BUSY 事件分发 (对齐 C# FoEProgressEventArgs.Busy*) =====================

/// FoE BUSY 事件 (ETG.1000.6 Table 93).
///
/// 从站 Flash 烧写过程中周期回传的"请继续等待"帧, 携带当前进度信息.
#[derive(Debug, Clone)]
pub struct FoEBusyEvent {
    /// 从站索引
    pub slave_index: u16,
    /// 已完成量 (Done 字段)
    pub done: u16,
    /// 总量 (Entire 字段)
    pub entire: u16,
    /// 可选文本 (BusyText 字段, 无文本时为空串)
    pub text: String,
    /// 当前 BUSY 重试计数
    pub retry_idx: i32,
    /// 估算百分比 (0-100, entire=0 时为 0)
    pub percent: u32,
}

impl FoEBusyEvent {
    fn new(slave: u16, done: u16, entire: u16, text: String, retry_idx: i32) -> Self {
        let percent = if entire > 0 {
            (((done as u64) * 100) / entire as u64).min(100) as u32
        } else {
            0
        };
        Self {
            slave_index: slave,
            done,
            entire,
            text,
            retry_idx,
            percent,
        }
    }
}

/// FoE BUSY 回调类型 (对齐任务清单 `set_busy_hook` 签名).
pub type FoEBusyCallback = Arc<dyn Fn(&FoEBusyEvent) + Send + Sync>;

/// 每主站独立的回调队列. key=master_index, value=回调列表.
fn busy_registry() -> &'static Mutex<std::collections::HashMap<u16, Vec<FoEBusyCallback>>> {
    static REG: OnceLock<Mutex<std::collections::HashMap<u16, Vec<FoEBusyCallback>>>> =
        OnceLock::new();
    REG.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
}

/// 当前已注册到 DLL 的主站集合 (用于 clear 时是否要回调 `FOESetBusyHook(None)`).
fn busy_bound_masters() -> &'static Mutex<std::collections::HashSet<u16>> {
    static BOUND: OnceLock<Mutex<std::collections::HashSet<u16>>> = OnceLock::new();
    BOUND.get_or_init(|| Mutex::new(std::collections::HashSet::new()))
}

/// C FFI trampoline — DLL 通过本函数把 BUSY 帧回传到 Rust 侧,
/// 快照当前主站的 Rust 回调列表后依次调用. `panic!` 被 `catch_unwind` 吞掉,
/// 保证不会从 FFI 边界抛出.
extern "C" fn busy_trampoline(
    slave: u16,
    done: u16,
    entire: u16,
    text: *const std::os::raw::c_char,
    retry_idx: std::os::raw::c_int,
) {
    let text_str = if text.is_null() {
        String::new()
    } else {
        unsafe { CStr::from_ptr(text) }
            .to_string_lossy()
            .into_owned()
    };

    // 注意: trampoline 无法知道 master_index (DLL C FFI 没传这个参数),
    // 所以我们把所有已注册主站的回调都调一遍 — 每个回调里用户自行根据
    // slave_index 过滤. 这与 C# `FoEProgressCallback` 的形态一致.
    let snapshots: Vec<(u16, Vec<FoEBusyCallback>)> = match busy_registry().lock() {
        Ok(g) => g.iter().map(|(k, v)| (*k, v.clone())).collect(),
        Err(_) => return,
    };

    if snapshots.is_empty() {
        return;
    }

    for (_master, cbs) in &snapshots {
        for cb in cbs {
            let ev = FoEBusyEvent::new(slave, done, entire, text_str.clone(), retry_idx as i32);
            let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| cb(&ev)));
        }
    }
}

/// 注册一个 BUSY 回调到指定主站. 首次注册时调用 `FOESetBusyHook` 绑定 DLL.
fn add_busy_hook(master_index: u16, cb: FoEBusyCallback) {
    if let Ok(mut reg) = busy_registry().lock() {
        reg.entry(master_index).or_default().push(cb);
    }

    // 若本主站尚未绑定 DLL, 绑定一次
    let need_bind = match busy_bound_masters().lock() {
        Ok(mut set) => set.insert(master_index),
        Err(_) => false,
    };
    if need_bind {
        unsafe {
            let _ = ffi::FOESetBusyHook(master_index, Some(busy_trampoline));
        }
    }
}

/// 清空指定主站所有 BUSY 回调并通知 DLL 解绑.
fn clear_busy_hooks(master_index: u16) {
    if let Ok(mut reg) = busy_registry().lock() {
        reg.remove(&master_index);
    }
    let was_bound = match busy_bound_masters().lock() {
        Ok(mut set) => set.remove(&master_index),
        Err(_) => false,
    };
    if was_bound {
        unsafe {
            let _ = ffi::FOESetBusyHook(master_index, None);
        }
    }
}
