// Copyright (c) 2016-2020 Fabian Schuiki

//! Facilities to manage declarations and resolve names.

#![deny(missing_docs)]

use std::collections::{HashMap, HashSet};

use crate::common::errors::*;
use crate::common::score::Result;
use crate::common::source::Spanned;
use crate::common::Verbosity;

use crate::score::{Def, ResolvableName, ScopeRef, ScoreContext};

/// A scope.
#[derive(Clone, Debug)]
pub struct Scope {
    /// The parent scope.
    pub parent: Option<ScopeRef>,

    /// The definitions made in this scope.
    pub defs: HashMap<ResolvableName, Vec<Spanned<Def>>>,

    /// The definitions imported from other scopes.
    pub imported_defs: HashMap<ResolvableName, Vec<Spanned<Def>>>,

    /// The explicitly imported scopes.
    pub imported_scopes: HashSet<ScopeRef>,
}

impl Scope {
    /// Create a new empty scope.
    pub fn new(parent: Option<ScopeRef>) -> Scope {
        Scope {
            parent: parent,
            defs: HashMap::new(),
            imported_defs: HashMap::new(),
            imported_scopes: HashSet::new(),
        }
    }
}

impl<'lazy, 'sb, 'ast, 'ctx> ScoreContext<'lazy, 'sb, 'ast, 'ctx> {
    /// Lookup a scope and perform an operation on it.
    pub fn with_scope<F, R>(&self, scope: ScopeRef, f: F) -> Result<R>
    where
        F: FnOnce(&mut Scope) -> Result<R>,
    {
        let mut tbl = self.sb.scope2_table.borrow_mut();
        let scp = match tbl.get_mut(&scope) {
            Some(s) => s,
            None => {
                self.emit(DiagBuilder2::bug(format!(
                    "scope {:?} does not exist`",
                    scope
                )));
                return Err(());
            }
        };
        f(scp)
    }

    /// Create a subscope of another scope.
    pub fn subscope(&self, scope: ScopeRef, parent: ScopeRef) {
        self.sb
            .scope2_table
            .borrow_mut()
            .insert(scope, Scope::new(Some(parent)));
    }

    /// Define a new name in a scope.
    pub fn define(&self, scope: ScopeRef, name: Spanned<ResolvableName>, def: Def) -> Result<()> {
        if self.sess.opts.verbosity.contains(Verbosity::NAMES) {
            debugln!("define `{}` as {:?} in scope {:?}", name.value, def, scope);
        }
        self.with_scope(scope, |scope| match def {
            // Handle overloadable cases.
            Def::Enum(_) => {
                scope
                    .defs
                    .entry(name.value)
                    .or_insert_with(|| Vec::new())
                    .push(Spanned::new(def, name.span));
                Ok(())
            }

            // Handle unique cases.
            _ => {
                let ins = scope
                    .defs
                    .insert(name.value, vec![Spanned::new(def, name.span)]);
                if let Some(existing) = ins {
                    self.emit(
                        DiagBuilder2::error(format!("`{}` has already been declared", name.value))
                            .span(name.span)
                            .add_note("Previous declaration was here:")
                            .span(existing.last().unwrap().span),
                    );
                    Err(())
                } else {
                    Ok(())
                }
            }
        })
    }

    /// Import a definition into a scope.
    pub fn import_def(
        &self,
        scope: ScopeRef,
        name: Spanned<ResolvableName>,
        def: Def,
    ) -> Result<()> {
        self.with_scope(scope, |scope| {
            scope
                .imported_defs
                .entry(name.value)
                .or_insert_with(|| Vec::new())
                .push(Spanned::new(def, name.span));
            Ok(())
        })
    }

    /// Import an entire scope into another scope.
    pub fn import_scope(&self, scope: ScopeRef, into: ScopeRef) -> Result<()> {
        self.with_scope(into, |into| {
            into.imported_scopes.insert(scope);
            Ok(())
        })
    }
}
