From 69d29007ef922d337393cbc22aa600e43aaf1008 Mon Sep 17 00:00:00 2001
From: Rak Laptudirm <rak@laptudirm.com>
Date: Fri, 26 Apr 2024 20:56:50 +0530
Subject: [PATCH] chore: start with working options

---
 uai/src/client.rs    |  20 +++++---
 uai/src/cmd.rs       |  11 ++--
 uai/src/context.rs   |  23 +++++++--
 uai/src/inbuilt.rs   |  74 +++++++++++++++++++++++++--
 uai/src/parameter.rs | 119 ++++++++++++++++++++++++++++++++++++++++++-
 5 files changed, 228 insertions(+), 19 deletions(-)

diff --git a/uai/src/client.rs b/uai/src/client.rs
index 21958ce..89d71b6 100644
--- a/uai/src/client.rs
+++ b/uai/src/client.rs
@@ -26,7 +26,7 @@ use super::{Command, FlagValues, RunError, RunErrorType};
 /// Commands sent from the GUI are automatically parsed and executed according
 /// to the Command schema provided by the user to the Client.
 pub struct Client<T: Send, E: RunError> {
-    context: inbuilt::Context,
+    initial_context: inbuilt::Context,
     commands: HashMap<String, Command<T, E>>,
 }
 
@@ -41,7 +41,7 @@ impl<T: Send + 'static, E: RunError + 'static> Client<T, E> {
 
         // Make the context thread safe to allow commands to run in parallel.
         let context = Arc::new(Mutex::new(context));
-        let our_ctx = Arc::new(Mutex::new(self.context.clone()));
+        let our_ctx = Arc::new(Mutex::new(self.initial_context.clone()));
 
         // Iterate over the lines in the input, since Commands for the GUI are
         // separated by newlines and we want to parse each Command separately.
@@ -139,7 +139,8 @@ impl<T: Send + 'static, E: RunError + 'static> Client<T, E> {
         }
 
         // Parsing complete, run the Command and handle any errors.
-        cmd.run(context, flags).map_err(|e| e.into())
+        cmd.run(context, flags, self.initial_context.option_values.clone())
+            .map_err(|e| e.into())
     }
 }
 
@@ -155,7 +156,7 @@ impl<T: Send, E: RunError> Client<T, E> {
     #[rustfmt::skip]
     pub fn new() -> Self {
         Client::<T, E> {
-            context: Default::default(),
+            initial_context: Default::default(),
             commands: HashMap::new(),
         }
     }
@@ -178,17 +179,22 @@ impl<T: Send, E: RunError> Client<T, E> {
     }
 
     pub fn option(mut self, name: &str, option: Parameter) -> Self {
-        self.context.options.insert(name.to_string(), option);
+        self.initial_context
+            .options
+            .insert(name.to_string(), option.clone());
+        self.initial_context
+            .option_values
+            .insert_default(name.to_string(), &option);
         self
     }
 
     pub fn engine(mut self, name: &str) -> Self {
-        self.context.engine = name.to_owned();
+        self.initial_context.engine = name.to_owned();
         self
     }
 
     pub fn author(mut self, name: &str) -> Self {
-        self.context.author = name.to_owned();
+        self.initial_context.author = name.to_owned();
         self
     }
 }
diff --git a/uai/src/cmd.rs b/uai/src/cmd.rs
index 564ea9a..3375354 100644
--- a/uai/src/cmd.rs
+++ b/uai/src/cmd.rs
@@ -16,7 +16,7 @@ use std::fmt;
 use std::sync::{Arc, Mutex};
 use std::thread;
 
-use crate::Context;
+use crate::{Context, ParameterValues};
 
 use super::{Flag, FlagValues};
 
@@ -38,9 +38,14 @@ impl<T: Send + 'static, E: RunError + 'static> Command<T, E> {
     /// run runs the current Command with the given context and flag values.
     /// A new thread is spawned and detached to run parallel Commands. It returns
     /// the error returned by the Command's execution, or [`Ok`] for parallel.
-    pub fn run(&self, context: &Arc<Mutex<T>>, flags: FlagValues) -> Result<(), E> {
+    pub fn run(
+        &self,
+        context: &Arc<Mutex<T>>,
+        flags: FlagValues,
+        options: ParameterValues,
+    ) -> Result<(), E> {
         // Clone values which might be moved by spawning a new thread.
-        let context = Context::new(context, flags);
+        let context = Context::new(context, flags, options);
         let func = self.run_fn;
 
         if self.parallel {
diff --git a/uai/src/context.rs b/uai/src/context.rs
index 49b1c4d..1649658 100644
--- a/uai/src/context.rs
+++ b/uai/src/context.rs
@@ -1,16 +1,21 @@
 use std::sync::{Arc, Mutex, MutexGuard};
 
-use crate::FlagValues;
+use crate::{FlagValues, Number, ParameterValues};
 
 pub struct Context<T: Send> {
     context: Arc<Mutex<T>>,
     flags: FlagValues,
+    options: ParameterValues,
 }
 
 impl<T: Send> Context<T> {
-    pub fn new(context: &Arc<Mutex<T>>, flags: FlagValues) -> Context<T> {
+    pub fn new(context: &Arc<Mutex<T>>, flags: FlagValues, options: ParameterValues) -> Context<T> {
         let context = Arc::clone(context);
-        Context { context, flags }
+        Context {
+            context,
+            flags,
+            options,
+        }
     }
 
     pub fn lock(&self) -> MutexGuard<'_, T> {
@@ -28,4 +33,16 @@ impl<T: Send> Context<T> {
     pub fn get_array_flag(&self, name: &str) -> Option<Vec<String>> {
         self.flags.get_array(name)
     }
+
+    pub fn get_check_option(&self, name: &str) -> Option<bool> {
+        self.options.get_check(name)
+    }
+
+    pub fn get_string_option(&self, name: &str) -> Option<String> {
+        self.options.get_string(name)
+    }
+
+    pub fn get_spin_option(&self, name: &str) -> Option<Number> {
+        self.options.get_spin(name)
+    }
 }
diff --git a/uai/src/inbuilt.rs b/uai/src/inbuilt.rs
index 2328bb1..5fb3526 100644
--- a/uai/src/inbuilt.rs
+++ b/uai/src/inbuilt.rs
@@ -13,7 +13,7 @@
 
 use std::collections::HashMap;
 
-use crate::{quit, Parameter, RunErrorType};
+use crate::{error, quit, Flag, Parameter, ParameterValues, RunErrorType};
 use lazy_static::lazy_static;
 
 pub type Command = crate::Command<Context, RunErrorType>;
@@ -21,24 +21,74 @@ pub type Command = crate::Command<Context, RunErrorType>;
 lazy_static! {
     pub static ref COMMANDS: HashMap<String, Command> = HashMap::from(
         [
-            ("quit", Command::new(|_ctx, _flags| quit!())),
+            ("quit", Command::new(|_ctx| quit!())),
             (
                 "isready",
-                Command::new(|_ctx, _flags| {
+                Command::new(|_ctx| {
                     println!("readyok");
                     Ok(())
                 })
             ),
             (
                 "uai",
-                Command::new(|ctx, _flags| {
-                    let ctx = ctx.lock().unwrap();
+                Command::new(|ctx| {
+                    let ctx = ctx.lock();
 
                     println!("id name {}", ctx.engine);
                     println!("id author {}", ctx.author);
                     println!();
+                    if !ctx.options.is_empty() {
+                        for (name, option) in ctx.options.clone() {
+                            println!("option name {} type {}", name, option);
+                        }
+
+                        println!();
+                    }
                     println!("uaiok");
 
+                    Ok(())
+                })
+            ),
+            (
+                "setoption",
+                Command::new(|ctx| {
+                    let name = ctx.get_single_flag("name");
+                    let value = ctx.get_array_flag("value");
+
+                    if name.is_none() || value.is_none() {
+                        return error!("expected \"name\" and \"value\" flags");
+                    }
+
+                    let name = name.unwrap();
+                    let value = value.unwrap().join(" ");
+
+                    let mut ctx = ctx.lock();
+
+                    ctx.setoption(&name, &value).map_err(RunErrorType::Error)
+                })
+                .flag("name", Flag::Single)
+                .flag("value", Flag::Variadic)
+            ),
+            (
+                "options",
+                Command::new(|ctx| {
+                    let ctx = ctx.lock();
+
+                    for (name, option) in ctx.options.clone() {
+                        print!("option name {} value ", name);
+                        match option {
+                            Parameter::Check(_) => {
+                                println!("{}", ctx.option_values.get_check(&name).unwrap())
+                            }
+                            Parameter::String(_) | Parameter::Combo(_, _) => {
+                                println!("{}", ctx.option_values.get_string(&name).unwrap())
+                            }
+                            Parameter::Spin(_, _, _) => {
+                                println!("{}", ctx.option_values.get_spin(&name).unwrap())
+                            }
+                        }
+                    }
+
                     Ok(())
                 })
             )
@@ -56,6 +106,19 @@ pub struct Context {
     pub author: String,
 
     pub options: HashMap<String, Parameter>,
+    pub option_values: ParameterValues,
+}
+
+impl Context {
+    fn setoption(&mut self, name: &str, value: &str) -> Result<(), String> {
+        let option = self.options.get(name);
+        if option.is_none() {
+            return Err(format!("unknown option \"{}\"", name));
+        }
+
+        self.option_values
+            .insert(name.to_owned(), option.unwrap(), value)
+    }
 }
 
 impl Default for Context {
@@ -64,6 +127,7 @@ impl Default for Context {
             engine: "Nameless v0.0.0".to_string(),
             author: "Anonymous".to_string(),
             options: HashMap::new(),
+            option_values: Default::default(),
         }
     }
 }
diff --git a/uai/src/parameter.rs b/uai/src/parameter.rs
index ba1d3d4..a6399a8 100644
--- a/uai/src/parameter.rs
+++ b/uai/src/parameter.rs
@@ -1,3 +1,8 @@
+use std::collections::HashMap;
+use std::fmt;
+
+pub type Number = i32;
+
 #[derive(Clone)]
 pub enum Parameter {
     /// Check represents a checkbox parameter which can be true or false.
@@ -16,7 +21,7 @@ pub enum Parameter {
     /// value, and the third argument is the maximum value of this parameter. The
     /// minimum and maximum bounds are inclusive and and the default must be in
     /// the range defined by them.
-    Spin(i32, i32, i32),
+    Spin(Number, Number, Number),
 
     /// Combo represents a combo box which can have the value of one of the
     /// predefined strings.
@@ -25,3 +30,115 @@ pub enum Parameter {
     /// list of predefined strings. The default value must be included in the list.
     Combo(String, Vec<String>),
 }
+
+#[derive(Clone, Default)]
+pub struct ParameterValues {
+    checks: HashMap<String, bool>,
+    strings: HashMap<String, String>,
+    numbers: HashMap<String, Number>,
+}
+
+impl ParameterValues {
+    pub fn get_check(&self, name: &str) -> Option<bool> {
+        self.checks.get(name).copied()
+    }
+
+    pub fn get_string(&self, name: &str) -> Option<String> {
+        self.strings.get(name).cloned()
+    }
+
+    pub fn get_spin(&self, name: &str) -> Option<Number> {
+        self.numbers.get(name).copied()
+    }
+}
+
+impl ParameterValues {
+    pub fn insert(
+        &mut self,
+        name: String,
+        option: &Parameter,
+        value_str: &str,
+    ) -> Result<(), String> {
+        match option {
+            Parameter::Check(_) => {
+                let value = value_str.parse();
+                if value.is_err() {
+                    return Err(format!(
+                        "option {}: expected boolean, received {}",
+                        name, value_str
+                    ));
+                }
+                self.checks.insert(name, value.unwrap());
+            }
+            Parameter::String(_) => {
+                self.strings.insert(name, value_str.to_owned());
+            }
+            Parameter::Spin(_, min, max) => {
+                let value = value_str.parse();
+                if value.is_err() {
+                    return Err(format!(
+                        "option {}: expected a number, received {}",
+                        name, value_str
+                    ));
+                }
+
+                let value = value.unwrap();
+                if value < *min || value > *max {
+                    return Err(format!(
+                        "option {}: expected a number between {} and {} (inclusive), received {}",
+                        name, min, max, value_str
+                    ));
+                }
+                self.numbers.insert(name, value);
+            }
+            Parameter::Combo(_, strings) => {
+                let value = value_str.to_owned();
+                if strings.contains(&value) {
+                    return Err(format!(
+                        "option {}: {} is not one of the combo strings",
+                        name, value
+                    ));
+                }
+                self.strings.insert(name, value);
+            }
+        };
+
+        Ok(())
+    }
+
+    pub fn insert_default(&mut self, name: String, option: &Parameter) {
+        match option {
+            Parameter::Check(default) => {
+                self.checks.insert(name, *default);
+            }
+            Parameter::String(default) => {
+                self.strings.insert(name, default.clone());
+            }
+            Parameter::Spin(default, _, _) => {
+                self.numbers.insert(name, *default);
+            }
+            Parameter::Combo(default, _) => {
+                self.strings.insert(name, default.clone());
+            }
+        };
+    }
+}
+
+impl fmt::Display for Parameter {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Parameter::Check(default) => write!(f, "check default {}", default),
+            Parameter::String(default) => write!(f, "string default {}", default),
+            Parameter::Spin(default, min, max) => {
+                write!(f, "spin default {} min {} max {}", default, min, max)
+            }
+            Parameter::Combo(default, combos) => {
+                write!(f, "combo default {}", default)?;
+                for combo in combos {
+                    write!(f, " var {}", combo)?
+                }
+                Ok(())
+            }
+        }
+    }
+}