throne 0.5.0

Scripting language for game prototyping and story logic
Documentation
use crate::context::Context as ThroneContext;
use crate::parser;
use crate::rule::{self, Rule};
use crate::string_cache::{Atom, StringCache};
use crate::token::{Phrase, PhraseGroup, Token};
use crate::update::{self, update};

use wasm_bindgen::prelude::*;

use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

#[wasm_bindgen]
pub fn init() {
    std::panic::set_hook(Box::new(console_error_panic_hook::hook));
}

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

#[wasm_bindgen]
pub struct Context {
    throne_context: ThroneContext,
}

impl From<parser::Error> for JsValue {
    fn from(e: parser::Error) -> Self {
        let js_error = js_sys::Error::new(&format!("{}", e));
        js_sys::Object::define_property(
            &js_error,
            &JsValue::from("throne_span"),
            js_sys::Object::try_from(&JsValue::from(LineColSpanDescriptor {
                value: e.pest.line_col.into(),
            }))
            .unwrap(),
        );
        js_error.into()
    }
}

fn js_from_update_result(result: Result<(), update::Error>, rules: &[Rule]) -> Result<(), JsValue> {
    match result {
        Err(e) => {
            let js_error = js_sys::Error::new(&format!("{}", e));
            if let Some(rule_source_span) = e.rule(rules).map(|r| r.source_span) {
                js_sys::Object::define_property(
                    &js_error,
                    &JsValue::from("throne_span"),
                    js_sys::Object::try_from(&JsValue::from(LineColSpanDescriptor {
                        value: rule_source_span.into(),
                    }))
                    .unwrap(),
                );
            }
            Err(js_error.into())
        }
        Ok(()) => Ok(()),
    }
}

#[wasm_bindgen]
struct LineColSpanDescriptor {
    pub value: LineColSpan,
}

#[wasm_bindgen]
#[derive(Copy, Clone)]
pub struct LineColSpan {
    pub line_start: usize,
    pub line_end: usize,
    pub col_start: usize,
    pub col_end: usize,
}

impl From<pest::error::LineColLocation> for LineColSpan {
    fn from(line_col: pest::error::LineColLocation) -> Self {
        match line_col {
            pest::error::LineColLocation::Pos((line, col)) => LineColSpan {
                line_start: line,
                line_end: line,
                col_start: col,
                col_end: col,
            },
            pest::error::LineColLocation::Span((line_start, col_start), (line_end, col_end)) => {
                LineColSpan {
                    line_start,
                    line_end,
                    col_start,
                    col_end,
                }
            }
        }
    }
}

impl From<rule::LineColSpan> for LineColSpan {
    fn from(span: rule::LineColSpan) -> Self {
        LineColSpan {
            line_start: span.line_start,
            line_end: span.line_end,
            col_start: span.col_start,
            col_end: span.col_end,
        }
    }
}

#[wasm_bindgen]
impl Context {
    pub fn from_text(text: &str) -> Result<Context, JsValue> {
        Ok(Context {
            throne_context: ThroneContext::from_text(text)?,
        })
    }

    pub fn push_state(&mut self, text: &str) {
        self.throne_context.push_state(text);
    }

    pub fn remove_state_by_first_atom(&mut self, text: &str) {
        let atom = self.throne_context.str_to_atom(text);
        self.throne_context
            .core
            .state
            .remove_pattern([Some(atom)], false);
    }

    pub fn update(&mut self) -> Result<(), JsValue> {
        js_from_update_result(
            self.throne_context.update(),
            &self.throne_context.core.rules,
        )
    }

    pub fn update_with_side_input(
        &mut self,
        side_input: Option<js_sys::Function>,
    ) -> Result<(), JsValue> {
        if let Some(side_input) = side_input {
            let core = &mut self.throne_context.core;
            let string_cache = &mut self.throne_context.string_cache;
            let side_input = |phrase: &Phrase| {
                let js_phrase = js_value_from_phrase(phrase, string_cache);
                let result = side_input.call1(&JsValue::null(), &js_phrase);
                match result {
                    Ok(v) => {
                        if js_sys::Array::is_array(&v) {
                            let arr = js_sys::Array::from(&v);
                            let mut out = vec![phrase[0].clone()];
                            for item in arr.iter().skip(1) {
                                if let Some(s) = item.as_string() {
                                    out.push(Token::new(&s, 0, 0, string_cache));
                                } else if let Some(n) = item.as_f64() {
                                    out.push(Token::new_integer(n as i32, 0, 0));
                                } else {
                                    return None;
                                }
                            }
                            Some(out.normalize())
                        } else {
                            None
                        }
                    }
                    Err(_) => None,
                }
            };
            js_from_update_result(update(core, side_input), &core.rules)
        } else {
            self.update()
        }
    }

    pub fn get_state(&self) -> JsValue {
        let string_cache = &self.throne_context.string_cache;
        let js_phrases = self
            .throne_context
            .core
            .state
            .get_all()
            .iter()
            .map(|phrase| js_value_from_phrase(phrase, string_cache))
            .collect::<js_sys::Array>();
        JsValue::from(js_phrases)
    }

    pub fn get_state_hashes(&self) -> JsValue {
        let js_hashes = self
            .throne_context
            .core
            .state
            .get_all()
            .iter()
            .map(|phrase| {
                let mut hasher = DefaultHasher::new();
                phrase.hash(&mut hasher);
                JsValue::from(hasher.finish().to_string())
            })
            .collect::<js_sys::Array>();
        JsValue::from(js_hashes)
    }

    pub fn print(&self) {
        log(&format!("{}", self.throne_context));
    }
}

fn js_value_from_phrase(phrase: &Phrase, string_cache: &StringCache) -> JsValue {
    let mut result = vec![];
    for group in phrase.groups() {
        if group.len() == 1 {
            result.push(js_value_from_atom(group[0].atom, string_cache));
        } else {
            result.push(js_value_from_phrase(&group.normalize(), string_cache));
        }
    }

    JsValue::from(result.iter().collect::<js_sys::Array>())
}

fn js_value_from_atom(atom: Atom, string_cache: &StringCache) -> JsValue {
    if let Some(string) = string_cache.atom_to_str(atom) {
        JsValue::from(string)
    } else if let Some(n) = StringCache::atom_to_integer(atom) {
        JsValue::from(n)
    } else {
        JsValue::null()
    }
}

#[cfg(test)]
mod tests {
    use wasm_bindgen_test::wasm_bindgen_test;

    use super::*;
    use crate::token::tokenize;

    #[wasm_bindgen_test]
    fn test_js_value_from_phrase_nested() {
        let mut string_cache = StringCache::new();
        let phrase = tokenize("t1 (t21 (t221 t222 t223) t23) t3", &mut string_cache);
        let js_phrase = js_value_from_phrase(&phrase, &string_cache);
        assert_eq!(
            format!("{:?}", js_phrase),
            r#"JsValue(["t1", ["t21", ["t221", "t222", "t223"], "t23"], "t3"])"#
        );
    }

    #[wasm_bindgen_test]
    fn test_js_value_from_phrase_nested2() {
        let mut string_cache = StringCache::new();
        let phrase = tokenize("t1 (t21 (t221 t222 t223))", &mut string_cache);
        log(&format!("{:#?}", phrase));
        let js_phrase = js_value_from_phrase(&phrase, &string_cache);
        assert_eq!(
            format!("{:?}", js_phrase),
            r#"JsValue(["t1", ["t21", ["t221", "t222", "t223"]]])"#
        );
    }
}