use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use bevy_ecs::prelude::{Commands, In, Res, ResMut, Resource, World};
use bevy_ecs::system::{IntoSystem, SystemParam};
use plurimus::core::ratatui_core::style::Style;
use plurimus::core::ratatui_core::text::Line;
use plurimus_filepicker::{FilePickerDecorator, RowDecoration};
use retiretui_engine::plan::{Plan, Scenario};
use super::browse::{Badges, Browsing, FilePick};
use crate::commands::tui::command::{Outcome, Pending};
use crate::commands::tui::compare::Compared;
use crate::commands::tui::confirm::Confirm;
use crate::commands::tui::edit::{self, Draft};
use crate::commands::tui::journal;
use crate::commands::tui::session::{self, Session};
use crate::commands::tui::setup;
use crate::commands::tui::tools;
use crate::commands::tui::watch::Watch;
const EXTENSION: &str = "toml";
const STATEMENT_EXTENSION: &str = "xml";
const SAVE_OVER: &str = "Overwrite {}?";
const COMPARED: &str = "compared";
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Kind {
Plan,
Scenario,
Invalid,
}
impl Kind {
fn of(path: &Path) -> Self {
let Ok(text) = std::fs::read_to_string(path) else {
return Self::Invalid;
};
match Scenario::from_toml_str(&text) {
Ok(Some(_)) => Self::Scenario,
Ok(None) if Plan::from_toml_str(&text).is_ok() => Self::Plan,
_ => Self::Invalid,
}
}
const fn badge(self) -> &'static str {
match self {
Self::Plan => "plan",
Self::Scenario => "scenario",
Self::Invalid => "invalid",
}
}
}
pub(super) fn decorate(
badges: Badges,
dim: Style,
compared: &Compared,
) -> Option<FilePickerDecorator> {
let compared: Vec<PathBuf> = match badges {
Badges::None => return None,
Badges::Kind => Vec::new(),
Badges::Compared => compared.paths().map(Path::to_path_buf).collect(),
};
Some(FilePickerDecorator(Box::new(move |path| {
if compared.iter().any(|held| held == path) {
return RowDecoration::default().with_trailing(Line::styled(COMPARED, dim));
}
let kind = Kind::of(path);
let row = RowDecoration::default().with_trailing(Line::styled(kind.badge(), dim));
if kind == Kind::Invalid {
row.with_style(dim)
} else {
row
}
})))
}
#[derive(Resource, Clone, Copy)]
pub struct Pickers {
pub open: FilePick,
save_as: FilePick,
compare: FilePick,
pub ladder: FilePick,
pub claims: FilePick,
new_plan: FilePick,
pub earnings: FilePick,
}
pub fn register(world: &mut World) {
let pickers = Pickers {
open: FilePick {
title: "Open",
extension: EXTENSION,
accepts_new: false,
badges: Badges::Kind,
chosen: world.register_system(open_chosen),
},
save_as: writing(world, "Save as", write_as, SAVE_OVER),
compare: FilePick {
title: "Compare with",
extension: EXTENSION,
accepts_new: false,
badges: Badges::Compared,
chosen: world.register_system(compare_chosen),
},
ladder: writing(
world,
"Write overlay",
tools::ladders::write_overlay,
tools::OVERLAY_OVER,
),
claims: writing(
world,
"Write overlay",
tools::claims::write_overlay,
tools::OVERLAY_OVER,
),
new_plan: writing(world, "New plan as", setup::write_new, SAVE_OVER),
earnings: FilePick {
title: "Import earnings from",
extension: STATEMENT_EXTENSION,
accepts_new: false,
badges: Badges::None,
chosen: world.register_system(edit::record_statement),
},
};
world.insert_resource(pickers);
}
pub fn workspace_holds_a_plan(session: &Session) -> bool {
std::fs::read_dir(session.workspace())
.into_iter()
.flatten()
.flatten()
.map(|entry| entry.path())
.filter(|path| path.extension() == Some(OsStr::new(EXTENSION)))
.any(|path| Kind::of(&path) != Kind::Invalid)
}
pub fn open(pickers: Res<Pickers>, mut browsing: ResMut<Browsing>) -> Outcome {
browsing.open(pickers.open);
Outcome::Done
}
pub fn save_as(
pickers: Res<Pickers>,
draft: Res<Draft>,
mut browsing: ResMut<Browsing>,
) -> Outcome {
if let Some(refusal) = draft.refuse_if_invalid() {
return refusal;
}
browsing.open(pickers.save_as);
Outcome::Done
}
pub fn name_new_plan(pickers: Res<Pickers>, mut browsing: ResMut<Browsing>) {
browsing.open(pickers.new_plan);
}
pub fn compare_with(pickers: Res<Pickers>, mut browsing: ResMut<Browsing>) -> Outcome {
browsing.open(pickers.compare);
Outcome::Done
}
fn open_chosen(In(path): In<PathBuf>, mut pending: ResMut<Pending>) {
pending.defer_open(path);
}
fn compare_chosen(In(path): In<PathBuf>, session: Res<Session>, mut compared: ResMut<Compared>) {
if is_the_document(&session, &path) {
journal::warn(format!("{} is the document", session.file_name()));
return;
}
compared.toggle(path, &session.tables);
}
fn is_the_document(session: &Session, path: &Path) -> bool {
let document = session
.plan_path
.as_deref()
.and_then(|document| document.canonicalize().ok());
document.is_some_and(|document| path.canonicalize().is_ok_and(|path| path == document))
}
fn writing<M: 'static>(
world: &mut World,
title: &'static str,
write: impl IntoSystem<In<PathBuf>, (), M> + Copy + Send + Sync + 'static,
question: &'static str,
) -> FilePick {
let chosen = move |In(path): In<PathBuf>, mut asking: Asking| {
asking.write_or_ask(path, write, question);
};
FilePick {
title,
extension: EXTENSION,
accepts_new: true,
badges: Badges::Kind,
chosen: world.register_system(chosen),
}
}
#[derive(SystemParam)]
struct Asking<'w, 's> {
confirm: ResMut<'w, Confirm>,
commands: Commands<'w, 's>,
}
impl Asking<'_, '_> {
fn write_or_ask<Marker: 'static>(
&mut self,
path: PathBuf,
write: impl IntoSystem<In<PathBuf>, (), Marker> + Send + Sync + 'static,
question: &str,
) {
if !path.exists() {
self.commands.run_system_cached_with(write, path);
return;
}
let question = question.replacen("{}", &session::file_name(&path), 1);
self.confirm.ask(question, "Overwrite", move |commands| {
commands.run_system_cached_with(write, path);
});
}
}
fn write_as(In(path): In<PathBuf>, world: &mut World) {
if let Err(refusal) = edit::write_draft(world.resource::<Draft>(), &path) {
journal::warn(refusal);
return;
}
world.resource_mut::<Draft>().saved_as();
world.insert_resource(Watch::at(path.clone()));
let mut session = world.resource_mut::<Session>();
session.plan_path = Some(path);
journal::say(format!("saved {}", session.file_name()));
}