import ast
import logging
import os
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union

from boa3.builtin.compile_time import NeoMetadata
from boa3.internal import constants
from boa3.internal.analyser.astanalyser import IAstAnalyser
from boa3.internal.analyser.importanalyser import ImportAnalyser
from boa3.internal.analyser.model.ManifestSymbol import ManifestSymbol
from boa3.internal.analyser.model.functionarguments import FunctionArguments
from boa3.internal.analyser.model.optimizer import UndefinedType
from boa3.internal.analyser.model.symbolscope import SymbolScope
from boa3.internal.exception import CompilerError, CompilerWarning
from boa3.internal.model.builtin.builtin import Builtin
from boa3.internal.model.builtin.decorator import ContractDecorator
from boa3.internal.model.builtin.decorator.builtindecorator import IBuiltinDecorator
from boa3.internal.model.builtin.method.builtinmethod import IBuiltinMethod
from boa3.internal.model.callable import Callable
from boa3.internal.model.decorator import IDecorator
from boa3.internal.model.event import Event
from boa3.internal.model.expression import IExpression
from boa3.internal.model.imports.importsymbol import BuiltinImport, Import
from boa3.internal.model.imports.package import Package
from boa3.internal.model.method import Method
from boa3.internal.model.module import Module
from boa3.internal.model.property import Property
from boa3.internal.model.symbol import ISymbol
from boa3.internal.model.type.annotation.metatype import MetaType
from boa3.internal.model.type.annotation.uniontype import UnionType
from boa3.internal.model.type.classes.classscope import ClassScope
from boa3.internal.model.type.classes.classtype import ClassType
from boa3.internal.model.type.classes.contractinterfaceclass import ContractInterfaceClass
from boa3.internal.model.type.classes.pythonclass import PythonClass
from boa3.internal.model.type.classes.userclass import UserClass
from boa3.internal.model.type.collection.icollection import ICollectionType as Collection
from boa3.internal.model.type.collection.sequence.sequencetype import SequenceType
from boa3.internal.model.type.type import IType, Type
from boa3.internal.model.variable import Variable

class ModuleAnalyser(IAstAnalyser, ast.NodeVisitor):
    """
    This class is responsible for mapping the locals of the functions and modules

    The methods with the name starting with 'visit_' are implementations of methods from the :class:`NodeVisitor` class.
    These methods are used to walk through the Python abstract syntax tree.

    :ivar modules: a dictionary that maps each module with its name. Empty by default.
    :ivar symbols: a dictionary that maps the global symbols.
    """

    def __init__(self, analyser, symbol_table: Dict[str, ISymbol],
                 filename: str = None, root_folder: str = None,
                 analysed_files: Optional[dict] = None,
                 import_stack: Optional[List[str]] = None,
                 log: bool = False,
                 fail_fast: bool = True):
        super().__init__(analyser.ast_tree, filename, root_folder, log, fail_fast)
        self.modules: Dict[str, Module] = {}
        self.symbols: Dict[str, ISymbol] = symbol_table

        from boa3.internal.analyser.analyser import Analyser
        if isinstance(analysed_files, dict):
            for file_path, file_analyser in analysed_files.copy().items():
                fixed_path = file_path.replace(os.sep, constants.PATH_SEPARATOR)
                if file_path != fixed_path:
                    analysed_files.pop(file_path)
                    analysed_files[fixed_path] = file_analyser
        else:
            analysed_files = {}

        analysed_files[filename.replace(os.sep, constants.PATH_SEPARATOR)] = analyser
        self._analysed_files: Dict[str, Analyser] = analysed_files

        if isinstance(import_stack, list):
            import_stack = [file_path.replace(os.sep, constants.PATH_SEPARATOR)
                            if isinstance(file_path, str) else file_path
                            for file_path in import_stack]
        else:
            import_stack = []
        self._import_stack: List[str] = import_stack

        self._builtin_functions_to_visit: Dict[str, IBuiltinMethod] = {}
        self._current_module: Module = None
        self._current_class: UserClass = None
        self._current_method: Method = None
        self._current_event: Event = None

        self._deploy_method: Optional[Method] = None

        self._annotated_variables: List[str] = []
        self._global_assigned_variables: List[str] = []
        self._scope_stack: List[SymbolScope] = []

        self._metadata: NeoMetadata = None
        self._metadata_node: ast.AST = ast.parse('')
        self._manifest_symbols: Dict[Tuple[ManifestSymbol, str, int], Callable] = {}
        self.imported_nodes: List[ast.AST] = []

        if self.filename:
            self._tree.filename = self.filename
        self.analyse_visit(self._tree)

        analyser.metadata = self._metadata if self._metadata is not None else NeoMetadata()

    @property
    def _current_scope(self) -> Union[Method, Module, UserClass, None]:
        """
        Returns the scope that is currently being analysed

        :return: the current scope. Return None if it is the global scope
        :rtype: Method or Module or None
        """
        if self._current_method is not None:
            return self._current_method
        if self._current_class is not None:
            return self._current_class
        return self._current_module

    @property
    def _current_symbol_scope(self) -> Optional[SymbolScope]:
        if len(self._scope_stack) > 0:
            return self._scope_stack[-1]
        else:
            return None

    @property
    def global_symbols(self) -> Dict[str, ISymbol]:
        """
        Returns the symbols in global scope

        :return: the symbol table of the global scope
        """
        global_symbols: Dict[str, ISymbol] = {}

        global_symbols.update(self.symbols)
        for mod in self.modules.values():
            global_symbols.update(mod.symbols)

        return global_symbols

    @property
    def analysed_files(self) -> Dict[str, Any]:
        return self._analysed_files.copy()

    def __include_variable(self, var_id: str, var_type_id: Union[str, IType],
                           source_node: ast.AST,
                           var_enumerate_type: IType = Type.none, assignment: bool = True):
        """
        Includes the variable in the symbol table if the id was not used

        :param var_id: variable id
        :param var_type_id: variable type id
        :type var_type_id: str or IType
        :param var_enumerate_type: variable value type id if var_type_id is a SequenceType
        """
        if not isinstance(var_id, str) and isinstance(var_id, Iterable):
            variables = [var_name.id if isinstance(var_name, ast.Name) else self.visit(var_name).id
                         for var_name in source_node.targets[0].elts]
            var_types = self.visit(source_node.value)
        else:
            variables = [var_id]
            var_types = [var_type_id]

        for x in range(min(len(variables), len(var_types))):
            var_id, var_type_id = variables[x], var_types[x]

            outer_symbol = self.get_symbol(var_id)
            if var_id in self._current_symbol_scope.symbols:
                if hasattr(outer_symbol, 'set_is_reassigned'):
                    # don't mark as reassigned if it is outside of a function
                    is_module_scope = isinstance(self._current_scope, Module)
                    if not is_module_scope:
                        outer_symbol.set_is_reassigned()
                    self.__set_source_origin(source_node, is_module_scope)
            else:
                if (not isinstance(source_node, ast.Global) and
                        (not hasattr(source_node, 'targets') or not isinstance(source_node.targets[x], ast.Subscript))):
                    if outer_symbol is not None:
                        self._log_warning(
                            CompilerWarning.NameShadowing(source_node.lineno, source_node.col_offset, outer_symbol, var_id)
                        )

                var_type = None
                if isinstance(var_type_id, SequenceType):
                    var_type = var_type_id
                    var_enumerate_type = self.get_enumerate_type(var_type)
                elif isinstance(var_type_id, IType):
                    var_type = var_type_id

                # when setting a None value to a variable, set the variable as any type
                if var_type is Type.none:
                    var_type = Type.any

                if isinstance(var_type, IType) or var_type is None:
                    # if type is None, the variable type depends on the type of a expression
                    if isinstance(source_node, ast.Global):
                        var = outer_symbol
                    else:
                        if isinstance(var_type, SequenceType):
                            var_type = var_type.build_collection(var_enumerate_type)
                        var = Variable(var_type, origin_node=source_node)

                    self._current_symbol_scope.include_symbol(var_id, var)
                    if isinstance(source_node, ast.AnnAssign):
                        self._annotated_variables.append(var_id)
            if hasattr(self._current_scope, 'assign_variable') and assignment:
                self._global_assigned_variables.append(var_id)

    def __include_callable(self, callable_id: str, callable: Callable):
        """
        Includes the method in the symbol table if the id was not used

        :param callable_id: method id
        :param callable: method to be included
        """
        if ((self._current_scope is self._current_class or callable_id not in self._current_scope.symbols)
                and hasattr(self._current_scope, 'include_callable')):
            already_exists = not self._current_scope.include_callable(callable_id, callable)
        else:
            symbol = self.get_symbol(callable_id)
            already_exists = symbol is not None and not isinstance(symbol, IBuiltinMethod)

        if already_exists:
            self._log_error(CompilerError.DuplicatedIdentifier(callable.origin.lineno,
                                                               callable.origin.col_offset,
                                                               callable_id))

        if callable.is_public:
            # check if the external name + argument number is unique
            manifest_name = callable.external_name if callable.external_name is not None else callable_id
            manifest_id = (ManifestSymbol.get_manifest_symbol(callable), manifest_name, len(callable.args))

            if manifest_id in self._manifest_symbols:
                self._log_error(CompilerError.DuplicatedManifestIdentifier(callable.origin.lineno,
                                                                           callable.origin.col_offset,
                                                                           manifest_name, len(callable.args)
                                                                           ))
            else:
                self._manifest_symbols[manifest_id] = callable

    def __include_class_variable(self, cl_var_id: str, cl_var: Variable):
        """
        Includes the class variable in the current class

        :param cl_var_id: variable name
        :param cl_var: variable to be included
        """
        if cl_var_id not in self._current_scope.class_variables:
            self._current_class.include_symbol(cl_var_id, cl_var, ClassScope.CLASS)

    def __set_source_origin(self, source_node: ast.AST, scope_is_correct: bool = True):
        if scope_is_correct or self._current_scope == self._deploy_method:
            source_node.origin = self._tree

    def get_symbol(self, symbol_id: str,
                   is_internal: bool = False,
                   check_raw_id: bool = False,
                   origin_node: ast.AST = None) -> Optional[ISymbol]:
        for scope in reversed(self._scope_stack):
            if symbol_id in scope.symbols:
                return scope.symbols[symbol_id]

            if check_raw_id:
                found_symbol = self._search_by_raw_id(symbol_id, list(scope.symbols.values()))
                if found_symbol is not None:
                    return found_symbol

        if symbol_id in self._current_scope.symbols:
            # the symbol exists in the local scope
            return self._current_scope.symbols[symbol_id]
        elif symbol_id in self._current_module.symbols:
            # the symbol exists in the module scope
            return self._current_module.symbols[symbol_id]

        if check_raw_id:
            found_symbol = self._search_by_raw_id(symbol_id, list(self._current_scope.symbols.values()))
            if found_symbol is not None:
                return found_symbol

            found_symbol = self._search_by_raw_id(symbol_id, list(self._current_module.symbols.values()))
            if found_symbol is not None:
                return found_symbol

        return super().get_symbol(symbol_id, is_internal, check_raw_id, origin_node)

    def get_annotation(self, value: Any, use_metatype: bool = False, accept_none: bool = False) -> Optional[IType]:
        if not isinstance(value, ast.AST):
            return None

        annotation_type = self.get_type(value, use_metatype)
        if not isinstance(annotation_type, PythonClass):
            return annotation_type
        if hasattr(value, 'value') and value.value is None and annotation_type is Type.none:
            return annotation_type

        if isinstance(value, (ast.Constant, ast.NameConstant, ast.List, ast.Tuple, ast.Dict, ast.Set)):
            # annotated types should only accept types
            return None
        return annotation_type

    def _check_annotation_type(self, node: ast.AST, origin_node: Optional[ast.AST] = None):
        if node is None:
            return

        if origin_node is None:
            origin_node = node

        if self.get_annotation(node) is None:
            actual_type = self.get_type(node)
            self._log_error(
                CompilerError.MismatchedTypes(
                    origin_node.lineno, origin_node.col_offset,
                    expected_type_id=type.__name__,
                    actual_type_id=actual_type.identifier
                ))

    # region Log

    def _log_import(self, import_from: str):
        if self._log:
            logging.getLogger(constants.BOA_LOGGING_NAME).info("Importing '{0}'\t <{1}>".format(import_from, self.filename))

    def _log_unresolved_import(self, origin_node: ast.AST, import_id: str):
        if self._log:
            self._log_error(
                CompilerError.UnresolvedReference(
                    line=origin_node.lineno,
                    col=origin_node.col_offset,
                    symbol_id=import_id
                )
            )

    # endregion

    # region Metadata

    def _read_metadata_object(self, function: ast.FunctionDef):
        """
        Gets the metadata object defined in this function

        :param function:
        """
        if self._metadata is not None:
            # metadata function has been defined already
            self._log_warning(
                CompilerWarning.RedeclaredSymbol(
                    line=function.lineno, col=function.col_offset,
                    symbol_id=Builtin.Metadata.identifier
                )
            )
        # this function must have a return and no arguments
        elif len(function.args.args) != 0:
            self._log_error(
                CompilerError.UnexpectedArgument(
                    line=function.lineno, col=function.col_offset
                )
            )
        elif not any(isinstance(stmt, ast.Return) for stmt in function.body):
            self._log_error(
                CompilerError.MissingReturnStatement(
                    line=function.lineno, col=function.col_offset,
                    symbol_id=function.name
                )
            )
        else:
            function.returns = None
            function.decorator_list = []

            imports: List[ast.AST] = []
            other_instructions: List[ast.AST] = []
            for node in self._tree.body:
                if node == function:
                    # metadata function must be right after all the imports, so it executes correctly
                    continue

                if isinstance(node, (ast.ImportFrom, ast.Import)):
                    imports.append(node)
                else:
                    other_instructions.append(node)

            module: ast.Module = ast.parse('')
            module.body = imports + [function] + other_instructions
            ast.copy_location(module, function)
            namespace = {}

            import sys
            file_dir = os.path.abspath(os.path.dirname(self.filename))
            sys_path = sys.path.copy()

            try:
                from boa3.internal import utils
                if os.path.abspath(self.root_folder) != file_dir:
                    sc_paths = [os.path.abspath(self.root_folder), file_dir]
                else:
                    sc_paths = [file_dir]

                sys.path = sc_paths + utils.list_inner_packages(file_dir) + sys_path

                # executes the function
                code = compile(module, filename='<boa3>', mode='exec')
                exec(code, namespace)
            except ModuleNotFoundError:
                # will fail if any imports can't be executed
                # in this case, the error is already logged
                return
            except BaseException as inner_exception:
                # reordering the module tree may raise unexpected exceptions
                # ignore if it has generated the metadata function
                if function.name not in namespace:
                    raise inner_exception
            finally:
                sys.path = sys_path

            obj: Any = namespace[function.name]()
            node: ast.AST = function.body[-1] if len(function.body) > 0 else function
            # return must be a NeoMetadata object
            if not isinstance(obj, NeoMetadata):
                obj_type = self.get_type(obj).identifier if self.get_type(obj) is not Type.any else type(obj).__name__
                self._log_error(
                    CompilerError.MismatchedTypes(
                        line=node.lineno, col=node.col_offset,
                        expected_type_id=NeoMetadata.__name__,
                        actual_type_id=obj_type
                    )
                )
                return

            # validate if the extras field can be converted to json
            try:
                import json
                json.dumps(obj.extras)
            except BaseException as e:
                self._log_error(
                    CompilerError.InvalidType(
                        line=node.lineno, col=node.col_offset,
                        symbol_id=str(e)
                    )
                )
                return

            # validates the metadata attributes types
            attributes: Dict[str, Any] = {attr: value
                                          for attr, value in dict(obj.__dict__).items()
                                          if attr in Builtin.metadata_fields}
            if any(not isinstance(value, Builtin.metadata_fields[attr]) for attr, value in attributes.items()):
                for expected, actual in [(Builtin.metadata_fields[attr], type(v_type))
                                         for attr, v_type in attributes.items()
                                         if not isinstance(v_type, Builtin.metadata_fields[attr])]:
                    if isinstance(expected, Iterable):
                        expected_id = 'Union[{0}]'.format(', '.join([tpe.__name__ for tpe in expected]))
                    elif hasattr(expected, '__name__'):
                        expected_id = expected.__name__
                    else:
                        expected_id = str(expected)

                    self._log_error(
                        CompilerError.MismatchedTypes(
                            line=node.lineno, col=node.col_offset,
                            expected_type_id=expected_id,
                            actual_type_id=actual.__name__
                        )
                    )
            else:
                # if the function was defined correctly, sets the metadata object of the smart contract
                self._metadata = obj
                self._metadata_node = function  # for error messages only

    # endregion

    # region AST

    def visit_ImportFrom(self, import_from: ast.ImportFrom):
        """
        Includes methods and variables from other modules into the current scope

        :param import_from:
        """
        self._log_import(import_from.module)
        analyser = self._analyse_module_to_import(import_from, import_from.module)
        if analyser is not None:
            import_alias: Dict[str] = \
                {alias.name: alias.asname if alias.asname is not None else alias.name for alias in import_from.names}

            new_symbols: Dict[str, ISymbol] = analyser.export_symbols(list(import_alias.keys()))

            # check if the wildcard is used and filter the symbols
            if constants.IMPORT_WILDCARD in import_alias:
                import_alias.pop(constants.IMPORT_WILDCARD)
                for imported_symbol_id in new_symbols:
                    # add the symbols imported with the wildcard without specific aliases in the dict
                    if imported_symbol_id not in import_alias:
                        import_alias[imported_symbol_id] = imported_symbol_id

            # includes the module to be able to generate the functions
            imported_module = self._build_import(analyser.path, analyser.tree, analyser, import_alias)
            self._current_scope.include_symbol(import_from.module, imported_module)

            for name, alias in import_alias.items():
                if name in new_symbols:
                    self._current_scope.include_symbol(alias, imported_module.symbols[name])
                else:
                    # if there's a symbol that couldn't be loaded, log a compiler error
                    self._log_unresolved_import(import_from, name)

    def visit_Import(self, import_node: ast.Import):
        """
        Includes methods and variables from other modules into the current scope

        :param import_node:
        """
        import_alias: Dict[str] = \
            {alias.name: alias.asname if alias.asname is not None else alias.name for alias in import_node.names}

        for target, alias in import_alias.items():
            self._log_import(target)
            analyser = self._analyse_module_to_import(import_node, target)
            if analyser is not None:
                new_symbols: Dict[str, ISymbol] = analyser.export_symbols()
                for symbol in [symbol for symbol in analyser.symbols if symbol not in new_symbols]:
                    # if there's a symbol that couldn't be loaded, log a compiler error
                    self._log_unresolved_import(import_node, '{0}.{1}'.format(target, symbol))

                imported_module = self._build_import(analyser.path, analyser.tree, analyser)
                self._current_scope.include_symbol(alias, imported_module)

    def _build_import(self, origin: str, syntax_tree: ast.AST,
                      import_analyser: ImportAnalyser,
                      imported_symbols: Dict[str, ISymbol] = None) -> Import:

        if import_analyser.is_builtin_import:
            return BuiltinImport(origin, syntax_tree, import_analyser, imported_symbols)

        return Import(origin, syntax_tree, import_analyser, imported_symbols)

    def _analyse_module_to_import(self, origin_node: ast.AST, target: str) -> Optional[ImportAnalyser]:
        already_imported = {imported.origin: imported.analyser
                            for imported in self._current_module.symbols.values()
                            if isinstance(imported, Import) and imported.analyser is not None
                            }
        already_imported.update(self._analysed_files)

        try:
            analyser = ImportAnalyser(import_target=target,
                                      root_folder=self.root_folder,
                                      importer_file=self.filename,
                                      already_imported_modules=already_imported,
                                      import_stack=self._import_stack.copy(),
                                      log=self._log,
                                      fail_fast=self._fail_fast)

            if analyser.recursive_import:
                self._log_error(
                    CompilerError.CircularImport(line=origin_node.lineno,
                                                 col=origin_node.col_offset,
                                                 target_import=target,
                                                 target_origin=self.filename)
                )

            elif not analyser.can_be_imported:
                circular_import_error = next((error for error in analyser.errors
                                              if isinstance(error, CompilerError.CircularImport)),
                                             None)

                if circular_import_error is not None:
                    # if the problem was a circular import, the error was already logged
                    self.errors.append(circular_import_error)
                elif hasattr(analyser, 'is_namespace_package') and analyser.is_namespace_package:
                    return analyser
                else:
                    self._log_unresolved_import(origin_node, target)

            else:
                analyser.update_external_analysed_files(self._analysed_files)
                return analyser

        except CompilerError.CompilerError as error_on_import:
            if self._log:
                self.errors.append(error_on_import)
                raise error_on_import
            else:
                self._log_error(error_on_import)

    def visit_Module(self, module: ast.Module):
        """
        Visitor of the module node

        Fills module symbol table

        :param module:
        """
        mod: Module = Module()
        self._current_module = mod
        self._scope_stack.append(SymbolScope())

        global_stmts = []
        function_stmts = []
        for stmt in module.body:
            if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
                function_stmts.append(stmt)
            elif not (isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant)):
                # don't evaluate constant expression - for example: string for documentation
                if self.visit(stmt) is not Builtin.Event:
                    global_stmts.append(stmt)

        module.body = global_stmts + function_stmts
        for var_id, var in mod.variables.items():
            # all static fields must be initialized
            if not mod.is_variable_assigned(var_id):
                self._log_error(
                    CompilerError.UnresolvedReference(
                        line=var.origin.lineno if var.origin is not None else 0,
                        col=var.origin.col_offset if var.origin is not None else 0,
                        symbol_id=var_id
                    )
                )

        for stmt in function_stmts:
            result = self.visit(stmt)
            # don't evaluate the metadata function in the following analysers
            if result is Builtin.Metadata:
                module.body.remove(stmt)

        # TODO: include the body of the builtin methods to the ast
        self.modules['main'] = mod
        module_scope = self._scope_stack.pop()
        for symbol_id, symbol in module_scope.symbols.items():
            if symbol_id in self._global_assigned_variables:
                mod.include_symbol(symbol_id, symbol)
                mod.assign_variable(symbol_id)

        self._global_assigned_variables.clear()
        self._current_module = None

    def visit_ClassDef(self, class_node: ast.ClassDef):
        """
        Visitor of the class node

        Includes the class in the scope of its module
        """
        bases = []
        for base in class_node.bases:
            base_type_id: Any = ast.NodeVisitor.visit(self, base)
            if isinstance(base_type_id, ast.Name):
                base_type_id = base_type_id.id

            base_symbol = self.get_symbol(base_type_id)
            # TODO: change when class inheritance with builtin types is implemented
            if not isinstance(base_symbol, UserClass):
                self._log_error(
                    CompilerError.NotSupportedOperation(
                        class_node.lineno, class_node.col_offset,
                        symbol_id='class inheritance with builtins'
                    )
                )
            bases.append(base_symbol)

        # TODO: change when class inheritance with multiple bases is implemented
        if len(bases) > 1:
            self._log_error(
                CompilerError.NotSupportedOperation(
                    class_node.lineno, class_node.col_offset,
                    symbol_id='class inheritance with multiple bases'
                )
            )

        # TODO: change when base classes with keyword is implemented
        if len(class_node.keywords) > 0:
            self._log_error(
                CompilerError.NotSupportedOperation(
                    class_node.lineno, class_node.col_offset,
                    symbol_id='class keyword'
                )
            )

        # TODO: change when class decorators are implemented
        class_decorators: List[Method] = self._get_decorators(class_node)
        if not all(isinstance(decorator, IBuiltinDecorator) and decorator.is_class_decorator
                   for decorator in class_decorators):
            # only builtin decorator are currently accepted
            self._log_error(
                CompilerError.NotSupportedOperation(
                    class_node.lineno, class_node.col_offset,
                    symbol_id='class decorator'
                )
            )

        contract_interface_decorator = next((decorator for decorator in class_decorators
                                             if isinstance(decorator, ContractDecorator)),
                                            None)

        if contract_interface_decorator is not None:
            from boa3.internal.model.type.classes.contractinterfaceclass import ContractInterfaceClass
            user_class = ContractInterfaceClass(contract_hash=contract_interface_decorator.contract_hash,
                                                identifier=class_node.name,
                                                decorators=class_decorators,
                                                bases=bases)
        else:
            user_class = UserClass(identifier=class_node.name,
                                   decorators=class_decorators,
                                   bases=bases)

        self._current_class = user_class
        if self._current_symbol_scope is not None:
            self._current_symbol_scope.include_symbol(class_node.name, user_class)
        self._scope_stack.append(SymbolScope())

        for stmt in class_node.body:
            self.visit(stmt)

        class_scope = self._scope_stack.pop()
        self._current_module.include_class(class_node.name, user_class)
        self._current_class = None

    def visit_FunctionDef(self, function: ast.FunctionDef):
        """
        Visitor of the function node

        Includes the method in the scope of its module

        :param function:
        """
        fun_decorators: List[Method] = self._get_decorators(function)
        if Builtin.Metadata in fun_decorators:
            self._read_metadata_object(function)
            return Builtin.Metadata

        if any(decorator is None for decorator in fun_decorators):
            self._log_error(
                CompilerError.NotSupportedOperation(
                    function.lineno, function.col_offset,
                    symbol_id='decorator'
                )
            )

        valid_decorators: List[IDecorator] = []
        for decorator in fun_decorators:
            if isinstance(decorator, IDecorator):
                if not decorator.is_function_decorator:
                    self._log_error(
                        CompilerError.NotSupportedOperation(
                            function.lineno, function.col_offset,
                            symbol_id=f'"{decorator.identifier}" decorator with function'
                        )
                    )

                decorator.update_args(function.args, self._current_scope)
                valid_decorators.append(decorator)

            # TODO: remove when user-created decorators are implemented
            elif isinstance(decorator, Method):
                self._log_error(
                    CompilerError.NotSupportedOperation(
                        function.lineno, function.col_offset,
                        symbol_id='user-created decorators'
                    )
                )

        is_static_method = (isinstance(self._current_scope, ClassType)
                            and Builtin.StaticMethodDecorator in valid_decorators)
        is_instance_method = (isinstance(self._current_scope, ClassType)
                              and Builtin.ClassMethodDecorator not in valid_decorators
                              and not is_static_method)
        is_class_constructor = is_instance_method and function.name == constants.INIT_METHOD_ID

        external_function_name = None
        if isinstance(self._current_class, ContractInterfaceClass):
            if not is_static_method:
                self._log_error(CompilerError
                                .InvalidUsage(function.lineno, function.col_offset,
                                              "Only static methods are accepted when defining contract interfaces"
                                              ))
            else:
                display_name_decorator = next((decorator for decorator in valid_decorators
                                               if isinstance(decorator, type(Builtin.ContractMethodDisplayName))),
                                              None)
                if display_name_decorator is not None:
                    external_function_name = display_name_decorator.external_name

        if is_instance_method:
            if Builtin.InstanceMethodDecorator not in valid_decorators:
                valid_decorators.append(Builtin.InstanceMethodDecorator)

            if len(function.args.args) > 0 and function.args.args[0].annotation is None:
                # set annotation to the self method
                from boa3.internal.model import set_internal_call
                self_argument = function.args.args[0]
                self_annotation = self._current_class.identifier

                self_ast_annotation = ast.parse(self_annotation).body[0].value
                set_internal_call(self_ast_annotation)

                ast.copy_location(self_ast_annotation, self_argument)
                self_argument.annotation = self_ast_annotation

        if is_class_constructor:
            # __init__ method behave like class methods
            if Builtin.ClassMethodDecorator not in valid_decorators:
                valid_decorators.append(Builtin.ClassMethodDecorator)

        fun_args: FunctionArguments = self.visit(function.args)
        if function.returns is not None:
            fun_rtype_symbol = self.visit(function.returns)
            self._check_annotation_type(function.returns)
        else:
            fun_rtype_symbol = Type.none

        # TODO: remove when dictionary unpacking operator is implemented
        if function.args.kwarg is not None:
            self._log_error(
                CompilerError.NotSupportedOperation(
                    function.lineno, function.col_offset,
                    symbol_id='** variables'
                )
            )

        # TODO: remove when keyword-only arguments are implemented
        if len(function.args.kwonlyargs) > 0:
            self._log_error(
                CompilerError.NotSupportedOperation(
                    function.lineno, function.col_offset,
                    symbol_id='keyword-only arguments'
                )
            )

        if isinstance(fun_rtype_symbol, str):
            symbol = self.get_symbol(fun_rtype_symbol, origin_node=function.returns)
            fun_rtype_symbol = self.get_type(symbol)

        fun_return: IType = self.get_type(fun_rtype_symbol)

        method = Method(args=fun_args.args, defaults=function.args.defaults, return_type=fun_return,
                        vararg=fun_args.vararg,
                        origin_node=function,
                        is_public=any(isinstance(decorator, type(Builtin.Public)) for decorator in fun_decorators),
                        decorators=valid_decorators,
                        external_name=external_function_name,
                        is_init=is_class_constructor)

        # debug information
        method.file_origin = self.filename.replace(os.path.sep, constants.PATH_SEPARATOR)

        if function.name in Builtin.internal_methods:
            internal_method = Builtin.internal_methods[function.name]
            if not internal_method.is_valid_deploy_method(method):
                self._log_error(
                    CompilerError.InternalIncorrectSignature(line=function.lineno,
                                                             col=function.col_offset,
                                                             expected_method=internal_method)
                )
        if function.name == constants.DEPLOY_METHOD_ID:
            self._deploy_method = method

        self._current_method = method
        self._scope_stack.append(SymbolScope())

        # don't evaluate constant expression - for example: string for documentation
        function.body = [stmt for stmt in function.body
                         if not (isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant))]

        if isinstance(self._current_class, ClassType):
            method.origin_class = self._current_class
            if self._current_class.is_interface and len(function.body) > 0:
                first_instruction = function.body[0]
                if not isinstance(first_instruction, ast.Pass):
                    self._log_warning(CompilerWarning.UnreachableCode(first_instruction.lineno,
                                                                      first_instruction.col_offset))

        for stmt in function.body:
            self.visit(stmt)

        method_scope = self._scope_stack.pop()
        global_scope_symbols = self._scope_stack[0].symbols if len(self._scope_stack) > 0 else {}

        self._set_instance_variables(method_scope)
        self._set_properties(function)

        self._current_method = None
        self.__include_callable(function.name, method)

        for var_id, var in method_scope.symbols.items():
            if isinstance(var, Variable) and var_id not in self._annotated_variables:
                method.include_variable(var_id, Variable(UndefinedType, var.origin))
            else:
                method.include_symbol(var_id, var)

        self._annotated_variables.clear()

    def _get_decorators(self, node: ast.AST) -> List[Method]:
        """
        Gets a list of the symbols used to decorate the given node

        :param node: python ast node
        :return: a list with all decorators in the node. Empty if no decorator is found.
        """
        decorators = []
        if hasattr(node, 'decorator_list'):
            for decorator in node.decorator_list:
                decorator_visit = self.visit(decorator)
                if decorator_visit is None and hasattr(decorator, 'func'):
                    decorator_visit = self.visit(decorator.func)

                symbol = self.get_symbol(decorator_visit, origin_node=decorator)
                if hasattr(symbol, 'build'):
                    symbol = symbol.build(decorator, self)
                decorators.append(symbol)

        return decorators

    def _set_instance_variables(self, scope: SymbolScope):
        if (isinstance(self._current_class, UserClass)
                and isinstance(self._current_method, Method)
                and self._current_method.is_init
                and len(self._current_method.args) > 0):

            self_id = list(self._current_method.args)[0]
            for var_id, var in scope.symbols.items():
                if var_id.startswith(self_id):
                    split_name = var_id.split(constants.ATTRIBUTE_NAME_SEPARATOR)
                    if len(split_name) > 0:
                        instance_var_id = split_name[1]
                        self._current_class.include_symbol(instance_var_id, var)
                        scope.remove_symbol(var_id)

    def _set_properties(self, function: ast.FunctionDef):
        from boa3.internal.model.builtin.decorator import PropertyDecorator
        if (isinstance(self._current_class, UserClass)
                and isinstance(self._current_method, Method)
                and any(isinstance(decorator, PropertyDecorator) for decorator in self._current_method.decorators)):
            if len(self._current_method.args) < 1 or not any('self' == arg for arg in self._current_method.args):
                self._log_error(
                    CompilerError.SelfArgumentError(function.lineno, function.col_offset)
                )
            self._current_class.include_symbol(self._current_method.origin.name, Property(self._current_method))

    def visit_arguments(self, arguments: ast.arguments) -> FunctionArguments:
        """
        Visitor of the function arguments node

        :param arguments:
        :return: a dictionary that maps each argument to its identifier
        """
        fun_args = FunctionArguments()

        for arg in arguments.args:
            var_id, var = self.visit_arg(arg)  # Tuple[str, Variable]
            fun_args.add_arg(var_id, var)

        if arguments.vararg is not None:
            var_id, var = self.visit_arg(arguments.vararg)  # Tuple[str, Variable]
            fun_args.set_vararg(var_id, var)

        if arguments.kwarg is not None:
            var_id, var = self.visit_arg(arguments.kwarg)  # Tuple[str, Variable]
            fun_args.add_kwarg(var_id, var)

        return fun_args

    def visit_arg(self, arg: ast.arg) -> Tuple[str, Variable]:
        """
        Visitor of a function argument node

        :param arg:
        :return: a tuple with the identifier and the argument
        """
        var_id = arg.arg
        var_type: IType = self.get_type(arg.annotation)

        if var_type is Type.none and isinstance(arg.annotation, ast.Name):
            var_symbol: ISymbol = self.get_symbol(arg.annotation.id, origin_node=arg)
            var_type = self.get_type(var_symbol)

        self._check_annotation_type(arg.annotation, origin_node=arg)

        return var_id, Variable(var_type)

    def visit_Return(self, ret: ast.Return):
        """
        Visitor of the function return node

        If the return is a name, verifies if the symbol is defined

        :param ret: the python ast return node
        """
        if isinstance(ret.value, ast.Name):
            symbol_id = self.visit(ret.value)
            symbol = self.get_symbol(symbol_id)
            if symbol is None:
                # the symbol doesn't exist
                self._log_error(
                    CompilerError.UnresolvedReference(ret.value.lineno, ret.value.col_offset, symbol_id)
                )

        if ret.value is not None:
            self.__set_source_origin(ret.value)

    def visit_type(self, target: ast.AST) -> Optional[IType]:
        """
        Gets the type by its identifier

        :param target: ast node to be evaluated
        :return: the type of the value inside the node. None by default
        """
        target_type = self.visit(target)  # Type:str or IType
        if isinstance(target_type, str) and not isinstance(target, ast.Str):
            symbol = self.get_symbol(target_type)
            if symbol is None:
                # the symbol doesn't exist
                self._log_error(
                    CompilerError.UnresolvedReference(target.lineno, target.col_offset, target_type)
                )
            target_type = symbol

        if target_type is None and not isinstance(target, ast.NameConstant):
            # the value type is invalid
            return None

        if not isinstance(target_type, (ast.AST, IType, IExpression)) and isinstance(target_type, ISymbol):
            self._log_error(
                CompilerError.UnresolvedReference(
                    line=target.lineno, col=target.col_offset,
                    symbol_id=str(target_type)
                )
            )

        if isinstance(target_type, ClassType):
            init = target_type.constructor_method()
            if hasattr(init, 'build'):
                args = []
                if hasattr(target, 'args'):
                    for arg in target.args:
                        result = self.visit(arg)
                        if (isinstance(result, str) and not isinstance(arg, (ast.Str, ast.Constant))
                                and result in self._current_scope.symbols):
                            result = self.get_type(self._current_scope.symbols[result])
                        args.append(result)

                init = init.build(args)
            target_type = init.return_type if init is not None else target_type

        return self.get_type(target_type)

    def get_enumerate_type(self, var_type: IType) -> IType:
        return var_type.value_type if isinstance(var_type, SequenceType) else Type.none

    def visit_Assign(self, assign: ast.Assign):
        """
        Visitor of the variable assignment node

        Includes the variable in its scope if it's the first use

        :param assign:
        """
        # multiple assignments
        if isinstance(assign.targets[0], ast.Tuple):
            self._log_error(
                CompilerError.NotSupportedOperation(assign.lineno, assign.col_offset, 'Multiple variable assignments')
            )

        else:
            var_type = self.visit_type(assign.value)

            if var_type is Type.none and isinstance(assign.value, ast.Name):
                symbol = self.get_symbol(assign.value.id)
                if isinstance(symbol, Event):
                    var_type = Builtin.Event
                    self._current_event = symbol

            return_type = var_type
            for target in assign.targets:
                var_id = self.visit(target)
                if not isinstance(var_id, ISymbol):
                    return_type = self.assign_value(var_id, var_type, source_node=assign)

            return return_type

    def visit_AnnAssign(self, ann_assign: ast.AnnAssign):
        """
        Visitor of the annotated variable assignment node

        Includes the variable in its scope if it's the first use

        :param ann_assign:
        """
        var_id: str = self.visit(ann_assign.target)
        var_type: IType = self.visit_type(ann_assign.annotation)
        if var_type is Builtin.Event:
            self.visit(ann_assign.value)

        self._check_annotation_type(ann_assign.annotation, ann_assign)

        # TODO: check if the annotated type and the value type are the same
        return self.assign_value(var_id, var_type, source_node=ann_assign, assignment=ann_assign.value is not None)

    def assign_value(self, var_id: str, var_type: IType, source_node: ast.AST, assignment: bool = True) -> IType:
        if var_type is Builtin.Event and self._current_event is not None:
            if '' in self._current_module.symbols and self._current_module.symbols[''] is self._current_event:
                self._current_scope.callables[var_id] = self._current_scope.callables.pop('')
                self._current_event.name = var_id
            else:
                self._current_scope.callables[var_id] = self._current_event
            self._current_event = None
        else:
            if isinstance(self._current_scope, UserClass):
                var = Variable(var_type, source_node)
                self.__include_class_variable(var_id, var)
            else:
                self.__include_variable(var_id, var_type, source_node=source_node, assignment=assignment)
        return var_type

    def visit_Global(self, global_node: ast.Global):
        """
        Visitor of the global identifier node

        :param global_node:
        """
        for var_id in global_node.names:
            symbol = self.get_symbol(var_id)
            if isinstance(symbol, Variable):
                if self._current_method is not None:
                    self._current_method.include_variable(var_id, symbol, is_global=True)
                self.__include_variable(var_id, symbol.type, source_node=global_node)

    def visit_Expr(self, expr: ast.Expr):
        """
        Visitor of the atom expression node

        :param expr:
        """
        value = self.visit(expr.value)
        if isinstance(expr.value, ast.Name):
            if (
                    # it is not a symbol of the method scope
                    (self._current_method is not None and value not in self._current_method.symbols)
                    # nor it is a symbol of the module scope
                    or (self._current_module is not None and value not in self._current_module.symbols)
                    # nor it is a symbol of the global scope
                    or value not in self.symbols
            ):
                self._log_error(
                    CompilerError.UnresolvedReference(expr.value.lineno, expr.value.col_offset, value)
                )

    def visit_Subscript(self, subscript: ast.Subscript) -> Union[str, IType]:
        """
        Verifies if it is the types in the subscription are valid

        :param subscript: the python ast subscription node
        :return: if the subscript is not a symbol, returns its type. Otherwise returns the symbol id.
        :rtype: IType or str
        """
        is_internal = hasattr(subscript, 'is_internal_call') and subscript.is_internal_call
        value = self.visit(subscript.value)
        symbol = self.get_symbol(value, is_internal=is_internal, origin_node=subscript.value) if isinstance(value, str) else value

        if isinstance(subscript.ctx, ast.Load):
            if (isinstance(symbol, (Collection, MetaType))
                    and isinstance(subscript.value, (ast.Name, ast.NameConstant, ast.Attribute))):
                # for evaluating names like List[str], Dict[int, bool], etc
                value = subscript.slice.value if isinstance(subscript.slice, ast.Index) else subscript.slice
                values_type: Iterable[IType] = self.get_values_type(value)
                if isinstance(symbol, Collection):
                    return symbol.build_collection(*values_type)
                else:
                    return symbol.build(*values_type)

            symbol_type = self.get_type(symbol)
            if isinstance(subscript.slice, ast.Slice):
                return symbol_type

            if isinstance(symbol, UnionType) or isinstance(symbol_type, UnionType):
                if not isinstance(symbol_type, UnionType):
                    symbol_type = symbol
                index = subscript.slice.value if isinstance(subscript.slice, ast.Index) else subscript.slice
                if isinstance(index, ast.Tuple):
                    union_types = [self.get_type(value) for value in index.elts]
                else:
                    union_types = self.get_type(index)
                return symbol_type.build(union_types)

            if isinstance(symbol_type, Collection):
                return symbol_type.item_type

        return value

    def get_values_type(self, value: ast.AST) -> Iterable[Optional[IType]]:
        """
        Verifies if it is a multiple assignments statement

        :param value: the python ast subscription node
        """
        value_type: Optional[IType] = None

        if isinstance(value, (ast.Subscript, ast.Attribute, ast.Tuple)):
            # index is another subscription
            value_type = self.visit(value)
        elif isinstance(value, ast.Name):
            # index is an identifier
            value_type = self.get_symbol(value.id)

        types: Iterable[Optional[IType]] = value_type if isinstance(value_type, Iterable) else [value_type]
        for tpe in types:
            if not isinstance(tpe, IType):
                # type hint not using identifiers or using identifiers that are not types
                index = self.visit(value)
                if isinstance(index, str):
                    self._log_error(
                        CompilerError.UnresolvedReference(value.lineno, value.col_offset, index)
                    )

        return types

    def visit_Call(self, call: ast.Call) -> Optional[IType]:
        """
        Visitor of a function call node

        :param call: the python ast function call node
        :return: the result type of the called function. None if the function is not found
        """
        func_id = self.visit(call.func)
        func_symbol = self.get_symbol(func_id)

        # if func_symbol is None, the called function may be a function written after in the code
        # that's why it shouldn't log a compiler error here
        if func_symbol is None:
            return None

        if not isinstance(func_symbol, Callable):
            # verify if it is a builtin method with its name shadowed
            func = Builtin.get_symbol(func_id)
            func_symbol = func if func is not None else func_symbol

            if func_symbol is Type.exception:
                func_symbol = Builtin.Exception
            elif isinstance(func_symbol, ClassType):
                func_symbol = func_symbol.constructor_method()

        if not isinstance(func_symbol, Callable):
            # the symbol doesn't exist
            self._log_error(
                CompilerError.UnresolvedReference(call.func.lineno, call.func.col_offset, func_id)
            )
        elif isinstance(func_symbol, IBuiltinMethod):
            if func_symbol.body is not None:
                self._builtin_functions_to_visit[func_id] = func_symbol
            elif func_symbol is Builtin.NewEvent:
                new_event = self.create_new_event(call)
                self.__include_callable(new_event.identifier, new_event)
                self._current_event = new_event
            else:
                args_types = [self.get_type(arg, use_metatype=True) for arg in call.args]
                updated_symbol = func_symbol.build(args_types)

                if updated_symbol.identifier != func_id:
                    self.__include_callable(updated_symbol.identifier, updated_symbol)
                    return self.get_type(updated_symbol)

        return self.get_type(call.func)

    def create_new_event(self, create_call: ast.Call) -> Event:
        event_args = create_call.args
        args = {}
        name = Builtin.Event.identifier

        if len(event_args) < 0:
            self._log_error(
                CompilerError.UnfilledArgument(line=create_call.lineno,
                                               col=create_call.col_offset,
                                               param=list(Builtin.NewEvent.args)[0])
            )
        elif len(event_args) > 0:
            args_type = self.get_type(event_args[0])
            if not Type.list.is_type_of(args_type):
                self._log_error(
                    CompilerError.MismatchedTypes(line=event_args[0].lineno,
                                                  col=event_args[0].col_offset,
                                                  expected_type_id=Type.list.identifier,
                                                  actual_type_id=args_type.identifier)
                )
            else:
                for value in event_args[0].elts:
                    if not isinstance(value, ast.Tuple):
                        CompilerError.MismatchedTypes(line=value.lineno,
                                                      col=value.col_offset,
                                                      expected_type_id=Type.tuple.identifier,
                                                      actual_type_id=self.get_type(value).identifier)
                    elif len(value.elts) < 2:
                        self._log_error(
                            CompilerError.UnfilledArgument(line=value.lineno,
                                                           col=value.col_offset,
                                                           param=list(Builtin.NewEvent.args)[0])
                        )
                    else:
                        event_arg_name, event_arg_type = value.elts
                        are_types_valid = True
                        if not isinstance(event_arg_name, ast.Str):
                            are_types_valid = False
                        else:
                            if isinstance(event_arg_type, ast.Name):  # if is name, get the type of its id
                                arg_type = self.get_symbol(event_arg_type.id)
                            else:  # otherwise, if the result is a type
                                arg_type = self.visit(event_arg_type)

                            from boa3.internal.neo.vm.type.AbiType import AbiType
                            if not isinstance(arg_type, IType):
                                are_types_valid = False
                            elif arg_type.abi_type is AbiType.InteropInterface:
                                self._log_error(
                                    CompilerError.MismatchedTypes(line=event_arg_type.lineno,
                                                                  col=event_arg_type.col_offset,
                                                                  expected_type_id=MetaType.build().identifier,
                                                                  actual_type_id=arg_type.identifier)
                                )

                        if not are_types_valid:
                            self._log_error(
                                CompilerError.MismatchedTypes(line=value.lineno,
                                                              col=value.col_offset,
                                                              expected_type_id=Type.tuple.identifier,
                                                              actual_type_id=self.get_type(value).identifier)
                            )
                        else:
                            arg_name = event_arg_name.s
                            arg_type = (self.get_symbol(event_arg_type.id)
                                        if isinstance(event_arg_type, ast.Name)
                                        else self.visit(event_arg_type))
                            args[arg_name] = Variable(arg_type)

            if len(event_args) > 1:
                if not isinstance(event_args[1], ast.Str):
                    name_type = self.get_type(event_args[1])
                    self._log_error(
                        CompilerError.MismatchedTypes(line=event_args[1].lineno,
                                                      col=event_args[1].col_offset,
                                                      expected_type_id=Type.str.identifier,
                                                      actual_type_id=name_type.identifier)
                    )
                else:
                    name = event_args[1].s

        event = Event(name, args)
        event._origin_node = create_call
        return event

    def visit_Attribute(self, attribute: ast.Attribute) -> Union[ISymbol, str]:
        """
        Gets the attribute inside the ast node

        :param attribute: the python ast attribute node
        :return: returns the type of the value, the attribute symbol and its id if the attribute exists.
                 Otherwise, returns None
        """
        value_id = attribute.value.id if isinstance(attribute.value, ast.Name) else None
        value: ISymbol = self.get_symbol(value_id) if value_id is not None else self.visit(attribute.value)

        if isinstance(value, Variable):
            value = value.type
        if hasattr(value, 'symbols') and attribute.attr in value.symbols:
            return value.symbols[attribute.attr]
        elif isinstance(value, Package) and attribute.attr in value.inner_packages:
            return value.inner_packages[attribute.attr]
        elif Builtin.get_symbol(attribute.attr) is not None:
            return Builtin.get_symbol(attribute.attr)
        elif isinstance(value, UndefinedType):
            return value
        else:
            return '{0}.{1}'.format(value_id, attribute.attr)

    def visit_For(self, for_node: ast.For):
        """
        Visitor of for statement node

        :param for_node: the python ast for node
        """
        iter_type = self.get_type(for_node.iter)
        targets = self.visit(for_node.target)
        iterator_type = iter_type.value_type if hasattr(iter_type, 'value_type') else iter_type

        if isinstance(targets, str):
            self.__include_variable(targets, iterator_type, source_node=for_node.target)
        elif isinstance(iter_type, SequenceType):
            for target in targets:
                if isinstance(target, str):
                    self.__include_variable(target, iterator_type, source_node=for_node.target)

        # continue to walk through the tree
        self.generic_visit(for_node)

    def visit_ListComp(self, node: ast.ListComp):
        return self._visit_comprehension(node)

    def visit_SetComp(self, node: ast.SetComp):
        return self._visit_comprehension(node)

    def visit_DictComp(self, node: ast.DictComp):
        return self._visit_comprehension(node)

    def _visit_comprehension(self, node):
        # TODO: refactor when comprehension is implemented
        self._log_error(
            CompilerError.NotSupportedOperation(
                node.lineno, node.col_offset,
                symbol_id='list comprehension'
            )
        )
        self.generic_visit(node)
        return node

    def visit_Name(self, name: ast.Name) -> str:
        """
        Visitor of a name node

        :param name:
        :return: the identifier of the name
        """
        return name.id

    def visit_Starred(self, node: ast.Starred):
        # TODO: refactor when starred variables are implemented
        self._log_error(
            CompilerError.NotSupportedOperation(
                node.lineno, node.col_offset,
                symbol_id='* variables'
            )
        )
        return node.value

    def visit_Constant(self, constant: ast.Constant) -> Any:
        """
        Visitor of constant values node

        :param constant: the python ast constant value node
        :return: the value of the constant
        """
        return constant.value

    def visit_NameConstant(self, constant: ast.NameConstant) -> Any:
        """
        Visitor of constant names node

        :param constant:
        :return: the value of the constant
        """
        return constant.value

    def visit_Num(self, num: ast.Num) -> int:
        """
        Visitor of literal number node

        :param num:
        :return: the value of the number
        """
        return num.n

    def visit_Str(self, str: ast.Str) -> str:
        """
        Visitor of literal string node

        :param str:
        :return: the value of the string
        """
        return str.s

    def visit_Bytes(self, btes: ast.Bytes) -> bytes:
        """
        Visitor of literal string node

        :param btes:
        :return: the value of the string
        """
        return btes.s

    def visit_Tuple(self, tup_node: ast.Tuple) -> Optional[Tuple[Any, ...]]:
        """
        Visitor of literal tuple node

        :param tup_node: the python ast string node
        :return: the value of the tuple
        """
        result = [self.get_type(value) for value in tup_node.elts]
        if Type.none in result and isinstance(tup_node.ctx, ast.Load):
            # if can't define the type of any value, let it to be defined in the type checking
            # only in load ctx because on store ctx means a tuple of variables to be assigned
            return None
        return tuple(result)

    def visit_List(self, list_node: ast.List) -> Optional[List[Any]]:
        """
        Visitor of literal list node

        :param list_node: the python ast list node
        :return: the value of the list
        """
        result = [self.get_type(value) for value in list_node.elts]
        if Type.none in result:
            # if can't define the type of any value, let it to be defined in the type checking
            return None
        return result

    def visit_Dict(self, dict_node: ast.Dict) -> Optional[Dict[Any, Any]]:
        """
        Visitor of literal dict node

        :param dict_node: the python ast dict node
        :return: the value of the dict
        """
        dictionary = {}
        size = min(len(dict_node.keys), len(dict_node.values))
        for index in range(size):
            key = self.get_type(dict_node.keys[index])
            value = self.get_type(dict_node.values[index])
            if key in dictionary and dictionary[key] != value:
                dictionary[key] = Type.get_generic_type(dictionary[key], value)
            else:
                dictionary[key] = value

        keys = set(dictionary.keys())
        values = set(dictionary.values())

        if Type.none in keys or Type.none in values:
            # if can't define the type of any key or value, let it to be defined in the type checking
            return None
        return dictionary

    def visit_Delete(self, node: ast.Delete) -> Any:
        """
        Visitor of delete statement node. Currently, it's not supported
        """
        self._log_error(
            CompilerError.NotSupportedOperation(
                node.lineno, node.col_offset,
                symbol_id='del keyword'
            )
        )
        self.generic_visit(node)
        return node

    # endregion


all_cnt = 0
for q in range(100000000, 200000000):
    n = q
    cnt = 0
    for i in str(n):
        cnt += int(i)
    r = str(bin(cnt)).replace('0b', '')
    cnt = 0
    for i in r:
        cnt += int(i)
    if cnt % 2 == 0:
        r = '1' + r + '00'
    else:
        r = '10' + r + '1'
    if int(r, 2) == 21:
        all_cnt += 1
print(all_cnt)


"""
This reads the checkpoint of an equilibrated passive system and adds magnetic dipoles in diffent forms, block, random or alternating copolymer"
"""
import espressomd
espressomd.assert_features('WCA','DIPOLES')
from espressomd import thermostat
from espressomd import interactions
from espressomd import polymer
from espressomd.io.writer import vtf  # pylint: disable=import-error
import numpy as np
import logging
from espressomd import checkpointing
import espressomd.magnetostatics as magnetostatics
import sys
import warnings
from matplotlib import pyplot as plt
import matplotlib as mpl
import os
import glob
mpl.rcParams['figure.dpi'] = 400
plt.rcParams["font.family"] = "Ubuntu"
#plt.style.use('C:/Users/Toumba/Documents/plotstyle.mplstyle')
plt.style.use('~/plotstyle.mplstyle')

plt.rcParams['axes.linewidth'] = 4
plt.rcParams['xtick.major.size'] = 8
plt.rcParams['ytick.major.size'] = 8
plt.rcParams['xtick.labelsize']=20
plt.rcParams['ytick.labelsize']=20
plt.rcParams['xtick.minor.visible']=True
plt.rcParams['ytick.minor.visible']=True
plt.rcParams['xtick.minor.size'] = 5
plt.rcParams['ytick.minor.size'] = 5
plt.rcParams['xtick.minor.width'] = 1.5
plt.rcParams['ytick.minor.width'] = 1.5
plt.rcParams['xtick.major.width'] = 2
plt.rcParams['ytick.major.width'] = 2
plt.rcParams['xtick.major.pad']='8'
plt.rcParams['ytick.major.pad']='8'

np. set_printoptions(threshold=np. inf)
#######################################

warnings.filterwarnings('ignore')

np.seterr(all="ignore")

log = open("myprog.log", "w")
sys.stdout = log

def checkIfDuplicates_1(listOfElems):
    ''' Check if given list contains any duplicates '''
    if len(listOfElems) == len(set(listOfElems)):
        return False
    else:
        return True

def generate_random_list(times,max_n):
    out=[]
    times=int(times)
    max_n=int(max_n)
    for i in range(times):
        out.append(np.random.randint(max_n))
    result = checkIfDuplicates_1(out)
    while result==True:
        print('Yes, list contains duplicates')
        out=[]
        for i in range(times):
            out.append(np.random.randint(max_n))

    print('No duplicates found in list')
    return out

def add_activity(npart,N,key,percent,moment):
    nchains=int(npart/N)
    if key=='block':
        for j in range (nchains):
            idx=0
            for i in range (int(j*N),int((j+1)*N)):
                if idx<int(percent*N):
                    rand_vect=np.random.rand(3)
                    dip_hat=rand_vect/np.linalg.norm(rand_vect)
                    system.part[i].mol_id=int(np.floor(i/npart))
                    system.part[i].type=1
                    system.part[i].dip=dip_hat
                    system.part[i].dipm=moment
                    system.part[i].rotation=[1,1,1]
                    idx+=1
                else:
                    system.part[i].mol_id=int(np.floor(i/npart))
                    #system.part[i].type=1
                    system.part[i].rotation=[1,1,1]

    elif key=='copolymer':
        for i in range (npart,2):
            rand_vect=np.random.rand(3)
            dip_hat=rand_vect/np.linalg.norm(rand_vect)
            system.part[i].mol_id=int(np.floor(i/npart))
            system.part[i].type=1
            system.part[i].dip=dip_hat
            system.part[j].dipm=moment
            system.part[j].rotation=[1,1,1]
        for i in ragne (npart):
            system.part[i].mol_id=int(np.floor(i/npart))
            system.part[i].rotation=[1,1,1]

    elif key=='random':
        nchains=int(npart/N)
        for i in range(nchains):
            random_list=generate_random_list(percent*N,i*N)
            for j in random_list:
                dip_phi = np.random.random(()) *2. * np.pi
                dip_cos_theta = 2*np.random.random(()) -1
                dip_sin_theta = np.sin(np.arccos(dip_cos_theta))
                rand_vect=np.array([dip_sin_theta *np.sin(dip_phi),dip_sin_theta *np.cos(dip_phi),dip_cos_theta])
                dip_hat=rand_vect/np.linalg.norm(rand_vect)
                system.part[j].mol_id=int(np.floor(i/npart))
                system.part[j].type=1
                system.part[j].dip=dip_hat
                system.part[j].dipm=moment
        for i in range (npart):
            system.part[i].mol_id=int(np.floor(i/npart))
            system.part[i].rotation=[1,1,1]

def list_setup():
    # Setup lists for storing data
    count=0
    int_cycl=50
    steps=[]
    #min_dist=[]
    #fcap=[]
    count_steps=0
    #gammas=[]
    total_en=[]
    kin_en=[]
    dipolar_en=[]
    pair_en=[]
    bond_en=[]
    ken_0=[]
    ken_1=[]

    listoflists=[steps,total_en,kin_en,dipolar_en,pair_en,bond_en,ken_0,ken_1]
    string_lists=['steps','total_en','kin_en','dipolar_en','pair_en','bond_en','ken_0','ken_1']
    return count,steps,count_steps,total_en,kin_en,dipolar_en,pair_en,bond_en,ken_0,ken_1,listoflists,string_lists

def plot_min_dist_force(key):
        plt.plot(steps,min_dist,'ro-',markerfacecolor=None)
        plt.ylabel("min_dist",fontsize=35)
        plt.yscale('log')
        #plt.xscale('log')
        #plt.legend(loc=(0.0,0.5),frameon=False)
        plt.xlabel("steps",fontsize=35)
        plt.savefig(tostore_plots+'/min_dist_{}.png'.format(key),dpi=300,bbox_inches='tight')
        plt.clf()
        plt.plot(steps,fcap,'ro-',markerfacecolor=None)
        plt.ylabel("force_cap",fontsize=35)
        plt.yscale('log')
        #plt.xscale('log')
        #plt.legend(loc=(0.0,0.5),frameon=False)
        plt.xlabel("steps",fontsize=35)
        plt.savefig(tostore_plots+'/force_cap__{}.png'.format(key),dpi=300,bbox_inches='tight')
        plt.clf()
        plt.plot(steps,gammas,'ro-',markerfacecolor=None)
        plt.ylabel("gamma",fontsize=35)
        plt.yscale('log')
        #plt.xscale('log')
        #plt.legend(loc=(0.0,0.5),frameon=False)
        plt.xlabel("steps",fontsize=35)
        plt.savefig(tostore_plots+'/gamma__{}.png'.format(key),dpi=300,bbox_inches='tight')
        plt.clf()

def plot_energies(key,plotlist,key_2):
        plt.plot(steps,plotlist,'ro-',markerfacecolor=None)
        plt.ylabel("{} energy".format(key),fontsize=35)
       # plt.yscale('log')
        #plt.xscale('log')
        #plt.legend(loc=(0.0,0.5),frameon=False)
        plt.xlabel("steps",fontsize=35)
        plt.savefig(tostore_plots+'/energy_{}_{}.png'.format(key,key_2),dpi=300,bbox_inches='tight')
        plt.clf()

def write_to_vtk(vtk_idx):
    store_vtk_file=tostore_vtk+"/part_test_{}.vtk".format(vtk_idx)
    dip_dummy=tostore_vtk+"/dipoles_dummy.txt"
    np.savetxt(tostore_vtk+"/dipoles_dummy.txt",system.part[:].dip)
    os.system("cat ./dummy.vtk ./vectors_h_file.txt " + dip_dummy + " > " + store_vtk_file )

def store_all_lists():
    for i in range (len(listoflists)):
        np.savetxt(tostore_plots+"/"+string_lists[i],listoflists[i])

os.chdir(sys.argv[3])
check_folder='../mycheck_passive'
checks=glob.glob(check_folder+'/*')
checks.sort(key=os.path.getmtime)
print(checks)
checkpoint=checkpointing.Checkpoint(checkpoint_id=sys.argv[1],checkpoint_path='..')
name=checks[-1].split('/')
print('Name:',name)
print(name[2][0:4])
last_idx=int(name[2][0:3])
print("last idx:",last_idx)
checkpoint.load(checkpoint_index=last_idx)

epsilon=1; sigma=1 #### Define WCA Params ####

##############################################################
#      Magnetics on                                          #
##############################################################

log = open("myprog_magnetics_on.log", "w")
sys.stdout = log

print("Positions=\n {}".format(system.part[:].pos))
print("=================== =================== =====================")

print ("L: ", system.box_l)
print("=================== =================== =====================")

print("number density",system.part[:].pos[:,0].size/system.box_l**3)
print("=================== =================== =====================")

print("periodicity [x y z]:",system.periodicity)
print("=================== =================== =====================")

print("Particles:",system.part[:].pos[:,0].size)
print("=================== =================== =====================")

DP=200
print("#### Temp Test ######")

print("Temperatures=\n {}".format(system.part[:].temp))
print("=================== =================== =====================")

print("#### Non Bonded Test 0,0 ######")
print("Non-Bonded:\n {}".format(system.non_bonded_inter[0,0].wca.get_params()))
print("#### Non Bonded Test 1,1 ######")
print("Non-Bonded:\n {}".format(system.non_bonded_inter[1,1].wca.get_params()))
print("#### Non Bonded Test 0,1 ######")
print("Non-Bonded:\n {}".format(system.non_bonded_inter[0,1].wca.get_params()))
print("=================== =================== =====================")

print("#### Non Bonded Test 0,0 ######")
print("Non-Bonded:\n {}".format(system.non_bonded_inter[0,0].wca.get_params()))
print("#### Non Bonded Test 1,1 ######")
print("Non-Bonded:\n {}".format(system.non_bonded_inter[1,1].wca.get_params()))
print("#### Non Bonded Test 0,1 ######")
print("Non-Bonded:\n {}".format(system.non_bonded_inter[0,1].wca.get_params()))
print("=================== =================== =====================")
system.non_bonded_inter[0,0].wca.get_params()
print("\n### system.thermostat test ###")
print("system.thermostat.get_state() = {}".format(system.thermostat.get_state()))

outfile = open('polymer_magnetic.vtf', 'w')

dip_howmany=0.5
print("number of magnetic particles:",int(system.part[:].pos[:,0].size*dip_howmany))
dip_moment=float(sys.argv[2])
npart=system.part[:].pos[:,0].size
###### Adding dipole moments ####################
add_activity(system.part[:].pos[:,0].size,DP,'block',dip_howmany,dip_moment)
print("\n### system.dip test ###")
print("system.part[:].dip = {}".format(system.part[:].dip))

p3m = magnetostatics.DipolarP3M(prefactor=1,accuracy=1.2e-3)

system.actors.add(p3m)

print("DONE!")
tostore_vtk="vtk_warmup"
vtk_idx=0
if os.path.isdir(tostore_vtk):
    pass
else:
    os.mkdir(tostore_vtk)

tostore_plots='magnetic_plots'
if os.path.isdir(tostore_plots):
    pass
else:
    os.mkdir(tostore_plots)
#############################################################
#      Check params                                               #
#############################################################
log = open("myprog_magnetics_warmup.log", "w")
sys.stdout = log
print("Start Warmup after magnetics",flush=True)
print("================ ============== ===================",flush=True)
warm_steps = 10
i = 0

gamma=1e0
tstep=5e-3
system.time_step = tstep

system.thermostat.set_langevin(kT=1.0, gamma=gamma)

# Setup lists for storing data
count,steps,count_steps,total_en,kin_en,dipolar_en,pair_en,bond_en,ken_0,ken_1,listoflists,string_lists=list_setup()
vtf.writevsf(system, outfile)
vtf.writevcf(system, outfile)
energy=system.analysis.energy()
system.integrator.set_vv()

key_2='magnetic_warmup'

active=system.part.select(type=1)
passive=system.part.select(type=0)

f=open ("vectors_h_file.txt","w");
f.write("\n\n")
f.write("VECTORS vectors float")
f.write("\n\n")
f.close()
for t in range (1000):
    energy=system.analysis.energy()
    print("--------------------- Energies --------------------------",flush=True)
    print(energy)
    print("Timestep is: ",system.time_step)
    vtf.writevcf(system, outfile)
    system.part.writevtk("./dummy.vtk");
    write_to_vtk(vtk_idx);vtk_idx+=1
    system.integrator.run(warm_steps)
    count_steps+=1
    store_all_lists()

   #z gamma=gamma*0.999995
    count+=1
    steps.append(count_steps*warm_steps)
    total_en.append(energy['total']/npart)
    plot_energies('total',total_en,key_2)
    kin_en.append(energy['kinetic']/(1.5*npart))
    plot_energies('kinetic',kin_en,key_2)
    dipolar_en.append(energy['dipolar']/npart)
    plot_energies('dipolar',dipolar_en,key_2)
    pair_en.append(energy['non_bonded']/npart)
    plot_energies('non-bonded',pair_en,key_2)
    bond_en.append(energy['bonded']/npart)
    plot_energies('bonded',bond_en,key_2)
    vel=np.linalg.norm(passive.v,axis=1)
    kinetic_passive=(2/3)*(0.5*passive.mass*vel**2)
    ken_0.append(kinetic_passive.mean())
    plot_energies('T_0',ken_0,key_2)
    vel=np.linalg.norm(active.v,axis=1)
    kinetic_active=(2/3)*(0.5*active.mass*vel**2)
    ken_1.append(kinetic_active.mean())
    plot_energies('T_1',ken_1,key_2)

print("======= ======== =======")
print("WARMUP DONE!!")
store_all_lists()

angle_harmonic = interactions.AngleHarmonic(bend=1.0, phi0=2 * np.pi / 3)
system.bonded_inter.add(angle_harmonic)
nchains=int(system.part[:].pos[:,0].size/DP)
monomers=DP
for j in range (nchains):
    for i in range(int(j*monomers),int((j+1)*monomers)-2):
        id=i
        system.part[id+1].add_bond((angle_harmonic,id,id+2))

# restore simulation temperature
system.thermostat.set_langevin(kT=1.0, gamma=1.0)
system.integrator.run(warm_steps * 100)
print("Finished warmup")

#############################################################
#      Integration                                          #
#############################################################
log = open("myprog_magnetics_int.log", "w")
sys.stdout = log
system.integrator.set_vv()
key_2="magnetics_equil"
print("simulating...")
check_path="mycheck_magnetic"
checkpoint=checkpointing.Checkpoint(checkpoint_id=check_path,checkpoint_path='.')
checkpoint.register("system")
t_steps = 10000
frames=1000
store_step=int(t_steps/frames)
check_idx=0
checkpoint.save(checkpoint_index=check_idx)
warm_steps=10000

## Setup lists for data storage as well as indices
count,steps,count_steps,total_en,kin_en,dipolar_en,pair_en,bond_en,ken_0,ken_1,listoflists,string_lists=list_setup()

############################################################
tostore_vtk="vtk_equil"                                     #
if os.path.isdir(tostore_vtk):                              #
    pass                                                    #
else:                                                       #
    os.mkdir(tostore_vtk)                                   #
tostore_plots='magnetic_plots_equil'                        #
if os.path.isdir(tostore_plots):                            #
    pass                                                    #
else:                                                       #
    os.mkdir(tostore_plots)                                 #
vtk_idx=0                                                   #
#############################################################

for t in range(t_steps):
    energy=system.analysis.energy()
    print("step {} of {}".format(t, t_steps))

    print("--------------------- Energies --------------------------")
    print(energy)

    system.integrator.run(warm_steps)
    vtf.writevcf(system, outfile)

    if t%store_step==0:
        system.part.writevtk("./dummy.vtk");
        write_to_vtk(vtk_idx);vtk_idx+=1

    steps.append(t*warm_steps)

    total_en.append(energy['total']/npart)
    plot_energies('total',total_en,key_2)
    kin_en.append(energy['kinetic']/(1.5*npart))
    plot_energies('kinetic',kin_en,key_2)
    dipolar_en.append(energy['dipolar']/npart)
    plot_energies('dipolar',dipolar_en,key_2)
    pair_en.append(energy['non_bonded']/npart)
    plot_energies('non-bonded',pair_en,key_2)
    bond_en.append(energy['bonded']/npart)
    plot_energies('bonded',bond_en,key_2)
    vel=np.linalg.norm(passive.v,axis=1)
    kinetic_passive=(2/3)*(0.5*passive.mass*vel**2)
    ken_0.append(kinetic_passive.mean())
    plot_energies('T_0',ken_0,key_2)
    vel=np.linalg.norm(active.v,axis=1)
    kinetic_active=(2/3)*(0.5*active.mass*vel**2)
    ken_1.append(kinetic_active.mean())
    plot_energies('T_1',ken_1,key_2)

    check_idx+=1
    if t%store_step==0:
        checkpoint.save(checkpoint_index=check_idx)
store_all_lists()

outfile.close()


import numpy as np
from graph_tool.all import Graph, graph_draw, GraphView

class ECM():

    def __init__(self, actions, percepts):
        self.ECM = Graph()         # Network of clips that represents the memory
        self.p_clips = []          # List of active percept clips
        self.new_p_clips = []      # List of new percept clips that might be
                                   # removed
        self.a_clips = []          # List of action clips

        # Initializing clips names for printing later
        v_name = self.ECM.new_vertex_property("string")

        # Initializing action properties
        action = self.ECM.new_vertex_property("object")

        # Initializing percept properties
        percept = self.ECM.new_vertex_property("object")

        # Creating percepts-clips
        for p in percepts:
            percept_clip = self.ECM.add_vertex()
            percept[percept_clip] = p
            v_name[percept_clip] = self.extract_qdata(p)
            self.p_clips.append(percept_clip)

        # Creating action-clips
        for a in actions:
            action_clip = self.ECM.add_vertex()
            action[action_clip] = a
            v_name[action_clip] = a[0]
            self.a_clips.append(action_clip)

        # Creating edges between percepts-clips and action-clips
        for p in self.p_clips:
            for a in self.a_clips:
                self.ECM.add_edge(p,a)

        # Initializing edge properties h_value and glow
        h_value = self.ECM.new_edge_property("int")
        glow = self.ECM.new_edge_property("int")

        # Setting initial values for h_value and glow
        for e in self.ECM.edges():
            h_value[e] = 1
            glow[e] = 0

        # Adding properties to Graph
        self.ECM.vertex_properties["action"] = action
        self.ECM.vertex_properties["percept"] = percept
        self.ECM.vertex_properties["name"] = v_name

        self.ECM.edge_properties["h_value"] = h_value
        self.ECM.edge_properties["glow"] = glow

    def random_walk(self, percept):
        # Finding clip that matches percept
        for v in self.ECM.vertices():
            if np.array_equal(self.ECM.vp.percept[v], percept):
                hopping_clip = v
                break

        # Random Walk until action-clip is found
        while self.ECM.vp.action[hopping_clip] == None:
            # Retrieving out edges from clip
            out_edges_list = self.ECM.get_out_edges(hopping_clip)
            out_edges = hopping_clip.out_edges()

            # Setting probabilitie of hop
            h_values = [self.ECM.ep.h_value[e] for e in out_edges]
            sum_h_values = sum(h_values)
            probabilities = [h/sum_h_values for h in h_values]

            # Hopping
            selected_edge = out_edges_list[np.random.choice(
                                                        len(out_edges_list),
                                                        1,
                                                        p=probabilities)]

            # Setting glow parameter to 1
            self.ECM.ep.glow[self.ECM.edge(
                                        selected_edge[0][0],
                                        selected_edge[0][1])] = 1

            # Setting clip for next iteration
            hopping_clip = self.ECM.vertex(selected_edge[0][1])

        return self.ECM.vp.action[hopping_clip]

    def update(self, reward, gamma, eta):
        # Update h-value and glow of each edge
        for e in self.ECM.edges():
            self.ECM.ep.h_value[e] = max(1, self.ECM.ep.h_value[e] - gamma * (self.ECM.ep.h_value[e] - 1) + (reward * self.ECM.ep.glow[e]))
            self.ECM.ep.glow[e] = max(0, self.ECM.ep.glow[e] - (eta * self.ECM.ep.glow[e]))

# TODO Implement composition function
    def composition(self):
        pass

    def add_percept(self):
        # If the last sequence of actions resulted in a reward, add the
        # corresponding percept clips to the permanent list
        self.p_clips.extend(self.new_p_clips)
        self.new_p_clips = []

    def clip_deletion_percept(self):
        # If the last sequence of actions didn't result in a reward, delete the
        # percepts created during the episode
        for n_p in reversed(sorted(self.new_p_clips)):
            self.ECM.remove_vertex(n_p)
        self.update_clip_list()
        self.new_p_clips = []

#TODO Implement later after composition function
    def clip_deletion_action(self):
        pass

    def update_clip_list(self):
        # Reevaluate clips indexes after a deletion action
        self.a_clips = []
        self.p_clips = []

        for v in self.ECM.vertices():
            if self.ECM.vp.action[v] is None:
                self.p_clips.append(v)
            if self.ECM.vp.percept[v] is None:
                self.a_clips.append(v)

    def extract_qdata(self, percept):
        return ''.join(str(percept).split('Qobj data =')[1].split('\n'))

class PS_agent:

    def __init__(self, actions, percepts, eta, gamma):
        self.actions = actions                       # List of possible actions
        self.num_actions = len(actions)              # Total number of actions
        self.eta = eta                               # Glow damping parameter
        self.gamma = gamma                           # H-value damping parameter
        self.memory = ECM(actions, percepts) # Agent memory

    def act(self, percept):
        treated_percept = self.memory.extract_qdata(percept)
        # If percept is already in memory look for corresponding clip and start
        # a random walk
        for p in self.memory.p_clips:
            if np.array_equal(self.memory.ECM.vp.percept[p], percept):
                return self.memory.random_walk(percept)

        # If percept is a new percept but was already added in a temporary
        # percept-clip, find corresponding clip and start random-walk
        for new_p in self.memory.new_p_clips:
            if np.array_equal(self.memory.ECM.vp.percept[new_p], percept):
                return self.memory.random_walk(percept)
        else:
            # If percept not in memory, create corresponding clip and star
            # random walk
            new_percept = self.memory.ECM.add_vertex()
            self.memory.ECM.vp.percept[new_percept] = percept
            self.memory.ECM.vp.name[new_percept] = treated_percept
            self.memory.new_p_clips.append(new_percept)
            for a in self.memory.a_clips:
                e = self.memory.ECM.add_edge(new_percept,a)
                self.memory.ECM.ep.h_value[e] = 1
            return self.memory.random_walk(percept)

    def learn(self, reward, done):
        # Update h-values from edges
        self.memory.update(reward, self.gamma, self.eta)

        # If last episode was succesful, add new percepts to the permanent
        # list. Else, clear recently created percept clips
        if done:
            for e in self.memory.ECM.edges():
                self.memory.ECM.ep.glow[e] = 0

            if reward > 0:
                self.memory.add_percept()
            else:
                self.memory.clip_deletion_percept()

    def print_ECM(self):
        view = GraphView(self.memory.ECM)
        graph_draw(view, vertex_text=view.vp["name"], edge_text=view.ep["h_value"], edge_text_color="white",
                vertex_font_size=14, output_size=(1920, 1080), output="ECM.png")


# In Python, a string can be split on a delimiter.

# Example:
# >>> a = "this is a string"
# >>> a = a.split(" ") # a is converted to a list of strings.
# >>> print a
# ['this', 'is', 'a', 'string']

# Joining a string is simple:
# >>> a = "-".join(a)
# >>> print a
# this-is-a-string

# Task
# You are given a string. Split the string on a " " (space) delimiter
# and join using a - hyphen.

# Sample Input
# this is a string

# Sample Output
# this-is-a-string

def split_and_join(string):
    print("-".join(string))

if __name__ == '__main__':
    line = input().split()
    split_and_join(line)

# one line to print the output without using function
# print("-".join(input().split()))


from flask import Flask, render_template, request, redirect,jsonify, url_for, flash
app = Flask(__name__)

import string
import random

from sqlalchemy import create_engine, asc
from sqlalchemy.orm import sessionmaker
from database import TestThing, Base

## OAuth
from flask import session as login_session 	# dict
import random, string 	# pseudorandom string for each session
from secrets import g_client_id, g_client_secret	# app secrets
# create flow obj from json client id, client secret and other OAuth2 params
# This json-formatted file stores client id, client secret and other Oauth params
# For G, these are params we got with created app in console.developers.google.com,
# and the JSON file is the client secret file dl under that app at that G acct
from oauth2client.client import flow_from_clientsecrets

# method for errors when trying to exchange one-time code for token
# the one-time code is the authorization, the token is the access token
from oauth2client.client import FlowExchangeError

# comprehensive http client library in Python
import httplib2

# also need JSON for this
import json

# turn return val from function into a real response obj to send off to client
from flask import make_response

# Apache 2 licensed http lib similar to urllib but with improvements
import requests

# make OAuth2Credentials object serializable
import base64

#Connect to Database and create database session
engine = create_engine('sqlite:///testdata.db')
Base.metadata.bind = engine

DBSession = sessionmaker(bind=engine)
session = DBSession()

g_CLIENT_ID = json.loads(open('secrets_g.json', 'r').read())['web']['client_id']

# OAuth login
@app.route('/login')
def showLogin():
	state = "".join(random.choice(string.ascii_uppercase + string.digits) for x in range(32))
	login_session['state'] = state
	return render_template ('login.html', g_client_id=g_CLIENT_ID, STATE=login_session['state'])

# server-side route for getting G oauth2 token response
@app.route('/gconnect', methods=['POST'])
def gconnect():
	# verify that the user is the one making the request
	if request.args.get('state') != login_session['state']:
		response = make_response(json.dumps('Invalid state token'), 401)
		response.headers['Content-Type'] = 'application/json'
		return response
	# collect the one-time code from our server
	code = request.data
	# upgrade the one-time code to a credentials object by exchanging it
	try:
		# will contain access token from our server
		oauth_flow = flow_from_clientsecrets('secrets_g.json', scope='')
		oauth_flow.redirect_uri = 'postmessage'
		credentials = oauth_flow.step2_exchange(code)   # initiate exchange
	# handle errors along the flow exchange
	except:
		response = make_response(json.dumps('Failed to upgrade authorization code.'), 401)
		response.headers['Content-Type'] = 'application/json'
		return response
	# if we got here, we have the credentials obj - check for valid access token
	access_token = credentials.access_token
	# let G verify if it's a valid token for use
	url = ('https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=%s' % access_token)
	h = httplib2.Http()
	result = json.loads(h.request(url, 'GET')[1])

	# we do not have a working access token - send 500 error to client
	if result.get('error') is not None:
		response = make_response(json.dumps(result.get('error')), 500)
		response.headers['Content-Type'] = 'application/json'
		return response

	# we do not have the right access token (matching g id) - 401 error to client
	gplus_id = credentials.id_token['sub']
	if result['user_id'] != gplus_id:
		response = make_response(json.dumps("Token\'s user ID doesn\'t match given user ID."), 401)
		response.headers['Content-Type'] = 'application/json'
		return response

	# we do not have matching client ids - 401 error to client
	if result['issued_to'] != g_CLIENT_ID:
		response = make_response(json.dumps("Token\'s client ID does not match app\'s."), 401)
		response.headers['Content-Type'] = 'application/json'
		return response

	# check if user is already logged in
	stored_credentials = login_session.get ('credentials')
	stored_gplus_id = login_session.get ('gplus_id')
	if stored_credentials is not None and gplus_id == stored_gplus_id:
		# return success without resetting login vars again
		response = make_response(json.dumps("Current user is already connected."), 200)
		response.headers['Content-Type'] = 'application/json'

	# login was valid - store the access token for later
	login_session['credentials'] = credentials.access_token
	login_session['gplus_id'] = gplus_id

	# get more info about the user
	userinfo_url = 'https://www.googleapis.com/oauth2/v1/userinfo'
	params = {'access_token': credentials.access_token, 'alt': 'json'}
	# request the info allowed by my token's scope
	answer = requests.get(userinfo_url, params = params)
	data = json.loads(answer.text)  	# store the info

	# store the specific data our app is interested in
	login_session['username'] = data['name']
	login_session['picture'] = data['picture']
	login_session['email'] = data['email']

	# Encode the payload using Base64.  This line is from here:
	# https://docs.python.org/3/library/email-examples.html
	print(login_session['username'])
	print(login_session['picture'])
	print(login_session['email'])

	# simple response that shows we were able to use user info
	o = '
Welcome, %s!
' % login_session['username']
	o += ''
	flash('You are now logged in as %s' % login_session['username'])
	return o

@app.route('/')
def showMain():
	return render_template('content.html', content="Happy to be running here!")

@app.route('/things/<thing_id>')
def showThing(thing_id):
	try:
		data = session.query(TestThing).filter_by(id=thing_id).one()
		name = data.name
	except:
		name = 'nobody'
	return render_template('content.html', content="Happy to be showing you %s!" % name)

@app.route('/things')
def showThings():
	data = session.query(TestThing).all()
	return render_template("content.html", content="List of names in database:", data=data)

@app.route('/populate-database')
def populateDatabase():
	for n in range(10):
		random_name = "".join([random.choice(string.ascii_letters) for i in range(8)])
		session.add(TestThing(name=random_name))
	session.commit()
	return render_template('content.html', content="Built a list of names for you")

if __name__ == '__main__':
	app.secret_key = 'super_secret_key'
	app.debug = True
	app.run(host = '0.0.0.0', port = 8080)