xiangqi_tui 0.1.0

Chinese chess (Xiangqi) TUI client with UCI/UCCI engine and opening book support
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use crate::engine::analysis_store::EngineAnalysisStore;
use crate::engine::analysis_types::{EngineAnalyzeResult, EngineInfoCandidate};

use super::analyze_infinite_line::patch_store_from_state;
use super::engine_core::UciUcciEngine;
use super::info_state::{
    EngineInfoState, apply_parsed_info_to_state, select_main_line_from_candidates,
};
#[cfg(test)]
use super::test_hook::try_test_analyze_hook;
use super::types::{EngineAnalyzeRequest, EngineStdoutPoll};
use super::ui_helpers::stub_result;
use crate::engine::protocol::parse_uci_style_info_tokens;
use crate::runtime_log;

impl UciUcciEngine {
    pub fn analyze_autoplay_once_with_cancel(
        &mut self,
        fen: &str,
        depth: Option<i32>,
        movetime_ms: Option<i32>,
        search_nodes: Option<i32>,
        progress_store: Option<&Arc<Mutex<EngineAnalysisStore>>>,
        cancel: Option<&Arc<AtomicBool>>,
    ) -> EngineAnalyzeResult {
        self.analyze_inner(
            EngineAnalyzeRequest {
                fen,
                depth,
                movetime_ms,
                search_moves: None,
                search_nodes,
                multipv_override: Some(1),
                cancel: cancel.cloned(),
            },
            progress_store,
        )
    }

    fn analyze_inner(
        &mut self,
        req: EngineAnalyzeRequest<'_>,
        progress_store: Option<&Arc<Mutex<EngineAnalysisStore>>>,
    ) -> EngineAnalyzeResult {
        #[cfg(test)]
        if let Some(v) = try_test_analyze_hook(&req) {
            return v;
        }

        let EngineAnalyzeRequest {
            fen,
            depth,
            movetime_ms,
            search_moves,
            search_nodes,
            multipv_override,
            cancel,
        } = req;
        if self.engine_path.is_none() {
            return stub_result();
        }
        if self.rt.lock().map(|g| g.is_none()).unwrap_or(true) {
            self.start();
        }
        if self.rt.lock().map(|g| g.is_none()).unwrap_or(true) {
            return stub_result();
        }
        let _ = self.send_cmd("stop");
        let multi_pv = multipv_override.unwrap_or(1);
        if let Err(e) = self.send_cmd(&format!("setoption name MultiPV value {multi_pv}")) {
            runtime_log::warn(format!(
                "[engine_analyze] send_err stage=set_multipv err={e}"
            ));
            self.terminate_locked();
            return stub_result();
        }
        if !fen.trim().is_empty() {
            if let Err(e) = self.send_cmd(&format!("position fen {}", fen.trim())) {
                runtime_log::warn(format!(
                    "[engine_analyze] send_err stage=position_fen err={e}"
                ));
                self.terminate_locked();
                return stub_result();
            }
        } else if let Err(e) = self.send_cmd("position startpos") {
            runtime_log::warn(format!(
                "[engine_analyze] send_err stage=position_startpos err={e}"
            ));
            self.terminate_locked();
            return stub_result();
        }
        self.clear_queue();
        let mut go_suffix = String::new();
        if let Some(moves) = search_moves {
            let filtered: Vec<&str> = moves
                .iter()
                .map(|s| s.trim())
                .filter(|s| !s.is_empty())
                .collect();
            if !filtered.is_empty() {
                go_suffix.push_str(" searchmoves ");
                go_suffix.push_str(&filtered.join(" "));
            }
        }
        if let Some(mt) = movetime_ms.filter(|&m| m > 0) {
            if let Err(e) = self.send_cmd(&format!("go movetime {mt}{go_suffix}")) {
                runtime_log::warn(format!(
                    "[engine_analyze] send_err stage=go_movetime err={e}"
                ));
                self.terminate_locked();
                return stub_result();
            }
        } else if let Some(n) = search_nodes.filter(|&n| n > 0) {
            if let Err(e) = self.send_cmd(&format!("go nodes {n}{go_suffix}")) {
                runtime_log::warn(format!("[engine_analyze] send_err stage=go_nodes err={e}"));
                self.terminate_locked();
                return stub_result();
            }
        } else {
            let d = depth.unwrap_or(8).max(1);
            if let Err(e) = self.send_cmd(&format!("go depth {d}{go_suffix}")) {
                runtime_log::warn(format!("[engine_analyze] send_err stage=go_depth err={e}"));
                self.terminate_locked();
                return stub_result();
            }
        }
        let mut st = EngineInfoState::new();
        let push_progress = |st: &EngineInfoState| {
            let Some(store) = progress_store else {
                return;
            };
            if let Ok(mut guard) = store.lock() {
                patch_store_from_state(&mut guard, fen, st);
            }
        };
        let deadline = Instant::now() + Duration::from_secs(30);
        let mut got_best = false;
        while Instant::now() < deadline && !got_best {
            if cancel
                .as_ref()
                .map(|c| c.load(Ordering::SeqCst))
                .unwrap_or(false)
            {
                let _ = self.send_cmd("stop");
                runtime_log::warn("[engine_analyze] cancelled_by_flag");
                break;
            }
            match self.poll_line(Duration::from_millis(120)) {
                EngineStdoutPoll::Disconnected { child_status } => {
                    runtime_log::warn(format!(
                        "[engine_analyze] disconnected child_status={child_status}"
                    ));
                    break;
                }
                EngineStdoutPoll::Tick => {}
                EngineStdoutPoll::Line(line) => {
                    let line = line.trim();
                    if line.starts_with("info ") {
                        let parts: Vec<&str> = line.split_whitespace().collect();
                        if let Some(parsed) = parse_uci_style_info_tokens(&parts) {
                            apply_parsed_info_to_state(&parsed, &mut st);
                            push_progress(&st);
                        }
                    } else if line.starts_with("bestmove") {
                        let tok: Vec<&str> = line.split_whitespace().collect();
                        if tok.len() >= 2 {
                            st.best_move = tok[1].to_string();
                        }
                        got_best = true;
                    }
                }
            }
        }
        if !got_best {
            let _ = self.send_cmd("stop");
            runtime_log::warn("[engine_analyze] bestmove_timeout_or_disconnected; fallback_result");
        }
        let cand_list: Vec<EngineInfoCandidate> = st.cands_by_rank.values().cloned().collect();
        let mut score_cp: Option<i64> = None;
        if !cand_list.is_empty() {
            let (main_bm, main_sc, main_pv, main_d, main_mate) = select_main_line_from_candidates(
                &cand_list,
                &st.best_move,
                st.score,
                &st.pv,
                st.depth_seen,
                st.mate,
            );
            st.best_move = main_bm;
            st.score = main_sc;
            st.pv = main_pv;
            st.depth_seen = main_d;
            st.mate = main_mate;
            score_cp = cand_list[0].score_cp.map(i64::from);
        }
        if let Some(store) = progress_store
            && let Ok(mut guard) = store.lock()
        {
            patch_store_from_state(&mut guard, fen, &st);
        }
        EngineAnalyzeResult {
            best_move: st.best_move,
            score: st.score,
            score_cp,
            pv: st.pv,
            depth: st.depth_seen,
            candidates: cand_list,
            search_time_ms: st.search_time_ms,
            nps: st.nps,
            nodes: st.nodes,
            wdl: st.wdl,
            mate: st.mate,
        }
    }
}