# vim: fileencoding=utf-8
#
from collections import OrderedDict
from jinja2 import Environment, PackageLoader
from pprint import pprint
from copy import copy
import re
try:
    from lxml.etree.ElementTree import fromstring
except ImportError:
    from xml.etree.ElementTree import fromstring


class FormatError(Exception):
    pass


class Format():
    FORMAT = 'none'
    CALCULATE_VERSION = None

    def __init__(self, processing_methods):
        self._processing_methods = processing_methods
        self._document_dictionary = OrderedDict()
        self._item_to_add = OrderedDict()

        self.TEMPLATES_DIRECTORY = 'templates'

        self._fatal_error_flag = False
        self._ready_to_update = False
        self._match = False

        self._need_finish = False
        self._comments_processing = False

        self._join_before = False
        self._join_before_in_areas = False

        # для отладки.
        self._line_timer = 0

    def _lines_to_dictionary(self, document_lines):
        '''Основной метод для парсинга документа. Принимает список строк,
        к каждой строке применяет парсеры, определенные для некоторого формата.
        Первый парсер, которому удается разобрать строку используется для
        формирования словаря.'''
        # print('Lines processing...')
        for line in document_lines:
            # print(self._line_timer, '\t', line)
            for processing_method in self._processing_methods:
                try:
                    processing_method(line)
                except FormatError as error:
                    self._document_dictionary = OrderedDict()
                    raise FormatError("can not parse line: {}, reason: {}".
                                      format(line, str(error)))

                if self._is_match():
                    if self._is_ready_to_update():
                        self._document_dictionary.update(self._item_to_add)
                    break
            else:
                # Действия если не удалось разобрать строку.
                self._document_dictionary = OrderedDict()
                raise FormatError('can not parse line: {}'.
                                  format(line))

        if self._need_finish:
            self._finish_method()

    def _parse_xml_to_dictionary(self, xml_document_text):
        '''Метод для парсинга xml файлов.
        Файлы xml предварительно не разбиваются на строки, а разбираются с
        помощью модуля lxml. Перевод в словарь осуществляется методами формата,
        рекурсивно вызывающимися в зависимости от типа тега.'''
        root = fromstring(xml_document_text)
        self._document_dictionary = self._processing_methods[root.tag](root)

    def print_dictionary(self):
        '''Метод для отладки.'''
        pprint(self._document_dictionary)

    def join_template(self, template):
        '''Метод запускающий наложение шаблона.'''
        self._join(self._document_dictionary,
                   template._document_dictionary,
                   self._join_before)

    def _get_list_of_logic_lines(self, text):
        '''Метод разбивающий документ на список логических строк -- то есть
        учитывающий при разбиении возможность разбиение одной строки на
        несколько с помощью бэкслеша. В некоторых форматах переопределен.'''
        list_of_lines = []
        lines_to_join = []
        for line in text.splitlines():
            line = line.strip()
            if line == '':
                continue
            if not line.endswith("\\"):
                lines_to_join.append(line)
                joined_line = "".join(lines_to_join)
                list_of_lines.append(joined_line)
                lines_to_join = []
            else:
                lines_to_join.append(line[:-1])
        return list_of_lines

    def _join(self, original, template, join_before):
        '''Основной метод для наложения шаблонов путем объединения их словарей
        выполняемого рекурсивно.'''
        if template == OrderedDict():
            return

        if join_before:
            forwarded_items = OrderedDict()

        for key_value in template:
            if key_value[0] == '!':
                # Удаление соответствующего элемента из original.
                # Сначала получаем ключ без символа действия.
                if isinstance(key_value, tuple):
                    item_to_delete = ('',) + key_value[1:]
                elif isinstance(key_value, str):
                    item_to_delete = key_value[1:]

                # Удаляем соответствующий элемент, если он есть в оригинале.
                if item_to_delete in original.keys():
                    del(original[item_to_delete])
            elif key_value[0] == '-':
                # Замена соответствующего элемента из original.
                # Сначала получаем ключ без символа действия.
                if isinstance(key_value, tuple):
                    item_to_replace = ('',) + key_value[1:]
                elif isinstance(key_value, str):
                    item_to_replace = key_value[1:]
                else:
                    # Сюда надо вставить исключение.
                    pass

                # Если соответствующего элемента нет в оригинале -- пропускаем.
                if item_to_replace not in original.keys():
                    continue

                # Если секция для замены в шаблоне пустая -- удаляем
                # соответствующую секцию.
                if isinstance(template[key_value], dict) and\
                   template[key_value] == OrderedDict():
                    original.pop(item_to_replace)
                    continue

                # Если символ замены стоит перед параметром, а не перед
                # секцией -- просто заменяем значение параметра.
                if not isinstance(template[key_value], dict):
                    original[item_to_replace] = template[key_value]
                    continue

                # Если обработка комментариев включена -- сохраняем
                # комментарии к заменяемой секции.
                if self._comments_processing:
                    if '#' in original[item_to_replace]:
                        replacement = OrderedDict({'#':
                                                   original[item_to_replace]
                                                   ['#']}
                                                  )
                        # накладываем словарь шаблона на пустой словарь, чтобы
                        # выполнить все управляющие элементы, которые
                        # могут туда попасть.
                        self._join(replacement,
                                   template[key_value],
                                   self._join_before_in_areas)
                    else:
                        replacement = OrderedDict()
                        self._join(replacement,
                                   template[key_value],
                                   self._join_before_in_areas)

                    # Если после наложения шаблона словарь замены оказался
                    # пустым -- удаляем соотвествующий элемент в оригинале.
                    if (replacement == OrderedDict() or
                            replacement.keys() == {'#'}):
                        del(original[item_to_replace])
                    else:
                        original[item_to_replace] = replacement
                else:
                    original[item_to_replace] = OrderedDict()
                    self._join(original[item_to_replace],
                               template[key_value],
                               self._join_before_in_areas)

                    if (original[item_to_replace] == OrderedDict() or
                            original[item_to_replace].keys() == {'#'}):
                        del(original[item_to_replace])

            elif key_value not in original.keys():
                if isinstance(template[key_value], dict):
                    dictionary_to_add = OrderedDict()
                    self._join(dictionary_to_add,
                               template[key_value],
                               self._join_before_in_areas)
                    if dictionary_to_add != OrderedDict():
                        if not join_before:
                            original[key_value] = dictionary_to_add
                        else:
                            forwarded_items[key_value] = dictionary_to_add
                else:
                    if not join_before:
                        original[key_value] = template[key_value]
                    else:
                        forwarded_items[key_value] = template[key_value]
            else:
                if isinstance(original[key_value], dict) and \
                   isinstance(template[key_value], dict):
                    self._join(original[key_value],
                               template[key_value],
                               self._join_before_in_areas)
                else:
                    if self._comments_processing:
                        # Я пока еще не понял почему, но должно быть так:
                        if not original[key_value] and not template[key_value]:
                            continue
                        original[key_value][-1] = template[key_value][-1]
                    else:
                        original[key_value] = template[key_value]
            if join_before:
                for key_value in reversed(forwarded_items.keys()):
                    original[key_value] = forwarded_items[key_value]
                    original.move_to_end(key_value, last=False)

    def make_template(self, template):
        '''Метод для запуска генерации шаблонов путем сравнения пары исходных
        файлов.'''
        full_diff, set_to_check = self.compare_dictionaries(
                                                  self._document_dictionary,
                                                  template._document_dictionary
                                                           )
        template_object = copy(self)
        template_object._document_dictionary = full_diff
        return template_object

    def compare_dictionaries(self, dict_1, dict_2):
        '''Основной метод для генерации шаблонов путем сравнения пары исходных
        файлов. Работает рекурсивно.'''
        to_remove_dictionary = OrderedDict()
        to_add_dictionary = OrderedDict()
        to_replace_dictionary = OrderedDict()
        unchanged_set = set()

        to_remove = dict_1.keys() - dict_2.keys()

        if '#' in to_remove:
            to_remove.remove('#')

        for key in dict_1:
            if key in to_remove:
                if isinstance(key, tuple):
                    new_key = ('!', *key[1:])
                else:
                    new_key = '!{}'.format(key)
                if isinstance(dict_1[key], dict):
                    to_remove_dictionary.update({new_key: dict_1[key]})
                else:
                    if self._comments_processing:
                        to_remove_dictionary.update({new_key:
                                                     [dict_1[key][-1]]})
                    else:
                        to_remove_dictionary.update({new_key: dict_1[key]})

        to_add = dict_2.keys() - dict_1.keys()

        if '#' in to_add:
            to_add.remove('#')

        for key in dict_2:
            if key in to_add:
                if isinstance(dict_2[key], dict):
                    section = dict_2[key].copy()
                    if '#' in section:
                        section.remove('#')
                    to_add_dictionary.update({key: section})
                else:
                    if self._comments_processing:
                        to_add_dictionary.update({key: [dict_2[key][-1]]})
                    else:
                        to_add_dictionary.update({key: dict_2[key]})

        intersect = dict_1.keys() & dict_2.keys()
        for key in intersect:
            if (isinstance(dict_1[key], dict) and
                    isinstance(dict_2[key], dict) and
                    dict_1[key] != dict_2[key]):
                diff, set_to_check = self.compare_dictionaries(dict_1[key],
                                                               dict_2[key])
                if set_to_check:
                    to_add_dictionary.update({key: diff})
                else:
                    if isinstance(key, tuple):
                        new_key = ('-', *key[1:])
                    else:
                        new_key = '-{}'.format(key)
                    to_replace_dictionary.update({new_key:
                                                  dict_2[key]})
            elif dict_1[key] != dict_2[key]:
                if self._comments_processing:
                    to_add_dictionary.update({key: [dict_2[key][-1]]})
                else:
                    to_add_dictionary.update({key: dict_2[key]})
            else:
                unchanged_set.add(key)

        full_diff = OrderedDict()
        full_diff.update(**to_remove_dictionary,
                         **to_replace_dictionary,
                         **to_add_dictionary)
        return full_diff, unchanged_set

    @property
    def document_text(self):
        '''Метод для получения текста документа. Использует jinja2 для
        рендеринга документа.'''
        file_loader = PackageLoader('calculate.templates.format',
                                    self.TEMPLATES_DIRECTORY)
        formats_environment = Environment(loader=file_loader,
                                          trim_blocks=True,
                                          lstrip_blocks=True)
        formats_environment.globals.update(zip=zip)
        formats_environment.add_extension('jinja2.ext.do')
        template = formats_environment.get_template(self.FORMAT)
        document_text = template.render(
                                  document_dictionary=self._document_dictionary
                                       )
        return '{}{}'.format(self.header, document_text)

    def _finish_method(self):
        '''Метод для выполнения заключительных действий парсинга.
        Переопределяется в форматах. Вызывается при self._need_finish = True'''
        pass

    def _is_ready_to_update(self):
        '''Метод для проверки флага self._ready_to_update, указывающего, что
        сформированная форматом секция документа, находящаяся в
        self._item_to_add, может быть добавлена в словарь документа.'''
        is_ready, self._ready_to_update = self._ready_to_update, False
        return is_ready

    def _is_match(self):
        '''Метод для проверки флага self._is_match, указывающего что текущий
        парсер, использованный форматом, смог распарсить строку и использовать
        другие парсеры не нужно.'''
        is_match, self._match = self._match, False
        return is_match

    def _get_header_and_document_text(self, input_text,
                                      template_path,
                                      already_changed=False):
        '''Метод для создания заголовка измененного файла и удаления его из
        текста исходного файла.'''
        header_pattern = (r'^{0}' + r'-' * 79 + r'\n' +
                          r'{0} Modified by Calculate Utilities [\d\w\.]*\n' +
                          r'{0} Processed template files:\n' +
                          r'(?P<template_paths>({0}\s*[/\w\d\-_\.]*\n)+)' +
                          r'{0}' + r'-' * 79 + r'\n?').format(
                                                           self.comment_symbol)
        template_paths = []
        if already_changed:
            header_regex = re.compile(header_pattern)
            parsing_result = header_regex.search(input_text)
            for template in parsing_result.\
                    groupdict()['template_paths'].strip().split('\n'):
                if template.startswith(self.comment_symbol):
                    template = template[len(self.comment_symbol):]
                template_paths.append(template.strip())
        template_paths.append(template_path)
        header = ('{0}' + '-' * 79 + '\n' +
                  '{0} Modified by Calculate Utilities {1}\n' +
                  '{0} Processed template files:\n' +
                  '{0} ' + '\n{0} '.join(template_paths) + '\n' +
                  '{0}' + '-' * 79 + '\n').format(self.comment_symbol,
                                                  self.CALCULATE_VERSION)
        document_text = re.sub(header_pattern, '', input_text)
        return header, document_text

    def __bool__(self):
        return bool(self._document_dictionary)
