empress 3.0.3

A D-Bus MPRIS daemon for controlling media players.
use std::{
    borrow::{Borrow, Cow},
    collections::HashMap,
    fmt,
};

use serde_json::Map;

use super::{ffi, functions::Functions};

pub(super) fn json(value: &Value) -> String {
    serde_json::to_string(&value).unwrap_or_else(|_| "<error>".into())
}

#[expect(
    clippy::ref_option,
    reason = "This is a domain-specific helper function"
)]
pub(super) fn assert_no_topic<D: fmt::Debug>(topic: &Option<CowValue>, d: &D) -> Result<()> {
    match topic {
        Some(_) => Err(Error::ExtraTopic(format!("{d:?}"))),
        None => Ok(()),
    }
}

#[expect(
    clippy::ref_option,
    reason = "This is a domain-specific helper function"
)]
pub(super) fn assert_topic<D: fmt::Debug>(topic: &Option<CowValue<'_>>, d: &D) -> Result<()> {
    match topic {
        Some(_) => Ok(()),
        None => Err(Error::NoTopic(format!("{d:?}"))),
    }
}

#[inline]
pub(super) fn is_null_like(val: &Value) -> bool {
    match val {
        Value::Null => true,
        Value::String(s) if s.is_empty() => true,
        _ => false,
    }
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Error writing format output")]
    Stream(#[source] StreamError),
    #[error("No pipe input available when evaluating {0}")]
    NoTopic(String),
    #[error("Unexpected pipe input when evaluating {0}")]
    ExtraTopic(String),
    #[error("Error executing function {0:?}")]
    Ffi(String, #[source] ffi::Error),
    #[error("Variable name {0:?} already exists")]
    Shadow(String),
    #[error("Value {} must be a boolean", json(.0))]
    BadCondition(Value),
    #[error("Value {val} has no field {1:?}", val = json(.0))]
    BadPath(Value, String),
    #[error("Value {} has no index {}", json(.0), json(.1))]
    BadIndex(Value, Value),
    #[error("No value found named {0:?}")]
    NoValue(String),
    #[error("No function found named {0:?}")]
    NoFunction(String),
}

#[derive(Debug, thiserror::Error)]
pub enum StreamError {
    #[error("An I/O error occurred")]
    Io(#[from] fmt::Error),
    #[error("Cannot format {} as a string", json(.0))]
    Unprintable(Value),
}

impl<T: Into<StreamError>> From<T> for Error {
    fn from(err: T) -> Self { Self::Stream(err.into()) }
}

pub type Result<T, E = Error> = std::result::Result<T, E>;
pub use serde_json::Value;
pub type CowValue<'a> = Cow<'a, Value>;

pub struct State<'a, W> {
    pub locals: HashMap<&'a str, CowValue<'a>>,
    out: W,
}

impl<W> State<'_, W> {
    #[inline]
    pub fn new(out: W) -> Self {
        Self {
            locals: HashMap::new(),
            out,
        }
    }

    #[inline]
    pub fn out_mut(&mut self) -> &mut W { &mut self.out }

    // #[inline]
    // pub fn finish(self) -> W {
    //     self.out
    // }
}

impl<W: fmt::Write> State<'_, W> {
    pub fn write_value<V: Borrow<Value>>(&mut self, value: V) -> Result<(), StreamError> {
        write_value(value, &mut self.out)
    }
}

pub struct Context {
    pub values: Map<String, Value>,
    pub functions: Functions,
}

impl fmt::Debug for Context {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Context")
            .field("values", &self.values)
            .finish_non_exhaustive()
    }
}

pub trait EvalMut<'a> {
    type Output;

    fn eval_mut<W: fmt::Write>(
        &'a self,
        ctx: &'a Context,
        state: &mut State<'a, W>,
        topic: Option<CowValue<'a>>,
    ) -> Result<Self::Output>;
}

impl<'a, T: EvalMut<'a, Output = ()>> EvalMut<'a> for Vec<T> {
    type Output = ();

    fn eval_mut<W: fmt::Write>(
        &'a self,
        ctx: &'a Context,
        state: &mut State<'a, W>,
        topic: Option<CowValue<'a>>,
    ) -> Result<Self::Output> {
        for val in self {
            val.eval_mut(ctx, state, topic.clone())?;
        }

        Ok(())
    }
}

pub trait Eval<'a> {
    type Output;

    fn eval<W>(
        &'a self,
        ctx: &'a Context,
        state: &State<'a, W>,
        topic: Option<CowValue<'a>>,
    ) -> Result<Self::Output>;
}

pub fn write_value<V: Borrow<Value>, W: fmt::Write>(
    value: V,
    mut out: W,
) -> Result<(), StreamError> {
    let value = value.borrow();
    match value {
        Value::Null => Ok(()),
        Value::Number(n) => out.write_fmt(format_args!("{n}")).map_err(Into::into),
        Value::String(s) => out.write_str(s).map_err(Into::into),
        Value::Bool(_) | Value::Array(_) | Value::Object(_) => {
            Err(StreamError::Unprintable(value.clone()))
        },
    }
}

pub fn stringify<V: Borrow<Value>>(value: V) -> Result<String, StreamError> {
    let mut s = String::new();
    write_value(value, &mut s).map(|()| s)
}