use crate::assembler::Bundle;
use crate::errors::{ContextError, ContextErrorReason};
use crate::node::{PNode, PString};
use crate::opcodes::CONTROL_FUNCTIONS;
use std::collections::HashMap;

// The name of the global context as used internally..
const GLOBAL_CONTEXT: &str = "Global";

/// Context holds information about the different scopes being defined, the
/// current scope, and has a map of all the variables defined for each scope.
#[derive(Debug)]
pub struct Context {
    stack: Vec<String>,
    map: HashMap<String, HashMap<String, Bundle>>,
}

impl Context {
    /// Returns a new empty context.
    pub fn new() -> Self {
        Context {
            stack: vec![],
            map: HashMap::from([(String::from(GLOBAL_CONTEXT), HashMap::new())]),
        }
    }

    /// Returns the value of the variable represented by the given `id`. Note
    /// that this `id` can be scoped or not, and this function will try to pick
    /// the variable from the right scope.
    pub fn get_variable(&self, id: &PString) -> Result<Bundle, ContextError> {
        // First of all, figure out the name of the scope and the real name of
        // the variable. If this was not scoped at all (None case when trying to
        // rsplit by the "::" operator), then we assume it's a global variable.
        let (scope_name, var_name) = match id.value.rsplit_once("::") {
            Some((scope, name)) => (scope, name),
            None => (self.name(), id.value.as_str()),
        };

        // And with that, the only thing left is to find the scope and the
        // variable in it.
        match self.map.get(scope_name) {
            Some(scope) => match scope.get(var_name) {
                Some(var) => Ok(var.clone()),
                None => Err(ContextError {
                    message: format!(
                        "could not find variable '{}' in {}",
                        var_name,
                        self.to_human_with(scope_name)
                    ),
                    line: id.line,
                    reason: ContextErrorReason::UnknownVariable,
                }),
            },
            None => Err(ContextError {
                message: format!("did not find scope '{}'", scope_name),
                line: id.line,
                reason: ContextErrorReason::BadScope,
            }),
        }
    }

    /// Sets a value for a variable defined in the assignment `node`. If
    /// `overwrite` is set to true, then this value will be set even if the
    /// variable already existed, otherwise it will return a ContextError
    pub fn set_variable(
        &mut self,
        id: &PString,
        bundle: &Bundle,
        overwrite: bool,
    ) -> Result<(), ContextError> {
        let scope_name = self.name().to_string();
        let scope = self.map.get_mut(&scope_name).unwrap();

        match scope.get_mut(&id.value) {
            Some(sc) => {
                if !overwrite {
                    return Err(ContextError {
                        message: format!(
                            "'{}' already defined in {}: you cannot re-assign variables",
                            id.value,
                            self.to_human()
                        ),
                        line: id.line,
                        reason: ContextErrorReason::Redefinition,
                    });
                }
                *sc = bundle.clone();
            }
            None => {
                scope.insert(id.value.clone(), bundle.to_owned());
            }
        }

        Ok(())
    }

    /// Change the current context given a `node`. Returns a tuple which states:
    ///   0. Whether the context has changed.
    ///   1. Whether a caller can bundle nodes safely.
    pub fn change_context(&mut self, node: &PNode) -> Result<(bool, bool), ContextError> {
        // The parser already guarantees that the control node is
        // from a function that we already know, so calling `unwrap`
        // is not dangerous.
        let control = CONTROL_FUNCTIONS
            .get(&node.value.value.to_lowercase())
            .unwrap();

        // If the control function does not touch the context, leave early.
        if !control.touches_context {
            return Ok((false, true));
        }

        // And push/pop the context depending on the control being used.
        match node.value.value.as_str() {
            ".macro" => {
                self.context_push(&node.left.clone().unwrap());
                Ok((true, false))
            }
            ".proc" | ".scope" => {
                self.context_push(&node.left.clone().unwrap());
                Ok((true, true))
            }
            ".endmacro" | ".endproc" | ".endscope" => {
                self.context_pop(&node.value)?;
                Ok((true, true))
            }
            _ => Ok((false, true)),
        }
    }

    pub fn force_context_switch(&mut self, name: &String) {
        self.stack.push(name.to_owned());
    }

    pub fn force_context_pop(&mut self) {
        if !self.stack.is_empty() {
            self.stack.truncate(self.stack.len() - 1);
        }
    }

    // Pushes a new context given a `node`, which holds the identifier of the
    // new scope.
    fn context_push(&mut self, id: &PNode) {
        let name = match self.stack.last() {
            Some(n) => format!("{}::{}", n, id.value.value),
            None => id.value.value.clone(),
        };

        // Actually push the name to the stack and initialize it on the variable
        // map.
        self.stack.push(name.clone());
        self.map.entry(name).or_default();
    }

    // Pops out the latest context that was pushed.
    fn context_pop(&mut self, id: &PString) -> Result<(), ContextError> {
        if self.stack.is_empty() {
            return Err(ContextError {
                message: format!("missplaced '{}' statement", id.value),
                reason: ContextErrorReason::BadScope,
                line: id.line,
            });
        }

        self.stack.truncate(self.stack.len() - 1);
        Ok(())
    }

    // Returns the name of the current context.
    pub fn name(&self) -> &str {
        match self.stack.last() {
            Some(name) => name,
            None => GLOBAL_CONTEXT,
        }
    }

    // Returns a human-readable string representing the current context.
    fn to_human(&self) -> String {
        match self.stack.last() {
            Some(n) => format!("'{}'", n),
            None => "the global scope".to_string(),
        }
    }

    // Returns a human-readable string representing the given context.
    fn to_human_with(&self, name: &str) -> String {
        if name == GLOBAL_CONTEXT {
            "the global scope".to_string()
        } else {
            format!("'{}'", name)
        }
    }
}

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