use std::ops::Range;
use crate::text::lowercase;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Form {
Ubiquitous,
EventDriven,
StateDriven,
Unwanted,
Optional,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Span {
pub(crate) start: usize,
pub(crate) end: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Parse {
pub(crate) form: Form,
pub(crate) precondition: Option<Span>,
pub(crate) response: Span,
}
pub(crate) fn parse(text: &str, ranges: &[Range<usize>]) -> Option<Parse> {
let tokens: Vec<String> = ranges
.iter()
.map(|range| lowercase(&text[range.clone()]))
.collect();
let comma_before =
|index: usize| index > 0 && text[ranges[index - 1].end..ranges[index].start].contains(',');
ubiquitous(&tokens, &comma_before)
.or_else(|| prefixed(&tokens, &comma_before, "when", Form::EventDriven))
.or_else(|| prefixed(&tokens, &comma_before, "while", Form::StateDriven))
.or_else(|| unwanted(&tokens))
.or_else(|| prefixed(&tokens, &comma_before, "where", Form::Optional))
}
fn ubiquitous(tokens: &[String], comma_before: &dyn Fn(usize) -> bool) -> Option<Parse> {
let first = tokens.first()?;
if is_prefix(first) || first == "then" {
return None;
}
let shall = shall_after(tokens, 1)?;
if (1..=shall).any(comma_before)
|| (first != "the" && tokens[1..shall].iter().any(|token| token == "the"))
{
return None;
}
system_is_nonempty(tokens, 0, shall)?;
Some(Parse {
form: Form::Ubiquitous,
precondition: None,
response: span(shall + 1, tokens.len()),
})
}
fn prefixed(
tokens: &[String],
comma_before: &dyn Fn(usize) -> bool,
prefix: &str,
form: Form,
) -> Option<Parse> {
if tokens.first().is_none_or(|token| token != prefix) {
return None;
}
let shall = shall_after(tokens, 2)?;
let comma = (2..shall).rev().find(|&index| comma_before(index));
let boundary = comma.or_else(|| (2..shall).find(|&index| tokens[index] == "the"))?;
if contains_other_prefix(&tokens[1..boundary], prefix) {
return None;
}
system_is_nonempty(tokens, boundary, shall)?;
Some(Parse {
form,
precondition: Some(span(1, boundary)),
response: span(shall + 1, tokens.len()),
})
}
fn unwanted(tokens: &[String]) -> Option<Parse> {
if tokens.first().is_none_or(|token| token != "if") {
return None;
}
(2..tokens.len().saturating_sub(1)).find_map(|then| {
if tokens[then] != "then" || contains_other_prefix(&tokens[1..then], "if") {
return None;
}
let shall = shall_after(tokens, then + 2)?;
system_is_nonempty(tokens, then + 1, shall)?;
Some(Parse {
form: Form::Unwanted,
precondition: Some(span(1, then)),
response: span(shall + 1, tokens.len()),
})
})
}
fn system_is_nonempty(tokens: &[String], start: usize, shall: usize) -> Option<()> {
let start = if tokens.get(start).is_some_and(|token| token == "the") {
start + 1
} else {
start
};
(start < shall).then_some(())
}
fn is_prefix(token: &str) -> bool {
matches!(token, "when" | "while" | "if" | "where")
}
const fn span(start: usize, end: usize) -> Span {
Span { start, end }
}
fn shall_after(tokens: &[String], first_position: usize) -> Option<usize> {
(first_position..tokens.len().saturating_sub(1)).find(|&index| tokens[index] == "shall")
}
fn contains_other_prefix(tokens: &[String], allowed: &str) -> bool {
tokens
.iter()
.any(|token| is_prefix(token) && token != allowed)
}