use ccalc_engine::env::Value;
use crate::style::{
StyleColor, StyleSpec, looks_like_style_str, parse_color_token, parse_style_str,
};
pub fn extract_file_arg(args: &[Value]) -> (Vec<Value>, Option<String>) {
if let Some(last) = args.last()
&& let Some(s) = as_str(last)
&& (s == "ascii" || s.ends_with(".svg") || s.ends_with(".png"))
{
return (args[..args.len() - 1].to_vec(), Some(s));
}
(args.to_vec(), None)
}
pub fn extract_flat(v: &Value) -> Result<Vec<f64>, String> {
match v {
Value::Scalar(f) => Ok(vec![*f]),
Value::Matrix(m) => Ok(m.iter().copied().collect()),
_ => Err("plot: numeric array argument required".into()),
}
}
pub fn extract_vector(v: &Value) -> Result<Vec<f64>, String> {
match v {
Value::Scalar(f) => Ok(vec![*f]),
Value::Matrix(m) => {
let (r, c) = (m.nrows(), m.ncols());
if r == 1 || c == 1 {
Ok(m.iter().copied().collect())
} else {
Err(format!("plot: expected a vector, got {r}×{c} matrix"))
}
}
_ => Err("plot: numeric vector argument required".into()),
}
}
pub fn extract_matrix(v: &Value) -> Result<(Vec<f64>, usize, usize), String> {
match v {
Value::Matrix(m) => {
let nrows = m.nrows();
let ncols = m.ncols();
let mut data = Vec::with_capacity(nrows * ncols);
for r in 0..nrows {
for c in 0..ncols {
data.push(m[[r, c]]);
}
}
Ok((data, nrows, ncols))
}
Value::Scalar(f) => Ok((vec![*f], 1, 1)),
_ => Err("imagesc: expected a numeric matrix".into()),
}
}
#[allow(clippy::type_complexity)]
pub fn extract_style_and_file_arg(
args: &[Value],
) -> Result<(Vec<Value>, Option<StyleSpec>, Option<String>), String> {
extract_style_and_file_arg_min(args, 1)
}
#[allow(clippy::type_complexity)]
pub fn extract_style_and_file_arg_min(
args: &[Value],
min_data: usize,
) -> Result<(Vec<Value>, Option<StyleSpec>, Option<String>), String> {
let (mut data_args, path) = extract_file_arg(args);
let mut extra_lw: Option<f32> = None;
let mut extra_ms: Option<u32> = None;
loop {
let len = data_args.len();
if len < 2 {
break;
}
if let Some(key) = as_str(&data_args[len - 2]) {
if key.eq_ignore_ascii_case("linewidth") {
extra_lw = Some(match &data_args[len - 1] {
Value::Scalar(f) if *f > 0.0 => *f as f32,
_ => return Err("linewidth: value must be a positive number".into()),
});
data_args.truncate(len - 2);
continue;
}
if key.eq_ignore_ascii_case("markersize") {
extra_ms = Some(match &data_args[len - 1] {
Value::Scalar(f) if *f >= 1.0 => *f as u32,
_ => return Err("markersize: value must be a positive integer".into()),
});
data_args.truncate(len - 2);
continue;
}
}
break;
}
let len = data_args.len();
if len >= 2
&& let Some(key) = as_str(&data_args[len - 2])
&& key.eq_ignore_ascii_case("color")
{
let sc = value_to_style_color(&data_args[len - 1])?;
data_args.truncate(len - 2);
return Ok((
data_args,
Some(StyleSpec {
color: Some(sc),
line_width: extra_lw,
marker_size: extra_ms,
..StyleSpec::default()
}),
path,
));
}
let rgb_style = if data_args.len() > min_data {
if let Some(Value::Matrix(m)) = data_args.last() {
if m.nrows() == 1 && m.ncols() == 3 && m.iter().all(|&v| (0.0..=1.0).contains(&v)) {
let clamp = |v: f64| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
Some(StyleColor(
clamp(m[[0, 0]]),
clamp(m[[0, 1]]),
clamp(m[[0, 2]]),
))
} else {
None
}
} else {
None
}
} else {
None
};
if let Some(sc) = rgb_style {
data_args.pop();
return Ok((
data_args,
Some(StyleSpec {
color: Some(sc),
line_width: extra_lw,
marker_size: extra_ms,
..StyleSpec::default()
}),
path,
));
}
let mut style: Option<StyleSpec> = None;
if let Some(last) = data_args.last()
&& let Some(s) = as_str(last)
&& looks_like_style_str(&s)
{
let mut sp = parse_style_str(&s)?;
sp.line_width = extra_lw;
sp.marker_size = extra_ms;
style = Some(sp);
data_args.pop();
} else if extra_lw.is_some() || extra_ms.is_some() {
style = Some(StyleSpec {
line_width: extra_lw,
marker_size: extra_ms,
..StyleSpec::default()
});
}
Ok((data_args, style, path))
}
fn value_to_style_color(v: &Value) -> Result<StyleColor, String> {
match v {
Value::Str(s) | Value::StringObj(s) => parse_color_token(s)
.ok_or_else(|| format!("plot: '{s}' is not a recognised color name or hex code")),
Value::Matrix(m) if m.nrows() == 1 && m.ncols() == 3 => {
let clamp = |v: f64| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
Ok(StyleColor(
clamp(m[[0, 0]]),
clamp(m[[0, 1]]),
clamp(m[[0, 2]]),
))
}
_ => Err("plot: 'color' value must be a color name string or 1×3 matrix".into()),
}
}
fn as_str(v: &Value) -> Option<String> {
match v {
Value::Str(s) | Value::StringObj(s) => Some(s.clone()),
_ => None,
}
}