# -*- coding: utf-8 -*-

"""
Основные функции для работы с файлами конфигурации.

Файлы конфигурации представляют собой XML-файлы определенной структуры. Часто,
разные файлы конфигурации содержат одинаковые блоки. Этот модуль содержит
функции, для разбора частоиспользуемых блоков конфигурации.

Используемые модули:
    - lxml - библиотека для разбора XML (установка: easy_install lxml);

К основным параметрам, которые управляют процессом импорта-экспорта относятся:
    - Параметры времени. При импорте-экспорте следует учитывать из какого
      источника (L{stocks3.core.source}) пришли данные о котировках и
      корретировать время в зависимости от временной зоны источника. При этом
      некоторые источники вообще не отсылают время (только дату), необходимо
      обрабатывать такие случае и брать, например, время закрытия биржи или
      текущее время.

    - Параметры точности. Как при экспорте так и при импорте необходимо
      управлять точностью значений. Увеличить точность мы не можем (упираемся в
      точность источника), а вот уменьшить - запросто.

Всю конфигурацию можно распределить по этапам процесса обработки данных котировок:
    - Конфигурация транспорта.
    - Конфигурация импорта.
    - Конфигурация экспорта.

Сейчас о конфигурации можно сказать только то, что на хранится в виде XML.
Каждый конкретный модуль должен сам знать, как прочитать свою конфигурацию.
"""

import sys
import json

from stocks3.share import messages
from stocks3.core.common import get_field

# Обязательно нужен lxml.
from lxml import etree

__author__ = "Zasimov Alexey"
__email__ = "zasimov-a@yandex-team.ru"


def parse_xml(fobj):
    """
    Разбирает входной XML-файл. Эта функция нужна лишь для того, чтобы импорт и
    настройка парсера XML лежала в одном модуле.
    """
    tree = etree.parse(fobj)
    tree.xinclude()
    return tree


def parse_xml_fromstring(data):
    """
    Разбирает входной XML-файл. Эта функция нужна лишь для того, чтобы импорт и
    настройка парсера XML лежала в одном модуле.
    """
    utf8_parser = etree.XMLParser(encoding='utf-8')
    tree = etree.fromstring(data.encode('utf-8'), parser=utf8_parser)
    tree = tree.getroottree()
    tree.xinclude()
    return tree


def str_to_bool(bool_string):
    """
    Конвертируем строчку в bool-значение.

    @type bool_string: строка
    @param bool_string: Строка, описывающее булево значение.
            - Истинными значениями считаются: 1, true, yes, y.
            - Ложными значениями считаются: 0, false, no, n.

            Регистр неважен.
    @exception ValueError: Генерируется, если boolString содержит что-то
    непотребное.
    """
    if not bool_string:
        return False

    s = bool_string.lower()
    if s in ["true", "1", "yes", "y"]:
        return True
    elif s in ["false", "0", "no", "n"]:
        return False
    else:
        raise ValueError("Invalid boolean value: %s" % bool_string)


class ConfigurationError(Exception):
    pass


def config_check_value(func):
    """
    Оборачиваются функции, которые могут сгенерировать ValueError. Этот
    декоратор преобразует исключение ValueError в исключение
    ConfigurationError.
    """

    def value_error_to_config_error(self, path, attrib=None, default=None):
        try:
            return func(self, path, attrib, default)
        except ValueError as e:
            if attrib:
                message = "Value error, node %s.%s: %s" % (path, attrib, e)
            else:
                message = "Value error, node %s: %s" % (path, e)
            raise ConfigurationError(message)

    value_error_to_config_error.__name__ = func.__name__
    return value_error_to_config_error


class JSONConfigurable(object):
    def __init__(self, configfile="config/config.json"):
        self.configstore = {}
        self.loaded = False
        try:
            with open(configfile) as config:
                self.configstore = json.load(config)
                self.loaded = True
        except IOError as e:
            print('could not load json config:', e)
        except json.JSONDecodeError as e:
            print('could not load json config:', e)

    def make_config(self):
        if self.loaded:
            print('config loaded and called make_config')
        pass

    def read_item(self, path, default_value=''):
        if isinstance(path, str):
            path = path.split('.')

        if isinstance(path, (list, tuple)):
            node = self.configstore
            for element in path:
                try:
                    if element in node:
                        node = node[element]
                    else:
                        return default_value
                except Exception as e:
                    print(e)
            return node
        else:
            return default_value

    @staticmethod
    def create_object(factory, config, **kwargs):
        """
        Создает объекты, с заданной конфигурацией.

        @type factory: L{ConfigurableFactory}
        @param factory: Фабрика объектов.
        @param config: Мапа с конфигом
        """
        class_name = config["class"]
        return factory.create_object_json(class_name, config, **kwargs)


class Configurable(object):
    """
    Все класс, порожденные от этого класса, тем или иным образом создаются на
    основе конфигурации, хранящейся в XML-файле.

    После инициализации объект этого класса получает в распоряжении дерево
    XML-файла - L{Configurable.tree}.
    """

    def __init__(self, tree, node, **kwargs):
        """
        @param tree: XML-дерево.
        @param node: XML-узел с конфигурацией для данного объекта.
        """
        self.tree = tree
        self.node = node
        if not kwargs.get("noconfig", False):
            self._safe_makeConfig()

    def _safe_makeConfig(self):
        if hasattr(self, "_conf_is_loaded"):
            return
        else:
            self.makeConfig()
            self._conf_is_loaded = True

    def makeConfig(self):
        """
        Должна быть переопределена в дочерних классах. Собственна эта функция и
        загружает конфигурацию из переданной нам ветки self.node.
        """
        return self

    def createObjects(self, factory, path, *args):
        """
        Создает объекты, конфигурация которых прописана в ветке path.

        Например:
         - C{source.createObjects(factories.transports,"../transports/transport")}

        @type factory: L{ConfigurableFactory}
        @param factory: Фабрика объектов.
        @param path: Путь до узлов с конфигурацией.
        """
        nodes = self._get_nodes_by_path(path)
        return _map_active(factory, self.tree, nodes, *args)

    def _get_nodes_by_path(self, path):
        """
        Возвращает список узлов по пути path.
        """
        if not path:
            return [self.node]
        else:
            return self.node.xpath(path)

    def _get_node_by_path(self, path):
        """
        Возвращает узел по его адресу.

        @type path: xpath
        @param path: Адрес узла.
        @exception ConfigurationError: Найдено больше или меньше одного узла.
        """
        nodes = self._get_nodes_by_path(path)
        if len(nodes) != 1:
            raise ConfigurationError("Expected one %s node. Received: %s" % (path, len(nodes)))
        return nodes[0]

    def readString(self, path, attrib=None, default=None):
        """
        Читает строку из файла конфигурации.

        @type path: строка
        @param path: Адрес узла XML-файла.
        @type attrib: строка
        @param attrib: Имя атрибута. Если имя атрибута не указано, то возвращается text.
        @param default: Значение по умолчанию. Если искомый атрибут не найден и
                        указано значение по умолчанию, то возвращается оно.
                        Иначе генерируется исключение KeyError.
        """
        try:
            node = self._get_node_by_path(path)
        except ConfigurationError:
            if default is None:
                raise
            else:
                return default
        return read_attrib_or_text(node, attrib, default)

    @config_check_value
    def readInt(self, path, attrib=None, default=None):
        assert default is None or type(default) == int, "readInt"
        return int(self.readString(path, attrib, default))

    @config_check_value
    def readFloat(self, path, attrib=None, default=None):
        assert default is None or type(default) == float, "readFloat"
        return float(self.readString(path, attrib, default))

    @config_check_value
    def readBool(self, path, attrib=None, default=None):
        assert default is None or type(default) == bool, "readBool"
        r = self.readString(path, attrib, default)
        if type(r) == bool:
            return r
        else:
            return str_to_bool(r)

    @staticmethod
    def _info(s):
        # FIXME: try-except-pass
        try:
            messages.info(s)
        except:
            pass


class ConfigurableFactoryError(Exception):
    def __init__(self, message, native_exception):
        Exception.__init__(self, message)
        self.nativeException = native_exception
        self.exc_info = None


class ConfigurableFactory(object):
    """
    Фабрика для создания конфигурируемых объектов.

    Для создания кофигурируемых объектов (L{Configurable}) достаточно иметь ссылку на узел XML-файла с конфигурацией.
    """

    def __init__(self):
        self._classes = {}  # словарь с классами, объекты которых может создавать фабрика

    def register(self, name, cls):
        """
        Регистрируем класс cls в фабрике под именем name.

        @param name: Имя класса в фабрике.
        @param cls: Регистрируемый класс.
        """
        self._classes[name] = cls

    def create(self, name, tree, config_node, *args):
        """
        Создает объект класса name.

        @param name: Имя создаваемого класса.
        @param tree
        @param config_node: узел XML-файла с конфигурацей создаваемого объекта.
        """
        try:
            cls = self._classes[name]
            try:
                return cls(tree, config_node, *args)
            except Exception as e:
                ce = ConfigurableFactoryError(
                    "Construction error for class %s: %s: %s" % (name, e.__class__.__name__, e), e)
                ce.exc_info = sys.exc_info()
                raise ce
        except KeyError as e:
            raise ConfigurableFactoryError("Unknown class: %s" % name, e)

    def create_object_json(self, name: str, config: dict, **kwargs):
        """
        Создает объект класса name с конфигурацией из json.

        @param name: Имя создаваемого класса.
        @param config: узел XML-файла с конфигурацей создаваемого объекта.
        """
        try:
            cls = self._classes[name]
            try:
                return cls(config=config, **kwargs)
            except Exception as e:
                ce = ConfigurableFactoryError(
                    "Construction error for class %s: %s: %s" % (name, e.__class__.__name__, e), e)
                ce.exc_info = sys.exc_info()
                raise ce
        except KeyError as e:
            raise ConfigurableFactoryError("Unknown class: %s" % name, e)


def _map_active(factory, tree, nodes, *args):
    """
    Отображает узлы конфигурации nodes на объекты. Для создания объектов
    используется фабрика factory. В результате каждому узла из nodes будет
    сопоставлен объект.

    Узлы описываются в виде тегов:

    <name class="<class_name>" active="True|False" priority="<digit>"/>

    @type factory: L{ConfigurableFactory}
    @param factory: Фабрика для создания объектов.
    @param nodes: Узлы файла конфигурации.
    """
    objects = []
    for node in nodes:
        active = str_to_bool(node.attrib.get("active", "true"))
        if active:  # Нас интересуют только активные узлы
            class_name = node.attrib["class"]
            priority = int(node.attrib.get("priority", 0))
            obj = factory.create(class_name, tree, node, *args)
            objects.append((priority, obj))
    # Сортируем объекты по приоритету
    return [x[1] for x in sorted(objects, key=lambda x: x[0])]


def _check_default(path, attrib, default):
    """
    Используется из L{readAttribOrText}.
    """
    if default is not None:
        return default
    else:
        if attrib:
            message = "Attribute %s.%s not found." % (path, attrib)
        else:
            message = "Node %s not found." % path
        raise ConfigurationError(message)


# FIXME: убрать параметр path отсюда
def read_attrib_or_text(node, attrib, default, path=""):
    """
    Читает или атрибут attrib или текст узла с именем attrib.
    """
    if attrib:
        try:
            return node.attrib[attrib]
        except KeyError:
            # Пробуем отыскать text
            text_node = node.find(attrib)
            if text_node is not None:
                return text_node.text
            else:
                return _check_default(path, attrib, default)
    else:
        return node.text


class Configuration(object):
    def __init__(self, filename='config/config.json'):
        self.config = {}
        self.read(filename)

    def read(self, filename='config/config.json'):
        try:
            with open(filename, 'r') as f:
                self.config = json.load(f)
        except Exception as e:
            print('bad config')
            raise e

    def __getitem__(self, item):
        if item in self.config:
            return self.config[item]
        else:
            return None

    def get(self, path):
        return get_field(self.config, path)
