use crate::utils::Ptr;
use indexmap::IndexSet;
use std::collections::HashMap;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Scope<'ast> {
    TopLevel,
    Block(Ptr<'ast, syn::Block>),
    Generics(Ptr<'ast, syn::Generics>),
    Module(Ptr<'ast, syn::ItemMod>),
    File(Ptr<'ast, syn::File>),
}

impl<'ast> From<&'ast syn::Block> for Scope<'ast> {
    fn from(block: &'ast syn::Block) -> Self {
        Scope::Block(Ptr(block))
    }
}
impl<'ast> From<&'ast syn::Generics> for Scope<'ast> {
    fn from(generics: &'ast syn::Generics) -> Self {
        Scope::Generics(Ptr(generics))
    }
}
impl<'ast> From<&'ast syn::ItemMod> for Scope<'ast> {
    fn from(module: &'ast syn::ItemMod) -> Self {
        Scope::Module(Ptr(module))
    }
}
impl<'ast> From<&'ast syn::File> for Scope<'ast> {
    fn from(file: &'ast syn::File) -> Self {
        Scope::File(Ptr(file))
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct ScopePriority(usize);

impl From<Option<usize>> for ScopePriority {
    fn from(value: Option<usize>) -> Self {
        value.map(|v| Self(v + 1)).unwrap_or(NOT_IN_SCOPE)
    }
}

const NOT_IN_SCOPE: ScopePriority = ScopePriority(0);

#[derive(Debug, Clone)]
struct ScopeInfo<'ast> {
    accessible: IndexSet<Scope<'ast>>,
}

impl<'ast> ScopeInfo<'ast> {
    fn new(scope: Scope<'ast>, parent_info: &ScopeInfo<'ast>) -> Self {
        let mut res = ScopeInfo {
            accessible: parent_info.accessible.clone(),
        };
        res.accessible.insert(scope);
        res
    }
    fn get(&self, scope: Scope<'ast>) -> ScopePriority {
        self.accessible.get_index_of(&scope).into()
    }
}

#[derive(Debug, Clone)]
pub struct AllScopeInfo<'ast> {
    scope_info: HashMap<Scope<'ast>, ScopeInfo<'ast>>,
    current_scopes: Vec<Scope<'ast>>,
}

impl Default for AllScopeInfo<'static> {
    fn default() -> Self {
        let mut res = Self {
            scope_info: Default::default(),
            current_scopes: Default::default(),
        };
        let mut default_scope_info = ScopeInfo {
            accessible: Default::default(),
        };
        default_scope_info.accessible.insert(Scope::TopLevel);
        res.scope_info.insert(Scope::TopLevel, default_scope_info);
        res.current_scopes.push(Scope::TopLevel);
        res
    }
}

impl<'ast> AllScopeInfo<'ast> {
    /// Use during expr visit.
    pub fn push(&mut self, scope: Scope<'ast>) {
        self.current_scopes.push(scope);
    }
    /// Use during static visit
    pub fn push_static(&mut self, new_scope: Scope<'ast>) {
        let parent_scope = self.current_scope();
        let parent_info = self
            .scope_info
            .get(&parent_scope)
            .expect("scope_info missing in push_static");
        self.scope_info
            .insert(new_scope, ScopeInfo::new(new_scope, parent_info));
        self.current_scopes.push(new_scope);
    }
    pub fn pop(&mut self, scope: Scope<'ast>) {
        let popped = self.current_scopes.pop();
        assert_eq!(popped, Some(scope));
    }
    pub fn current_scope(&self) -> Scope<'ast> {
        self.current_scopes.last().cloned().expect("no scope")
    }
    fn get_scope_info(&self, scope: Scope<'ast>) -> &ScopeInfo<'ast> {
        self.scope_info.get(&scope).expect("scope_info missing")
    }
}

#[derive(Debug, Clone)]
pub struct ScopedCollection<'ast, T> {
    contents: HashMap<syn::Ident, Vec<(Scope<'ast>, T)>>,
}

impl<T> Default for ScopedCollection<'_, T> {
    fn default() -> Self {
        Self {
            contents: Default::default(),
        }
    }
}
impl<'ast, T> ScopedCollection<'ast, T> {
    pub fn insert(&mut self, path: syn::Ident, scope: Scope<'ast>, item: T) {
        let v = self.contents.entry(path).or_default();
        v.push((scope, item));
    }
    pub fn get(
        &self,
        ident: &syn::Ident,
        scope: Scope<'ast>,
        all_scope_info: &AllScopeInfo<'ast>,
    ) -> Result<&T, &'static str> {
        let v = self.contents.get(ident).ok_or("no object with that name")?;
        let current_scope_info = all_scope_info.get_scope_info(scope);
        let mut current_priority = NOT_IN_SCOPE;
        let mut res = Err("object with that name not in scope");
        for (scope, item) in v {
            let priority = current_scope_info.get(*scope);
            if priority == NOT_IN_SCOPE {
                continue;
            } else if priority == current_priority {
                res = Err("multiple objects with that name in scope")
            } else if priority > current_priority {
                current_priority = priority;
                res = Ok(item);
            }
        }
        res
    }
}
