# vim: fileencoding=utf-8
#
import re
import inspect
from typing import Callable, Any, Union, List, Generator
from calculate.templates.template_processor import DirectoryProcessor
from calculate.variables.datavars import (
    DependenceAPI,
    DependenceError,
    VariableNode,
    NamespaceNode,
    HashType,
    TableType,
    StringType,
    )
from calculate.variables.loader import Datavars
from calculate.utils.io_module import IOModule
from collections.abc import Iterable, Mapping


class TaskInitializationError(Exception):
    pass


class ScriptError(Exception):
    pass


class TaskError(Exception):
    def __init__(self, message, handlers=set()):
        super().__init__(message)
        self.handlers = handlers


class ActionError(Exception):
    '''Класс исключения для создания штатных исключений в функциях действий.'''
    pass


class ConditionError(Exception):
    pass


class Script:
    '''Класс скрипта, собирает задачи, обработчики, блоки и прочее, хранит их
    в себе. Создает экземпляры лаунчеров.'''
    def __init__(self, script_id: str,
                 args: List[Any] = [],
                 success_message: str = None,
                 failed_message: str = None,
                 interrupt_message: str = None):
        self._id: str = script_id
        self.__items: List[Union['Task', 'Block', 'Handler', 'Run']] = None
        self.__tasks_is_set: bool = False

        self.__args: list = args

        self.__success_message: str = success_message
        self.__failed_message: str = failed_message
        self.__interrupt_message: str = interrupt_message

    @property
    def id(self) -> str:
        return self._id

    @property
    def args(self) -> list:
        return self.__args

    @property
    def items(self) -> list:
        return self.__items

    @property
    def success_message(self) -> str:
        return self.__success_message

    @property
    def failed_message(self) -> str:
        return self.__failed_message

    @property
    def interrupt_message(self) -> str:
        return self.__interrupt_message

    def tasks(self, *items: List[Union['Task', 'Block', 'Handler', 'Run']]
              ) -> 'Script':
        '''Метод для указания задач и контейнеров, из которых состоит скрипт.
        '''
        if self.__tasks_is_set:
            raise ScriptError("Script object is immutable.")

        self.__items = items
        self.__tasks_is_set = bool(self.__items)
        return self

    def make_launcher(self, output: IOModule,
                      datavars: Union[Datavars, NamespaceNode],
                      namespace: NamespaceNode = None) -> 'ScriptLauncher':
        '''Метод для создания экземпляра объекта, для запуска данного
        скрипта.'''
        return ScriptLauncher(self, output, datavars, namespace)


class ScriptLauncher:
    def __init__(self, script: Script,
                 output: IOModule,
                 datavars: Union[Datavars, NamespaceNode],
                 namespace: NamespaceNode):
        '''Метод для инициализации, состоящей в установке модулей вывода,
        модуля переменных, текущего пространства имен, а также создании
        переменных скрипта.'''
        self._script = script

        self._output = output
        self._datavars = datavars
        self._namespace = namespace

        self._script_namespace, self._args_vars = self.make_script_variables(
                                                                   script.id,
                                                                   script.args,
                                                                   datavars)

    def run(self) -> None:
        '''Метод для запуска задач, содержащихся в данном скрипте.'''
        essential_error = None
        founded_handlers = []
        handlers_to_run = set()

        for task_item in self._script.items:
            if isinstance(task_item, Handler):
                founded_handlers.append(task_item)
            elif essential_error is None:
                try:
                    output = task_item.run(self._output,
                                           self._datavars,
                                           self._script_namespace,
                                           self._namespace)
                    handlers_to_run.update(output)
                except Exception as error:
                    if isinstance(error, TaskError):
                        handlers_to_run.update(error.handlers)
                    essential_error = error

        for handler in founded_handlers:
            if handler.id not in handlers_to_run:
                continue
            try:
                handler.run(self._output, self._datavars,
                            self._script_namespace, self._namespace)
            except Exception as error:
                # TODO Разобраться что делать с ошибками в обработчике.
                self._output.set_error(f"error in handler '{task_item.id}':"
                                       f" {str(error)}")
        if essential_error is not None:
            raise essential_error

    @staticmethod
    def make_script_variables(script_id,
                              args: List[Any],
                              datavars: Union[Datavars, NamespaceNode]
                              ) -> NamespaceNode:
        '''Метод для создания переменных скрипта. Создает пространство имен
        скрипта и переменных аргументов.'''
        if 'scripts' not in datavars:
            scripts = ScriptLauncher.make_scripts_namespace(datavars)
        else:
            scripts = datavars.scripts

        if script_id not in scripts:
            current_script = NamespaceNode(script_id)
            scripts.add_namespace(current_script)
        else:
            current_script = scripts[script_id]
            # current_script.clear()

        args_vars = []
        for arg in args:
            if arg not in current_script.variables:
                args_vars.append(VariableNode(arg, current_script))
            else:
                args_vars.append(current_script.variables[arg])
        return current_script, args_vars

    @staticmethod
    def make_scripts_namespace(datavars: Union[Datavars, NamespaceNode]
                               ) -> NamespaceNode:
        '''Метод создающий пространства имен, необходимые для работы задач.'''
        # Для тестирования скорее.
        scripts = NamespaceNode('scripts')
        datavars.add_namespace(scripts)

        env = NamespaceNode('env')
        scripts.add_namespace(env)

        loop = NamespaceNode('loop')
        env.add_namespace(loop)

        return scripts

    def __call__(self, *args):
        '''Метод для запуска скрипта с аргументами.'''
        if len(args) < len(self._script.args):
            raise ScriptError(
                             (f"script '{self._script.id}' missing"
                              f" {len(self._script.args) - len(args)} required"
                              " arguments: '") +
                              "', '".join(self._script.args[len(args):]) + "'")
        elif len(args) > len(self._script.args):
            raise ScriptError(f"script '{self._script.id}' takes"
                              f" {len(self._script.args)} arguments, but"
                              f" {len(args)} were given")

        args_values = self._get_args_values(args, self._datavars,
                                            self._namespace)
        for arg_variable, arg_value in zip(self._args_vars, args_values):
            arg_variable.source = arg_value
        self.run()

    def _get_args_values(self, args: List[Any],
                         datavars: Union[Datavars, NamespaceNode],
                         namespace: NamespaceNode):
        '''Метод для получения значений аргументов.'''
        args_values = []
        for argument in args:
            if isinstance(argument, str):
                variable = DependenceAPI.find_variable(
                                                   argument, datavars,
                                                   current_namespace=namespace)
                args_values.append(variable.get_value())
            elif isinstance(argument, Static):
                args_values.append(argument.value)
            else:
                args_values.append(argument)
        return args_values


class RunTemplate:
    '''Класс запускателя наложения шаблонов.'''
    def __init__(self, id: str = '',
                 action: str = '',
                 package: str = '',
                 chroot_path: str = '',
                 root_path: str = '',
                 dbpkg: bool = True,
                 essential: bool = True,
                 **group_packages):
        self._id: str = id
        self._action: str = action
        self._package: str = package or None
        self._chroot_path: str = chroot_path or None
        self._root_path: str = root_path or None

        self._dbpkg = dbpkg
        self._essential = essential
        self._groups: dict = group_packages

    def _initialize(self, output: IOModule,
                    datavars: Union[Datavars, NamespaceNode]):
        # Проверяем наличие идентификатора.
        if not self._id:
            error_msg = "id is not set for templates runner"
            if self._essential:
                raise TaskError(error_msg)
            else:
                output.set_error(error_msg)
                return set()

        # Проверяем наличие параметра action, который обязателен.
        if not self._action:
            error_msg = ("action parameter is not set for templates runner"
                         f" '{self._id}'")
            if self._essential:
                raise TaskError(error_msg)
            else:
                output.set_error(error_msg)
                return set()

        # Если установлен chroot_path -- устанавливаем значение соответствующей
        # переменной.
        if self._chroot_path is not None:
            if 'cl_chroot_path' in datavars.main:
                datavars.main['cl_chroot_path'].source = self._chroot_path
            else:
                VariableNode("cl_chroot_path", datavars.main,
                             variable_type=StringType,
                             source=self._chroot_path)

        # Если установлен root_path -- устанавливаем значение соответствующей
        # переменной.
        if self._root_path is not None:
            if 'cl_root_path' in datavars.main:
                datavars.main['cl_root_path'].source = self._root_path
            else:
                VariableNode("cl_root_path", datavars.main,
                             variable_type=StringType,
                             source=self._chroot_path)

        if self._groups:
            groups = list(self._groups.keys())
            for group, atoms in groups:
                if isinstance(atoms, str):
                    self._groups[group] = [name.strip() for name
                                           in atoms.split(',')]

    def run(self, output: IOModule,
            datavars: Union[Datavars, NamespaceNode],
            script_namespace: NamespaceNode,
            namespace: NamespaceNode) -> set:
        '''Метод для запуска наложения шаблонов.'''
        self._initialize(output, datavars)
        template_processor = DirectoryProcessor(self._action,
                                                datavars_module=datavars,
                                                package=self._package,
                                                output_module=output,
                                                dbpkg=self._dbpkg,
                                                namespace=namespace,
                                                **self._groups)
        changed_files = template_processor.process_template_directories()
        self._create_result_var(script_namespace,
                                changed_files=changed_files)
        return set()

    def _create_result_var(self, script_namespace: NamespaceNode,
                           changed_files: dict = {},
                           skipped: list = []) -> None:
        '''Метод для создания переменной задачи, в которую отправляются
        результаты вычислений, имеющихся в функции action.'''
        VariableNode(self._id, script_namespace, variable_type=HashType,
                     source={"changed": changed_files,
                             "skipped": skipped})


class Run:
    '''Класс запускателя скриптов.'''
    def __init__(self, script: Script, namespace: str = None,
                 args: List[Any] = [],
                 when: 'Var' = None,
                 essential: bool = True):
        self._script: Script = script
        self._namespace: str = namespace
        self._args: List[Any] = args

        self._condition: Var = when

        self._essential = essential

    def run(self, output: IOModule,
            datavars: Union[Datavars, NamespaceNode],
            parent_namespace: NamespaceNode,
            namespace: NamespaceNode) -> set:
        '''Метод для запуска скрипта, указанного в запускателе.'''
        if self._condition is None or self._condition(datavars,
                                                      namespace,
                                                      parent_namespace):
            # пространство имен или наследуем, либо берем по указанному пути.
            if not self._namespace:
                self._namespace = namespace
            elif isinstance(self._namespace, str):
                parts = self._namespace.split('.')
                parts.reverse()
                self._namespace = datavars[parts.pop()]
                while parts:
                    self._namespace = self._namespace[parts.pop()]

            try:
                # if not self._script._initialized:
                launcher = self._script.make_launcher(
                                                     output, datavars,
                                                     namespace=self._namespace)
                launcher(*self._args)
            except (ScriptError, TaskError) as error:
                if self._essential:
                    raise ScriptError(f"essential script '{self._script._id}'"
                                      f" is failed. Reason: {error}")
                output.set_error(f"essential script '{self._script._id}'"
                                 f" is failed. Reason: {error}")
            return set()


class Static:
    '''Класс, оборачивающий значение, которое не должно восприниматься как
    переменная.'''
    def __init__(self, value: Any):
        self._value: Any = value

    @property
    def value(self) -> Any:
        return self._value


class Task:
    '''Класс реализующий задачи -- объекты, исполняющие указанные функции
    действия action, и содержащий всю необходимую информацию для их работы.'''
    def __init__(self, **kwargs):
        if 'id' in kwargs:
            self._id: str = kwargs['id']
        else:
            raise TaskInitializationError("id is not set for task")

        # Имя не обязательно.
        self._name: str = kwargs.get('name', None)

        if 'action' in kwargs:
            self._action = kwargs['action']
        else:
            raise TaskInitializationError("action is not set for task"
                                          f" '{self._name or self._id}'")

        # Список аргументов и их типы
        self._args_sources: Union[tuple, list] = kwargs.get('args', [])
        self._args_types: List[type] = []
        self._arg_names: List[str] = []
        self._args: Union[tuple, list] = []

        # Переменные, которым через set будет присвоен результат работы action.
        self._set: Union[list, tuple] = kwargs.get('set', None)
        self._return_type: type = None

        # Условие выполнения задачи.
        self._condition: Var = kwargs.get('when', None)
        # Существенность задачи. Если задача существенная, ошибка в ней
        # приведет к остановке скрипта, если несущественная -- ошибка будет
        # записана в переменную этой задачи, а скрипт будет выполняться дальше.
        self._essential: bool = kwargs.get('essential', True)

        # Параметр для указания обработчиков, которые нужно запустить в конце
        # выполнения скрипта.
        self._handlers = set()
        handlers: set = kwargs.get('notify', set())
        if handlers:
            if isinstance(handlers, (tuple, list)):
                self._handlers.update(handlers)
            else:
                self._handlers.add(handlers)

        self._initialized: bool = False

        self._namespace: NamespaceNode = None
        self._script_namespace: NamespaceNode = None
        self._datavars: Union[Datavars, NamespaceNode] = None
        self._output: IOModule = None

    @property
    def essential(self) -> bool:
        return self._essential

    @property
    def initialized(self) -> bool:
        return self._initialized

    def _initialize(self, output: IOModule,
                    datavars: Union[Datavars, NamespaceNode],
                    script_namespace: NamespaceNode,
                    namespace: NamespaceNode) -> 'Task':
        '''Метод для инициализации задач, привязывает задачу к указанному
        модулю переменных, модулю вывода, формирует список аргументов,
        определяет их типы и находит переменные, к которым в последствии
        нужно будет присвоить значение, возвращенное action.'''
        self._datavars = datavars
        self._output = output
        self._namespace = namespace
        self._script_namespace = script_namespace

        if not self._initialized:
            self._arg_names,\
                self._args_types,\
                self._return_type = self._get_action_info(self._action)
            self._args = self._update_arguments(self._args_sources)

            if self._set is not None:
                self._set = self._get_set_variables(self._set, datavars,
                                                    namespace)
            self._initialized = True
        else:
            self._args = self._update_arguments(self._args_sources)

    def run(self, output: IOModule,
            datavars: Union[Datavars, NamespaceNode],
            script_namespace: NamespaceNode,
            namespace: NamespaceNode) -> set:
        '''Метод для запуска данной задачи.'''
        self._initialize(output, datavars, script_namespace, namespace)

        if (self._condition is None or self._condition(
                                                      self._datavars,
                                                      self._namespace,
                                                      self._script_namespace)):
            arguments_values = self._get_args_values(self._args,
                                                     self._args_types)
            try:
                result = self._action(*arguments_values)
                self._create_task_var(result=result,
                                      success=True)

                if self._return_type:
                    self._check_return_type(result)

                if self._set:
                    self._set_variables(result)

                return self._handlers
            except Exception as error:
                if self._essential and not isinstance(error, ActionError):
                    raise TaskError(
                                f"essential task '{self._name or self._id}' is"
                                f" failed. Reason: {error}")
                self._create_task_var(success=False,
                                      error_message=str(error))
        return set()

    def _get_action_info(self, action: Callable):
        '''Метод для получения информации о функции действия.'''
        arguments = [arg.name for arg in inspect.signature(
                                                   action).parameters.values()]
        annotation = action.__annotations__
        action_types = []
        for argument in arguments:
            action_types.append(annotation.get(argument, None))

        return arguments, action_types, annotation.get('return', None)

    def _update_arguments(self, args_sources: List[Any]) -> tuple:
        '''Метод для обновления и установки аргументов функции действия.'''
        arguments = []
        # Проверяем количество аргументов.
        args_length = len(self._arg_names)
        if 'output' in self._arg_names:
            if (args_length - 1) != len(self._args_sources):
                raise TaskInitializationError(f"action function takes"
                                              f" {args_length - 1} arguments"
                                              f" but {len(args_sources)}"
                                              " is given")
        elif args_length != len(self._args_sources):
            raise TaskInitializationError(f"action function takes"
                                          f" {args_length} arguments but"
                                          f" {len(args_sources)} is given")

        # Формируем список значений аргументов.
        args_iterator = iter(args_sources)

        for arg_name in self._arg_names:
            if arg_name == 'output':
                arguments.append(self._output)
            else:
                argument = next(args_iterator)
                if isinstance(argument, str):
                    try:
                        variable = DependenceAPI.find_variable(
                                             argument,
                                             self._datavars,
                                             current_namespace=self._namespace)
                        arguments.append(variable)
                    except DependenceError as error:
                        raise TaskInitializationError(str(error))
                elif isinstance(argument, Static):
                    arguments.append(argument.value)
                else:
                    arguments.append(argument)
        return tuple(arguments)

    def _get_args_values(self, args: List[Any],
                         args_types: List[type]) -> List[Any]:
        '''Метод для получения значений из списка аргументов функции action.'''
        arguments_values = []
        for arg, arg_type in zip(args, args_types):
            if isinstance(arg, VariableNode):
                value = arg.get_value()
                if arg.variable_type is HashType:
                    value = value.get_hash()
                elif arg.variable_type is TableType:
                    value = value.get_table()

                arguments_values.append(self._check_type(value, arg_type))
            else:
                arguments_values.append(self._check_type(arg, arg_type))
        return arguments_values

    def _check_type(self, value: Any, arg_type: type) -> Any:
        '''Метод для проверки типа аргумента.'''
        if arg_type is None or isinstance(value, arg_type):
            return value
        else:
            raise TaskError(f"'{arg_type}' is expected, not {type(value)}")

    def _create_task_var(self,
                         result: Any = None,
                         success: bool = True,
                         error_message: str = None) -> None:
        '''Метод для создания переменной задачи, в которую отправляются
        результаты вычислений, имеющихся в функции action.'''
        VariableNode(self._id, self._script_namespace, variable_type=HashType,
                     source={'result': result,
                             'success': success,
                             'error_message': error_message})

    def _get_set_variables(self, to_set: Union[str, list, tuple, dict],
                           datavars: Union[NamespaceNode, Datavars],
                           namespace: NamespaceNode) -> Union[str, list, dict]:
        '''Метод для поиска переменных, в которые нужно положить значения,
        полученные в результате вычислений в action.'''
        if isinstance(to_set, str):
            return DependenceAPI.find_variable(to_set, datavars,
                                               current_namespace=namespace)
        elif isinstance(to_set, (list, tuple)):
            vars_to_set = []
            for var in to_set:
                vars_to_set.append(
                                DependenceAPI.find_variable(
                                                  var, datavars,
                                                  current_namespace=namespace))
            return vars_to_set
        elif isinstance(to_set, dict):
            for key in to_set:
                variable = DependenceAPI.find_variable(
                                                   to_set[key], datavars,
                                                   current_namespace=namespace)
                to_set[key] = variable
            return to_set
        else:
            raise TaskError(f"set value must be {dict}, {list}, {tuple} or "
                            f"{str}, not {type(to_set)}")

    def _set_variables(self, result: Any) -> None:
        '''Метод для присвоения переменным результатов работы функции action.
        '''
        self._check_set_vars_types(result)

        if isinstance(self._set, dict):
            if not isinstance(result, dict):
                raise TaskError(f"set parameter value is '{dict}' but action"
                                f" result is '{type(result)}'")
            for key in self._set:
                if key in result:
                    self._set[key].set(result[key])
        elif isinstance(self._set, (list, tuple)):
            for var in self._set:
                var.set(result)
        else:
            self._set.set(result)

    def _check_set_vars_types(self, result: Any) -> None:
        '''Метод для проверки соответствия типов значений, полученных из
        action, и переменных, в которые нужно положить эти значения.'''
        # Теперь проверяем соответствие типа переменных из set и типа значений.
        if isinstance(self._set, dict):
            for key, variable in self._set.items():
                if (key in result and not
                   isinstance(result[key],
                              variable.variable_type.python_type)):
                    raise TaskError("can not set value of"
                                    f" '{type(result[key])}' type to the"
                                    " variable of"
                                    f" '{variable.variable_type.name}'"
                                    " type")
        elif isinstance(self._set, (list, tuple)):
            for variable in self._set:
                if not isinstance(result, variable.variable_type.python_type):
                    raise TaskError(f"can not set value of '{type(result)}'"
                                    " type to the variable of"
                                    f" '{variable.variable_type.name}' type")
        else:
            if not isinstance(result, self._set.variable_type.python_type):
                raise TaskError(f"can not set value of '{type(result)}'"
                                " type to the variable of"
                                f" '{self._set.variable_type.name}' type")

    def _check_return_type(self, result: Any) -> None:
        '''Метод для проверки типа значения, возвращенного функцией action.'''
        if not isinstance(result, self._return_type):
            raise TaskError(f"action returned value of '{type(result)}' "
                            f"type, but expected is '{self._return_type}'")


class Loop:
    '''Базовый класс всех объектов цикла для блоков.'''
    def initialize_loop(self, datavars: Union[Datavars, NamespaceNode],
                        script_namespace: NamespaceNode,
                        namespace: NamespaceNode) -> None:
        '''Метод для инициализации объекта цикла.'''
        pass

    def get_looper(self) -> Generator[bool, bool, None]:
        '''Генератор, предназначенный для управления циклом при запуске блока
        задач.'''
        yield True
        yield False


class For(Loop):
    '''Класс цикла блока, использующего указанное итерируемое значение.'''
    def __init__(self, value: str, iter_arg: Union[str, Iterable]):
        self._iter_arg = iter_arg
        self._item_name = value

        self._iterable_value = None
        self._item_variable = None
        self._initialized = False

    def initialize_loop(self, datavars: Union[Datavars, NamespaceNode],
                        script_namespace: NamespaceNode,
                        namespace: NamespaceNode) -> None:
        '''Метод для инициализации объекта цикла.'''
        self._iterable_value = self._get_iterable_value(self._iter_arg,
                                                        datavars,
                                                        namespace)
        self._item_variable = self._get_item_variable(self._item_name,
                                                      script_namespace)
        self._initialized = True

    def get_looper(self) -> Generator[bool, bool, None]:
        '''Генератор, предназначенный для управления циклом при запуске блока
        задач.'''
        if not self._initialized:
            yield False
        else:
            for item in self._iterable_value:
                self._item_variable.source = item
                yield True
            yield False

    def _get_iterable_value(self, iter_argument: Union[str, Iterable],
                            datavars: Union[Datavars, NamespaceNode],
                            namespace: NamespaceNode) -> Iterable:
        '''Метод для получения и проверки значения, которое будем далее
        итерировать.'''
        value = self._iter_arg

        if isinstance(self._iter_arg, str):
            variable = DependenceAPI.find_variable(self._iter_arg,
                                                   datavars,
                                                   current_namespace=namespace)
            value = variable.get_value()
            if variable.variable_type is HashType:
                value = value.get_hash()
            elif variable.variable_type is TableType:
                value = value.table_hash()

        if not isinstance(value, Iterable):
            raise TaskError("For() argument '{value}' is not iterable.")
        if isinstance(value, Mapping):
            value = value.items()
        return value

    def _get_item_variable(self, item_name: str,
                           script_namespace: NamespaceNode) -> VariableNode:
        '''Метод для получения или создания переменной, в которой будет
        храниться текущее значение, взятое из итерируемого значения.'''
        if item_name in script_namespace:
            item_variable = script_namespace[item_name]
        else:
            item_variable = VariableNode(item_name, script_namespace)
        return item_variable


class While(Loop):
    '''Класс цикла блока, аналогичного while.'''
    def __init__(self, condition: 'Var'):
        self._condition: Var = condition

        self._datavars: Union[Datavars, NamespaceNode] = None
        self._namespace: NamespaceNode = None

    def initialize_loop(self, datavars: Union[Datavars, NamespaceNode],
                        script_namespace: NamespaceNode,
                        namespace: NamespaceNode) -> None:
        '''Метод для инициализации объекта цикла.'''
        self._datavars: Union[Datavars, NamespaceNode] = datavars
        self._namespace: Union[NamespaceNode, None] = namespace
        self._script_namespace: NamespaceNode = script_namespace

    def get_looper(self) -> Generator[bool, bool, None]:
        '''Генератор, предназначенный для управления циклом при запуске блока
        задач.'''
        while True:
            condition_result = self._condition(self._datavars,
                                               self._namespace,
                                               self._script_namespace)
            yield condition_result


class Until(Loop):
    '''Класс цикла блока, аналогичного конструкции do ... while.'''
    def __init__(self, condition: 'Var'):
        self._condition: Var = condition

        self._datavars: Union[Datavars, NamespaceNode] = None
        self._namespace: Datavars = None

    def initialize_loop(self, datavars: Union[Datavars, NamespaceNode],
                        script_namespace: NamespaceNode,
                        namespace: NamespaceNode) -> None:
        '''Метод для инициализации объекта цикла.'''
        self._datavars: Union[Datavars, NamespaceNode] = datavars
        self._namespace: Union[NamespaceNode, None] = namespace
        self._script_namespace: NamespaceNode = script_namespace

    def get_looper(self) -> Generator[bool, bool, None]:
        '''Генератор, предназначенный для управления циклом при запуске блока
        задач.'''
        yield True
        while True:
            yield self._condition(self._datavars,
                                  self._namespace,
                                  self._script_namespace)


class Block:
    '''Класс блока задач.'''
    def __init__(self, *tasks: List[Task],
                 when: Union['Var', None] = None,
                 loop: Loop = Loop()):
        self._tasks: List[Task] = tasks
        self._rescue_tasks: List[Task] = None

        self._condition: 'Var' = when

        if not isinstance(loop, Loop):
            raise TaskError('loop block parameter must be Loop type')
        self._loop: Loop = loop

    def run(self, output: IOModule,
            datavars: Union[Datavars, NamespaceNode],
            script_namespace: NamespaceNode,
            namespace: NamespaceNode) -> None:
        '''Метод для запуска выполнения задач, содержащихся в блоке.'''
        handlers = set()

        if (self._condition is None or self._condition(datavars, namespace,
                                                       script_namespace)):
            self._loop.initialize_loop(datavars, script_namespace, namespace)
            looper = self._loop.get_looper()
            while next(looper):
                for task in self._tasks:
                    try:
                        output = task.run(output, datavars, script_namespace,
                                          namespace)
                        handlers.update(output)
                    except Exception as error:
                        if self._rescue_tasks is not None:
                            self._run_rescue(output, datavars,
                                             script_namespace, namespace)
                        if isinstance(error, TaskError):
                            error.handlers.update(handlers)
                        raise error
        return handlers

    def _run_rescue(self, output: IOModule,
                    datavars: Union[Datavars, NamespaceNode],
                    script_namespace: NamespaceNode,
                    namespace: NamespaceNode) -> None:
        '''Метод для запуска задач, указанных в rescue. Эти задачи выполняются,
        если возникает ошибка при выполнении важной задачи, содержащейся в
        блоке.'''
        for task in self._rescue_tasks:
            try:
                task.run(output, datavars, script_namespace, namespace)
            except Exception:
                # TODO разобраться с тем, что делать если ошибка закралась в
                # задачи из rescue.
                pass

    def rescue(self, *tasks: List[Task]) -> 'Block':
        '''Метод для задания задач, выполняемых, если некоторая важная задача,
        содержащаяся в блоке выполняется с ошибкой.'''
        self._rescue_tasks = tasks
        return self


class Handler:
    '''Класс обработчика -- объекта скрипта, имеющего идентификатор,
    запускаемого в конце выполнения скрипта в том случае, если его
    идентификатор присутствовал в параметре notify хотя бы у одной успешно
    выполненной задачи.'''
    def __init__(self, handler_id, *tasks: List[Task]):
        self._id: str = handler_id
        self._tasks: List[Task] = tasks

    @property
    def id(self) -> str:
        return self._id

    def run(self, output: IOModule,
            datavars: Union[Datavars, NamespaceNode],
            script_namespace: NamespaceNode,
            namespace: NamespaceNode) -> None:
        '''Метод для запуска выполнения задач, содержащихся в обработчике.'''
        handlers = set()
        for task in self._tasks:
            try:
                handlers.update(task.run(output, datavars, script_namespace,
                                         namespace))
            except Exception as error:
                output.set_error(f"error during execution handle '{self._id}'"
                                 f" in task '{task._name or task._id}':"
                                 f" {str(error)}")
        return handlers


class ConditionValue:
    '''Базовый класс для всех объектов, предназначенных для создания условий.
    '''
    def __init__(self, value: Any):
        self.value: Any = value

    def get(self, datavars: Union[Datavars, NamespaceNode],
            namespace: NamespaceNode,
            script_namespace: NamespaceNode) -> Any:
        return self.value


class Var(ConditionValue):
    '''Класс для создания условий выполнения задач.'''
    def __init__(self, *variable: List[VariableNode],
                 value: Any = None, other: Any = None,
                 tasks: Union[List[str], None] = None,
                 function: Union[Callable, None] = None):
        if variable:
            self.variable: Union[VariableNode, str, None] = variable[0]
        else:
            self.variable = None

        self.value: Any = value
        self.other: Any = other
        self.function: Callable = function

        self.tasks: List[str] = tasks

    def get(self, datavars: Union[Datavars, NamespaceNode],
            namespace: NamespaceNode,
            script_namespace: NamespaceNode) -> Any:
        '''Метод для расчета итогового значения ноды условия выполнения задачи.
        '''
        if self.function is not None:
            if self.tasks:
                return self.function(self.tasks, script_namespace)
            elif self.other is not None:
                return self.function(self.get_value(datavars, namespace,
                                                    script_namespace),
                                     self.get_other(datavars, namespace,
                                                    script_namespace))
            else:
                return self.function(self.get_value(datavars, namespace,
                                                    script_namespace))
        return self.get_value(datavars, namespace, script_namespace)

    def get_value(self, datavars: Union[Datavars, NamespaceNode],
                  namespace: NamespaceNode,
                  script_namespace: NamespaceNode) -> Any:
        '''Метод для получения собственного значения ноды.'''
        if self.value is not None:
            return self.value.get(datavars, namespace, script_namespace)
        elif self.variable is not None:
            if isinstance(self.variable, str):
                self.variable = DependenceAPI.find_variable(
                                                   self.variable, datavars,
                                                   current_namespace=namespace)
            return self.variable.get_value()
        else:
            raise ConditionError('Can not get value for condition')

    def get_other(self, datavars: Union[Datavars, NamespaceNode],
                  namespace: NamespaceNode,
                  script_namespace: NamespaceNode) -> Any:
        '''Метод для получения значения ноды, необходимой для выполнения
        бинарной операции.'''
        if isinstance(self.other, ConditionValue):
            return self.other.get(datavars, namespace, script_namespace)
        else:
            return self.other

    def __call__(self, datavars: Union[Datavars, NamespaceNode],
                 namespace: NamespaceNode,
                 script_namespace: NamespaceNode) -> Any:
        return self.get(datavars, namespace, script_namespace)

    def __eq__(self, other: Any) -> 'Var':
        return Var(value=self, other=other, function=lambda x, y: x == y)

    def __ne__(self, other: Any) -> 'Var':
        return Var(value=self, other=other, function=lambda x, y: x != y)

    def __lt__(self, other: Any) -> 'Var':
        return Var(value=self, other=other, function=lambda x, y: x < y)

    def __gt__(self, other: Any) -> 'Var':
        return Var(value=self, other=other, function=lambda x, y: x > y)

    def __le__(self, other: Any) -> 'Var':
        return Var(value=self, other=other, function=lambda x, y: x <= y)

    def __ge__(self, other: Any) -> 'Var':
        return Var(value=self, other=other, function=lambda x, y: x >= y)

    def __and__(self, other: bool) -> 'Var':
        return Var(value=self, other=other, function=lambda x, y: x and y)

    def __or__(self, other: bool) -> 'Var':
        return Var(value=self, other=other, function=lambda x, y: x or y)

    def __invert__(self) -> 'Var':
        return Var(value=self, function=lambda x: not x)

    def __lshift__(self, other: Any) -> 'Var':
        '''Метод для переопределения операции <<, которая теперь играет роль
        in.'''
        return Var(value=self, other=other, function=lambda x, y: y in x)

    def has(self, other: Any) -> 'Var':
        '''Метод аналогичный операции in.'''
        return Var(value=self, other=other, function=lambda x, y: y in x)

    def match(self, pattern: str) -> 'Var':
        '''Метод для проверки с помощью регулярки, если в строке нечто,
        соответствующее паттерну.'''
        return Var(value=self, other=pattern,
                   function=lambda x, y: bool(re.search(y, x)))

    def regex(self, pattern: str, repl: str) -> 'Var':
        '''Метод для реализации sub'''
        return Var(value=self, other=(pattern, repl),
                   function=lambda x, y: re.sub(*y, x))

    def replace(self, original: str, repl: str) -> 'Var':
        '''Метод для реализации replace.'''
        return Var(value=self, other=(original, repl),
                   function=lambda x, y: x.replace(*y))

    def __repr__(self) -> 'Var':
        return ("<Condition value>")


def Done(*tasks: List[str]) -> Var:
    '''Функция создающая объект Var, получающий информацию о том, все ли
    указанные методы выполнены.'''
    return Var(tasks=tasks,
               function=_is_done)


def DoneAny(*tasks: List[str]) -> Var:
    '''Функция создающая объект Var, получающий информацию о том, есть ли
    среди указанных методов те, которые выполнены.'''
    return Var(tasks=tasks,
               function=_is_done_any)


def Success(*tasks: List[str]) -> Var:
    '''Функция создающая объект Var, получающий информацию о том, все ли
    указанные методы выполнены и притом выполнены успешно.'''
    return Var(tasks=tasks,
               function=_is_success)


def SuccessAny(*tasks: List[str]) -> Var:
    '''Функция создающая объект Var, получающий информацию о том, есть ли
    среди указанных методов те, которые выполнены успешно.'''
    return Var(tasks=tasks,
               function=_is_success_any)


def Failed(*tasks: List[str]) -> Var:
    '''Функция создающая объект Var, получающий информацию о том, все ли
    указанные методы не выполнены или выполнены с ошибкой.'''
    return Var(tasks=tasks,
               function=_is_failed)


def FailedAny(*tasks: List[str]) -> Var:
    '''Функция создающая объект Var, получающий информацию о том, есть ли
    среди указанных методов те, которые не выполнены или выполнены с ошибкой.
    '''
    return Var(tasks=tasks,
               function=_is_failed_any)


def _is_done(tasks, script_namespace: NamespaceNode) -> bool:
    '''Функция проверяющая по содержимому пространства имен tasks все ли методы
    из указанных выполнены.'''
    for task in tasks:
        if task not in script_namespace:
            return False
    return True


def _is_done_any(tasks, script_namespace: NamespaceNode) -> bool:
    '''Функция проверяющая по содержимому пространства имен tasks есть ли среди
    указанных методов те, которые выполнены.'''
    for task in tasks:
        if task in script_namespace:
            return True
    return False


def _is_success(tasks, script_namespace: NamespaceNode) -> bool:
    '''Функция проверяющая по содержимому пространства имен tasks все ли методы
    из указанных выполнены успешно.'''
    for task in tasks:
        if (task not in script_namespace or
           not script_namespace[task].get_value().get_hash()['success']):
            return False
    return True


def _is_success_any(tasks, script_namespace: NamespaceNode) -> bool:
    '''Функция проверяющая по содержимому пространства имен tasks есть ли среди
    указанных методов те, которые выполнены успешно.'''
    for task in tasks:
        if (task in script_namespace and
           script_namespace[task].get_value().get_hash()['success']):
            return True
    return False


def _is_failed(tasks, script_namespace: NamespaceNode) -> bool:
    '''Функция проверяющая по содержимому пространства имен tasks все ли методы
    из указанных не выполнены или выполнены с ошибкой.'''
    for task in tasks:
        if (task in script_namespace and
           script_namespace[task].get_value().get_hash()['success']):
            return False
    return True


def _is_failed_any(tasks, script_namespace: NamespaceNode) -> bool:
    '''Функция проверяющая по содержимому пространства имен tasks есть ли среди
    указанных методов те, которые не выполнены или выполнены с ошибкой.'''
    for task in tasks:
        if (task not in script_namespace or
           not script_namespace[task].get_value().get_hash()['success']):
            return True
    return False
