use std::collections::BTreeMap;
use std::path::Path;
use serde_json::Value;
use xtalforge_commands::Document;
use xtalforge_core::{Limits, Profile, Structure};
use crate::Error;
use crate::answers::{Answer, DocumentSummary};
#[derive(Debug, Clone, PartialEq)]
pub struct SessionOptions {
pub limits: Limits,
}
impl Default for SessionOptions {
fn default() -> Self {
Self {
limits: Limits::new(Profile::Standard),
}
}
}
#[derive(Debug)]
pub(crate) struct Entry {
document: Document,
source: Option<String>,
}
#[derive(Debug)]
pub struct Session {
documents: BTreeMap<String, Entry>,
active: Option<String>,
options: SessionOptions,
}
impl Default for Session {
fn default() -> Self {
Self::new()
}
}
impl Session {
#[must_use]
pub fn new() -> Self {
Self::with_options(SessionOptions::default())
}
#[must_use]
pub fn with_options(options: SessionOptions) -> Self {
Self {
documents: BTreeMap::new(),
active: None,
options,
}
}
#[must_use]
pub fn options(&self) -> &SessionOptions {
&self.options
}
#[must_use]
pub fn active_name(&self) -> Option<&str> {
self.active.as_deref()
}
pub fn structure(&self, name: Option<&str>) -> Result<&Structure, Error> {
Ok(self.document(name)?.structure())
}
#[must_use]
pub fn active(&self) -> Option<&Document> {
self.active_name()
.and_then(|name| self.documents.get(name))
.map(|entry| &entry.document)
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.documents.contains_key(name)
}
#[must_use]
pub fn len(&self) -> usize {
self.documents.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.documents.is_empty()
}
pub(crate) fn resolve(&self, name: Option<&str>) -> Result<String, Error> {
match name {
Some(name) => {
if self.documents.contains_key(name) {
Ok(name.to_owned())
} else {
Err(Error::UnknownDocument(name.to_owned()))
}
}
None => self
.active_name()
.map(str::to_owned)
.ok_or(Error::NoDocument),
}
}
pub(crate) fn document(&self, name: Option<&str>) -> Result<&Document, Error> {
let name = self.resolve(name)?;
self.documents
.get(&name)
.map(|entry| &entry.document)
.ok_or(Error::UnknownDocument(name))
}
pub(crate) fn document_mut(&mut self, name: Option<&str>) -> Result<&mut Document, Error> {
let name = self.resolve(name)?;
self.documents
.get_mut(&name)
.map(|entry| &mut entry.document)
.ok_or(Error::UnknownDocument(name))
}
pub(crate) fn source(&self, name: &str) -> Option<&str> {
self.documents
.get(name)
.and_then(|entry| entry.source.as_deref())
}
pub(crate) fn insert(
&mut self,
name: impl Into<String>,
structure: Structure,
source: Option<String>,
) -> String {
let name = name.into();
self.documents.insert(
name.clone(),
Entry {
document: Document::new(structure),
source,
},
);
self.active = Some(name.clone());
name
}
pub(crate) fn available_name(&self, path: Option<&str>, fallback: &str) -> String {
let stem = path
.and_then(|path| Path::new(path).file_stem())
.and_then(|stem| stem.to_str())
.filter(|stem| !stem.is_empty())
.unwrap_or(fallback);
if !self.documents.contains_key(stem) {
return stem.to_owned();
}
let mut index = 2;
loop {
let candidate = format!("{stem}-{index}");
if !self.documents.contains_key(&candidate) {
return candidate;
}
index += 1;
}
}
pub(crate) fn remove(&mut self, name: &str) -> bool {
let removed = self.documents.remove(name).is_some();
if removed && self.active.as_deref() == Some(name) {
self.active = self.documents.keys().next().cloned();
}
removed
}
#[must_use]
pub fn summaries(&self) -> Vec<DocumentSummary> {
self.documents
.iter()
.filter_map(|(name, entry)| summary(name, entry).ok())
.collect()
}
#[must_use]
pub fn documents_answer(&self) -> Answer {
Answer::Documents {
documents: self.summaries(),
active: self.active.clone(),
}
}
pub fn run(&mut self, verb: &str, arguments: &Value) -> Result<Answer, Error> {
if crate::verbs::lookup(verb).is_none() {
return Err(Error::UnknownVerb(verb.to_owned()));
}
crate::run::run(self, verb, arguments)
}
}
pub(crate) fn summary(name: &str, entry: &Entry) -> Result<DocumentSummary, Error> {
let structure = entry.document.structure();
let summary = structure.summary()?;
Ok(DocumentSummary {
name: name.to_owned(),
title: summary.name,
formula: summary.formula,
n_sites: summary.n_sites,
volume: summary.volume,
density: summary.density,
source: entry.source.clone(),
dirty: entry.document.dirty(),
})
}
pub(crate) fn entry_summary(
name: &str,
document: &Document,
source: Option<&str>,
) -> Result<DocumentSummary, Error> {
let summary = document.structure().summary()?;
Ok(DocumentSummary {
name: name.to_owned(),
title: summary.name,
formula: summary.formula,
n_sites: summary.n_sites,
volume: summary.volume,
density: summary.density,
source: source.map(str::to_owned),
dirty: document.dirty(),
})
}