"""
Walker interpreter for jac code in AST form

This interpreter should be inhereted from the class that manages state
referenced through self.
"""
from jaseci.graph.node import node
from jaseci.jac.interpreter.interp import interp
from jaseci.jac.jac_set import jac_set
from jaseci.jac.machine.jac_scope import jac_scope
from jaseci.jac.ir.jac_code import jac_ir_to_ast
from jaseci.utils.id_list import id_list


class walker_interp(interp):
    """Jac interpreter mixin for objects that will execute Jac code"""

    # Walker only executes statements, sentinels handle attr_stmts

    def run_walker(self, jac_ast):
        """
        walker: KW_WALKER NAME namespaces? walker_block;
        """
        kid = self.set_cur_ast(jac_ast)
        if jac_ast.name == "walker_block":  # used in jac tests
            self.scope_and_run(jac_ast, self.run_walker_block)
        else:
            self.scope_and_run(kid[-1], self.run_walker_block)

    def run_walker_block(self, jac_ast):
        """
        walker_block:
            LBRACE attr_stmt* walk_entry_block? (
                statement
                | walk_activity_block
            )* walk_exit_block? RBRACE;
        """
        kid = self.set_cur_ast(jac_ast)
        if self.current_step == 0:
            for i in kid:
                if i.name == "attr_stmt":
                    self.run_attr_stmt(jac_ast=i, obj=self)

        archs = self.get_arch_for(self.current_node).arch_with_supers()
        act_list = id_list(self)
        for i in archs:
            act_list += i.entry_action_ids
        self.auto_trigger_node_actions(nd=self.current_node, act_list=act_list)

        for i in kid:
            if i.name == "walk_entry_block":
                self.run_walk_entry_block(i)
            if i.name == "statement":
                self.run_statement(i)
            if i.name == "walk_activity_block":
                self.run_walk_activity_block(i)
            if i.name == "walk_exit_block":
                self.run_walk_exit_block(i)

        # self.trigger_activity_actions()
        archs = self.get_arch_for(self.current_node).arch_with_supers()
        act_list = id_list(self)
        for i in archs:
            act_list += i.exit_action_ids
        self.auto_trigger_node_actions(nd=self.current_node, act_list=act_list)

    def run_node_ctx_block(self, jac_ast):
        """
        node_ctx_block: name_list code_block;
        """
        kid = self.set_cur_ast(jac_ast)
        for i in self.run_name_list(kid[0]):
            if self.get_arch_for(self.current_node).is_instance(i):
                self.run_code_block(kid[1])
                return

    def run_walk_entry_block(self, jac_ast):
        """
        walk_entry_block: KW_WITH KW_ENTRY code_block;
        """
        kid = self.set_cur_ast(jac_ast)
        if self.current_step == 0:
            self.in_entry_exit = True
            self.run_code_block(kid[2])
            self.in_entry_exit = False

    def run_walk_exit_block(self, jac_ast):
        """
        walk_exit_block: KW_WITH KW_EXIT code_block;
        """
        kid = self.set_cur_ast(jac_ast)
        self._stopped = None
        if len(self.next_node_ids) == 0:
            self.in_entry_exit = True
            self.run_code_block(kid[2])
            self.in_entry_exit = False

    def run_walk_activity_block(self, jac_ast):
        """
        walk_activity_block: KW_WITH KW_ACTIVITY code_block;
        """
        kid = self.set_cur_ast(jac_ast)
        self.run_code_block(kid[2])

    def run_walker_action(self, jac_ast):
        """
        walker_action:
            ignore_action
            | take_action
            | KW_DISENGAGE (report_action | SEMI);
        """
        kid = self.set_cur_ast(jac_ast)
        if kid[0].name == "KW_DISENGAGE":
            if kid[1].name == "report_action":
                self.run_report_action(kid[1])
            self._stopped = "stop"
            self.next_node_ids.remove_all()
        else:
            expr_func = getattr(self, f"run_{kid[0].name}")
            expr_func(kid[0])

    def run_ignore_action(self, jac_ast):
        """
        ignore_action: KW_IGNORE expression SEMI;
        """
        kid = self.set_cur_ast(jac_ast)
        result = self.run_expression(kid[1]).value
        if isinstance(result, node):
            self.ignore_node_ids.add_obj(result)
        elif isinstance(result, jac_set):
            self.ignore_node_ids.add_obj_list(result)
        else:
            self.rt_error(f"{result} is not ignorable type (i.e., nodes)", kid[1])

    def run_take_action(self, jac_ast):
        """
        take_action:
            KW_TAKE expression (SEMI | else_stmt);
        """
        kid = self.set_cur_ast(jac_ast)
        result = self.run_expression(kid[1]).value
        before = len(self.next_node_ids)
        if isinstance(result, node):
            self.next_node_ids.add_obj(result, allow_dups=True)
        elif isinstance(result, jac_set):
            self.next_node_ids.add_obj_list(result, allow_dups=True)
        elif result:
            self.rt_error(f"{result} is not destination type (i.e., nodes)", kid[1])
        after = len(self.next_node_ids)
        if before >= after and kid[2].name == "else_stmt":
            self.run_else_stmt(kid[2])
        after = len(self.next_node_ids)

    def run_preset_in_out(self, jac_ast, obj, act):
        """
        preset_in_out:
            DBL_COLON expr_list? (DBL_COLON | COLON_OUT expression);

        obj: The node or edge with preset
        act: The action associated with preset
        """
        kid = self.set_cur_ast(jac_ast)
        param_list = []
        m = interp(parent_override=self.parent(), caller=self)
        arch = self.get_arch_for(obj)
        m.push_scope(
            jac_scope(parent=self, has_obj=obj, action_sets=[arch.activity_action_ids])
        )
        m._jac_scope.set_agent_refs(cur_node=self.current_node, cur_walker=self)

        if kid[1].name == "expr_list":
            param_list = m.run_expr_list(kid[1]).value
        try:
            result = act.trigger(param_list, self._jac_scope, self)
        except Exception as e:
            self.rt_error(f"Internal Exception: {e}", m._cur_jac_ast)
            result = None
        if kid[-1].name == "expression":
            dest = m.run_expression(kid[-1])
            dest.value = result
            dest.write(kid[-1])

    # Helper Functions ##################
    def auto_trigger_node_actions(self, nd, act_list):
        already_executed = []  # handles inhereted duplicates, (overriding)
        for i in act_list.obj_list():
            if (
                i.access_list
                and self.name not in i.access_list
                or i.name in already_executed
            ):
                continue
            if i.preset_in_out:
                self.run_preset_in_out(jac_ir_to_ast(i.preset_in_out), nd, i)
            else:
                self.call_ability(nd=nd, name=i.name, act_list=act_list)
            if not i.preset_in_out:  # All preset in and outs get executed
                already_executed.append(i.name)

    def viable_nodes(self):
        """Returns all nodes that shouldnt be ignored"""
        ret = jac_set()
        for i in self.current_node.attached_nodes():
            if i not in self.ignore_node_ids.obj_list():
                ret.add_obj(i)
        return ret

    def scope_and_run(self, jac_ast, run_func):
        """
        Helper to run ast elements with execution scope added
        (Useful for running arbitrary code blocks as one-offs)
        """
        arch = self.get_arch_for(self.current_node)
        self.push_scope(
            jac_scope(
                parent=self,
                has_obj=self,
                action_sets=[self.activity_action_ids, arch.activity_action_ids],
            )
        )
        self._jac_scope.set_agent_refs(cur_node=self.current_node, cur_walker=self)

        run_func(jac_ast)
        self.pop_scope()
