# vim: fileencoding=utf-8
#
from pyparsing import originalTextFor, Literal, Word, printables, OneOrMore,\
                      Optional
from .base_format import Format
from ..template_engine import ParametersContainer
from collections import OrderedDict
try:
    from lxml.etree import Element, SubElement, ElementTree, tostring
except ImportError:
    from xml.etree.ElementTree import Element, SubElement, ElementTree, \
                                      tostring
import re


class XMLGConfFormat(Format):
    FORMAT = 'xml_gconf'
    EXECUTABLE = False

    _initialized = False

    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 = OrderedDict({'gconf': self._gconf,
                                          'entry': self._entry,
                                          'dir': self._dir,
                                          'stringvalue': self._stringvalue,
                                          'default': self._default,
                                          'local_schema': self._local_schema,
                                          'li': self._li,
                                          'longdesc': self._longdesc,
                                          'unknown': self._unknown})
        super().__init__(processing_methods)

        self._initialize_parser()
        self._join_before = join_before

        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 = "<?xml version='1.0' encoding='UTF-8'?>\n"

        document_text = document_text.strip()
        self._parse_xml_to_dictionary(document_text)

    @classmethod
    def _initialize_parser(cls):
        '''Метод для инициализации парсерa.'''
        action_symbols = (Literal('!') | Literal('-'))

        name = originalTextFor(OneOrMore(Word(printables)))

        cls._node_name = Optional(action_symbols)('action') + name('name')
        cls._initialized = True

    def _entry(self, xml_element):
        '''Метод для парсинга тега entry.'''
        try:
            element_items = OrderedDict(xml_element.attrib)

            name = element_items.pop('name')
            parsing_result = self._node_name.parseString(name)

            if 'value' in element_items:
                entry_value = element_items.pop('value')

            elif 'ltype' in element_items:
                entry_value = []
                for child in xml_element:
                    item_to_add = self._processing_methods.get(child.tag,
                                                               self._unknown
                                                               )(child)
                    entry_value.append(item_to_add)
            else:
                entry_value = OrderedDict()
                for child in xml_element:
                    item_to_add = self._processing_methods.get(child.tag,
                                                               self._unknown
                                                               )(child)
                    entry_value.update(item_to_add)
            element_name = (parsing_result.action,
                            xml_element.tag,
                            ('name', parsing_result.name),
                            *element_items.items())
            return OrderedDict({element_name: entry_value})
        except Exception:
            # Какая-то обработка ошибки.
            return OrderedDict()

    def _gconf(self, xml_element):
        '''Метод для парсинга тега gconf.'''
        output_dictionary = OrderedDict()
        element_name = ('', xml_element.tag)
        for child in xml_element:
            item_to_add = self._processing_methods.get(child.tag,
                                                       self._unknown)(child)
            output_dictionary.update(item_to_add)

        return OrderedDict({element_name: output_dictionary})

    def _dir(self, xml_element):
        '''Метод для парсинга тега dir.'''
        output_dictionary = OrderedDict()
        try:
            parsing_result = self._node_name.parseString(
                                                     xml_element.attrib['name']
                                                        )
            element_name = (parsing_result.action,
                            xml_element.tag,
                            ('name', parsing_result.name))
        except Exception:
            # Какая-то обработка ошибки.
            return OrderedDict()

        for child in xml_element:
            item_to_add = self._processing_methods.get(child.tag,
                                                       self._unknown)(child)
            output_dictionary.update(item_to_add)

        return OrderedDict({element_name: output_dictionary})

    def _longdesc(self, xml_element):
        '''Метод для парсинга тега longdesc.'''
        element_name = ('', 'longdesc')
        description = xml_element.text

        if description is not None:
            return OrderedDict({element_name: description})
        else:
            # Пустая строка. Возможно ошибка.
            # Какая-то обработка ошибки.
            return OrderedDict({element_name: ''})

    def _local_schema(self, xml_element):
        '''Метод для парсинга тега local_schema.'''
        output_dictionary = OrderedDict()
        try:
            element_name = ('', xml_element.tag,
                            *xml_element.items())

        except Exception:
            # Какая-то обработка ошибки.
            return OrderedDict()

        for child in xml_element:
            item_to_add = self._processing_methods.get(child.tag,
                                                       self._unknown)(child)
            output_dictionary.update(item_to_add)

        return OrderedDict({element_name: output_dictionary})

    def _stringvalue(self, xml_element):
        '''Метод для парсинга тега stringvalue.'''
        element_name = ('', 'stringvalue')
        value = xml_element.text

        if value is not None:
            return OrderedDict({element_name: value})
        else:
            # Пустая строка. Возможно ошибка.
            # Какая-то обработка ошибки.
            return OrderedDict({element_name: ''})

    def _default(self, xml_element):
        '''Уже не акутальный метод, вместо него теперь _unknown'''
        output_dictionary = OrderedDict()
        element_name = ('', xml_element.tag, *xml_element.items())

        for child in xml_element:
            item_to_add = self._processing_methods.get(child.tag,
                                                       self._unknown)(child)
            output_dictionary.update(item_to_add)

        return OrderedDict({element_name: output_dictionary})

    def _li(self, xml_element):
        '''Метод для разбора элементов с тегом li.'''
        child = next(iter(xml_element))

        list_element = self._processing_methods.get(child.tag,
                                                    self._unknown)(child)
        # Единственным возможным типом списковых значений пока является string.
        string_value = next(iter(list_element.values()))
        return string_value

    def _unknown(self, xml_element):
        # Действия если элемент неизвестен.
        element_name = ('', Element.tag)
        return OrderedDict({element_name: 'Unknown element'})

    @property
    def document_text(self):
        '''Метод для получения исходного текста документа. Использует
        рекурсивный метод _build_section.'''
        gconf_header = next(iter(self._document_dictionary))

        root = Element('gconf')

        self._build_section(root, self._document_dictionary[gconf_header])

        document_tree = ElementTree(root)
        xml_document = tostring(document_tree,
                                encoding="UTF-8",
                                pretty_print=True).decode()
        return '{}{}'.format(self.header, xml_document)

    def _build_section(self, current_element, dictionary):
        '''Метод для перевода словаря xml-документа обратно в текст документа.
        Для этого рекурсивно строит дерево xml-документа пригодное работы lxml.
        '''
        for dict_element in dictionary.keys():
            element_tag = dict_element[1]
            element_attributes = OrderedDict({key: value for key, value in
                                              dict_element[2:]})

            if element_tag == 'dir' or element_tag == 'local_schema'\
               or element_tag == 'default':
                include_element = SubElement(current_element,
                                             element_tag,
                                             **element_attributes)
                self._build_section(include_element,
                                    dictionary[dict_element])

            elif element_tag == 'entry':
                if isinstance(dictionary[dict_element], OrderedDict):
                    include_element = SubElement(current_element,
                                                 element_tag,
                                                 **element_attributes)

                    self._build_section(include_element,
                                        dictionary[dict_element])
                elif 'ltype' in element_attributes:
                    if element_attributes['ltype'] == 'string':
                        entry_element = SubElement(current_element,
                                                   element_tag,
                                                   **element_attributes)
                        for value in dictionary[dict_element]:
                            list_element = SubElement(entry_element,
                                                      'li', type='string')
                            include_element = SubElement(list_element,
                                                         'stringvalue')
                            include_element.text = value

                else:
                    include_element = SubElement(current_element,
                                                 element_tag,
                                                 **element_attributes,
                                                 value=dictionary[dict_element]
                                                 )
            elif element_tag == 'longdesc' or element_tag == 'stringvalue':
                include_element = SubElement(current_element,
                                             element_tag)
                include_element.text = dictionary[dict_element]

    def _get_header_and_document_text(self, input_text,
                                      template_path,
                                      already_changed=False):
        '''Метод для создания заголовка измененного файла и удаления его из
        текста исходного файла.'''
        header_pattern = (r'<!--' + r'-' * 76 + r'\n' +
                          r'\s*Modified by Calculate Utilities [\d\w\.]*\n' +
                          r'\s*Processed template files:\n' +
                          r'\s*(?P<template_paths>(\s*[/\w\d\-_\.]*\n)+)' +
                          r'-' * 77 + r'-->\n?')
        template_paths = []
        if already_changed:
            header_regex = re.compile(header_pattern)
            parsing_result = header_regex.search(input_text)
            template_paths.extend(parsing_result.groupdict()[
                                         'template_paths'].strip().split('\n'))
        template_paths.append(template_path)
        header = ("<?xml version='1.0' encoding='UTF-8'?>\n" +
                  '<!--' + '-' * 76 + '\n' +
                  'Modified by Calculate Utilities {}\n' +
                  'Processed template files:\n' +
                  '\n'.join(template_paths) + '\n' +
                  '-' * 77 + '-->\n').format(self.CALCULATE_VERSION)
        document_text = re.sub(header_pattern, '', input_text)
        return header, document_text
