use std::collections::HashMap;
use std::hash::Hash;
use std::marker::PhantomData;

pub trait VarInfo {}
pub trait SignalInfo {}
pub trait ComponentInfo {}

#[derive(Clone)]
pub struct OnlyVars;
impl VarInfo for OnlyVars {}
#[derive(Clone)]
pub struct OnlySignals;
impl SignalInfo for OnlySignals {}
#[derive(Clone)]
pub struct OnlyComponents;
impl ComponentInfo for OnlyComponents {}
#[derive(Clone)]
pub struct FullEnvironment;
impl VarInfo for FullEnvironment {}
impl SignalInfo for FullEnvironment {}
impl ComponentInfo for FullEnvironment {}

pub type VarEnvironment<VC> = RawEnvironment<OnlyVars, (), (), VC>;
pub type SignalEnvironment<SC> = RawEnvironment<OnlySignals, (), SC, ()>;
pub type ComponentEnvironment<CC> = RawEnvironment<OnlyComponents, CC, (), ()>;
pub type CircomEnvironment<CC, SC, VC> = RawEnvironment<FullEnvironment, CC, SC, VC>;

pub enum CircomEnvironmentError {
    NonExistentSymbol,
}

#[derive(Clone)]
pub struct RawEnvironment<T, CC, SC, VC> {
    components: HashMap<String, CC>,
    inputs: HashMap<String, SC>,
    outputs: HashMap<String, SC>,
    intermediates: HashMap<String, SC>,
    variables: Vec<VariableBlock<VC>>,
    behaviour: PhantomData<T>,
}
impl<T, CC, SC, VC> Default for RawEnvironment<T, CC, SC, VC> {
    fn default() -> Self {
        let variables = vec![VariableBlock::new()];
        RawEnvironment {
            components: HashMap::new(),
            inputs: HashMap::new(),
            outputs: HashMap::new(),
            intermediates: HashMap::new(),
            variables,
            behaviour: PhantomData,
        }
    }
}
impl<T, CC, SC, VC> RawEnvironment<T, CC, SC, VC>
where
    T: VarInfo + SignalInfo + ComponentInfo,
{
    pub fn has_symbol(&self, symbol: &str) -> bool {
        self.has_signal(symbol) || self.has_component(symbol) || self.has_variable(symbol)
    }
}
impl<T, CC, SC, VC> RawEnvironment<T, CC, SC, VC> {
    pub fn merge(
        left: RawEnvironment<T, CC, SC, VC>,
        right: RawEnvironment<T, CC, SC, VC>,
        using: fn(VC, VC) -> VC,
    ) -> RawEnvironment<T, CC, SC, VC> {
        let mut components = left.components;
        let mut inputs = left.inputs;
        let mut outputs = left.outputs;
        let mut intermediates = left.intermediates;
        components.extend(right.components);
        inputs.extend(right.inputs);
        outputs.extend(right.outputs);
        intermediates.extend(right.intermediates);
        let mut variables_left = left.variables;
        let mut variables_right = right.variables;
        let mut variables = Vec::new();
        while !variables_left.is_empty() && !variables_right.is_empty() {
            let left_block = variables_left.pop().unwrap();
            let right_block = variables_right.pop().unwrap();
            let merged_blocks = VariableBlock::merge(left_block, right_block, using);
            variables.push(merged_blocks);
        }
        variables.reverse();
        RawEnvironment {
            components,
            inputs,
            intermediates,
            outputs,
            variables,
            behaviour: PhantomData,
        }
    }
}
impl<T, CC, SC, VC> RawEnvironment<T, CC, SC, VC>
where
    T: VarInfo,
{
    fn block_with_variable_symbol(&self, symbol: &str) -> Option<&VariableBlock<VC>> {
        let variables = &self.variables;
        let mut act = variables.len();
        while act > 0 {
            if VariableBlock::contains_variable(&variables[act - 1], symbol) {
                return Some(&variables[act - 1]);
            }
            act -= 1;
        }
        None
    }
    fn mut_block_with_variable_symbol(&mut self, symbol: &str) -> Option<&mut VariableBlock<VC>> {
        let variables = &mut self.variables;
        let mut act = variables.len();
        while act > 0 {
            if VariableBlock::contains_variable(&variables[act - 1], symbol) {
                return Some(&mut variables[act - 1]);
            }
            act -= 1;
        }
        None
    }
    pub fn new() -> RawEnvironment<T, CC, SC, VC> {
        RawEnvironment::default()
    }
    pub fn add_variable_block(&mut self) {
        self.variables.push(VariableBlock::new());
    }
    pub fn remove_variable_block(&mut self) {
        assert!(!self.variables.is_empty());
        self.variables.pop();
    }
    pub fn add_variable(&mut self, variable_name: &str, content: VC) {
        assert!(!self.variables.is_empty());
        let last_block = self.variables.last_mut().unwrap();
        last_block.add_variable(variable_name, content);
    }
    pub fn has_variable(&self, symbol: &str) -> bool {
        self.block_with_variable_symbol(symbol).is_some()
    }

    pub fn get_variable(&self, symbol: &str) -> Option<&VC> {
        let possible_block = self.block_with_variable_symbol(symbol);
        if let Some(block) = possible_block {
            Some(block.get_variable(symbol))
        } else {
            None
        }
    }
    pub fn get_mut_variable(&mut self, symbol: &str) -> Option<&mut VC> {
        let possible_block = self.mut_block_with_variable_symbol(symbol);
        if let Some(block) = possible_block {
            Some(block.get_mut_variable(symbol))
        } else {
            None
        }
    }
    pub fn get_variable_res(&self, symbol: &str) -> Result<&VC, CircomEnvironmentError> {
        let possible_block = self.block_with_variable_symbol(symbol);
        if let Some(block) = possible_block {
            Ok(block.get_variable(symbol))
        } else {
            Err(CircomEnvironmentError::NonExistentSymbol)
        }
    }
    pub fn remove_variable(&mut self, symbol: &str) {
        let possible_block = self.mut_block_with_variable_symbol(symbol);
        if let Some(block) = possible_block {
            block.remove_variable(symbol)
        }
    }
    pub fn get_variable_or_break(&self, symbol: &str, file: &str, line: u32) -> &VC {
        assert!(self.has_variable(symbol), "Method call in file {file} line {line}");
        if let Ok(v) = self.get_variable_res(symbol) {
            v
        } else {
            unreachable!();
        }
    }
    pub fn get_mut_variable_mut(
        &mut self,
        symbol: &str,
    ) -> Result<&mut VC, CircomEnvironmentError> {
        let possible_block = self.mut_block_with_variable_symbol(symbol);
        if let Some(block) = possible_block {
            Ok(block.get_mut_variable(symbol))
        } else {
            Err(CircomEnvironmentError::NonExistentSymbol)
        }
    }
    pub fn get_mut_variable_or_break(&mut self, symbol: &str, file: &str, line: u32) -> &mut VC {
        assert!(self.has_variable(symbol), "Method call in file {file} line {line}");
        if let Ok(v) = self.get_mut_variable_mut(symbol) {
            v
        } else {
            unreachable!();
        }
    }
    pub fn variable_iter(&self) -> impl Iterator<Item = (&String, &VC)> {
        self.variables.iter().flat_map(|block| block.iter())
    }
}

impl<T, CC, SC, VC> RawEnvironment<T, CC, SC, VC>
where
    T: ComponentInfo,
{
    pub fn add_component(&mut self, component_name: &str, content: CC) {
        self.components.insert(component_name.to_string(), content);
    }
    pub fn remove_component(&mut self, component_name: &str) {
        self.components.remove(component_name);
    }
    pub fn has_component(&self, symbol: &str) -> bool {
        self.components.contains_key(symbol)
    }
    pub fn get_component(&self, symbol: &str) -> Option<&CC> {
        self.components.get(symbol)
    }
    pub fn get_mut_component(&mut self, symbol: &str) -> Option<&mut CC> {
        self.components.get_mut(symbol)
    }
    pub fn get_component_res(&self, symbol: &str) -> Result<&CC, CircomEnvironmentError> {
        self.components.get(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol)
    }
    pub fn get_component_or_break(&self, symbol: &str, file: &str, line: u32) -> &CC {
        assert!(self.has_component(symbol), "Method call in file {file} line {line}");
        self.components.get(symbol).unwrap()
    }
    pub fn get_mut_component_res(
        &mut self,
        symbol: &str,
    ) -> Result<&mut CC, CircomEnvironmentError> {
        self.components.get_mut(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol)
    }
    pub fn get_mut_component_or_break(&mut self, symbol: &str, file: &str, line: u32) -> &mut CC {
        assert!(self.has_component(symbol), "Method call in file {file} line {line}");
        self.components.get_mut(symbol).unwrap()
    }
}

impl<T, CC, SC, VC> RawEnvironment<T, CC, SC, VC>
where
    T: SignalInfo,
{
    pub fn add_input(&mut self, input_name: &str, content: SC) {
        self.inputs.insert(input_name.to_string(), content);
    }
    pub fn remove_input(&mut self, input_name: &str) {
        self.inputs.remove(input_name);
    }
    pub fn add_output(&mut self, output_name: &str, content: SC) {
        self.outputs.insert(output_name.to_string(), content);
    }
    pub fn remove_output(&mut self, output_name: &str) {
        self.outputs.remove(output_name);
    }
    pub fn add_intermediate(&mut self, intermediate_name: &str, content: SC) {
        self.intermediates.insert(intermediate_name.to_string(), content);
    }
    pub fn remove_intermediate(&mut self, intermediate_name: &str) {
        self.intermediates.remove(intermediate_name);
    }
    pub fn has_input(&self, symbol: &str) -> bool {
        self.inputs.contains_key(symbol)
    }
    pub fn has_output(&self, symbol: &str) -> bool {
        self.outputs.contains_key(symbol)
    }
    pub fn has_intermediate(&self, symbol: &str) -> bool {
        self.intermediates.contains_key(symbol)
    }
    pub fn has_signal(&self, symbol: &str) -> bool {
        self.has_input(symbol) || self.has_output(symbol) || self.has_intermediate(symbol)
    }
    pub fn get_input(&self, symbol: &str) -> Option<&SC> {
        self.inputs.get(symbol)
    }
    pub fn get_mut_input(&mut self, symbol: &str) -> Option<&mut SC> {
        self.inputs.get_mut(symbol)
    }
    pub fn get_input_res(&self, symbol: &str) -> Result<&SC, CircomEnvironmentError> {
        self.inputs.get(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol)
    }
    pub fn get_input_or_break(&self, symbol: &str, file: &str, line: u32) -> &SC {
        assert!(self.has_input(symbol), "Method call in file {file} line {line}");
        self.inputs.get(symbol).unwrap()
    }
    pub fn get_mut_input_res(&mut self, symbol: &str) -> Result<&mut SC, CircomEnvironmentError> {
        self.inputs.get_mut(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol)
    }
    pub fn get_mut_input_or_break(&mut self, symbol: &str, file: &str, line: u32) -> &mut SC {
        assert!(self.has_input(symbol), "Method call in file {file} line {line}");
        self.inputs.get_mut(symbol).unwrap()
    }

    pub fn get_output(&self, symbol: &str) -> Option<&SC> {
        self.outputs.get(symbol)
    }
    pub fn get_mut_output(&mut self, symbol: &str) -> Option<&mut SC> {
        self.outputs.get_mut(symbol)
    }
    pub fn get_output_res(&self, symbol: &str) -> Result<&SC, CircomEnvironmentError> {
        self.outputs.get(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol)
    }
    pub fn get_output_or_break(&self, symbol: &str, file: &str, line: u32) -> &SC {
        assert!(self.has_output(symbol), "Method call in file {file} line {line}");
        self.outputs.get(symbol).unwrap()
    }
    pub fn get_mut_output_res(&mut self, symbol: &str) -> Result<&mut SC, CircomEnvironmentError> {
        self.outputs.get_mut(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol)
    }
    pub fn get_mut_output_or_break(&mut self, symbol: &str, file: &str, line: u32) -> &mut SC {
        assert!(self.has_output(symbol), "Method call in file {file} line {line}");
        self.outputs.get_mut(symbol).unwrap()
    }

    pub fn get_intermediate(&self, symbol: &str) -> Option<&SC> {
        self.intermediates.get(symbol)
    }
    pub fn get_mut_intermediate(&mut self, symbol: &str) -> Option<&mut SC> {
        self.intermediates.get_mut(symbol)
    }
    pub fn get_intermediate_res(&self, symbol: &str) -> Result<&SC, CircomEnvironmentError> {
        self.intermediates.get(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol)
    }
    pub fn get_intermediate_or_break(&self, symbol: &str, file: &str, line: u32) -> &SC {
        assert!(self.has_intermediate(symbol), "Method call in file {file} line {line}");
        self.intermediates.get(symbol).unwrap()
    }
    pub fn get_mut_intermediate_res(
        &mut self,
        symbol: &str,
    ) -> Result<&mut SC, CircomEnvironmentError> {
        self.intermediates.get_mut(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol)
    }
    pub fn get_mut_intermediate_or_break(
        &mut self,
        symbol: &str,
        file: &str,
        line: u32,
    ) -> &mut SC {
        assert!(self.has_intermediate(symbol), "Method call in file {file} line {line}");
        self.intermediates.get_mut(symbol).unwrap()
    }

    pub fn get_signal(&self, symbol: &str) -> Option<&SC> {
        if self.has_input(symbol) {
            self.get_input(symbol)
        } else if self.has_output(symbol) {
            self.get_output(symbol)
        } else if self.has_intermediate(symbol) {
            self.get_intermediate(symbol)
        } else {
            None
        }
    }
    pub fn get_mut_signal(&mut self, symbol: &str) -> Option<&mut SC> {
        if self.has_input(symbol) {
            self.get_mut_input(symbol)
        } else if self.has_output(symbol) {
            self.get_mut_output(symbol)
        } else if self.has_intermediate(symbol) {
            self.get_mut_intermediate(symbol)
        } else {
            None
        }
    }
    pub fn get_signal_res(&self, symbol: &str) -> Result<&SC, CircomEnvironmentError> {
        if self.has_input(symbol) {
            self.get_input_res(symbol)
        } else if self.has_output(symbol) {
            self.get_output_res(symbol)
        } else if self.has_intermediate(symbol) {
            self.get_intermediate_res(symbol)
        } else {
            Err(CircomEnvironmentError::NonExistentSymbol)
        }
    }
    pub fn get_signal_or_break(&self, symbol: &str, file: &str, line: u32) -> &SC {
        assert!(self.has_signal(symbol), "Method call in file {file} line {line}");
        if let Ok(v) = self.get_signal_res(symbol) {
            v
        } else {
            unreachable!();
        }
    }
    pub fn get_mut_signal_res(&mut self, symbol: &str) -> Result<&mut SC, CircomEnvironmentError> {
        if self.has_input(symbol) {
            self.get_mut_input_res(symbol)
        } else if self.has_output(symbol) {
            self.get_mut_output_res(symbol)
        } else if self.has_intermediate(symbol) {
            self.get_mut_intermediate_res(symbol)
        } else {
            Err(CircomEnvironmentError::NonExistentSymbol)
        }
    }
    pub fn get_mut_signal_or_break(&mut self, symbol: &str, file: &str, line: u32) -> &mut SC {
        assert!(self.has_signal(symbol), "Method call in file {file} line {line}");
        if let Ok(v) = self.get_mut_signal_res(symbol) {
            v
        } else {
            unreachable!();
        }
    }
}

#[derive(Clone)]
struct VariableBlock<VC> {
    variables: HashMap<String, VC>,
}
impl<VC> Default for VariableBlock<VC> {
    fn default() -> Self {
        VariableBlock { variables: HashMap::new() }
    }
}
impl<VC> VariableBlock<VC> {
    pub fn new() -> VariableBlock<VC> {
        VariableBlock::default()
    }
    pub fn add_variable(&mut self, symbol: &str, content: VC) {
        self.variables.insert(symbol.to_string(), content);
    }
    pub fn remove_variable(&mut self, symbol: &str) {
        self.variables.remove(symbol);
    }
    pub fn contains_variable(&self, symbol: &str) -> bool {
        self.variables.contains_key(symbol)
    }
    pub fn get_variable(&self, symbol: &str) -> &VC {
        assert!(self.contains_variable(symbol));
        self.variables.get(symbol).unwrap()
    }
    pub fn get_mut_variable(&mut self, symbol: &str) -> &mut VC {
        assert!(self.contains_variable(symbol));
        self.variables.get_mut(symbol).unwrap()
    }
    pub fn iter(&self) -> impl Iterator<Item = (&String, &VC)> {
        self.variables.iter()
    }
    pub fn merge(
        left: VariableBlock<VC>,
        right: VariableBlock<VC>,
        using: fn(VC, VC) -> VC,
    ) -> VariableBlock<VC> {
        let left_block = left.variables;
        let right_block = right.variables;
        let result_block = hashmap_union(left_block, right_block, using);
        VariableBlock { variables: result_block }
    }
}

fn hashmap_union<K, V>(
    l: HashMap<K, V>,
    mut r: HashMap<K, V>,
    merge_function: fn(V, V) -> V,
) -> HashMap<K, V>
where
    K: Hash + Eq,
{
    let mut result = HashMap::new();
    for (k, v) in l {
        if let Some(r_v) = r.remove(&k) {
            result.insert(k, merge_function(v, r_v));
        } else {
            result.insert(k, v);
        }
    }
    for (k, v) in r {
        result.entry(k).or_insert(v);
    }
    result
}
