//! Code to traverse the AST and drive the rest of scope analysis.
//!
//! This module is responsible for walking the AST. Other modules do the actual
//! analysis.
//!
//! Scope analysis is currently a second pass, after parsing, using the AST,
//! but the goal is to do this analysis as part of the parse phase, even when
//! no AST is built. So we try to keep AST use separate from the analysis code.

use crate::builder::{ScopeBuildError, ScopeDataMapAndScriptStencilList, ScopeDataMapBuilder};
use crate::data::FunctionDeclarationPropertyMap;
use ast::arena;
use ast::associated_data::AssociatedData;
use ast::source_atom_set::CommonSourceAtomSetIndices;
use ast::{types::*, visit::Pass};
use std::collections::HashMap;
use stencil::scope::ScopeDataMap;
use stencil::script::{ScriptStencilIndex, ScriptStencilList};

/// The result of scope analysis.
pub struct ScopePassResult<'alloc> {
    pub scope_data_map: ScopeDataMap,
    pub function_declarations: HashMap<ScriptStencilIndex, &'alloc Function<'alloc>>,
    pub function_stencil_indices: AssociatedData<ScriptStencilIndex>,
    pub function_declaration_properties: FunctionDeclarationPropertyMap,
    pub scripts: ScriptStencilList,
    pub error: Option<ScopeBuildError>,
}

/// The top-level struct responsible for extracting the necessary information
/// from the AST. The analysis itself is done mainly by the `ScopeDataMapBuilder`,
/// which has very limited interaction with the AST.
///
/// FIXME: This should be rewritten as a grammar extension.
#[derive(Debug)]
pub struct ScopePass<'alloc> {
    builder: ScopeDataMapBuilder,
    function_declarations: HashMap<ScriptStencilIndex, &'alloc Function<'alloc>>,
}

impl<'alloc> ScopePass<'alloc> {
    pub fn new() -> Self {
        Self {
            builder: ScopeDataMapBuilder::new(),
            function_declarations: HashMap::new(),
        }
    }
}

impl<'alloc> From<ScopePass<'alloc>> for ScopePassResult<'alloc> {
    fn from(pass: ScopePass<'alloc>) -> ScopePassResult<'alloc> {
        let ScopeDataMapAndScriptStencilList {
            scope_data_map,
            function_stencil_indices,
            function_declaration_properties,
            scripts,
            error,
        } = pass.builder.into();
        ScopePassResult {
            scope_data_map,
            function_declarations: pass.function_declarations,
            function_stencil_indices,
            function_declaration_properties,
            scripts,
            error,
        }
    }
}

impl<'alloc> Pass<'alloc> for ScopePass<'alloc> {
    fn enter_script(&mut self, _ast: &'alloc Script<'alloc>) {
        self.builder.before_script();
    }

    fn leave_script(&mut self, _ast: &'alloc Script<'alloc>) {
        self.builder.after_script();
    }

    fn enter_enum_statement_variant_block_statement(&mut self, block: &'alloc Block<'alloc>) {
        self.builder.before_block_statement(block);
    }

    fn leave_enum_statement_variant_block_statement(&mut self, _block: &'alloc Block<'alloc>) {
        self.builder.after_block_statement();
    }

    fn enter_variable_declaration(&mut self, ast: &'alloc VariableDeclaration<'alloc>) {
        match ast.kind {
            VariableDeclarationKind::Var { .. } => {
                self.builder.before_var_declaration();
            }
            VariableDeclarationKind::Let { .. } => {
                self.builder.before_let_declaration();
            }
            VariableDeclarationKind::Const { .. } => {
                self.builder.before_const_declaration();
            }
        }
    }

    fn leave_variable_declaration(&mut self, ast: &'alloc VariableDeclaration<'alloc>) {
        match ast.kind {
            VariableDeclarationKind::Var { .. } => {
                self.builder.after_var_declaration();
            }
            VariableDeclarationKind::Let { .. } => {
                self.builder.after_let_declaration();
            }
            VariableDeclarationKind::Const { .. } => {
                self.builder.after_const_declaration();
            }
        }
    }

    fn visit_binding_identifier(&mut self, ast: &'alloc BindingIdentifier) {
        self.builder.on_binding_identifier(ast.name.value);
    }

    // Given we override `visit_binding_identifier` above,
    // visit_identifier is not called for Identifier inside BindingIdentifier,
    // and this is called only for references, either
    // IdentifierExpression or AssignmentTargetIdentifier.
    fn visit_identifier(&mut self, ast: &'alloc Identifier) {
        self.builder.on_non_binding_identifier(ast.value);
    }

    fn visit_enum_expression_variant_this_expression(&mut self) {
        self.builder
            .on_non_binding_identifier(CommonSourceAtomSetIndices::this());
    }

    fn enter_enum_statement_variant_function_declaration(&mut self, ast: &'alloc Function<'alloc>) {
        if !self.builder.is_syntax_only_mode() {
            self.builder.enter_syntax_only_mode();
        }

        let name = if let Some(ref name) = ast.name {
            name.name.value
        } else {
            panic!("FunctionDeclaration should have name");
        };
        let fun_index =
            self.builder
                .before_function_declaration(name, ast, ast.is_generator, ast.is_async);
        self.function_declarations.insert(fun_index, ast);
    }

    fn leave_enum_statement_variant_function_declaration(&mut self, ast: &'alloc Function<'alloc>) {
        self.builder.after_function_declaration(ast);
    }

    fn enter_enum_expression_variant_function_expression(&mut self, ast: &'alloc Function<'alloc>) {
        if !self.builder.is_syntax_only_mode() {
            self.builder.enter_syntax_only_mode();
        }

        self.builder
            .before_function_expression(ast, ast.is_generator, ast.is_async);
    }

    fn leave_enum_expression_variant_function_expression(&mut self, ast: &'alloc Function<'alloc>) {
        self.builder.after_function_expression(ast);
    }

    fn visit_formal_parameters(&mut self, ast: &'alloc FormalParameters<'alloc>) {
        self.builder.before_function_parameters(ast);

        self.enter_formal_parameters(ast);
        for item in &ast.items {
            self.builder.before_parameter();
            self.visit_parameter(item);
        }
        if let Some(item) = &ast.rest {
            self.builder.before_rest_parameter();
            self.visit_binding(item);
        }
        self.leave_formal_parameters(ast);

        self.builder.after_function_parameters();
    }

    fn enter_enum_method_definition_variant_method(&mut self, ast: &'alloc Method<'alloc>) {
        self.builder
            .before_method(ast, ast.is_generator, ast.is_async);
        // FIXME: Call self.builder.on_function_name
    }

    fn leave_enum_method_definition_variant_method(&mut self, ast: &'alloc Method<'alloc>) {
        self.builder.after_method(ast);
    }

    /// Getter doesn't have FormalParameters.
    /// Call builder methods just before body.
    fn visit_getter(&mut self, ast: &'alloc Getter<'alloc>) {
        self.builder.before_getter(ast);
        // FIXME: Call self.builder.on_function_name

        self.enter_getter(ast);
        self.visit_class_element_name(&ast.property_name);

        // FIXME: Pass something that points `()` part of getter.
        self.builder.on_getter_parameter(ast);

        self.visit_function_body(&ast.body);
        self.leave_getter(ast);

        self.builder.after_getter(ast);
    }

    /// Setter doesn't have FormalParameters, but single Parameter.
    /// Call builder methods around it.
    fn visit_setter(&mut self, ast: &'alloc Setter<'alloc>) {
        self.builder.before_setter(ast);
        // FIXME: Call self.builder.on_function_name

        self.enter_setter(ast);
        self.visit_class_element_name(&ast.property_name);

        // FIXME: Pass something that points `(param)` part of setter,
        // including `(` and `)`.
        self.builder.before_setter_parameter(&ast.param);
        self.visit_parameter(&ast.param);
        self.builder.after_setter_parameter();

        self.visit_function_body(&ast.body);
        self.leave_setter(ast);

        self.builder.after_setter(ast);
    }

    fn leave_binding_with_default(&mut self, _ast: &'alloc BindingWithDefault<'alloc>) {
        self.builder.after_initializer();
    }

    fn enter_enum_property_name_variant_computed_property_name(
        &mut self,
        _ast: &'alloc ComputedPropertyName<'alloc>,
    ) {
        self.builder.before_computed_property_name();
    }

    fn enter_binding_pattern(&mut self, _ast: &'alloc BindingPattern<'alloc>) {
        self.builder.before_binding_pattern();
    }

    fn enter_function_body(&mut self, ast: &'alloc FunctionBody<'alloc>) {
        self.builder.before_function_body(ast);
    }

    fn leave_function_body(&mut self, _ast: &'alloc FunctionBody<'alloc>) {
        self.builder.after_function_body();
    }

    fn enter_enum_expression_variant_arrow_expression(
        &mut self,
        is_async: &'alloc bool,
        params: &'alloc FormalParameters<'alloc>,
        _body: &'alloc ArrowExpressionBody<'alloc>,
    ) {
        self.builder.before_arrow_function(*is_async, params);
    }

    fn leave_enum_expression_variant_arrow_expression(
        &mut self,
        _is_async: &'alloc bool,
        _params: &'alloc FormalParameters<'alloc>,
        body: &'alloc ArrowExpressionBody<'alloc>,
    ) {
        self.builder.after_arrow_function(body);
    }

    /// Arrow function with expression body.
    /// Use the expression as the node for function body.
    fn enter_enum_arrow_expression_body_variant_expression(
        &mut self,
        ast: &'alloc arena::Box<'alloc, Expression<'alloc>>,
    ) {
        let expr: &Expression = ast;
        self.builder.before_function_body(expr);
    }

    fn leave_enum_arrow_expression_body_variant_expression(
        &mut self,
        _ast: &'alloc arena::Box<'alloc, Expression<'alloc>>,
    ) {
        self.builder.after_function_body();
    }

    fn enter_catch_clause(&mut self, _ast: &'alloc CatchClause<'alloc>) {
        self.builder.before_catch_clause();
    }

    fn enter_call_expression(&mut self, ast: &'alloc CallExpression<'alloc>) {
        match &ast.callee {
            ExpressionOrSuper::Expression(expr) => match &**expr {
                Expression::IdentifierExpression(IdentifierExpression { name, .. }) => {
                    if name.value == CommonSourceAtomSetIndices::eval() {
                        self.builder.on_direct_eval();
                    }
                }
                _ => {}
            },
            _ => {}
        }
    }

    fn enter_class_declaration(&mut self, _ast: &'alloc ClassDeclaration<'alloc>) {
        self.builder.on_class();
    }

    fn enter_class_expression(&mut self, _ast: &'alloc ClassExpression<'alloc>) {
        self.builder.on_class();
    }

    fn enter_enum_statement_variant_with_statement(
        &mut self,
        _object: &'alloc arena::Box<'alloc, Expression<'alloc>>,
        _body: &'alloc arena::Box<'alloc, Statement<'alloc>>,
    ) {
        self.builder.on_with();
    }

    fn visit_enum_unary_operator_variant_delete(&mut self) {
        self.builder.on_delete();
    }

    fn enter_enum_statement_variant_for_statement(
        &mut self,
        init: &'alloc Option<VariableDeclarationOrExpression<'alloc>>,
        _test: &'alloc Option<arena::Box<'alloc, Expression<'alloc>>>,
        _update: &'alloc Option<arena::Box<'alloc, Expression<'alloc>>>,
        _block: &'alloc arena::Box<'alloc, Statement<'alloc>>,
    ) {
        match init {
            Some(VariableDeclarationOrExpression::VariableDeclaration(decl)) => match decl.kind {
                VariableDeclarationKind::Let { .. } | VariableDeclarationKind::Const { .. } => {
                    self.builder.on_lexical_for();
                }
                _ => {}
            },
            _ => {}
        }
    }

    fn enter_enum_statement_variant_for_in_statement(
        &mut self,
        left: &'alloc VariableDeclarationOrAssignmentTarget<'alloc>,
        _right: &'alloc arena::Box<'alloc, Expression<'alloc>>,
        _block: &'alloc arena::Box<'alloc, Statement<'alloc>>,
    ) {
        match left {
            VariableDeclarationOrAssignmentTarget::VariableDeclaration(decl) => match decl.kind {
                VariableDeclarationKind::Let { .. } | VariableDeclarationKind::Const { .. } => {
                    self.builder.on_lexical_for();
                }
                _ => {}
            },
            _ => {}
        }
    }

    fn enter_enum_statement_variant_switch_statement(
        &mut self,
        _discriminant: &'alloc arena::Box<'alloc, Expression<'alloc>>,
        _cases: &'alloc arena::Vec<'alloc, SwitchCase<'alloc>>,
    ) {
        self.builder.on_switch();
    }

    fn visit_enum_expression_variant_new_target_expression(&mut self) {
        self.builder.on_new_target();
    }
}
