# vim: fileencoding=utf-8
#
from .base_format import Format
from ..template_engine import ParametersContainer
from pyparsing import originalTextFor, Literal, Word, printables, OneOrMore,\
                      Optional
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 XMLXfceFormat(Format):
    FORMAT = 'xml_xfce'
    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({'channel': self._channel,
                                          'property': self._property,
                                          'value': self._value,
                                          '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()
        if document_text == '':
            self._document_dictionary = OrderedDict()
        else:
            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._ELEMENT_ATTRIBUTES = ('tag', 'name', 'type', 'value')
        cls._CHANNEL_ATTRIBUTES = ('tag', 'name', 'version')
        cls._VALUE_ATTRIBUTES = ('tag', 'type', 'value')

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

        if xml_element.attrib['type'] == 'empty':
            output = OrderedDict()
            for child in xml_element:
                child_value = self._processing_methods.get(child.tag,
                                                           self._unknown
                                                           )(child)
                output.update(child_value)

        elif xml_element.attrib['type'] == 'array':
            output = []
            for child in xml_element:
                child_value = self._processing_methods.get(child.tag,
                                                           self._unknown
                                                           )(child)
                output.append(child_value)
        else:
            try:
                output = xml_element.attrib['value']
            except KeyError:
                # Какая-то обработка ошибки.
                return OrderedDict()

        return OrderedDict({element_name: output})

    def _value(self, xml_element):
        '''Метод для парсинга тега value.'''
        try:
            value = (xml_element.tag,
                     xml_element.attrib['type'],
                     xml_element.attrib['value'])
        except KeyError:
            # Какая-то обработка ошибки.
            return ('',)
        return value

    def _channel(self, xml_element):
        '''Метод для парсинга тега channel.'''
        output_dictionary = OrderedDict()
        try:
            parsing_result = self._node_name.parseString(
                                                     xml_element.attrib['name']
                                                        )
            element_name = (parsing_result.action,
                            xml_element.tag,
                            parsing_result.name,
                            xml_element.attrib['version'])
        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 _unknown(self, xml_element):
        # Действия если элемент неизвестен.
        element_name = ('', xml_element.tag)
        return OrderedDict({element_name: 'Unknown element'})

    @property
    def document_text(self):
        '''Метод для получения исходного текста документа. Использует
        рекурсивный метод _build_section.'''
        channel = next(iter(self._document_dictionary.keys()))
        channel_head = OrderedDict(
                                   {key: value for key, value in
                                    zip(self._CHANNEL_ATTRIBUTES, channel[1:])}
                                  )

        root = Element(channel_head.pop('tag'), **channel_head)

        self._build_section(root, self._document_dictionary[channel])
        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_head = OrderedDict({key: value for key, value in
                                        zip(self._ELEMENT_ATTRIBUTES,
                                            dict_element[1:])})

            if element_head['type'] == 'empty':
                include_element = SubElement(current_element,
                                             element_head.pop('tag'),
                                             **element_head)
                self._build_section(include_element,
                                    dictionary[dict_element])

            elif element_head['type'] == 'array':
                include_element = SubElement(current_element,
                                             element_head.pop('tag'),
                                             **element_head)

                for list_element in dictionary[dict_element]:
                    list_element_head = OrderedDict(
                                                  {key: value for key, value in
                                                   zip(self._VALUE_ATTRIBUTES,
                                                       list_element)}
                                                   )
                    SubElement(include_element,
                               list_element_head.pop('tag'),
                               **list_element_head)
            else:
                SubElement(current_element,
                           element_head.pop('tag'),
                           **element_head,
                           value=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
