use std::sync::Arc;
use bevy_app::{App, Update};
use bevy_ecs::change_detection::{DetectChanges, DetectChangesMut};
use bevy_ecs::prelude::{IntoScheduleConfigs, Res, ResMut, Resource};
use retiretui_engine::market::{History, RunError, monte_carlo};
use retiretui_engine::params::TaxTables;
use retiretui_engine::plan::Plan;
use crate::commands::tui::compare::Compared;
use crate::commands::tui::nav::{ActivePage, Page};
use crate::commands::tui::present::{self, SAME};
use crate::commands::tui::session::{Projected, Session};
use crate::commands::tui::theme::Repainted;
use crate::commands::tui::tools::markets::MarketHistory;
use crate::commands::tui::tools::{Searches, Worker, running_text};
pub fn plugin(app: &mut App) {
app.init_resource::<Successes>();
app.add_systems(Update, work_through.before(Repainted));
}
const WAITING: &str = "waiting";
const FAILED: &str = "—";
const PERCENT: f64 = 100.0;
const SAME_POINTS: f64 = 0.05;
#[derive(Resource, Default)]
pub struct Successes {
answered: Vec<(Arc<Plan>, Option<f64>)>,
running: Option<Running>,
}
struct Running {
plan: Arc<Plan>,
worker: Worker<Result<f64, RunError>>,
total: usize,
shown: usize,
}
impl Running {
fn start(plan: &Plan, tables: TaxTables, history: History) -> Self {
let plan = Arc::new(plan.clone());
let ran = Arc::clone(&plan);
let worker = Worker::spawn(move |progress| {
let found = monte_carlo(&ran, &tables, &history, progress)?;
Ok(found.runs.success_rate())
});
let total = usize::try_from(plan.market().trials()).unwrap_or_default();
Self {
plan,
worker,
total,
shown: 0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Success {
Waiting,
Running { done: usize, total: usize },
Rate(f64),
Failed,
}
impl Success {
pub(crate) fn text(self) -> String {
match self {
Self::Waiting => WAITING.to_owned(),
Self::Running { done, total } => running_text(done, total),
Self::Rate(rate) => present::rate(rate),
Self::Failed => FAILED.to_owned(),
}
}
pub(crate) fn against(self, base: Self) -> String {
let (Self::Rate(own), Self::Rate(base)) = (self, base) else {
return self.text();
};
let points = (own - base) * PERCENT;
if points.abs() < SAME_POINTS {
SAME.to_owned()
} else {
format!("{points:+.1} pts")
}
}
}
impl Successes {
pub(crate) fn of(&self, plan: &Plan) -> Success {
if let Some((_, answer)) = self.answered.iter().find(|(held, _)| **held == *plan) {
return answer.map_or(Success::Failed, Success::Rate);
}
match &self.running {
Some(running) if *running.plan == *plan => Success::Running {
done: running.shown,
total: running.total,
},
_ => Success::Waiting,
}
}
#[cfg(test)]
pub fn is_running(&self) -> bool {
self.running.is_some()
}
fn has_answered(&self) -> bool {
let running = self.running.as_ref();
running.is_some_and(|running| running.worker.is_finished())
}
fn receive(&mut self) {
let Some(running) = self.running.take() else {
return;
};
let answer = match running.worker.join() {
Some(Ok(rate)) => Some(rate),
Some(Err(RunError::Cancelled)) => return,
Some(Err(RunError::Refused(_))) | None => None,
};
self.answered.push((running.plan, answer));
}
fn queue(
&mut self,
(kept, wanted): (&[&Plan], &[&Plan]),
tables: &TaxTables,
history: &History,
) {
self.answered
.retain(|(plan, _)| kept.contains(&plan.as_ref()));
let is_answered = |plan: &Plan| self.answered.iter().any(|(held, _)| **held == *plan);
let next = wanted.iter().copied().find(|plan| !is_answered(plan));
if self.running.as_ref().map(|running| running.plan.as_ref()) == next {
return;
}
self.running = next.map(|plan| Running::start(plan, tables.clone(), history.clone()));
}
}
fn count_runs(successes: &mut ResMut<Successes>) {
let running = successes.bypass_change_detection().running.as_mut();
let is_counted = running.is_some_and(|running| {
let done = running.worker.progress.done();
std::mem::replace(&mut running.shown, done) != done
});
if is_counted {
successes.set_changed();
}
}
fn wanted(active: Page, searches: Searches, plans: usize) -> usize {
match active {
Page::Compare => plans,
Page::Overview if searches.0 => 1,
_ => 0,
}
}
pub(crate) fn work_through(
(projected, compared, active): (Res<Projected>, Res<Compared>, Res<ActivePage>),
(session, history, searches): (Res<Session>, Res<MarketHistory>, Res<Searches>),
mut successes: ResMut<Successes>,
) {
let has_answered = successes.has_answered();
if has_answered {
successes.receive();
}
let is_moved = projected.is_changed()
|| compared.is_changed()
|| active.is_changed()
|| searches.is_changed();
if !has_answered && !is_moved {
count_runs(&mut successes);
return;
}
let document = std::iter::once(&projected.plan);
let kept: Vec<&Plan> = document.chain(compared.plans()).collect();
let wanted = wanted(active.0, *searches, kept.len());
successes.queue((&kept, &kept[..wanted]), &session.tables, &history.0);
}