# vim: fileencoding=utf-8
#
import os
import logging
import importlib
import importlib.util
from jinja2 import Environment, FileSystemLoader
from calculate.variables.datavars import (
    NamespaceNode,
    VariableNode,
    ListType,
    IntegerType,
    FloatType,
    IniType,
    TableType,
    Namespace,
    HashType,
    VariableNotFoundError,
    VariableError,
    )
from calculate.utils.gentoo import ProfileWalker
from calculate.utils.files import read_file, FilesError
from calculate.utils.tools import Singleton
from pyparsing import (
    Literal,
    Word,
    ZeroOrMore,
    Group,
    Optional,
    restOfLine,
    empty,
    printables,
    OneOrMore,
    lineno,
    line,
    SkipTo,
    LineEnd,
    Combine,
    nums,
    )
from enum import Enum
from contextlib import contextmanager


class LoaderError(Exception):
    '''Исключение выбрасываемое загрузчиком, если тот в принципе не может
    загрузить переменные.'''
    pass


class Define(Enum):
    assign = 0
    append = 1
    remove = 2


class CalculateIniParser(metaclass=Singleton):
    '''Класс парсера calculate.ini файлов.'''

    def __init__(self):
        self.operations = {"=": Define.assign,
                           "+=": Define.append,
                           "-=": Define.remove}

        lbrack = Literal("[")
        rbrack = Literal("]")

        # comma = Literal(",").suppress()
        comment_symbol = Literal(';') | Literal('#')

        # Define = self.Define
        value_operation = (Literal("=") | Combine(Literal("+") + Literal("="))
                           | Combine(Literal("-") + Literal("=")))

        comment = comment_symbol + Optional(restOfLine)

        section_name = (lbrack.suppress() + (~Word(nums)
                                             + Word(printables+'\t',
                                                    excludeChars='[]'))
                        + rbrack.suppress())

        value_name = Word(printables+'\t', excludeChars='=-+')

        # non_comma = Word(printables+'\t', excludeChars=',')
        clear_section = lbrack.suppress() + Group(empty) + rbrack.suppress()
        row_index = lbrack.suppress() + Word(nums) + rbrack.suppress()

        namespace_start = Group(OneOrMore(section_name)
                                + (clear_section | ~lbrack)
                                + LineEnd().suppress())

        table_start = Group(OneOrMore(section_name)
                            + (row_index | clear_section | ~lbrack)
                            + LineEnd().suppress())

        def add_lineno(string, location, tokens):
            tokens.append(lineno(location, string))

        section_start = (namespace_start('namespace') |
                         table_start('table'))
        section_start.setParseAction(add_lineno)

        # Если содержимое ini-файла не предваряется заголовком секции,
        # значит эта строка ошибочна.
        unexpected = Group(~section_start + SkipTo(LineEnd(),
                                                   include=True))("error")
        unexpected.setParseAction(self._unexpected_token)

        key_value = (~lbrack + value_name
                     + value_operation + empty
                     + restOfLine + LineEnd().suppress())

        def process_key_value(string, location, tokens):
            tokens[0] = tokens[0].strip()
            tokens[1] = tokens[1].strip()
            tokens.append(lineno(location, string))

        key_value.setParseAction(process_key_value)

        self.ini_section_parser = (section_start
                                   + Group(ZeroOrMore(
                                                Group(key_value
                                                      | unexpected)))
                                   | unexpected)
        self.ini_section_parser.ignore(comment)

    def parse(self, data: str):
        for tokens, start, end in self.ini_section_parser.scanString(data):
            if tokens.getName() == "error":
                if tokens[1].strip():
                    yield({'error': (tokens[0], tokens[1])})
                continue

            section, section_lineno, defkeys = tokens

            if section.getName() == 'namespace':
                section_list = section.asList()
                if section_list[-1] == []:
                    yield {'clear_section': (section_list[:-1],
                                             section_lineno)}
                else:
                    yield {'start_section': (section_list, section_lineno)}

                for defkey in defkeys:
                    if defkey.getName() == "error":
                        if defkey[1].strip():
                            yield({'error': (defkey[0], defkey[1])})
                        continue
                    yield {'define_key': (defkey[0], defkey[2],
                                          self.operations[defkey[1]],
                                          defkey[3])}
            else:
                table_list = section.asList()
                table_values = {}
                for defkey in defkeys:
                    if defkey.getName() == "error":
                        if defkey[1].strip():
                            yield({'error': (defkey[0], defkey[1])})
                        continue
                    table_values.update({defkey[0]: defkey[2]})
                yield {'start_table': (table_list, table_values,
                                       section_lineno)}

    def _unexpected_token(self, string, location, tokens):
        '''Метод вызываемый парсером, если обнаружена некорректная строка,
        предназначен для получения некорректной строки и ее дальнейшего
        разбора.'''
        return [lineno(location, string), line(location, string)]


class NamespaceIniFiller:
    '''Класс, предназначенный для наполнения Namespace объекта переменными
    из calculate.ini файла.'''
    available_sections = {'custom'}

    def __init__(self, restrict_creation=True):
        self.ini_parser = CalculateIniParser()
        self._errors = []

        # Флаги, определяющие возможность создания новых переменных и новых
        # пространств имен в данном пространстве имен.
        self.restricted = restrict_creation
        self.modify_only = False

    def fill(self, namespace: NamespaceNode, ini_file_text: str) -> None:
        '''Метод для разбора calculate.ini файла и добавления всех его
        переменных в указанное пространство имен.'''
        self.namespace = namespace
        self.current_namespace = self.namespace
        self._errors = []

        for parsed_line in self.ini_parser.parse(ini_file_text):
            self._line_processor(**parsed_line)

    def _line_processor(self, start_section=None,
                        clear_section=None,
                        start_table=None,
                        define_key=None,
                        error=None, **kwargs):
        '''Метод вызывающий обработку токенов, выдаваемых парсером в
        зависимости от их типа.'''
        if start_section is not None:
            self.start_section(*start_section)
        elif clear_section is not None:
            self.clear_section(*clear_section)
        elif start_table is not None:
            self.start_table(*start_table)
        elif define_key is not None:
            self.define_key(*define_key)
        elif error is not None:
            self._set_error(error[0], 'SyntaxError', error[1])

    def start_section(self, sections: str, lineno) -> None:
        '''Метод для получения доступа и создания пространств имен.'''
        if self.restricted:
            self.modify_only = sections[0] not in self.available_sections

        self.current_namespace = self.namespace
        for section in sections:
            if isinstance(self.current_namespace, Datavars):
                if section not in self.current_namespace:
                    self._set_error(lineno, 'VariableError',
                                    "variables package '{}' is not found.".
                                    format(section))
                    self.current_namespace = None
                    return
            elif isinstance(self.current_namespace, NamespaceNode):
                if section not in self.current_namespace.namespaces:
                    if (section in self.current_namespace.variables and
                       self.current_namespace[section].variable_type
                       is HashType):
                        # Если секция является хэшем, используем ее.
                        self.current_namespace = self.current_namespace.\
                            variables[section]
                        return
                    elif not self.modify_only:
                        self.current_namespace.add_namespace(
                                                    NamespaceNode(section))
                    else:
                        self._set_error(lineno, 'VariableError',
                                        "can not create namespace '{}.{}' in"
                                        " not 'custom' namespace.".format(
                                         self.current_namespace.get_fullname(),
                                         section))
                        self.current_namespace = None
                        return

            self.current_namespace = self.current_namespace.namespaces[section]

    def clear_section(self, sections: list, lineno) -> None:
        '''Метод для очистки пространства имен.'''
        if self.restricted:
            self.modify_only = sections[0] not in self.available_sections
        current_namespace = self.namespace

        for section in sections:
            if isinstance(current_namespace, Datavars):
                if section in current_namespace:
                    current_namespace = current_namespace[section]
                else:
                    self._set_error(lineno, 'VariableError',
                                    "can not clear"" namespace '{}'. Variables"
                                    " package '{}' is not found.".format(
                                                            ".".join(sections),
                                                            section))
                    return
            elif isinstance(current_namespace, NamespaceNode):
                if section in current_namespace.namespaces:
                    current_namespace = current_namespace[section]
                elif (section in current_namespace.variables and
                      current_namespace.variables[section].variable_type
                      is TableType):
                    table_variable = current_namespace.variables[section]
                    table_to_clear = table_variable.get_value()
                    table_to_clear.clear()
                    table_variable.source = table_to_clear
                    return
                else:
                    self._set_error(lineno, 'VariableError',
                                    "can not clear namespace '{}'. Namespace"
                                    " is not found.".format(
                                                           ".".join(sections)))
                    return

        if not self.modify_only:
            current_namespace.clear()
        else:
            self._set_error(lineno, 'VariableError',
                            "can not clear namespace '{}' from not 'custom'"
                            " namespace.".format(current_namespace.
                                                 get_fullname()))

    def start_table(self, sections: str, row, lineno) -> None:
        '''Метод для создания и модификации таблиц.'''
        if self.restricted:
            self.modify_only = sections[0] not in self.available_sections
        self.current_namespace = self.namespace
        row_index = int(sections.pop())
        table_name = sections.pop()
        for section in sections:
            if section not in self.current_namespace.namespaces:
                if not self.modify_only:
                    self.current_namespace.add_namespace(
                                                        NamespaceNode(section))
                else:
                    self._set_error(lineno, 'VariableError',
                                    "can not create table '{}.{}', namespace"
                                    " '{}' is not found.".format(
                                                          ".".join(sections),
                                                          table_name, section))
                    self.current_namespace = None
                    return
            self.current_namespace = self.current_namespace.namespaces[section]

        if table_name not in self.current_namespace.variables:
            if not self.modify_only:
                table_variable = VariableNode(table_name,
                                              self.current_namespace,
                                              variable_type=TableType,
                                              source=[row])
            else:
                self._set_error(lineno, 'VariableError',
                                "can not create table '{}.{}' in not 'custom'"
                                " namespace.".format(self.current_namespace.
                                                     get_fullname(),
                                                     table_name))
        else:
            table_variable = self.current_namespace.variables[table_name]
            table = table_variable.get_value()
            if row_index < len(table):
                table.change_row(row, row_index)
            else:
                table.add_row(row)
            table_variable.source = table

    def define_key(self, key: str, value: str, optype, lineno) -> None:
        '''Метод для создания и модификации переменных.'''
        if self.current_namespace is None:
            return

        if (isinstance(self.current_namespace, VariableNode) and
           self.current_namespace.variable_type is HashType):
            self.update_hash(key, value, optype, lineno)
        else:
            if optype == Define.assign:
                if key not in self.current_namespace:
                    self.define_variable(key, value, lineno)
                else:
                    self.change_value(key, value, lineno)
            elif optype == Define.append:
                if key not in self.current_namespace:
                    self.define_variable(key, value, lineno)
                else:
                    self.append_value(key, value, lineno)
            elif optype == Define.remove:
                if key not in self.current_namespace:
                    self.define_variable(key, value, lineno)
                else:
                    self.remove_value(key, value, lineno)

    def change_value(self, key: str, value: str, lineno) -> None:
        '''Метод для изменения значения переменной.'''
        variable = self.current_namespace[key]
        if variable.readonly:
            self._set_error(lineno, 'VariableError',
                            "can not change readonly variable "
                            f"'{self.current_namespace.get_fullname()}.{key}'")
            return
        variable.source = value

    def define_variable(self, key: str, value: str, lineno) -> None:
        '''Метод для создания переменных в calculate.ini файле.'''
        if not self.modify_only:
            VariableNode(key, self.current_namespace, variable_type=IniType,
                         source=value)
        else:
            self._set_error(lineno, 'VariableError',
                            "can not create variable '{}.{}' in not 'custom'"
                            " namespace.".format(
                                         self.current_namespace.get_fullname(),
                                         key))

    def append_value(self, key: str, value: str, lineno) -> None:
        '''Метод выполняющий действия возложенные на оператор +=.'''
        variable = self.current_namespace[key]
        if variable.readonly:
            self._set_error(lineno, 'VariableError',
                            "can not change readonly variable "
                            f"'{self.current_namespace.get_fullname()}.{key}'")
            return
        variable_value = variable.get_value()

        if variable.variable_type is IniType:
            value_list = value.split(',')
            variable_list = variable_value.split(',')

            for item in value_list:
                if item not in variable_list:
                    variable_list.append(item.strip())
            variable_value = ','.join(variable_list)

        elif variable.variable_type is ListType:
            value_list = value.split(',')

            for item in value_list:
                if item not in variable_value:
                    variable_value.append(item.strip())

        elif variable.variable_type is IntegerType:
            variable_value += int(value)

        elif variable.variable_type is FloatType:
            variable_value += float(value)

        variable.source = variable_value

    def remove_value(self, key: str, value: str, lineno) -> None:
        '''Метод выполняющий действия возложенные на оператор -=.'''
        variable = self.current_namespace[key]
        if variable.readonly:
            self._set_error(lineno, 'VariableError',
                            "can not change readonly variable "
                            f"'{self.current_namespace.get_fullname()}.{key}'")
            return
        variable_value = variable.get_value()

        if variable.variable_type is IniType:
            value_list = value.split(',')
            variable_list = [item.strip() for item in
                             variable_value.split(',')]

            for item in value_list:
                if item in variable_list:
                    variable_list.remove(item.strip())
            variable_value = ','.join(variable_list)

        elif variable.variable_type is ListType:
            value_list = value.split(',')
            for item in value_list:
                if item in variable_value:
                    variable_value.remove(item.strip())

        elif variable.variable_type is IntegerType:
            variable_value -= int(value)

        elif variable.variable_type is FloatType:
            variable_value -= float(value)

        variable.source = variable_value

    def update_hash(self, key: str, value: str, optype, lineno):
        '''Метод для изменения переменных хэшей через calculate.ini.'''
        if self.current_namespace.readonly:
            self._set_error(lineno, 'VariableError',
                            "can not change readonly hash variable "
                            f"'{self.current_namespace.get_fullname()}'")
            return
        hash_to_update = self.current_namespace.get_value().get_hash()
        if key not in hash_to_update:
            # Если ключ отсутствует в хэше, то проверяем, является ли он
            # фиксированным.
            if self.current_namespace.fixed:
                self._set_error(lineno, 'VariableError',
                                "key '{}' is unavailable for fixed hash '{}',"
                                " available keys: '{}'.".format(
                                         key,
                                         self.current_namespace.get_fullname(),
                                         ', '.join(self.current_namespace.
                                                   get_value()._fields)))
                return
            else:
                hash_to_update.update({key: value})

        elif optype == Define.assign:
            hash_to_update.update({key: value})

        elif optype == Define.append:
            current_value = hash_to_update[key]
            hash_to_update[key] = current_value + value

        elif optype == Define.remove:
            current_value = hash_to_update[key]
            if (isinstance(current_value, int) or
               isinstance(current_value, float)):
                hash_to_update[key] = current_value - value
            elif isinstance(current_value, str):
                value_list = [item.strip() for item in value.split(',')]
                current_value = [item.strip() for item in
                                 current_value.split(',')]
                for value_to_remove in value_list:
                    if value_to_remove in current_value:
                        current_value.remove(value_to_remove)
                hash_to_update[key] = ','.join(current_value)

        self.current_namespace.source = hash_to_update

    def _set_error(self, lineno, error_type, line):
        '''Метод для добавления ошибки в лог.'''
        self._errors.append("{}:{}: {}".format(error_type, lineno, line))

    @property
    def errors(self):
        errors = self._errors
        self._errors = []
        return errors


class VariableLoader:
    '''Класс загрузчика переменных из python-файлов и из ini-файлов.'''
    ini_basename = "calculate.ini"

    def __init__(self, datavars, variables_path, repository_map=None):
        self.datavars = datavars
        self.logger = datavars.logger
        self.ini_filler = NamespaceIniFiller()

        self.variables_path = variables_path
        self.variables_package = '.'.join(variables_path.split('/'))
        self.repository_map = repository_map

    def load_variables_package(self, package_name: str) -> None:
        '''Метод для загрузки пакетов с переменными.'''
        directory_path = os.path.join(self.variables_path, package_name)

        package = '{}.{}'.format(self.variables_package, package_name)
        package_namespace = NamespaceNode(package_name)

        self.datavars.root.add_namespace(package_namespace)
        self._fill_from_package(package_namespace, directory_path, package)

    def load_from_profile(self):
        '''Метод для загрузки переменных из calculate.ini профиля.'''
        # Проверяем наличие таблицы репозиториев в переменных.
        if self.repository_map == {}:
            return

        if self.repository_map is None:
            repositories_variable_path = ['os', 'gentoo', 'repositories']
            current_namespace = self.datavars
            for section in repositories_variable_path:
                if section in current_namespace:
                    current_namespace = current_namespace[section]
                else:
                    self.logger.error("Variable 'os.gentoo.repositories'"
                                      " is not found. Can not load profile"
                                      " variables.")
                    return
            self.repository_map = self._get_repository_map(self.datavars)

        # Проверяем наличие пути к профилю в переменных.
        if ('profile' in self.datavars.os.gentoo and
           'path' in self.datavars.os.gentoo.profile):
            profile_path = self.datavars.os.gentoo.profile.path
        else:
            self.logger.error("Variable 'os.gentoo.profile.path'"
                              " is not found. Can not load profile"
                              " variables.")
            return

        self.logger.info("Load variables from profile: '{}'.".format(
                                                                 profile_path))
        self._fill_from_profile_ini(profile_path)

    def load_user_variables(self):
        '''Метод для загрузки переменных из calculate.ini указанных в
        переменных env_order и env_path.'''
        try:
            env_order = self.datavars.system.env_order
            env_path = self.datavars.system.env_path
        except VariableNotFoundError as error:
            self.logger.warning("Can not load additional variables: {}".
                                format(str(error)))
            return

        for ini_file in env_order:
            self.logger.info("Loading variables from file: '{}'".format(
                                                                     ini_file))
            if ini_file in env_path:
                self.fill_from_custom_ini(env_path[ini_file].value)
                self.logger.info("Variables from '{}' are loaded".format(
                                                                     ini_file))
            else:
                self.logger.warning("File '{}' is not found. Variables are"
                                    " not loaded".format(ini_file))

    def _fill_from_package(self, current_namespace: NamespaceNode,
                           directory_path: str, package: str) -> None:
        '''Метод для зaполнения переменных из python-файла.'''
        file_nodes = []
        directory_nodes = []
        # Просматриваем директорию
        for node in os.scandir(directory_path):
            if node.is_dir():
                directory_nodes.append(node)
            elif node.is_file() and node.name.endswith('.py'):
                file_nodes.append(node)

        # Сначала загружаем переменные из файлов.
        for file_node in file_nodes:
            if not file_node.name.endswith('.py'):
                continue
            file_name = file_node.name[:-3]
            Namespace.set_current_namespace(current_namespace)
            # with self.test(file_name, current_namespace):
            #     importlib.import_module('{}.{}'.format(package, file_name))

            spec = importlib.util.spec_from_file_location(
                                        '{}.{}'.format(package, file_name),
                                        file_node.path)
            module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(module)

        # Обходим остальные директории.
        for directory_node in directory_nodes:
            namespace = NamespaceNode(directory_node.name)
            current_namespace.add_namespace(namespace)
            self._fill_from_package(namespace, directory_node.path,
                                    '{}.{}'.format(package,
                                                   directory_node.name))

    def _fill_from_profile_ini(self, profile_path):
        '''Метод для зaполнения переменных из ini-файла.'''
        profile_walker = ProfileWalker(self.ini_basename,
                                       self.repository_map)
        for file_path in profile_walker.find(profile_path):
            try:
                ini_file_text = read_file(file_path)
                self.ini_filler.fill(self.datavars, ini_file_text)
            except FilesError:
                self.logger.error("Can not load profile variables from"
                                  " unexisting file: {}".format(file_path))

    def _get_repository_map(self, datavars):
        '''Метод для получения из переменной словаря с репозиториями и путями
        к ним.'''
        return {repo['name']: repo['path']
                for repo in datavars.os.gentoo.repositories}

    def fill_from_custom_ini(self, file_path: str):
        '''Метод для заполнения переменных из конкретного указанного файла.'''
        if os.path.exists(file_path):
            ini_file_text = read_file(file_path)
            self.ini_filler.fill(self.datavars, ini_file_text)

            parsing_errors = self.ini_filler.errors
            if parsing_errors:
                for error in parsing_errors:
                    self.logger.error(error)
                self.logger.warning('Some variables was not loaded.')
            else:
                self.logger.info('All variables are loaded.')
        else:
            self.logger.error("Variables are not loaded. File '{}' does"
                              " not exist.".format(file_path))

    @contextmanager
    def test(self, file_name, namespace):
        '''Контекстный менеджер для тестирования.'''
        print('IMPORT: {}.{}'.format(namespace.get_fullname(), file_name))
        try:
            yield self
        finally:
            print('IMPORTED FROM: {}.{}'.format(namespace.get_fullname(),
                                                file_name))


class CalculateIniSaver:
    '''Класс для сохранения значений переменных в указанные ini-файлы.'''
    def __init__(self, ini_parser=None):
        self.ini_parser = CalculateIniParser()
        self.operations = {Define.assign: '=',
                           Define.append: '+=',
                           Define.remove: '-='}

        file_loader = FileSystemLoader('calculate/variables')
        environment = Environment(loader=file_loader)
        self.ini_template = environment.get_template('ini_template')

    def save_to_ini(self, target_path, variables_to_save):
        '''Метод для сохранения переменных в указанный ini-файл.'''
        ini_file_text = read_file(target_path)
        ini_dictionary = self._parse_ini(ini_file_text)

        for namespace in variables_to_save:
            if namespace in ini_dictionary:
                ini_dictionary[namespace].update(variables_to_save[namespace])
            else:
                ini_dictionary[namespace] = variables_to_save[namespace]

        ini_file_text = self._get_ini_text(ini_dictionary)
        with open(target_path, 'w') as ini_file:
            ini_file.write(ini_file_text)

    def _parse_ini(self, ini_file_text):
        '''Метод для разбора текста ini-файла в словарь, в который далее будут
        добавляться измененные переменные.'''
        current_namespace = None
        ini_dictionary = dict()

        for parsed_line in self.ini_parser.parse(ini_file_text):
            line_type = next(iter(parsed_line))
            line_content = parsed_line[line_type]

            if (line_type == 'start_section' or
               line_type == 'start_table'):
                current_namespace = tuple(line_content[0])
                if current_namespace not in ini_dictionary:
                    ini_dictionary[current_namespace] = dict()

            elif (line_type == 'clear_section' or
                  line_type == 'clear_table'):
                current_namespace = (*line_content[0], '')
                ini_dictionary[current_namespace] = dict()
            elif line_type == 'define_key':
                namespace = ini_dictionary[current_namespace]
                namespace.update({line_content[0]:
                                  (self.operations[line_content[2]],
                                   line_content[1])})
        return ini_dictionary

    def _get_ini_text(self, ini_dictionary):
        '''Метод для получения текста ini файла, полученного в результате
        наложения изменений из тегов save в шаблонах.'''
        ini_text = self.ini_template.render(ini_dictionary=ini_dictionary)
        return ini_text.strip()


class Datavars:
    '''Класс для хранения переменных и управления ими.'''
    def __init__(self, variables_path='calculate/vars', repository_map=None,
                 logger=None):
        self._variables_path = variables_path
        self._available_packages = self._get_available_packages()
        if logger is not None:
            self.logger = logger
        else:
            logger = logging.getLogger("main")
            # stream_handler = logging.StreamHandler()
            # logger.addHandler(stream_handler)
            self.logger = logger

        self.root = NamespaceNode('<root>')
        self._loader = VariableLoader(self, self._variables_path,
                                      repository_map=repository_map)
        Namespace.reset()
        Namespace.set_datavars(self)

        self._loader.load_from_profile()
        self._loader.load_user_variables()

        # Создаем словарь переменных, которые нужно сохранить потом в
        # ini-файлах.
        try:
            self.variables_to_save = {target: dict() for target in
                                      self.system.env_order if target in
                                      self.system.env_path}
        except VariableNotFoundError:
            self.variables_to_save = dict()

    def reset(self):
        '''Метод для сброса модуля переменных.'''
        self.root.clear()
        self.root = NamespaceNode('<root>')
        self._available_packages.clear()
        self._available_packages = self._get_available_packages()
        Namespace.set_datavars(self)

    def _get_available_packages(self) -> dict:
        '''Метод для получения словаря с имеющимися пакетами переменных
        и путями к ним.'''
        available_packages = dict()
        for file_name in os.listdir(self._variables_path):
            if file_name.startswith('__'):
                continue
            file_path = os.path.join(self._variables_path, file_name)
            if os.path.isdir(file_path):
                available_packages.update({file_name: file_path})
        return available_packages

    def _load_package(self, package_name):
        '''Метод для загрузки переменных содержащихся в указанном пакете.'''
        self.logger.info("Loading datavars package '{}'".format(package_name))
        try:
            self._loader.load_variables_package(package_name)
        except Exception as error:
            raise VariableError("Can not load datavars package: {}".
                                format(error))

    @property
    def available_packages(self):
        packages = set(self._available_packages)
        packages.update({'custom'})
        return packages

    def __getattr__(self, package_name: str):
        '''Метод возвращает ноду пространства имен, соответствующего искомому
        пакету.'''
        if package_name in self.root.namespaces:
            return self.root[package_name]
        elif package_name == 'custom':
            custom_namespace = NamespaceNode('custom')
            self.root.add_namespace(custom_namespace)
            return self.root[package_name]
        elif package_name not in self._available_packages:
            raise VariableNotFoundError("datavars package '{}' is not found".
                                        format(package_name))
        else:
            self._load_package(package_name)
            return self.root[package_name]

    def __getitem__(self, package_name: str) -> None:
        '''Метод возвращает ноду пространства имен, соответствующего искомому
        пакету.'''
        if package_name in self.root:
            return self.root[package_name]
        elif package_name == 'custom':
            custom_namespace = NamespaceNode('custom')
            self.root.add_namespace(custom_namespace)
            return self.root[package_name]
        elif package_name == 'tasks':
            self.create_tasks()
            return self.root[package_name]
        elif package_name not in self._available_packages:
            raise VariableNotFoundError("variables package '{}' is not found".
                                        format(package_name))
        else:
            self._load_package(package_name)
            return self.root[package_name]

    def __contains__(self, package_name):
        if package_name in self.root.namespaces:
            return True
        elif package_name == 'custom':
            custom_namespace = NamespaceNode('custom')
            self.root.add_namespace(custom_namespace)
            return True
        elif package_name == 'tasks':
            self.create_tasks()
            return True
        elif (package_name not in self._available_packages
                and package_name != 'custom'):
            return False
        else:
            self._load_package(package_name)
            return True

    def add_namespace(self, namespace_node):
        self.root.add_namespace(namespace_node)

    def create_tasks(self):
        '''Метод для создания всех необходимых пространств имен для работы
        задач.'''
        tasks = NamespaceNode('tasks')
        self.add_namespace(tasks)

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

        env.add_namespace('loop')

    @property
    def namespaces(self):
        return self.root.namespaces

    def save_variables(self):
        '''Метод для сохранения значений переменных в calculate.ini файлах.'''
        target_paths = self.system.env_path
        saver = CalculateIniSaver()

        for target in self.variables_to_save:
            if self.variables_to_save[target]:
                dict_to_save = self.variables_to_save[target]
                target_path = target_paths[target].value
                saver.save_to_ini(target_path, dict_to_save)
                self.logger.info("Variables for '{}' is saved in the"
                                 " file: {}".format(target, target_path))
