use std::collections::BTreeSet;
use bevy_ecs::prelude::Commands;
use retiretui_engine::plan::{ID_KEY, Plan};
use serde::Serialize;
use serde::de::DeserializeOwned;
use toml::{Table, Value};
use super::build::FormButton;
use super::cells::{Cell, Column, Shown};
use super::codec::{from_table, to_table};
use super::draft::Draft;
use super::offers::{RefSource, Vocabulary};
use crate::commands::tui::nav::Page;
use crate::commands::tui::present;
#[derive(Clone, Copy)]
pub struct FieldSpec {
pub key: &'static str,
pub label: &'static str,
pub help: &'static str,
pub blank: Option<&'static str>,
pub shown: Option<fn(&Table) -> bool>,
pub derived: Option<Derived>,
pub kind: FieldKind,
}
#[derive(Clone, Copy)]
pub struct Derived {
pub seed: fn(&Table) -> Value,
pub write: fn(&mut Table, &Value),
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum FieldKind {
Text,
Money,
Whole,
Flag,
Choice(Vocabulary),
Ref(RefSource),
Rate,
Share,
Growth,
Trigger,
Presence(&'static str),
Listed(usize),
Order(Vocabulary, usize),
}
pub const GROWTH_HELP: &str =
"Inflation follows the plan's rate, Fixed never grows, or a rate of its own such as 3%.";
pub const BLANK: &str = "None";
impl FieldSpec {
const fn new(key: &'static str, label: &'static str, kind: FieldKind) -> Self {
Self {
key,
label,
help: "",
blank: None,
shown: None,
derived: None,
kind,
}
}
pub const fn text(key: &'static str, label: &'static str) -> Self {
Self::new(key, label, FieldKind::Text)
}
pub const fn money(key: &'static str, label: &'static str) -> Self {
Self::new(key, label, FieldKind::Money)
}
pub const fn whole(key: &'static str, label: &'static str) -> Self {
Self::new(key, label, FieldKind::Whole)
}
pub const fn flag(key: &'static str, label: &'static str) -> Self {
Self::new(key, label, FieldKind::Flag)
}
pub const fn choice(key: &'static str, label: &'static str, vocabulary: Vocabulary) -> Self {
Self::new(key, label, FieldKind::Choice(vocabulary))
}
pub const fn refers(key: &'static str, label: &'static str, source: RefSource) -> Self {
Self::new(key, label, FieldKind::Ref(source))
}
pub const fn trigger(key: &'static str, label: &'static str) -> Self {
Self::new(key, label, FieldKind::Trigger)
}
pub const fn rate(key: &'static str, label: &'static str) -> Self {
Self::new(key, label, FieldKind::Rate)
}
pub const fn share(key: &'static str, label: &'static str) -> Self {
Self::new(key, label, FieldKind::Share)
}
pub const fn growth(key: &'static str, label: &'static str) -> Self {
Self::new(key, label, FieldKind::Growth).blank(present::FOLLOWS_INFLATION)
}
pub const fn presence(key: &'static str, label: &'static str, ticked: &'static str) -> Self {
Self::new(key, label, FieldKind::Presence(ticked))
}
pub const fn listed(key: &'static str, label: &'static str, back: usize) -> Self {
Self::new(key, label, FieldKind::Listed(back))
}
pub const fn ordered(
key: &'static str,
label: &'static str,
vocabulary: Vocabulary,
place: usize,
) -> Self {
Self::new(key, label, FieldKind::Order(vocabulary, place))
}
pub const fn help(self, help: &'static str) -> Self {
Self { help, ..self }
}
pub const fn blank(self, blank: &'static str) -> Self {
Self {
blank: Some(blank),
..self
}
}
pub const fn shown_when(self, shown: fn(&Table) -> bool) -> Self {
Self {
shown: Some(shown),
..self
}
}
pub const fn derived(self, seed: fn(&Table) -> Value, write: fn(&mut Table, &Value)) -> Self {
Self {
derived: Some(Derived { seed, write }),
..self
}
}
pub const fn is_required(&self) -> bool {
self.blank.is_none()
}
pub const fn blank_word(&self) -> &'static str {
match self.blank {
Some(word) => word,
None => BLANK,
}
}
}
pub trait Domain: 'static {
type Item: Serialize + DeserializeOwned;
const PAGE: Page;
const PURPOSE: &'static str;
const PATH: &'static str;
const SINGULAR: &'static str;
const IDENTITY: &'static str = ID_KEY;
const FIELDS: &'static [FieldSpec];
const COLUMNS: &'static [Column];
const BLANK: &'static str;
const RECORD: Option<fn(&Table) -> Vec<[String; 2]>> = None;
fn items(plan: &Plan) -> &[Self::Item];
fn items_mut(plan: &mut Plan) -> &mut Vec<Self::Item>;
}
pub trait Single: 'static {
type Item: Serialize + DeserializeOwned;
const PAGE: Page;
const PATHS: &'static [&'static str];
const FIELDS: &'static [FieldSpec];
fn get(plan: &Plan) -> Self::Item;
fn set(plan: &mut Plan, item: Self::Item);
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Target {
Draft,
Tool,
}
#[derive(Clone, Copy)]
pub struct Ops {
pub surface: Option<Page>,
pub title: &'static str,
pub target: Target,
pub paths: &'static [&'static str],
pub fields: &'static [FieldSpec],
pub item: fn(&Draft, usize) -> Option<Table>,
pub store: fn(&mut Draft, usize, Table) -> Result<(), String>,
pub typed: fn(Table) -> Option<Table>,
pub list: Option<ListOps>,
pub actions: [&'static str; 2],
pub after: Option<fn(FormButton, &mut Commands)>,
}
const ACTIONS: [&str; 2] = ["Discard", "Apply"];
pub trait ToolAnswers: DeserializeOwned + 'static {
const SLOT: &'static str;
}
#[derive(Clone, Copy)]
pub struct ListOps {
pub purpose: &'static str,
pub singular: &'static str,
pub identity: &'static str,
pub columns: &'static [Column],
pub count: fn(&Plan) -> usize,
pub rows: fn(&Plan) -> Vec<Vec<Cell>>,
pub blank: fn(&Plan) -> Table,
pub remove: fn(&mut Plan, usize),
pub record: Option<fn(&Table) -> Vec<[String; 2]>>,
}
impl Ops {
pub const fn of<D: Domain>() -> Self {
Self {
surface: Some(D::PAGE),
title: D::PAGE.title(),
target: Target::Draft,
paths: &[D::PATH],
fields: D::FIELDS,
item: item::<D>,
store: store::<D>,
typed: typed::<D::Item>,
list: Some(ListOps {
purpose: D::PURPOSE,
singular: D::SINGULAR,
identity: D::IDENTITY,
columns: D::COLUMNS,
count: count::<D>,
rows: rows::<D>,
blank: blank::<D>,
remove: remove::<D>,
record: D::RECORD,
}),
actions: ACTIONS,
after: None,
}
}
pub const fn single<S: Single>() -> Self {
Self {
surface: Some(S::PAGE),
title: S::PAGE.title(),
target: Target::Draft,
paths: S::PATHS,
fields: S::FIELDS,
item: single_item::<S>,
store: single_store::<S>,
typed: typed::<S::Item>,
list: None,
actions: ACTIONS,
after: None,
}
}
pub const fn tool<T: ToolAnswers>(
surface: Option<Page>,
title: &'static str,
fields: &'static [FieldSpec],
) -> Self {
Self {
surface,
title,
target: Target::Tool,
paths: &[],
fields,
item: tool_item::<T>,
store: tool_store::<T>,
typed: Some,
list: None,
actions: ACTIONS,
after: None,
}
}
pub const fn acting(
self,
actions: [&'static str; 2],
after: fn(FormButton, &mut Commands),
) -> Self {
Self {
actions,
after: Some(after),
..self
}
}
}
fn tool_item<T: ToolAnswers>(draft: &Draft, index: usize) -> Option<Table> {
(index == 0).then(|| draft.answers::<T>())
}
fn tool_store<T: ToolAnswers>(draft: &mut Draft, _: usize, table: Table) -> Result<(), String> {
from_table::<T>(table.clone())?;
draft.tools.insert(T::SLOT.to_owned(), Value::Table(table));
Ok(())
}
fn typed<Item: Serialize + DeserializeOwned>(table: Table) -> Option<Table> {
from_table::<Item>(table).ok().map(|item| to_table(&item))
}
fn count<D: Domain>(plan: &Plan) -> usize {
D::items(plan).len()
}
fn single_item<S: Single>(draft: &Draft, index: usize) -> Option<Table> {
(index == 0).then(|| to_table(&S::get(&draft.plan)))
}
fn single_store<S: Single>(draft: &mut Draft, _: usize, table: Table) -> Result<(), String> {
S::set(&mut draft.plan, from_table(table)?);
Ok(())
}
fn item<D: Domain>(draft: &Draft, index: usize) -> Option<Table> {
D::items(&draft.plan).get(index).map(to_table)
}
fn rows<D: Domain>(plan: &Plan) -> Vec<Vec<Cell>> {
let columns: Vec<Shown> = D::COLUMNS
.iter()
.map(|column| Shown::of(column, D::FIELDS, D::IDENTITY, plan))
.collect();
D::items(plan)
.iter()
.map(|item| {
let table = to_table(item);
columns
.iter()
.map(|shown| shown.cell(&table, plan))
.collect()
})
.collect()
}
fn store<D: Domain>(draft: &mut Draft, index: usize, table: Table) -> Result<(), String> {
let item = from_table(table)?;
let items = D::items_mut(&mut draft.plan);
match items.get_mut(index) {
Some(slot) => *slot = item,
None => items.push(item),
}
Ok(())
}
const OWNER_KEY: &str = "owner";
fn blank<D: Domain>(plan: &Plan) -> Table {
let mut blank: Table = D::BLANK.parse().unwrap_or_default();
if let (Some(slot), Some(person)) = (blank.get_mut(OWNER_KEY), plan.household.people.first()) {
*slot = Value::String(person.id.clone());
}
if D::IDENTITY == ID_KEY {
blank.insert(D::IDENTITY.to_owned(), Value::String(free_id::<D>(plan)));
}
blank
}
fn free_id<D: Domain>(plan: &Plan) -> String {
let taken: BTreeSet<String> = D::items(plan)
.iter()
.filter_map(|item| match to_table(item).remove(D::IDENTITY) {
Some(Value::String(id)) => Some(id),
_ => None,
})
.collect();
let kind = D::SINGULAR
.split(' ')
.next()
.unwrap_or_default()
.to_lowercase();
(1..=taken.len() + 1)
.map(|n| format!("{kind}-{n}"))
.find(|id| !taken.contains(id))
.unwrap_or(kind)
}
fn remove<D: Domain>(plan: &mut Plan, index: usize) {
let items = D::items_mut(plan);
if index < items.len() {
items.remove(index);
}
}