# vim: fileencoding=utf-8
#
from .base_format import Format
from ..template_engine import ParametersContainer
from collections import OrderedDict
from pyparsing import originalTextFor, Literal, Word, printables, OneOrMore,\
                      alphanums, ParseException, restOfLine, nums,\
                      delimitedList, Optional, Keyword, SkipTo, Group, Regex


class LDAPFormat(Format):
    FORMAT = 'ldap'
    EXECUTABLE = False

    _initialized = False

    comment_symbol = '#'

    def __new__(cls, *args, **kwargs):
        if not cls._initialized:
            cls._initialize_parser()
        return super().__new__(cls)

    def __init__(self, document_text: str,
                 template_path,
                 ignore_comments=False,
                 join_before=False,
                 add_header=False,
                 already_changed=False,
                 parameters=ParametersContainer()):
        processing_methods = [self._parse_comment_line,
                              self._parse_type_line,
                              self._parse_access_line,
                              self._parse_access_line_to_delete,
                              self._parse_syncrepl_line,
                              self._parse_syncrepl_line_to_delete,
                              self._parse_notunique_line,
                              self._parse_index_line,
                              self._parse_index_line_to_delete,
                              self._parse_plain_directive_line,
                              self._parse_plain_directive_line_to_delete]

        super().__init__(processing_methods)
        self._ignore_comments = ignore_comments
        self._comments_processing = True
        self._join_before = join_before
        self._need_finish = True

        if self._ignore_comments:
            self._current_type_section = OrderedDict()
        else:
            self._current_type_section = OrderedDict({'#': []})

        self._current_type = ('', 'global')

        self._last_comments_list = []

        if add_header and not ignore_comments:
            self.header, document_text = self._get_header_and_document_text(
                                               document_text,
                                               template_path,
                                               already_changed=already_changed)
        else:
            self.header = ''

        document_text = document_text.strip()
        if document_text == '':
            self._document_dictionary = OrderedDict()
        else:
            document_lines = self._get_list_of_logic_lines(document_text)
            self._lines_to_dictionary(document_lines)

    @classmethod
    def _initialize_parser(cls):
        '''Метод для инициализации парсеров.'''
        cls._comment_line = originalTextFor(
                             Literal(cls.comment_symbol)
                             + Regex(r'.*'))('comment')

        action_symbols = (Literal('!') | Literal('-'))
        assignment = Literal('=')

        # Для парсинга строк c директивами неуникальными для секции.
        not_unique_directives = originalTextFor(
                                                   Keyword('include') |
                                                   Keyword('moduleload')
                                               )

        not_unique_value = originalTextFor(
                                              OneOrMore(Word(printables))
                                          )('value')

        cls._not_unique_parser = (Optional(action_symbols,
                                           default='')('action')
                                  + not_unique_directives
                                  + not_unique_value + restOfLine.suppress())

        # Для выделения областей global, backend и database.
        type_sections_keywords = originalTextFor(
                                                    Keyword('backend') |
                                                    Keyword('database')
                                                )

        type_value = originalTextFor(Word(alphanums))

        cls._type_line = (Optional(action_symbols, default='')('action')
                          + type_sections_keywords
                          + type_value
                          + restOfLine.suppress())

        # Для парсинга конструкции syncrepl rid=<replica ID> <parameters>
        content_without_spaces = Word(printables, excludeChars='"')

        content_with_spaces = (Literal('"')
                               + OneOrMore(Word(printables,
                                                excludeChars='"'))
                               + Literal('"'))

        parameter_without_spaces = (Word(printables, excludeChars='"=')
                                    + assignment.suppress()
                                    + content_without_spaces)

        parameter_with_spaces = (Word(printables, excludeChars='"=')
                                 + assignment.suppress()
                                 + content_with_spaces)

        values = OneOrMore(originalTextFor(parameter_with_spaces |
                                           parameter_without_spaces))('Values')

        syncrepl_replica_id = originalTextFor(Literal('rid')
                                              + assignment.suppress()
                                              + Word(nums))('replicaID')

        cls._syncrepl_line_parser = (Group(Optional(action_symbols,
                                                    default='')('action')
                                           + Keyword('syncrepl')
                                           + syncrepl_replica_id)('name')
                                     + values('Values')
                                     + restOfLine.suppress())

        cls._syncrepl_value_parser = (Group(Optional(action_symbols,
                                                     default='')('action')
                                      + originalTextFor(
                                                         Word(
                                                             printables,
                                                             excludeChars='"='
                                                             )
                                                       ))('name')
                                      + assignment.suppress()
                                      + originalTextFor(
                                                      OneOrMore(
                                                              Word(printables)
                                                               )
                                                       )('value'))

        cls._syncrepl_line_to_delete_parser = (Group(Optional(
                                                              action_symbols,
                                                              default=''
                                                             )('action')
                                               + Keyword('syncrepl')
                                               + syncrepl_replica_id)('name')
                                               + restOfLine.suppress())

        # Для парсинга конструкции
        # access to <what> by <who>|<access level>|<control>
        access_keyword = originalTextFor(Literal('access to'))('keyword')

        value = originalTextFor(parameter_with_spaces |
                                parameter_without_spaces |
                                content_without_spaces |
                                content_with_spaces)

        cls._access_line_parser = (Group(Optional(action_symbols,
                                                  default='')('action')
                                   + access_keyword
                                   + value)('name')
                                   + Keyword('by').suppress()
                                   + delimitedList(
                                          originalTextFor(value +
                                                          SkipTo(
                                                              Keyword('by'),
                                                              include=False) |
                                                          restOfLine
                                                          ),
                                          delim='by'
                                                   )('Values'))

        cls._access_value_parser = (Group(Optional(action_symbols,
                                                   default='')('action')
                                    + originalTextFor(value))('name')
                                    + originalTextFor(
                                                      Optional(Word(alphanums))
                                                     )('value'))

        cls._access_line_to_delete_parser = (Group(action_symbols('action')
                                             + access_keyword
                                             + value
                                             + restOfLine.suppress())('name'))

        # Для парсинга строк с директивами index.
        cls._index_line_parser = (Group(Optional(action_symbols,
                                                 default='')('action')
                                        + Keyword('index')
                                        + originalTextFor(Word(printables))
                                        )('name')
                                  + originalTextFor(
                                                    OneOrMore(Word(printables))
                                                    )('value'))

        cls._index_line_to_delete_parser = (Group(action_symbols('action')
                                            + Keyword('index')
                                            + originalTextFor(
                                                            Word(printables)
                                                              ))('name'))

        # Для парсинга остальных директив.
        cls._directive_line_parser = (Group(Optional(action_symbols,
                                                     default='')('action')
                                      + originalTextFor(
                                                      Word(printables)
                                                        ))('name')
                                      + originalTextFor(
                                                      OneOrMore(Word(
                                                                     printables
                                                                    )
                                                                ))('value'))

        cls._directive_line_to_delete_parser = (action_symbols('action')
                                                + originalTextFor(
                                                               Word(printables)
                                                               ))('name')
        cls._initialized = True

    def _get_list_of_logic_lines(self, text):
        '''Метод для разбиения исходного документа на список логических строк,
        то есть с учетом того, что строка ldap файла начинающаяся с отступа
        является продолжением предыдущей.'''
        list_of_lines = []
        lines_to_join = []
        for line in text.splitlines():
            if line.strip() == '':
                continue

            if not line.startswith(' ') and not line.startswith('\t'):
                joined_line = "".join(lines_to_join)
                if joined_line != '':
                    list_of_lines.append(joined_line)
                lines_to_join = []

                line.strip()
            else:
                line = ' ' + line.strip()

            lines_to_join.append(line)

        joined_line = "".join(lines_to_join)
        list_of_lines.append(joined_line)

        return list_of_lines

    def _parse_type_line(self, line):
        '''Метод для парсинга строк с объявлением областей backend или database
        '''
        try:
            self._item_to_add = OrderedDict()
            parsing_result = self._type_line.parseString(line)
            self._match = True

            type_name = tuple(parsing_result.asList())

            if self._current_type in self._document_dictionary.keys():
                self._document_dictionary[self._current_type].update(
                                                     self._current_type_section
                                                                     )
            else:
                self._item_to_add[self._current_type] =\
                        self._current_type_section
                self._ready_to_update = True

            # Если глобальная область пуста -- передаем ее комментарии
            # следующей за ней области.
            if self._current_type == ('', 'global') and\
               list(self._current_type_section.keys()) == ['#']:
                self._last_comments_list = self._current_type_section['#']
                self._item_to_add[self._current_type] = OrderedDict()

            self._current_type_section = OrderedDict()
            self._current_type = type_name

            if self._last_comments_list != []:
                self._current_type_section['#'] = self._last_comments_list
                self._last_comments_list = []
        except ParseException:
            return

    def _parse_notunique_line(self, line):
        '''Метод для парсинга строк c директивами неуникальными для секции.
        Их приходится парсить полностью как ключ словаря.'''
        try:
            self._item_to_add = OrderedDict()
            parsing_result = self._not_unique_parser.parseString(line)
            self._match = True

            parsing_result.value = parsing_result.value.strip()

            not_unique_name = tuple(parsing_result.asList())
            parameter_value = ['']

            parameter_value = self._last_comments_list + parameter_value
            self._last_comments_list = []
            self._current_type_section[not_unique_name] = parameter_value
        except ParseException:
            return

    def _parse_access_line(self, line):
        '''Метод для парсинга строк содержащих конструкцию
        access to <what> by <who>|<access level>|<control>.'''
        try:
            parsing_result = self._access_line_parser.parseString(line)
            self._match = True

            values = [value.strip() for value in
                      parsing_result.Values.asList()]
            values.reverse()

            parameter_name = tuple(parsing_result.name)

            value_dictionary = OrderedDict()
            if self._last_comments_list != []:
                value_dictionary['#'] = self._last_comments_list
                self._last_comments_list = []

            for value in values:
                try:
                    value_parsing = self._access_value_parser.\
                            parseString(value)

                    param_name = tuple(value_parsing.name)
                    param_value = value_parsing.value
                    value_dictionary[param_name] = [param_value]
                except ParseException:
                    continue

            parameter_value = value_dictionary

            if parameter_name in self._current_type_section.keys():
                self._current_type_section[parameter_name].update(
                                                               value_dictionary
                                                                 )
            else:
                self._current_type_section[parameter_name] = parameter_value
        except ParseException:
            return

    def _parse_access_line_to_delete(self, line):
        '''Метод для парсинга строк, предписывающих удаление конструкций
        access to, если указано только ее название и значение What.'''
        try:
            parsing_result = self._access_line_to_delete_parser.parseString(
                                                                           line
                                                                           )
            self._match = True

            parameter_name = tuple(parsing_result.name.asList())

            if parsing_result.name.action == '-':
                return

            parameter_value = OrderedDict({'#': self._last_comments_list})
            self._last_comments_list = []
            self._current_type_section[parameter_name] = parameter_value
        except ParseException:
            return

    def _parse_syncrepl_line(self, line):
        '''Метод для парсинга строк содержащих конструкцию syncrepl
        rep=<ReplicaID>.'''
        try:
            parsing_result = self._syncrepl_line_parser.parseString(line)
            self._match = True

            values = [value.strip() for value in
                      parsing_result.Values.asList()]
            parameter_name = tuple(parsing_result.name.asList())

            value_dictionary = OrderedDict()
            if self._last_comments_list != []:
                value_dictionary['#'] = self._last_comments_list
                self._last_comments_list = []

            for value in values:
                try:
                    value_parsing = self._syncrepl_value_parser.parseString(
                                                                          value
                                                                          )
                    param_name = tuple(value_parsing.name.asList())
                    param_value = value_parsing.value
                    value_dictionary[param_name] = [param_value]
                except ParseException:
                    continue

            parameter_value = value_dictionary

            if parameter_name in self._current_type_section.keys():
                self._current_type_section[parameter_name].update(
                                                               value_dictionary
                                                                )
            else:
                self._current_type_section[parameter_name] = parameter_value
        except ParseException:
            return

    def _parse_syncrepl_line_to_delete(self, line):
        '''Метод для парсинга строк, предписывающих удаление конструкций
        syncrepl rid=<ReplicaID>, если указано только ее название и значение
        ReplicaID.'''
        try:
            parsing_result = self._syncrepl_line_to_delete_parser.parseString(
                                                                           line
                                                                             )
            self._match = True

            parameter_name = tuple(parsing_result.name.asList())

            if parsing_result.name.action == '-':
                return

            parameter_value = OrderedDict({'#': self._last_comments_list})
            self._last_comments_list = []
            self._current_type_section[parameter_name] = parameter_value
        except ParseException:
            return

    def _parse_index_line(self, line):
        '''Метод для парсинга строк с директивами index.'''
        try:
            parsing_result = self._index_line_parser.parseString(line)
            self._match = True

            parameter_name = tuple(parsing_result.name.asList())

            parameter_value = parsing_result.value

            parameter_value = self._last_comments_list + [parameter_value]
            self._last_comments_list = []
            self._current_type_section[parameter_name] = parameter_value
        except ParseException:
            return

    def _parse_index_line_to_delete(self, line):
        '''Метод для парсинга строк, предписывающих удаление директив index,
        если указано только из имя, но отсутвует значение.'''
        try:
            parsing_result = self._index_line_to_delete_parser.parseString(
                                                                           line
                                                                          )
            self._match = True

            parameter_name = tuple(parsing_result.name.asList())

            if parsing_result.name.action == '-':
                return

            parameter_value = self._last_comments_list
            self._last_comments_list = []
            self._current_type_section[parameter_name] = parameter_value
        except ParseException:
            return

    def _parse_plain_directive_line(self, line):
        '''Метод для парсинга строк с простыми уникальными для секции
        директивами.'''
        try:
            parsing_result = self._directive_line_parser.parseString(line)
            self._match = True

            parameter_name = tuple(parsing_result.name.asList())
            parameter_value = parsing_result.value

            parameter_value = self._last_comments_list + [parameter_value]
            self._last_comments_list = []
            self._current_type_section[parameter_name] = parameter_value
        except ParseException:
            return

    def _parse_plain_directive_line_to_delete(self, line):
        '''Метод для парсинга строк, предписывающих удаление простых уникальных
        директив, если указано только их имя, но отсутствует значение.'''
        try:
            parsing_result = self._directive_line_to_delete_parser.parseString(
                                                                           line
                                                                              )
            self._match = True

            parameter_name = tuple(parsing_result.name.asList())

            if parsing_result.action == '-':
                return

            parameter_value = self._last_comments_list
            self._last_comments_list = []
            self._current_type_section[parameter_name] = parameter_value
        except ParseException:
            return

    def _parse_comment_line(self, line):
        '''Метод для парсинга строк с комментариями и добавления их в список
        комментариев _last_comments_list, предназначенный для сбора
        комментариев и последующего их присваивания параметрам и секциям.'''
        try:
            parsing_result = self._comment_line.parseString(line)
            self._match = True
            print('comment is found')

            if not self._ignore_comments:
                # До того, как первый элемент встречен -- все комментарии
                # должны быть присвоены глобальной области.
                if self._current_type == ('', 'global') and\
                   list(self._current_type_section.keys()) == ['#']:
                    self._current_type_section['#'].append(
                                                         parsing_result.comment
                                                          )
                else:
                    self._last_comments_list.append(parsing_result.comment)

        except ParseException:
            return

    def _finish_method(self):
        '''Метод для завершения парсинга. В данном случае добавляет в итоговый
        словарь последнюю разобранную область.'''
        self._item_to_add = OrderedDict()
        if self._current_type in self._document_dictionary.keys():
            self._document_dictionary[self._current_type].update(
                                                     self._current_type_section
                                                                )
        else:
            self._item_to_add[self._current_type] = self._current_type_section
            self._document_dictionary.update(self._item_to_add)

        self._item_to_add = OrderedDict()
        self._current_type_section = OrderedDict()
