//! # Rant
//!
//! Rant is a language for procedural text generation.
//! It is designed to help you write more dynamic and expressive templates, dialogue, stories, names, test data, and much more.
//!
//! For language documentation, see the [Rant Reference](https://docs.rant-lang.org).
//! 
//! ## The Rant context
//!
//! All programs are run through a Rant context, represented by the [`Rant`] struct.
//! It allows you to execute Rant programs, define and retrieve global variables, manipulate the RNG, and compile Rant code.
//! 
//! ## Reading compiler errors
//! 
//! You will notice that the `Err` variant of the `Rant::compile*` methods is `()` instead of providing an error list. Instead, 
//! errors and warnings are reported via implementors of the [`Reporter`] trait, which allows the user to control what happens to messages emitted by the compiler.
//! Currently, Rant has two built-in `Reporter` implementations: the unit type `()`, and `Vec<CompilerMessage>`.
//! You can also make your own custom reporters to suit your specific needs.
//!
//! [`Rant`]: struct.Rant.html
//! [`Reporter`]: compiler/trait.Reporter.html
//! [`Vec<CompilerMessage>`]: compiler/struct.CompilerMessage.html


#![allow(dead_code)]
#![allow(unused_macros)]

pub mod compiler;
pub mod runtime;

mod collections;
mod convert;
mod lang;
mod random;
mod stdlib;
mod string;
mod util;
mod value;
mod var;

pub use collections::*;
pub use convert::*;
pub use string::*;
pub use value::*;
pub use var::*;

use crate::compiler::*;
use crate::lang::Sequence;
use crate::random::RantRng;
use crate::runtime::{RuntimeResult, IntoRuntimeResult, RuntimeError, RuntimeErrorType, VM};

use std::{path::Path, rc::Rc, cell::RefCell, fmt::Display, path::PathBuf, io::ErrorKind, collections::HashMap};
use std::env;
use fnv::FnvBuildHasher;
use rand::Rng;

type IOErrorKind = std::io::ErrorKind;

pub(crate) type InternalString = smartstring::alias::CompactString;

/// The build version according to the crate metadata at the time of compiling.
pub const BUILD_VERSION: &str = env!("CARGO_PKG_VERSION");

/// The name of the environment variable that Rant checks when checking the global modules path.
pub const ENV_MODULES_PATH_KEY: &str = "RANT_MODULES_PATH";

/// The Rant language version implemented by this library.
pub const RANT_VERSION: &str = "4.0";

/// The default name given to programs compiled from raw strings.
pub const DEFAULT_PROGRAM_NAME: &str = "program";

/// The file extension that Rant expects modules to have.
pub const RANT_FILE_EXTENSION: &str = "rant";

/// Name of global variable that stores cached modules.
pub(crate) const MODULES_CACHE_KEY: &str = "__MODULES";

pub(crate) type ModuleLoadResult = Result<RantProgram, ModuleLoadError>;

/// A Rant execution context.
#[derive(Debug)]
pub struct Rant {
  rng: Rc<RantRng>,
  debug_mode: bool,
  globals: HashMap<InternalString, RantVar, FnvBuildHasher>,
  options: RantOptions,
}

impl Rant {
  /// Creates a new Rant context with the default seed (0) and loads the standard library.
  pub fn new() -> Self {
    Self::with_seed(0)
  }
  
  /// Creates a new Rant context with the specified seed and loads the standard library.
  pub fn with_seed(seed: u64) -> Self {
    Self::with_options(RantOptions {
      seed,
      .. Default::default()
    })
  }

  /// Creates a new Rant context with a seed generated by a thread-local PRNG and loads the standard library.
  pub fn with_random_seed() -> Self {
    Self::with_options(RantOptions {
      seed: rand::thread_rng().gen(),
      .. Default::default()
    })
  }

  /// Creates a new Rant context with the specified options.
  pub fn with_options(options: RantOptions) -> Self {
    let mut rant = Self {
      debug_mode: options.debug_mode,
      globals: Default::default(),
      rng: Rc::new(RantRng::new(options.seed)),
      options,
    };

    // Load standard library
    if rant.options.use_stdlib {
      stdlib::load_stdlib(&mut rant);
    }

    rant
  }
}

impl Default for Rant {
  /// Creates a default `Rant` instance.
  fn default() -> Self {
    Self::new()
  }
}

impl Rant {
  /// Compiles a source string using the specified reporter.
  #[must_use = "compiling a program without storing or running it achieves nothing"]
  pub fn compile<R: Reporter>(&self, source: &str, reporter: &mut R) -> Result<RantProgram, CompilerErrorKind> {
    compiler::compile_string(source, reporter, self.debug_mode, RantProgramInfo {
      name: None,
      path: None,
    })
  }

  /// Compiles a source string using the specified reporter and source name.
  #[must_use = "compiling a program without storing or running it achieves nothing"]
  pub fn compile_named<R: Reporter>(&self, source: &str, reporter: &mut R, name: &str) -> Result<RantProgram, CompilerErrorKind> {
    compiler::compile_string(source, reporter, self.debug_mode, RantProgramInfo {
      name: Some(name.to_owned()),
      path: None,
    })
  }

  /// Compiles a source string without reporting problems.
  ///
  /// ## Note
  ///
  /// This method will not generate any compiler messages, even if it fails.
  ///
  /// If you require this information, use the `compile()` method instead.
  #[must_use = "compiling a program without storing or running it achieves nothing"]
  pub fn compile_quiet(&self, source: &str) -> Result<RantProgram, CompilerErrorKind> {
    compiler::compile_string(source, &mut (), self.debug_mode, RantProgramInfo {
      name: None,
      path: None,
    })
  }

  /// Compiles a source string without reporting problems and assigns it the specified name.
  ///
  /// ## Note
  ///
  /// This method will not generate any compiler messages, even if it fails.
  ///
  /// If you require this information, use the `compile()` method instead.
  #[must_use = "compiling a program without storing or running it achieves nothing"]
  pub fn compile_quiet_named(&self, source: &str, name: &str) -> Result<RantProgram, CompilerErrorKind> {
    compiler::compile_string(source, &mut (), self.debug_mode, RantProgramInfo {
      name: Some(name.to_owned()),
      path: None,
    })
  }
  
  /// Compiles a source file using the specified reporter.
  #[must_use = "compiling a program without storing or running it achieves nothing"]
  pub fn compile_file<P: AsRef<Path>, R: Reporter>(&self, path: P, reporter: &mut R) -> Result<RantProgram, CompilerErrorKind> {
    compiler::compile_file(path, reporter, self.debug_mode)
  }

  /// Compiles a source file without reporting problems.
  ///
  /// ## Note
  ///
  /// This method will not generate any compiler messages, even if it fails.
  ///
  /// If you require this information, use the `compile_file()` method instead.
  #[must_use = "compiling a program without storing or running it achieves nothing"]
  pub fn compile_file_quiet<P: AsRef<Path>>(&self, path: P) -> Result<RantProgram, CompilerErrorKind> {
    compiler::compile_file(path, &mut (), self.debug_mode)
  }

  /// Sets a global variable. This will auto-define the global if it doesn't exist. 
  ///
  /// If the global already exists and is a constant, the write will not succeed.
  ///
  /// Returns `true` if the write succeeded; otherwise, `false`.
  #[inline]
  pub fn set_global(&mut self, key: &str, value: RantValue) -> bool {
    if let Some(global_var) = self.globals.get_mut(key) {
      global_var.write(value)
    } else {
      self.globals.insert(InternalString::from(key), RantVar::ByVal(value));
      true
    }
  }

  /// Sets a global constant. This will auto-define the global if it doesn't exist.
  ///
  /// If the global already exists and is a constant, the write will not succeed.
  ///
  /// Returns `true` if the write succeeded; otherwise, `false`.
  #[inline]
  pub fn set_global_const(&mut self, key: &str, value: RantValue) -> bool {
    if let Some(global_var) = self.globals.get(key) {
      if global_var.is_const() {
        return false
      }
    }
    self.globals.insert(InternalString::from(key), RantVar::ByValConst(value));
    true
  }

  /// Sets a global's value, forcing the write even if the existing global is a constant.
  /// This will auto-define the global if it doesn't exist.
  #[inline]
  pub fn set_global_force(&mut self, key: &str, value: RantValue, is_const: bool) {
    self.globals.insert(InternalString::from(key), if is_const { RantVar::ByValConst(value) } else { RantVar::ByVal(value) });
  }

  /// Gets the value of a global variable.
  #[inline]
  pub fn get_global(&self, key: &str) -> Option<RantValue> {
    self.globals.get(key).map(|var| var.value_cloned())
  }

  /// Gets a global variable by its `RantVar` representation.
  #[inline]
  pub(crate) fn get_global_var(&self, key: &str) -> Option<&RantVar> {
    self.globals.get(key)
  }

  /// Sets a global variable to the provided `RantVar`.
  #[inline]
  pub(crate) fn set_global_var(&mut self, key: &str, var: RantVar) {
    self.globals.insert(InternalString::from(key), var);
  }

  /// Gets a mutable reference to the `RantVar` representation of the specified variable.
  #[inline]
  pub(crate) fn get_global_var_mut(&mut self, key: &str) -> Option<&mut RantVar> {
    self.globals.get_mut(key)
  }

  /// Returns `true` if a global with the specified key exists.
  #[inline]
  pub fn has_global(&self, key: &str) -> bool {
    self.globals.contains_key(key)
  }

  /// Removes the global with the specified key. Returns `true` if the global existed prior to removal.
  #[inline]
  pub fn delete_global(&mut self, key: &str) -> bool {
    self.globals.remove(key).is_some()
  }

  /// Iterates over the names of all globals stored in the context.
  #[inline]
  pub fn global_names(&self) -> impl Iterator<Item = &str> {
    self.globals.keys().map(|k| k.as_str())
  }
  
  /// Gets the current RNG seed.
  pub fn seed(&self) -> u64 {
    self.rng.seed()
  }
  
  /// Re-seeds the RNG with the specified seed.
  pub fn set_seed(&mut self, seed: u64) {
    self.rng = Rc::new(RantRng::new(seed));
  }
  
  /// Resets the RNG back to its initial state with the current seed.
  pub fn reset_seed(&mut self) {
    let seed = self.rng.seed();
    self.rng = Rc::new(RantRng::new(seed));
  }
  
  /// Runs a program and returns the output value.
  pub fn run(&mut self, program: &RantProgram) -> RuntimeResult<RantValue> {
    VM::new(self.rng.clone(), self, program).run()
  }

  /// Runs a program with the specified arguments and returns the output value.
  pub fn run_with<A>(&mut self, program: &RantProgram, args: A) -> RuntimeResult<RantValue>
  where A: Into<Option<HashMap<String, RantValue>>>
  {
    VM::new(self.rng.clone(), self, program).run_with(args)
  }

  /// Runs a Rant program and returns the generated output as a string.
  pub fn run_into_string(&mut self, program: &RantProgram) -> RuntimeResult<String> {
    let mut vm = VM::new(self.rng.clone(), self, program);
    Ok(vm.run()?.to_string())
  }

  /// Runs a Rant program with the specified arguments and returns the generated output as a string.
  pub fn run_into_string_with<A>(&mut self, program: &RantProgram, args: A) -> RuntimeResult<String> 
  where A: Into<Option<HashMap<String, RantValue>>>
  {
    let mut vm = VM::new(self.rng.clone(), self, program);
    Ok(vm.run_with(args)?.to_string())
  }

  /// Attempts to load and compile a module with the specified name.
  pub(crate) fn try_load_module(&mut self, module_path: &str, caller_origin: Rc<RantProgramInfo>) -> ModuleLoadResult {
    if !self.options.enable_require {
      return Err(ModuleLoadError {
        name: module_path.to_owned(),
        reason: ModuleLoadErrorReason::NotAllowed,
      })
    }

    // Try to find module path that exists
    if let Some(full_module_path) = self.find_module_path(module_path, caller_origin.path.as_deref()) {
      let mut errors = vec![];
      let compile_result = self.compile_file(full_module_path, &mut errors);
      match compile_result {
        Ok(module) => Ok(module),
        Err(err) => {
          Err(ModuleLoadError {
            name: module_path.to_owned(),
            reason: match err{
              CompilerErrorKind::SyntaxError => {
                ModuleLoadErrorReason::CompileFailed(errors)
              },
              CompilerErrorKind::IOError(ioerr) => {
                match ioerr {
                  IOErrorKind::NotFound => {
                    ModuleLoadErrorReason::NotFound
                  },
                  IOErrorKind::PermissionDenied => {
                    ModuleLoadErrorReason::NotAllowed
                  },
                  _ => ModuleLoadErrorReason::FileIOError(ioerr)
                }
              }
            }
          })
        }
      }
    } else {
      Err(ModuleLoadError {
        name: module_path.to_owned(),
        reason: ModuleLoadErrorReason::NotFound,
      })
    }
  }

  #[inline]
  fn find_module_path(&self, module_path: &str, dependant_path: Option<&str>) -> Option<PathBuf> {
    let module_path = PathBuf::from(
        module_path.replace("/", &String::from(std::path::MAIN_SEPARATOR))
      )
      .with_extension(RANT_FILE_EXTENSION);

    macro_rules! search_for_module {
      ($path:expr) => {
        let path = $path;
        // Construct full path to module
        if let Ok(full_module_path) = path
          .join(&module_path)
          .canonicalize() 
        {
          // Verify file is still in modules directory and it exists
          if full_module_path.starts_with(path) 
          && full_module_path.exists() 
          {
            return Some(full_module_path)
          }
        }
      }
    }

    // Search path of dependant running program
    if let Some(program_path) = 
      dependant_path
      .map(PathBuf::from)
      .as_deref()
      .and_then(|p| p.parent())
    {
      search_for_module!(program_path);
    }

    // Search local modules path
    if let Some(local_modules_path) = 
      self.options.local_modules_path
      .as_ref()
      .map(PathBuf::from)
      .or_else(||
        env::current_dir()
        .ok()
      )
      .and_then(|p| p.canonicalize().ok())
    {
      search_for_module!(local_modules_path);
    }

    // Check global modules, if enabled
    if self.options.enable_global_modules {
      if let Some(global_modules_path) = 
        env::var_os(ENV_MODULES_PATH_KEY)
        .map(PathBuf::from)
        .and_then(|p| p.canonicalize().ok())
      {
        search_for_module!(global_modules_path);
      }
    }
    
    None
  }
}

/// Provides options for customizing the creation of a `Rant` instance.
#[derive(Debug, Clone)]
pub struct RantOptions {
  /// Specifies whether the standard library should be loaded.
  pub use_stdlib: bool,
  /// Enables debug mode, which includes additional debug information in compiled programs and more detailed runtime error data.
  pub debug_mode: bool,
  /// The initial seed to pass to the RNG. Defaults to 0.
  pub seed: u64,
  /// Enables the [require] function, allowing modules to be loaded.
  pub enable_require: bool,
  /// Enables loading modules from RANT_MODULES_PATH.
  pub enable_global_modules: bool,
  /// Specifies a preferred module loading path with higher precedence than the global module path.
  /// If not specified, looks in the current working directory.
  pub local_modules_path: Option<String>,
}

impl Default for RantOptions {
  fn default() -> Self {
    Self {
      use_stdlib: true,
      debug_mode: false,
      seed: 0,
      enable_require: true,
      enable_global_modules: true,
      local_modules_path: None,
    }
  }
}

/// A compiled Rant program.
#[derive(Debug)]
pub struct RantProgram {
  info: Rc<RantProgramInfo>,
  root: Rc<Sequence>
}

impl RantProgram {
  pub(crate) fn new(root: Rc<Sequence>, info: Rc<RantProgramInfo>) -> Self {
    Self {
      info,
      root,
    }
  }

  /// Gets the display name of the program, if any.
  #[inline]
  pub fn name(&self) -> Option<&str> {
    self.info.name.as_deref()
  }

  /// Gets the path to the program's source file, if any.
  #[inline]
  pub fn path(&self) -> Option<&str> {
    self.info.path.as_deref()
  }

  /// Gets the metadata associated with the program.
  #[inline]
  pub fn info(&self) -> &RantProgramInfo {
    self.info.as_ref()
  }
}

/// Contains metadata used to identify a loaded program.
#[derive(Debug)]
pub struct RantProgramInfo {
  path: Option<String>,
  name: Option<String>,
}

impl RantProgramInfo {
  /// Gets the display name of the program, if any.
  #[inline]
  pub fn name(&self) -> Option<&str> {
    self.name.as_deref()
  }

  /// Gets tha path to the program's source file, if any.
  #[inline]
  pub fn path(&self) -> Option<&str> {
    self.path.as_deref()
  }
}

/// Represents an error that occurred when attempting to load a Rant module.
#[derive(Debug)]
pub struct ModuleLoadError {
  name: String,
  reason: ModuleLoadErrorReason,
}

impl ModuleLoadError {
  /// Gets the name of the module that failed to load.
  #[inline]
  pub fn name(&self) -> &str {
    &self.name
  }

  /// Gets the reason for the module load failure.
  #[inline]
  pub fn reason(&self) -> &ModuleLoadErrorReason {
    &self.reason
  }
}

/// Represents the reason for which a Rant module failed to load.
#[derive(Debug)]
pub enum ModuleLoadErrorReason {
  /// The module could not load because the calling context has module loading disabled.
  NotAllowed,
  /// The module was not found.
  NotFound,
  /// The module could not be compiled.
  CompileFailed(Vec<CompilerMessage>),
  /// The module could not load due to a file I/O error.
  FileIOError(ErrorKind),
}

impl Display for ModuleLoadError {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match self.reason() {
      ModuleLoadErrorReason::NotAllowed => write!(f, "module loading is disabled"),
      ModuleLoadErrorReason::NotFound => write!(f, "module '{}' not found", self.name()),
      ModuleLoadErrorReason::CompileFailed(msgs) => write!(f, "module '{}' failed to compile: {}",
      self.name(),
      msgs.iter().fold(String::new(), |mut acc, msg| {
        acc.push_str(&format!("[{}] {}\n", msg.severity(), msg.message())); 
        acc
      })),
      ModuleLoadErrorReason::FileIOError(ioerr) => write!(f, "file I/O error ({:?})", ioerr),
    }
  }
}

impl IntoRuntimeResult<RantProgram> for ModuleLoadResult {
  fn into_runtime_result(self) -> RuntimeResult<RantProgram> {
    self.map_err(|err| RuntimeError {
      description: err.to_string(),
      error_type: RuntimeErrorType::ModuleLoadError(err),
      stack_trace: None,
    })
  }
}