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

"""
Экспортер (L{Exporter}) - читает из конфигурации список экспортируемых
котировок. Управляет алгоритмом экспорта.

Экспорт выполняется в несколько этапов:
    1. Чтение списка котировок (L{Exporter.list_of_quotes_for_export}). Эти
       котировки - кандидаты на экспорт.
    3. Загрузка источников. У каждого источника есть parser, который поможет
       нам узнать список котировок, импортируемых данным источником. То есть -
       для каждого источника S мы будем знать список котировок. Чтобы все было
       хорошо мы вводим метод L{Source.getImportQuotesNames}.
    4. Определение активного источника для каждой котировки.
    5. Заполнение каждой котировки значениями (откуда они читаются - неважно).
       Значения читаются для каждого региона, то есть с котирвкой связывается
       множество регионов и с каждым регионом - множество значений.
    6. Вычисление дополнительных значений. Значение может вычисляться
       для цены или для котировки в целом (то есть используются два типа
       калькуляторов).
    7. Заполнение окружения шаблонизатора:
        - список квот (quotes), у каждой из которых есть список значений
          (prices).
        - каждая квота содержит поле source с информацией об источнике.
        - каждая квота содержит поле export - это информация для
          шаблонизатора.
    8. Запуск шаблонизатора.

Экспортер использует список калькуляторов (L{QCalculator}), каждый из которых
считывает нужное количество данных из базы (использует для этого L{Reader},
привязанные к экспортеру) и передает эти данные на обработку шаблонизаторам
(L{Templater}).

Экспортер запускается через L{Exporter.run_export}.

Замечание:
Так как разные экспортеры требуют различные данные (например, иногда требуется
10 значений, а иногда все), qcalculator получает доступ к базе данных через
связку qcalcualtor-exporter-reader-db.

Замечание: на выходе получаются файлы двух типов. Первый тип файлов содержит
информацию об одной котировке, второй тип - о некотором множестве котировок.

Ко второму типу файлов относятся файлы all.xml и index.xml. Они отличаются
набором котировок, хотя неизвестно почему это так. Также ко второму типу
относится csv-файл.

Важное замечание - котировка не экспортируется, если не найден источник,
активный для этой котировки. В этом случае исполнение прекращается со страшной
ошибкой.

"""
from __future__ import print_function
from stocks3.core.config import Configurable, ConfigurationError, str_to_bool
from stocks3.core import factories
from stocks3.core.exception import S3Exception, throw_only
from stocks3.share.quotesourcemap import get_quote_source_map
from stocks3.core.stock import AbstractPrice, Quote
from stocks3.share.normalization import load_normalization_list
from stocks3.share.scale import load_scale_information
from stocks3.core.config import parse_xml
from stocks3.core.default import Default
__author__ = "Zasimov Alexey"
__email__ = "zasimov-a@yandex-team.ru"


class S3ExporterException(S3Exception):
    STAGE = "export"


class Exporter(Configurable):

    def __init__(self, tree, node, default, sources, output_category_list, export_quotes=None):
        """
        @param tree: Смотрим L{Configurable}.
        @param node: Смотрим L{Configurable}.
        @param default: Настройки по умолчанию.
        @param sources: Список источников из которых берутся котировки.
        @param output_category_list: Категория файлов, которые нужно сформировать.
        """
        if not export_quotes:
            export_quotes = []
        self.default = default
        # Нас интересуют только активные источники.
        self.sources = map(lambda x: x[1], sources)
        self.output_category_list = output_category_list[:]
        self.user_export_quotes = export_quotes[:]
        self._config_is_readed = False
        # FIXME: запутался в вызовах
        self.tree = tree
        self.node = node
        Exporter.makeConfig(self)
        # Загружаем карту источников
        self.quoteSourceMap = get_quote_source_map(self.sources)
        # Загружаем котировки, которые могут быть экспортированы. В реальности
        # экспортируются или они, или котировки из списка self.user_export_quotes.
        # Отсечение выполняется в list_of...
        self.exportQuotes = self.get_export_quotes()
        Configurable.__init__(self, tree, node)

        self._readReader()

    def makeConfig(self):
        # Чтобы не грузилось второй раз в Configurable.__init__
        if self._config_is_readed:
            return
        Configurable.makeConfig(self)
        self._config_is_readed = True
        # Этот флажок устанавливается, если надо покричать при обнаружении неактивной котировки
        self.stop_if_inactive_quote_found = self.readBool("", "stop-if-inactive-quote-found", True)
        # Точность для значений (по умолчанию)
        self.precision = self.readInt("precision", "default")
        self.hot_precision = self.readInt("hot-precision")
        # Для всех котировок с klimit is None устанавливается это значение.
        self.klimit = self.readFloat("klimit")
        # Список калькуляторов читается лениво (L{Exporter._get_qcalculators}).
        self._qcalculators_is_loaded = False
        self._readCalculating()
        self._normalization = load_normalization_list(self.node, self.default.quotes_config)
        self._scales = load_scale_information(self.node, self.default.quotes_config)

    def _readReader(self):
        readers = self.createObjects(factories.readers, "reader", self)
        if len(readers) != 1:
            raise ConfigurationError("Please, check reader configuration.")
        self.reader = readers[0]

    def _readCalculating(self):
        """
        Загружаем список вычисляемых котировок.
        """
        self.calculating = []
        for quote_node in self.node.findall("calculating/quote"):
            quote_name = quote_node.attrib["name"]
            orig_quote_name = quote_node.attrib["orig-quote"]
            quote = self.default.quotes_config.getQuote(quote_name)
            self.calculating.append( (quote_name, quote, orig_quote_name) )

    def is_need_normalization(self, price):
        """
        Возвращает True, если значение этих котировок нужно нормализовать.

        Используется из L{QCalculator.construct_price}.
        """
        assert isinstance(price, AbstractPrice),\
            "Expected AbstractPrice for is_need_normalization, received %s." % price.__class__.__name__
        return self._normalization.get(price.quote.quote_id, False)

    def get_scale_factor(self, price):
        """
        """
        assert isinstance(price, AbstractPrice),\
            "For get_scale_factors expected AbstractPrice, but received %s." % price.__class__.__name__
        scale_info = self._scales.get(price.quote.quote_id, {})
        scale_from_quote = scale_info.get("from_quote", False)
        if scale_from_quote:
            scale = price.quote.numerator.scale
        else:
            scale = scale_info.get("scale", 1.0)
        return scale

    def _get_qcalculators(self):
        if not self._qcalculators_is_loaded:
            self._qcalculators = self.createObjects(factories.qcalculators, "qcalculators/qcalculator",
                                                    self, self.output_category_list)
            self._qcalculators_is_loaded = True
        return self._qcalculators
    qcalculators = property(_get_qcalculators)

    def _make_exportable_quote(self, quote, name, orig_quote_name=None):
        # Устанавливаем klimit по умолчанию
        quote.set_default_klimit(self.klimit)
        # Определяем активный источник
        try:
            if orig_quote_name is not None:
                source = self.quoteSourceMap[orig_quote_name]
            else:
                source = self.quoteSourceMap[name]
        except KeyError:
            if self.stop_if_inactive_quote_found:
                # Останавливаем, если нашли неактивную котировку и установлен флаг
                raise RuntimeError("Inactive quote: %s" % name.encode("utf-8"))
            source = None
        quote.attach_source(source)
        return quote

    def get_export_quotes(self):
        # Получаем список всех известных нам котировок
        quotes = self.default.quotes_config.getAllQuotes()
        # Создаем список экспортируемых котировок
        # FIXME: может быть медленно - перечитывается XML
        # FIXME: ещё один цикл - это плохо
        # Здесь мы делаем из обычных котировок - экспортируемые.
        # Просто считываем дополнительные данные (Смотрим L{ExportableQuote.makeConfig}).
        all_quotes = {}
        for name, quote in quotes:
            exportable_quote = self._make_exportable_quote(quote, name)
            all_quotes[exportable_quote.quote_id] = (name, exportable_quote, None)
        # Добавляем вычисляемые квоты
        # Здесь orig_quote - это котировка, по которой мы определим источник
        for name, quote, orig_quote_name in self.calculating:
            exportable_quote = self._make_exportable_quote(quote, name, orig_quote_name)
            if exportable_quote is None:
                continue
            all_quotes[exportable_quote.quote_id] = (name, exportable_quote, orig_quote_name)
        return all_quotes

    def getQuote(self, quote_id):
        """
        Возвращает котировочку.
        """
        try:
            # Смотрим выше
            return self.exportQuotes[quote_id][1]
        except KeyError:
            raise RuntimeError("Exporter.getQuote: Quote %s is disabled." % quote_id)

    def getQuoteId(self, name):
        return self.get_quote_id(name)

    def get_quote_id(self, name):
        """
        FIXME: Возможно, уже есть такой метод. Если такого метода нет, то нужно
        сделать.
        """
        return self.default.quotes_config.get_quote(name).quote_id

    def getQuoteByName(self, quote_name):
        # FIXME: тупим
        quote = self.default.quotes_config.getQuote(quote_name)
        return self.getQuote(quote.quote_id)

    def list_of_quotes_for_export(self):
        """
        Выдаем список экспортируемых котировок.
        """
        # Если мы находим котировку без источника - то либо останавливаемся
        # либо пропускаем эту котировку (в зависимости от флага stop_if_...).
        if len(self.user_export_quotes) == 0:
            quotes = self.exportQuotes.values()
        else:
            quotes = []
            for quote_id in self.user_export_quotes:
                quotes.append(self.exportQuotes[quote_id])
        for name, exportable_quote, orig_quote_name in quotes:
            # Считываем значения котировки и отдаем эту котировку дальше на
            # обработку.
            if exportable_quote.source is None:
                # Пропускаем неактивные котировки
                continue
            exportable_quote.make_aliases(self.exportQuotes)     # подгружаем псевдонимы
            if exportable_quote.alias_quote is not None:
                exportable_quote.alias_quote.attach_source(exportable_quote.source)
            yield exportable_quote

    @throw_only(S3ExporterException)
    def run_export(self):
        # Получаем список котировок
        quotes = self.list_of_quotes_for_export()
        # Каждую котировку обрабатываем калькулятором
        for quote in quotes:
            for qCalculator in self.qcalculators:
                qCalculator.run_calc(quote)
        # Запускаем процесс записи результатов (flush передается
        # шаблонизаторам)
        for qCalculator in self.qcalculators:
            qCalculator.flush()


DEF_EXPORTER_CONFIG = "config/exporter.xml"
DEFAULT_XML = "config/default-db.xml"


def make_exporter(sources, output_category_list=None, export_quotes=None):
    if output_category_list is None:
        output_category_list = []
    default = Default(DEFAULT_XML)
    tree = parse_xml(DEF_EXPORTER_CONFIG)
    root = tree.getroot()
    return Exporter(tree, root, default, sources, output_category_list, export_quotes)
