use std::{cell::RefCell, collections::HashMap, rc::Rc};

use crate::{
    errors::{ErrorKind, SloxError, SloxResult},
    tokens::Token,
};

use super::{native_fn, CallableRef, Value};

/// A mutable reference to an environment.
pub(super) type EnvironmentRef = Rc<RefCell<Environment>>;

/// A variable.
pub(super) type Variable = Option<Value>;

/// The execution environment.
#[derive(Debug)]
pub(super) struct Environment {
    pub(super) enclosing: Option<EnvironmentRef>,
    values: HashMap<String, Variable>,
}

impl Default for Environment {
    /// Create the default global environment. This includes native functions.
    fn default() -> Self {
        let mut env = Self {
            enclosing: None,
            values: HashMap::new(),
        };
        env.add_default_fun("clock", native_fn::clock());
        env
    }
}

impl Environment {
    /// Create an environment enclosed in another.
    pub fn create_child(parent: &EnvironmentRef) -> EnvironmentRef {
        Rc::new(RefCell::new(Self {
            enclosing: Some(parent.clone()),
            values: HashMap::default(),
        }))
    }

    /// Add a default function to the environment.
    fn add_default_fun(&mut self, name: &str, fun: CallableRef) {
        let value = Some(Value::Callable(fun));
        self.values.insert(name.to_owned(), value);
    }

    /// Define a new variable.
    pub fn define(&mut self, name: &Token, value: Variable) -> SloxResult<()> {
        if self.values.contains_key(&name.lexeme as &str) {
            Err(SloxError::with_token(
                ErrorKind::Runtime,
                name,
                format!("variable '{}' already defined in scope", name.lexeme),
            ))
        } else {
            self.values.insert(name.lexeme.clone(), value);
            Ok(())
        }
    }

    /// Get the value of a variable.
    pub fn get(&self, name: &Token) -> SloxResult<Value> {
        match self.values.get(&name.lexeme as &str) {
            None => match &self.enclosing {
                None => Err(SloxError::with_token(
                    ErrorKind::Runtime,
                    name,
                    format!("undefined variable '{}'", name.lexeme),
                )),
                Some(parent) => parent.borrow().get(name),
            },
            Some(None) => Err(SloxError::with_token(
                ErrorKind::Runtime,
                name,
                format!("variable '{}' has not been initialized", name.lexeme),
            )),
            Some(Some(value)) => Ok(value.clone()),
        }
    }

    /// Access a variable at a specified distance in a parent environment.
    pub fn get_at(&self, distance: usize, name: &Token) -> Value {
        self.ancestor(distance)
            .values
            .get(&name.lexeme as &str)
            .unwrap()
            .unwrap()
            .clone()
    }

    /// Access the ancestor environment at a specified distance from the current one.
    fn ancestor(&self, distance: usize) -> &Self {
        if distance == 0 {
            &self
        } else {
            self.enclosing.unwrap().borrow().ancestor(distance - 1)
        }
    }

    /// Assign a value to an existing variable.
    pub fn assign(&mut self, name: &Token, value: Value) -> SloxResult<()> {
        if self.values.contains_key(&name.lexeme as &str) {
            self.values.insert(name.lexeme.clone(), Some(value));
            Ok(())
        } else {
            match &mut self.enclosing {
                None => Err(SloxError::with_token(
                    ErrorKind::Runtime,
                    name,
                    format!("undefined variable '{}'", name.lexeme),
                )),
                Some(parent) => parent.borrow_mut().assign(name, value),
            }
        }
    }
}