use std::borrow::Cow;
use std::path::{Path, PathBuf};
use bevy_ecs::change_detection::{DetectChanges, DetectChangesMut};
use bevy_ecs::prelude::{Changed, ChildOf, Component, Entity, Query, Res, ResMut, Resource, With};
use bevy_ecs::system::SystemParam;
use plurimus::widgets::ActiveDescendant;
use retiretui_engine::params::TaxTables;
use retiretui_engine::plan::Plan;
use retiretui_engine::project::{Projection, YearRow, project};
use crate::commands::directory_of;
#[derive(Resource)]
pub struct Session {
pub plan_path: Option<PathBuf>,
launched: PathBuf,
pub tables: TaxTables,
}
pub const NO_DOCUMENT: &str = "no document is open";
const NO_DOCUMENT_NAME: &str = "no document";
pub fn blank_plan(start_year: i16) -> String {
BLANK_PLAN.replacen("START_YEAR", &start_year.to_string(), 1)
}
const BLANK_PLAN: &str = r#"schema = 1
[plan]
start_year = START_YEAR
horizon_age = 95
inflation = 0.025
[household]
filing = "single"
[[household.people]]
id = "me"
birth = 1970-01-01
[[accounts]]
id = "cash"
kind = "cash"
owner = "me"
balance = 0
"#;
#[derive(Resource)]
pub struct Projected {
pub plan: Plan,
pub projection: Projection,
}
impl Session {
pub fn at(path: PathBuf, tables: TaxTables) -> Self {
if path.is_dir() {
return Self {
plan_path: None,
launched: path,
tables,
};
}
let launched = directory_of(&path).to_path_buf();
Self {
plan_path: Some(path),
launched,
tables,
}
}
pub fn workspace(&self) -> &Path {
self.plan_path
.as_deref()
.map_or(&self.launched, directory_of)
}
pub fn document(&self) -> Result<&Path, String> {
self.plan_path
.as_deref()
.ok_or_else(|| NO_DOCUMENT.to_owned())
}
pub fn file_name(&self) -> Cow<'_, str> {
self.plan_path
.as_deref()
.map_or(Cow::Borrowed(NO_DOCUMENT_NAME), file_name)
}
pub fn is_empty(&self) -> bool {
self.plan_path.is_none()
}
}
pub fn file_name(path: &Path) -> Cow<'_, str> {
path.file_name()
.map_or(Cow::Borrowed(""), |name| name.to_string_lossy())
}
impl Projected {
pub fn blank(tables: &TaxTables, today: Today) -> Self {
let plan = Plan::from_toml_str(&blank_plan(today.0)).expect("the blank plan parses");
let projection = project(&plan, tables);
Self { plan, projection }
}
}
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug)]
pub struct Today(pub i16);
impl Today {
pub fn now() -> Self {
Self(jiff::Zoned::now().date().year())
}
}
#[derive(Resource, Default, Clone, Copy, PartialEq, Eq, Debug)]
pub struct YearCursor(pub Option<i16>);
impl YearCursor {
pub fn resolve(self, today: Today, planned: (i16, i16), shows: (i16, i16)) -> i16 {
let year = self.0.unwrap_or_else(|| clamp(today.0, planned));
clamp(year, shows)
}
}
fn clamp(year: i16, (first, last): (i16, i16)) -> i16 {
year.max(first).min(last)
}
pub fn span(years: &[YearRow]) -> (i16, i16) {
match (years.first(), years.last()) {
(Some(first), Some(last)) => (first.year, last.year),
_ => (i16::MIN, i16::MAX),
}
}
#[derive(Component, Clone, Copy)]
pub struct RowYear(pub i16);
pub fn track_cursor<T: Component>(
tables: Query<(Entity, &ActiveDescendant), (With<T>, Changed<ActiveDescendant>)>,
rows: Query<(&RowYear, &ChildOf)>,
projected: Res<Projected>,
today: Res<Today>,
mut cursor: ResMut<YearCursor>,
) {
let Some((table, &ActiveDescendant(Some(on)))) = tables.iter().next() else {
return;
};
let Ok((&RowYear(year), _)) = rows.get(on) else {
return;
};
let years = rows.iter().filter(|(_, parent)| parent.parent() == table);
let shows = years.fold((year, year), |(first, last), (&RowYear(row), _)| {
(first.min(row), last.max(row))
});
let planned = span(&projected.projection.years);
if cursor.resolve(*today, planned, shows) != year {
cursor.set_if_neq(YearCursor(Some(year)));
}
}
#[derive(Resource, Default)]
pub struct LedgerRun(pub Option<(String, Projected)>);
impl LedgerRun {
pub fn over<'a>(&'a self, projected: &'a Projected) -> &'a Projected {
self.0.as_ref().map_or(projected, |(_, run)| run)
}
}
#[derive(SystemParam)]
pub struct Shown<'w> {
pub projected: Res<'w, Projected>,
pub basis: Res<'w, Basis>,
cursor: Res<'w, YearCursor>,
today: Res<'w, Today>,
pub run: Res<'w, LedgerRun>,
}
impl Shown<'_> {
pub fn is_changed(&self) -> bool {
self.projected.is_changed()
|| self.basis.is_changed()
|| self.cursor.is_changed()
|| self.run.is_changed()
}
pub fn is_year_changed(&self) -> bool {
self.cursor.is_changed()
}
pub fn ledger(&self) -> &Projected {
self.run.over(&self.projected)
}
pub fn year(&self) -> i16 {
let planned = span(&self.projected.projection.years);
self.cursor.resolve(*self.today, planned, planned)
}
pub fn year_among(&self, shows: (i16, i16)) -> i16 {
let planned = span(&self.projected.projection.years);
self.cursor.resolve(*self.today, planned, shows)
}
pub fn row(&self) -> Option<&YearRow> {
let year = self.year();
let years = &self.ledger().projection.years;
years.iter().find(|row| row.year == year)
}
}
#[derive(Resource, Default, Clone, Copy, PartialEq, Eq)]
pub struct Basis {
pub nominal: bool,
}