use core::fmt;
use std::cell::Cell;
use std::collections::{BTreeMap, BTreeSet};
use time::Date;
use super::check::{Op, execute};
use super::holds::HoldView;
use super::state::{
AccountView, BalanceKey, ClearingView, EntryMeta, EntryView, Header, Movement, PeriodView,
Slice, StateView, Terms, Versioned,
};
use crate::model::{
AccountHandle, AccountPath, Amount, Balance, Currency, EntryId, IdempotencyKey, LedgerPolicy,
PeriodId, PostingRef,
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Read {
EntryByKey(IdempotencyKey),
AccountByPath(AccountPath),
Account(AccountHandle),
AccountTerms(AccountHandle, Date),
Currency(Currency, Date),
CurrencyLatest(Currency),
Policy(Date),
PolicyLatest,
PeriodAt(Date),
Period(PeriodId),
FirstUnsealed,
Entry(EntryId),
ReversedBy(EntryId),
Hold(EntryId),
OutstandingHoldDebits(AccountHandle, Currency),
OutstandingHoldCredits(AccountHandle, Currency),
Balance(BalanceKey),
ClearingByKey(IdempotencyKey),
ClearingAt(u64),
Applied(PostingRef),
AllBalances,
PostingsAfter(Date),
PnlSlices(Date),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Fact<const P: u8> {
EntryByKey(IdempotencyKey, Option<EntryMeta>),
AccountByPath(AccountPath, Option<AccountView>),
Account(AccountHandle, Option<AccountView>),
AccountTerms(AccountHandle, Date, Option<Terms>),
Currency(Currency, Date, Option<u8>),
CurrencyLatest(Currency, Option<Versioned<u8>>),
Policy(Date, LedgerPolicy),
PolicyLatest(Option<Versioned<LedgerPolicy>>),
PeriodAt(Date, Option<PeriodView<P>>),
Period(PeriodId, Option<PeriodView<P>>),
FirstUnsealed(Option<PeriodView<P>>),
Entry(EntryId, Option<EntryView<P>>),
ReversedBy(EntryId, Option<EntryId>),
Hold(EntryId, Option<HoldView<P>>),
OutstandingHoldDebits(AccountHandle, Currency, Amount<P>),
OutstandingHoldCredits(AccountHandle, Currency, Amount<P>),
Balance(BalanceKey, Balance<P>),
ClearingByKey(IdempotencyKey, Option<ClearingView<P>>),
ClearingAt(u64, Option<ClearingView<P>>),
Applied(PostingRef, Amount<P>),
AllBalances(Vec<(BalanceKey, Balance<P>)>),
PostingsAfter(Date, Vec<Movement<P>>),
PnlSlices(Date, Vec<Slice<P>>),
}
impl<const P: u8> Fact<P> {
#[must_use]
pub fn read(&self) -> Read {
match self {
Self::EntryByKey(key, _) => Read::EntryByKey(key.clone()),
Self::AccountByPath(path, _) => Read::AccountByPath(path.clone()),
Self::Account(handle, _) => Read::Account(*handle),
Self::AccountTerms(handle, on, _) => Read::AccountTerms(*handle, *on),
Self::Currency(code, on, _) => Read::Currency(*code, *on),
Self::CurrencyLatest(code, _) => Read::CurrencyLatest(*code),
Self::Policy(on, _) => Read::Policy(*on),
Self::PolicyLatest(_) => Read::PolicyLatest,
Self::PeriodAt(date, _) => Read::PeriodAt(*date),
Self::Period(id, _) => Read::Period(id.clone()),
Self::FirstUnsealed(_) => Read::FirstUnsealed,
Self::Entry(id, _) => Read::Entry(*id),
Self::ReversedBy(id, _) => Read::ReversedBy(*id),
Self::Hold(id, _) => Read::Hold(*id),
Self::OutstandingHoldDebits(handle, code, _) => {
Read::OutstandingHoldDebits(*handle, *code)
}
Self::OutstandingHoldCredits(handle, code, _) => {
Read::OutstandingHoldCredits(*handle, *code)
}
Self::Balance(key, _) => Read::Balance(*key),
Self::ClearingByKey(key, _) => Read::ClearingByKey(key.clone()),
Self::ClearingAt(position, _) => Read::ClearingAt(*position),
Self::Applied(posting, _) => Read::Applied(*posting),
Self::AllBalances(_) => Read::AllBalances,
Self::PostingsAfter(date, _) => Read::PostingsAfter(*date),
Self::PnlSlices(date, _) => Read::PnlSlices(*date),
}
}
#[must_use]
pub fn answer(view: &impl StateView<P>, read: &Read) -> Self {
match read {
Read::EntryByKey(key) => Self::EntryByKey(key.clone(), view.entry_by_key(key)),
Read::AccountByPath(path) => {
Self::AccountByPath(path.clone(), view.account_by_path(path))
}
Read::Account(handle) => Self::Account(*handle, view.account(*handle)),
Read::AccountTerms(handle, on) => {
Self::AccountTerms(*handle, *on, view.account_terms(*handle, *on))
}
Read::Currency(code, on) => Self::Currency(*code, *on, view.currency(*code, *on)),
Read::CurrencyLatest(code) => Self::CurrencyLatest(*code, view.currency_latest(*code)),
Read::Policy(on) => Self::Policy(*on, view.policy(*on)),
Read::PolicyLatest => Self::PolicyLatest(view.policy_latest()),
Read::PeriodAt(date) => Self::PeriodAt(*date, view.period_at(*date)),
Read::Period(id) => Self::Period(id.clone(), view.period(id)),
Read::FirstUnsealed => Self::FirstUnsealed(view.first_unsealed()),
Read::Entry(id) => Self::Entry(*id, view.entry(*id)),
Read::ReversedBy(id) => Self::ReversedBy(*id, view.reversed_by(*id)),
Read::Hold(id) => Self::Hold(*id, view.hold(*id)),
Read::OutstandingHoldDebits(handle, code) => Self::OutstandingHoldDebits(
*handle,
*code,
view.outstanding_hold_debits(*handle, *code),
),
Read::OutstandingHoldCredits(handle, code) => Self::OutstandingHoldCredits(
*handle,
*code,
view.outstanding_hold_credits(*handle, *code),
),
Read::Balance(key) => Self::Balance(*key, view.balance(*key)),
Read::ClearingByKey(key) => Self::ClearingByKey(key.clone(), view.clearing_by_key(key)),
Read::ClearingAt(position) => Self::ClearingAt(*position, view.clearing_at(*position)),
Read::Applied(posting) => Self::Applied(*posting, view.applied(*posting)),
Read::AllBalances => Self::AllBalances(view.all_balances()),
Read::PostingsAfter(date) => Self::PostingsAfter(*date, view.postings_after(*date)),
Read::PnlSlices(date) => Self::PnlSlices(*date, view.pnl_slices(*date)),
}
}
}
pub struct Snapshot<const P: u8> {
header: Header<P>,
facts: BTreeMap<Read, Fact<P>>,
misses: Cell<BTreeSet<Read>>,
}
impl<const P: u8> fmt::Debug for Snapshot<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Snapshot")
.field("header", &self.header)
.field("facts", &self.facts)
.finish_non_exhaustive()
}
}
impl<const P: u8> Snapshot<P> {
#[must_use]
pub fn new(header: Header<P>) -> Self {
Self {
header,
facts: BTreeMap::new(),
misses: Cell::new(BTreeSet::new()),
}
}
pub fn insert(&mut self, fact: Fact<P>) {
self.facts.insert(fact.read(), fact);
}
fn find<T>(&self, read: Read, pick: impl FnOnce(&Fact<P>) -> Option<T>) -> Option<T> {
let found = self.facts.get(&read).and_then(pick);
if found.is_none() {
let mut misses = self.misses.take();
misses.insert(read);
self.misses.set(misses);
}
found
}
fn take_misses(&self) -> Vec<Read> {
self.misses.take().into_iter().collect()
}
}
#[must_use]
pub fn plan<const P: u8>(snapshot: &Snapshot<P>, op: &Op<P>) -> Vec<Read> {
snapshot.take_misses();
let _decided = execute(snapshot, op);
snapshot.take_misses()
}
macro_rules! lookup {
($self:ident, $read:expr, $fact:pat => $value:expr) => {
$self.find($read, |fact| match fact {
$fact => Some($value),
_ => None,
})
};
}
impl<const P: u8> StateView<P> for Snapshot<P> {
fn header(&self) -> Header<P> {
self.header.clone()
}
fn complete(&self) -> bool {
let misses = self.misses.take();
let complete = misses.is_empty();
self.misses.set(misses);
complete
}
fn entry_by_key(&self, key: &IdempotencyKey) -> Option<EntryMeta> {
lookup!(self, Read::EntryByKey(key.clone()), Fact::EntryByKey(_, meta) => *meta).flatten()
}
fn account_by_path(&self, path: &AccountPath) -> Option<AccountView> {
lookup!(self, Read::AccountByPath(path.clone()), Fact::AccountByPath(_, account) => account.clone())
.flatten()
}
fn account(&self, handle: AccountHandle) -> Option<AccountView> {
lookup!(self, Read::Account(handle), Fact::Account(_, account) => account.clone()).flatten()
}
fn account_terms(&self, handle: AccountHandle, on: Date) -> Option<Terms> {
lookup!(self, Read::AccountTerms(handle, on), Fact::AccountTerms(_, _, terms) => terms.clone())
.flatten()
}
fn currency(&self, code: Currency, on: Date) -> Option<u8> {
lookup!(self, Read::Currency(code, on), Fact::Currency(_, _, exponent) => *exponent)
.flatten()
}
fn currency_latest(&self, code: Currency) -> Option<Versioned<u8>> {
lookup!(self, Read::CurrencyLatest(code), Fact::CurrencyLatest(_, latest) => latest.clone())
.flatten()
}
fn policy(&self, on: Date) -> LedgerPolicy {
lookup!(self, Read::Policy(on), Fact::Policy(_, policy) => policy.clone())
.unwrap_or_default()
}
fn policy_latest(&self) -> Option<Versioned<LedgerPolicy>> {
lookup!(self, Read::PolicyLatest, Fact::PolicyLatest(latest) => latest.clone()).flatten()
}
fn period_at(&self, date: Date) -> Option<PeriodView<P>> {
lookup!(self, Read::PeriodAt(date), Fact::PeriodAt(_, period) => period.clone()).flatten()
}
fn period(&self, id: &PeriodId) -> Option<PeriodView<P>> {
lookup!(self, Read::Period(id.clone()), Fact::Period(_, period) => period.clone()).flatten()
}
fn first_unsealed(&self) -> Option<PeriodView<P>> {
lookup!(self, Read::FirstUnsealed, Fact::FirstUnsealed(period) => period.clone()).flatten()
}
fn entry(&self, id: EntryId) -> Option<EntryView<P>> {
lookup!(self, Read::Entry(id), Fact::Entry(_, entry) => entry.clone()).flatten()
}
fn reversed_by(&self, id: EntryId) -> Option<EntryId> {
lookup!(self, Read::ReversedBy(id), Fact::ReversedBy(_, by) => *by).flatten()
}
fn hold(&self, id: EntryId) -> Option<HoldView<P>> {
lookup!(self, Read::Hold(id), Fact::Hold(_, hold) => hold.clone()).flatten()
}
fn outstanding_hold_debits(&self, handle: AccountHandle, currency: Currency) -> Amount<P> {
lookup!(self, Read::OutstandingHoldDebits(handle, currency), Fact::OutstandingHoldDebits(_, _, amount) => *amount)
.unwrap_or(Amount::ZERO)
}
fn outstanding_hold_credits(&self, handle: AccountHandle, currency: Currency) -> Amount<P> {
lookup!(self, Read::OutstandingHoldCredits(handle, currency), Fact::OutstandingHoldCredits(_, _, amount) => *amount)
.unwrap_or(Amount::ZERO)
}
fn balance(&self, key: BalanceKey) -> Balance<P> {
lookup!(self, Read::Balance(key), Fact::Balance(_, balance) => *balance)
.unwrap_or(Balance::ZERO)
}
fn clearing_by_key(&self, key: &IdempotencyKey) -> Option<ClearingView<P>> {
lookup!(self, Read::ClearingByKey(key.clone()), Fact::ClearingByKey(_, clearing) => clearing.clone())
.flatten()
}
fn clearing_at(&self, position: u64) -> Option<ClearingView<P>> {
lookup!(self, Read::ClearingAt(position), Fact::ClearingAt(_, clearing) => clearing.clone())
.flatten()
}
fn applied(&self, posting: PostingRef) -> Amount<P> {
lookup!(self, Read::Applied(posting), Fact::Applied(_, amount) => *amount)
.unwrap_or(Amount::ZERO)
}
fn all_balances(&self) -> Vec<(BalanceKey, Balance<P>)> {
lookup!(self, Read::AllBalances, Fact::AllBalances(balances) => balances.clone())
.unwrap_or_default()
}
fn postings_after(&self, date: Date) -> Vec<Movement<P>> {
lookup!(self, Read::PostingsAfter(date), Fact::PostingsAfter(_, movements) => movements.clone())
.unwrap_or_default()
}
fn pnl_slices(&self, through: Date) -> Vec<Slice<P>> {
lookup!(self, Read::PnlSlices(through), Fact::PnlSlices(_, slices) => slices.clone())
.unwrap_or_default()
}
}