//! This library is a text-adventure game engine that can be used to create
//! text based adventure games. It is designed to be used with a front-end
//! which can be written in any language. Implementing this library in a
//! language is a matter of writing a front-end an passing string data to
//! the library for parsing.
//!
//! The configuration of the game is done in the `Config` struct
//! and can be initialized both with YAML files and serialized
//! JSON data, so it is perfect for both web and desktop games.
//!
//! The `parse_input` and `parse_input_json` functions are the only
//! functions that need to be called by the front-end, but the library
//! exposes some of the internal structs and functions to help developers
//! understand how the library works, and to allow a little bit of flexibility
//! in how the library is used.
//!
//! # Example:
//! ```rust
//! use nightrunner_lib::{NightRunner, NightRunnerBuilder, ParsingResult};
//! use nightrunner_lib::util::test_helpers::mock_json_data;
//! let data = mock_json_data();
//! let mut nr = NightRunnerBuilder::new().with_json_data(&data).build().unwrap();
//! let result = nr.parse_input("look");
//! let json_result = nr.json_parse_input("look");
//! assert!(result.is_ok());
//! assert_eq!(result.unwrap(),
//!     ParsingResult::Look(
//!         "first room\n\nHere you see: \nan item1\nan item2\nsubject1".to_string()
//!     )
//! );
//! assert_eq!(json_result,
//!     r#"{"messageType":"look","data":"first room\n\nHere you see: \nan item1\nan item2\nsubject1"}"#.to_string()
//! );
//! ```
//!
//! for examples of valid YAML and JSON data, see the documentation for
//! the `config` module.
#![warn(missing_docs)]
use config::{Config, State};
use parser::interpreter::EventMessage;
use serde::{Deserialize, Serialize};
use std::{
    error::Error,
    fmt::{self, Display, Formatter},
};
extern crate console_error_panic_hook;
use util::parse_room_text;
/// Module containing the configuration code for this
/// library.
pub mod config;
/// The parser module contains a single function that
/// parses the input string and returns a `ParsingResult`.
pub mod parser;
/// Helper functions.
pub mod util;
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;

/// We use a type alias to make error handling easier.
pub type NRResult<T> = Result<T, Box<dyn Error>>;

/// This is the result of the parsing of the input.
/// Each variant contains the output for the game and
/// should be used by a front-end to display to the user.
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "messageType", content = "data")]
pub enum ParsingResult {
    /// Returned when the player sends a command corresponding
    /// to a verb that has VerbFunction::Help as its verb_function.
    /// The value is a string generated by the library displaying
    /// general commands as well as verbs available in the game.
    Help(String),
    /// Returned when the player sends a command for looking at the
    /// room, an item, or a subject. The value will be the message
    /// returned from the parser with the description associated with
    /// the room, item, or subject.
    Look(String),
    /// Returned when the player receives a new item, either by picking
    /// it up or by receiving it as the result of an event.
    NewItem(String),
    /// Returned when the player loses an item, either by dropping it
    /// or by losing it as the result of an event.
    DropItem(String),
    /// Returned when the player issues a command with a verb that
    /// has VerbFunction::Inventory as its verb_function. The value is
    /// a string containing each item in the player's inventory.
    Inventory(String),
    /// Returned when the player issues a command that interacts with
    /// a subject without a current event associated with it. The value
    /// is the default text for the subject.
    SubjectNoEvent(String),
    /// Returned when an event is triggered by the player's command. The
    /// returned struct contains the text to be displayed to the player.
    EventSuccess(EventMessage),
    /// Returned when the player issues a command with a verb that has
    /// VerbFunction::Quit as its verb_function. This variant is used
    /// to indicate to the front-end that the game should be quit.
    /// Implementation of how to quit the game is left to the front-end.
    Quit,
}

impl Display for ParsingResult {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        match self {
            ParsingResult::Help(msg) => write!(f, "{}", msg),
            ParsingResult::Look(msg) => write!(f, "{}", msg),
            ParsingResult::NewItem(msg) => write!(f, "{}", msg),
            ParsingResult::DropItem(msg) => write!(f, "{}", msg),
            ParsingResult::Inventory(msg) => write!(f, "{}", msg),
            ParsingResult::SubjectNoEvent(msg) => write!(f, "{}", msg),
            ParsingResult::EventSuccess(event_msg) => {
                let EventMessage {
                    message,
                    message_parts,
                    templated_words: _,
                } = event_msg;
                write!(f, "{}", message)?;
                for part in message_parts.values() {
                    write!(f, "{}", part)?;
                }
                Ok(())
            }
            ParsingResult::Quit => write!(f, "Quitting game"),
        }
    }
}

/// This is the main struct for this library
/// and represents the game. It holds the state
/// internally and passes it to the parser for
/// processing along with the provided input.
#[wasm_bindgen]
#[derive(Debug, PartialEq)]
pub struct NightRunner {
    state: State,
    previous_states: Vec<State>,
    future_states: Vec<State>,
}

/// Describes where the game configuration should be
/// loaded from. The actual parsing is deferred until
/// [`NightRunnerBuilder::build`] so that any error in
/// the configuration is surfaced from a single fallible
/// call rather than panicking while building.
#[derive(Debug, PartialEq, Eq)]
enum ConfigSource {
    Default,
    Path(String),
    Json(String),
}

/// You can use this to build a NightRunner
/// strut. While you can build the NightRunner
/// struct directly, using this builder is a
/// little more convenient.
///
/// # Examples:
/// ```rust
/// use nightrunner_lib::NightRunnerBuilder;
/// use nightrunner_lib::util::test_helpers::mock_json_data;
/// let data = mock_json_data();
/// let path_to_yaml = "fixtures/";
/// let nr1 = NightRunnerBuilder::new().with_json_data(&data);
/// let nr2 = NightRunnerBuilder::new().with_path_for_config(path_to_yaml);
/// ```
#[derive(Debug, PartialEq, Eq)]
pub struct NightRunnerBuilder {
    source: ConfigSource,
}
impl NightRunnerBuilder {
    /// Creates a new empty NightRunnerBuilder
    /// which contains an empty Config struct.
    pub fn new() -> NightRunnerBuilder {
        NightRunnerBuilder {
            source: ConfigSource::Default,
        }
    }
    /// Creates a new NightRunnerBuilder with YAML
    /// data from files in the specified path.
    pub fn with_path_for_config(mut self, path: &str) -> NightRunnerBuilder {
        self.source = ConfigSource::Path(path.to_string());
        self
    }
    /// Creates a new NightRunnerBuilder with JSON
    /// data serialized to a string. This data should
    /// contain the configuration for the whole game.
    pub fn with_json_data(mut self, data: &str) -> NightRunnerBuilder {
        self.source = ConfigSource::Json(data.to_string());
        self
    }
    /// Creates a new NightRunner struct. This will fail
    /// if the config is invalid or missing.
    pub fn build(self) -> NRResult<NightRunner> {
        let config = match self.source {
            ConfigSource::Default => Config::default(),
            ConfigSource::Path(path) => Config::from_path(&path)?,
            ConfigSource::Json(data) => Config::from_json(&data)?,
        };
        let state = State::init(config);
        Ok(NightRunner {
            state,
            previous_states: vec![],
            future_states: vec![],
        })
    }
}

impl Default for NightRunnerBuilder {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(not(target_arch = "wasm32"))]
/// # Nightrunner Rust Library
///
/// Use this implementation when using this library
/// in a Rust application.
impl NightRunner {
    /// This is the main function that executes the game. Pass
    /// the input string to this function and it will return
    /// a result that can be used on the front-end to display
    /// the game to the user.
    pub fn parse_input(&mut self, input: &str) -> NRResult<ParsingResult> {
        let (new_state, parsing_result) = parser::parse(&self.state, input)?;
        self.previous_states.push(self.state.clone());
        self.state = new_state;
        Ok(parsing_result)
    }
    /// This is the main function that executes the game. Pass
    /// the input string to this function and it will return
    /// a result that can be used on the front-end to display
    /// the game to the user.
    /// Unlike the `parse_input` function, this function will
    /// return the result in JSON format. This is useful for
    /// front-ends that can't integrate with a rust library.
    pub fn json_parse_input(&mut self, input: &str) -> String {
        let result = parser::parse(&self.state, input);
        match result {
            Ok((new_state, ok)) => {
                self.previous_states.push(self.state.clone());
                self.state = new_state;
                serde_json::to_string(&ok)
                    .unwrap_or_else(|_| r#"{"error":"failed to serialize result"}"#.to_string())
            }
            Err(err) => {
                let message = serde_json::to_string(&err.to_string())
                    .unwrap_or_else(|_| "\"an unknown error occurred\"".to_string());
                format!("{{\"error\":{}}}", message)
            }
        }
    }
    /// Rewinds the game state to the previous state.
    /// This is useful for undoing actions in the game.
    /// The state is saved in a stack, so you can rewind
    /// multiple times.
    pub fn rewind_state(&mut self) -> NRResult<ParsingResult> {
        if let Some(state) = self.previous_states.pop() {
            self.future_states.push(self.state.clone());
            self.state = state;
            Ok(ParsingResult::Look("Rewound to previous state".to_string()))
        } else {
            Err("No previous state to rewind to".into())
        }
    }
    /// Fast forwards the game state to the next state.
    /// This is useful for redoing actions in the game.
    /// The state is saved in a stack, so you can fast
    /// forward multiple times.
    pub fn fast_forward_state(&mut self) -> NRResult<ParsingResult> {
        if let Some(state) = self.future_states.pop() {
            self.previous_states.push(self.state.clone());
            self.state = state;
            Ok(ParsingResult::Look(
                "Fast forwarded to next state".to_string(),
            ))
        } else {
            Err("No future state to fast forward to".into())
        }
    }
    /// Returns the string with the game intro text. This can
    /// be used to display the game intro to the user, but isn't
    /// required.
    pub fn game_intro(&self) -> String {
        self.state.config.intro.clone()
    }
    /// Returns the text for the very first room of the game.
    ///
    /// Since there is no input to parse when the game starts,
    /// this function should be used to retrieve that text instead.
    pub fn first_room_text(&self) -> NRResult<EventMessage> {
        let narrative_id = self
            .state
            .rooms
            .first()
            .ok_or(parser::errors::NoRoom)?
            .narrative;
        let narrative_text = self
            .state
            .config
            .narratives
            .iter()
            .find(|n| n.id == narrative_id)
            .ok_or(parser::errors::InvalidNarrative)?
            .text
            .clone();
        let event_message = parse_room_text(&self.state, narrative_text, "".to_string(), None)?;
        Ok(event_message)
    }
}

#[cfg(any(target_arch = "wasm32", doc))]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "messageType", content = "data")]
#[serde(rename_all = "snake_case")]
/// When compiling for the web, this struct is used to
/// serialize the game state to JSON. `messageType` is
/// the type of the message returned by the library to
/// indicate which action was processed by the parser.
pub enum JsMessage {
    /// Returned when the player sends a command corresponding
    /// to a verb that has VerbFunction::Help as its verb_function.
    /// The value is a string generated by the library displaying
    /// general commands as well as verbs available in the game.
    Help(String),
    /// Returned when the player sends a command for looking at the
    /// room, an item, or a subject. The value will be the message
    /// returned from the parser with the description associated with
    /// the room, item, or subject.
    Look(String),
    /// Returned when the player receives a new item, either by picking
    /// it up or by receiving it as the result of an event.
    NewItem(String),
    /// Returned when the player loses an item, either by dropping it
    /// or by losing it as the result of an event.
    DropItem(String),
    /// Returned when the player issues a command with a verb that
    /// has VerbFunction::Inventory as its verb_function. The value is
    /// a string containing each item in the player's inventory.
    Inventory(String),
    /// Returned when the player issues a command that interacts with
    /// a subject without a current event associated with it. The value
    /// is the default text for the subject.
    SubjectNoEvent(String),
    /// Returned when an event is triggered by the player's command. The
    /// returned struct contains the text to be displayed to the player.
    EventSuccess(EventMessage),
    /// Returned when a parser result isn't applicable to the wasm library
    NoOp,
}

#[cfg(any(target_arch = "wasm32", doc))]
#[wasm_bindgen]
/// # Nightrunner Wasm Library
///
/// Use this implementation when using this library
/// in a WebAssembly browser application. This is the
/// implementation exposed when compiling with `--target=wasm32-unknown-unknown`.
impl NightRunner {
    /// When using the wasm library we won't have access to the
    /// builder patter, so the constructor needs to receive the
    /// configuration for games as a parameter.
    ///
    /// config should be a JSON string.
    #[wasm_bindgen(constructor)]
    pub fn new(config: &str) -> Result<NightRunner, JsError> {
        console_error_panic_hook::set_once();
        let config = Config::from_json(config).map_err(|e| JsError::new(&e.to_string()))?;
        let state = State::init(config);
        Ok(NightRunner {
            state,
            previous_states: vec![],
            future_states: vec![],
        })
    }
    /// This is the main function that executes the game. Pass
    /// the input string to this function and it will return
    /// a result that can be used on the front-end to display
    /// the game to the user.
    /// Unlike the non-wasm version, this function will return
    /// the result in JSON format. The conversion of the result
    /// to JSON is done by the `JsValue::from_serde` function from
    /// wasm_bindgen.
    pub fn parse(&mut self, input: &str) -> Result<JsValue, JsError> {
        let result = parser::parse(&self.state, input);
        match result {
            Ok((new_state, ok)) => {
                self.previous_states.push(self.state.clone());
                self.state = new_state;
                let message = match ok {
                    ParsingResult::Look(msg) => JsMessage::Look(msg),
                    ParsingResult::Help(msg) => JsMessage::Help(msg),
                    ParsingResult::NewItem(msg) => JsMessage::NewItem(msg),
                    ParsingResult::DropItem(msg) => JsMessage::DropItem(msg),
                    ParsingResult::Inventory(msg) => JsMessage::Inventory(msg),
                    ParsingResult::SubjectNoEvent(msg) => JsMessage::SubjectNoEvent(msg),
                    ParsingResult::EventSuccess(event_msg) => JsMessage::EventSuccess(event_msg),
                    ParsingResult::Quit => JsMessage::NoOp,
                };
                Ok(serde_wasm_bindgen::to_value(&message)?)
            }
            Err(err) => Err(JsError::new(&err.to_string())),
        }
    }

    /// Rewinds the game state to the previous state.
    /// This is useful for undoing actions in the game.
    /// The state is saved in a stack, so you can rewind
    /// multiple times.
    pub fn rewind_state(&mut self) -> Result<JsValue, JsError> {
        match self.previous_states.pop() {
            Some(state) => {
                self.future_states.push(self.state.clone());
                self.state = state;
                Ok(serde_wasm_bindgen::to_value(&JsMessage::Look(
                    "Rewound to previous state".to_string(),
                ))?)
            }
            None => Err(JsError::new("No previous state to rewind to")),
        }
    }

    /// Fast forwards the game state to the next state.
    /// This is useful for redoing actions in the game.
    /// The state is saved in a stack, so you can fast
    /// forward multiple times.
    pub fn fast_forward_state(&mut self) -> Result<JsValue, JsError> {
        match self.future_states.pop() {
            Some(state) => {
                self.previous_states.push(self.state.clone());
                self.state = state;
                Ok(serde_wasm_bindgen::to_value(&JsMessage::Look(
                    "Fast forwarded to next state".to_string(),
                ))?)
            }
            None => Err(JsError::new("No future state to fast forward to")),
        }
    }

    /// Returns the string with the game intro text. This can
    /// be used to display the game intro to the user, but isn't
    /// required.
    #[wasm_bindgen]
    pub fn game_intro(&self) -> String {
        self.state.config.intro.clone()
    }
    /// Returns the text for the very first room of the game.
    ///
    /// Since there is no input to parse when the game starts,
    /// this function should be used to retrieve that text instead.
    pub fn first_room_text(&self) -> Result<JsValue, JsError> {
        let narrative_id = self
            .state
            .rooms
            .first()
            .ok_or_else(|| JsError::new("There are no rooms in the game."))?
            .narrative;
        let narrative_text = self
            .state
            .config
            .narratives
            .iter()
            .find(|n| n.id == narrative_id)
            .ok_or_else(|| JsError::new(&parser::errors::InvalidNarrative.to_string()))?
            .text
            .clone();
        let event_message = parse_room_text(&self.state, narrative_text, "".to_string(), None)
            .map_err(|e| JsError::new(&e.to_string()))?;
        Ok(serde_wasm_bindgen::to_value(&event_message)?)
    }
}
