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

"""
Источник - объект, из которого возможно получение данных о котировках. Это
может быть база данных, сервер HTTP, файл, что угодно.

Основной класс этого модуля - L{Source}. Основной метод этого класса -
L{Source.runImport}. Этот метод запускает процесс импорта данных.
"""

import json
from stocks3.core.config import Configurable, JSONConfigurable, ConfigurationError
from stocks3.core import factories  # подключаем фабрики
from stocks3.core.default import Default
from stocks3.core.exception import S3Exception
from stocks3.core.place import MongoPlace
from stocks3.core.config import parse_xml
import os

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

# Класс источника, который используется по умолчанию
DEF_SOURCE_CLASS = "stocks3.sources.Standart"
DEFAULT_JSON_SOURCE_CLASS = "stocks3.sources.JsonStandart"
DEFAULT_XML = "config/default-db.xml"


class S3SourceError(S3Exception):
    STAGE = "source"


class BaseSource:
    pass


class Source(Configurable, BaseSource):
    """
    Из источника данные сначала забираются (это делает транспорт), потом
    разбираются (это делает парсер), потом вычисляются вычисляемые поля (каждый
    объект, выданные парсером, обрабатывается калькулятором), потом приводится
    в форму, понятную модулю записи в БД.

    Задача объекта класса L{Source} - создать транспорт, парсер, калькуляторы и
    модуль записи. То есть этот класс управляет процессом импорта
    (L{Source.runImport}).
    """

    def __init__(self, tree, node, default=Default(DEFAULT_XML)):
        """
        При создании ему передаются настройки по умолчанию (default).

        @type default: L{Default}
        @param default: Настройки по умолчанию.
        """
        self.default = default
        self.active = True
        self.sourceId = None
        self.source_inner_id = None
        # Человеческое имя источника.
        self.name = None
        self.weight = None
        self.transport_attempts = self.default.transport_attempts
        self.transport_sleep = self.default.transport_sleep

        self._transports = []
        self._parser = None
        self._saver = None
        Configurable.__init__(self, tree, node)

    def makeConfig(self):
        self.active = self.readBool("", "active", True)
        self.transport_attempts = self.readInt("", "transport-attempts", self.default.transport_attempts)
        self.transport_sleep = self.readInt("", "transport-sleep", self.default.transport_sleep)
        # Считываем параметры источника.
        """
        Обработка <source/>. Могут присутствовать параметры:
          - id - идентификатор источника;
          - weight - вес источника;
          - active - активен источник или нет?
        """
        self.sourceId = self.readString("/source", "id")
        self.source_inner_id = self.readInt("/source", "inner-id")
        # Человеческое имя источника.
        self.name = self.readString("/source", "name")
        self.weight = self.readInt("/source", "weight")
        self.active = self.readBool("/source", "active", True)
        # Считываем параметры калькулятора.
        self._readCalculators()
        # Считываем параметра saver'a.
        return self

    @property
    def transports(self):
        """
        Считываем списка транспортов, используя которые можно достучаться до
        данных котировок.

        Все транспорты описываются в секции transport.

        Сейчас программа работает только с одним транспортом.
        """
        if self._transports:
            return self._transports
        else:
            self._transports = self.createObjects(factories.transports, "transport", self)
            if len(self._transports) == 0:
                raise ConfigurationError("Please, check transport configuration.")
            return self._transports

    @property
    def parser(self):
        if self._parser:
            return self._parser
        else:
            parsers = self.createObjects(factories.parsers, "parser", self)
            if len(parsers) != 1:
                raise ConfigurationError("Please, check parser configuration.")
            self._parser = parsers[0]
            return self._parser

    @property
    def saver(self):
        if self._saver:
            return self._saver
        else:
            savers = self.createObjects(factories.savers, "saver", self)
            if len(savers) != 1:
                # Пытаемся взять Saver из конфигурации по умолчанию
                self._saver = self.default.getDefaultSaver(self)
                if self._saver is None:
                    raise ConfigurationError("Please, check saver configuration.")
            else:
                self._saver = savers[0]
            return self._saver

    def _readCalculators(self):
        calculators_node = self.node.find("calculators")
        if calculators_node is not None:
            dont_use_defaults_calculators = calculators_node.attrib.get("dont-use-default", False)
        else:
            dont_use_defaults_calculators = False
        if not dont_use_defaults_calculators:
            self.calculators = self.default.getDefaultCalculators()
        else:
            self.calculators = []
        self.calculators.extend(self.createObjects(factories.calculators, "calculators/calculator", self.default))

    def _make_place(self):
        place = MongoPlace(self, self.tree, self.node)
        return place

    def run_calculators(self, price):
        for calculator in self.calculators:
            price = calculator.run_calc(self, price)
        return price

    def process(self, transport, place, test=False, skip_cache=False):
        """
        Запускает процесс импорта данных из источника с использованием
        транспорта transport.
        """
        checks_filed = []
        transport.run_transfer(place)
        try:
            # Парсем данные источника
            for price in self.parser.run_parse(place, skip_cache=skip_cache):
                # Расчитываем поля. При это объект идет как по конвейеру.
                price = self.run_calculators(price)
                # Сохраняем данные котировки
                if self.saver.run_save(price, test) is None:
                    checks_filed.append(price)
            if not test:
                self.saver.run_flush()
        finally:
            # Очищаем ресурсы, занятые транспортом
            transport.clean()
        return checks_filed

    def transfer(self, test=False, skip_cache=False):
        res = []
        for transport in self.transports:
            place = self._make_place()
            success = False
            try:
                res = self.process(transport, place, test, skip_cache)
                # если из process вышли с exception'ом, то и в clean не будем апдейтить хеш
                success = True
            finally:
                if not test:
                    place.clean(success)
        return res

    def clean(self):
        pass

    def getImportQuotesNames(self):
        """
        Возвращает список имен котировок, которые забираются из этого
        источника.
        """
        return self.parser.getActiveQuotesNames()

    def set_disable_checks_list(self, disable_checks_list):
        """
        Указываем, для каких котировок отключаются проверки (этот метод
        вызывается из L{S3RunOpts._get_sources}).
        """
        self.saver.set_disable_checks_list(disable_checks_list)


class JsonSource(JSONConfigurable, BaseSource):
    """
    Из источника данные сначала забираются (это делает транспорт), потом
    разбираются (это делает парсер), потом вычисляются вычисляемые поля (каждый
    объект, выданные парсером, обрабатывается калькулятором), потом приводится
    в форму, понятную модулю записи в БД.

    Задача объекта класса L{Source} - создать транспорт, парсер, калькуляторы и
    модуль записи. То есть этот класс управляет процессом импорта
    (L{Source.runImport}).
    """

    def __init__(self, configfile):
        JSONConfigurable.__init__(self, configfile)
        if not self.loaded:
            raise Exception("could not load source: {}".format(configfile))

        self.active = self.configstore['active']
        self.sourceId = self.configstore['id']
        self.source_inner_id = self.configstore['inner-id']
        # Человеческое имя источника.
        self.name = self.configstore['name']
        self.link = self.configstore['source-link']
        self.weight = self.configstore['weight']
        self.timezone = self.configstore['timezone']

        self._transports = []
        self._parser = None
        self._saver = None

        # Считываем параметры источника.
        """
        Обработка <source/>. Могут присутствовать параметры:
          - id - идентификатор источника;
          - weight - вес источника;
          - active - активен источник или нет?
        """
        self.calculators = []

        # legacy
        self.transport_attempts = 0
        self.transport_sleep = 0
        self.default = Default(DEFAULT_XML)

        self._read_calculators()

    @property
    def transports(self):
        """
        Считываем список транспортов, используя которые можно достучаться до данных котировок.
        Все транспорты описываются в секции transport.
        Сейчас программа работает только с одним транспортом.
        """
        if self._transports:
            return self._transports
        else:
            for transport in self.configstore['transports']:
                self._transports.append(self.create_object(factories.transports, transport, source=self))
            if len(self._transports) == 0:
                raise ConfigurationError("Please, check transport configuration.")
            return self._transports

    @property
    def parser(self):
        if self._parser:
            return self._parser
        else:
            self._parser = self.create_object(factories.parsers, self.configstore['parser'], source=self)
            return self._parser

    @property
    def saver(self):
        if self._saver:
            return self._saver
        else:
            if 'saver' in self.configstore:
                self._saver = self.create_object(factories.savers, self.configstore['saver'])
            else:
                # Пытаемся взять Saver из конфигурации по умолчанию
                self._saver = self.default.getDefaultSaver(self)
                if self._saver is None:
                    raise ConfigurationError("Please, check saver configuration.")
            return self._saver

    def _read_calculators(self):
        # if 'calculators' in self.configstore:
        #     for calculator in self.configstore['calculators']:
        #         # self._transports.append(self.create_object(factories.transports, transport, source=self))
        #         pass
        self.calculators = self.default.getDefaultCalculators()

    def _make_place(self):
        place = MongoPlace(self, None, None)
        return place

    def run_calculators(self, price):
        for calculator in self.calculators:
            price = calculator.run_calc(self, price)
        return price

    def process(self, transport, place, test=False, skip_cache=False):
        """
        Запускает процесс импорта данных из источника с использованием
        транспорта transport.
        """
        checks_filed = []
        transport.run_transfer(place)
        try:
            # Парсем данные источника
            for price in self.parser.run_parse(place, skip_cache=skip_cache):
                # Расчитываем поля. При это объект идет как по конвейеру.
                price = self.run_calculators(price)
                # Сохраняем данные котировки
                if self.saver.run_save(price, test) is None:
                    checks_filed.append(price)
            if not test:
                self.saver.run_flush()
        finally:
            # Очищаем ресурсы, занятые транспортом
            transport.clean()
        return checks_filed

    def transfer(self, test=False, skip_cache=False):
        res = []
        for transport in self.transports:
            place = self._make_place()
            success = False
            try:
                res = self.process(transport, place, test, skip_cache)
                # если из process вышли с exception'ом, то и в clean не будем апдейтить хеш
                success = True
            finally:
                if not test:
                    place.clean(success)
        return res

    def clean(self):
        pass

    def getImportQuotesNames(self):
        """
        Возвращает список имен котировок, которые забираются из этого
        источника.
        """
        return self.parser.getActiveQuotesNames()

    def set_disable_checks_list(self, disable_checks_list):
        """
        Указываем, для каких котировок отключаются проверки (этот метод
        вызывается из L{S3RunOpts._get_sources}).
        """
        self.saver.set_disable_checks_list(disable_checks_list)


factories.sources.register("stocks3.sources.Standart", Source)
factories.sources.register("stocks3.sources.JsonSource", JsonSource)


class SourceLoader:
    def __init__(self, sources_list=None, run_inactive=False):
        self._sources = []
        self.sources_directory = "catalog/sources"
        self.sources_list = sources_list
        self.run_inactive = run_inactive

    @property
    def sources(self):
        """
        Ленивое вычисление списка источников.
        """
        if len(self._sources) == 0:
            self._sources = self.find_sources()
        return self._sources

    @staticmethod
    def get_source_class_from_config(root):
        """
        Возвращает имя класса источника. Если класс указан в ветке source, то
        возвращается он, иначе возвращается класс по умолчанию
        """
        global DEF_SOURCE_CLASS
        return root.attrib.get("class", DEF_SOURCE_CLASS)

    @staticmethod
    def make_source_with_xml_file(xml_file_path):
        tree = parse_xml(xml_file_path)
        root = tree.getroot()
        source_class = SourceLoader.get_source_class_from_config(root)
        source = factories.sources.create(source_class, tree, root)
        return source

    @staticmethod
    def make_source_with_json_file(json_file_path):
        return JsonSource(json_file_path)

    @staticmethod
    def find_config_files(root):
        """
        Возвращает список конфигурационных файлов, расположенных в директории
        root.
        """
        return filter(lambda x: x.endswith(".xml") or x.endswith(".json"), os.listdir(root))

    def find_sources(self):
        if self.sources_list is None or len(self.sources_list) == 0:
            # Если имена источников не указаны в параетрах - обрабатываем все
            # источники.
            sources_files = SourceLoader.find_config_files(self.sources_directory)
        else:
            # Иначе загружаем только указанные источники.
            sources_files = map(lambda x: x if x.endswith(".xml") or x.endswith(".json") else x + ".xml", self.sources_list)
        sources_files = map(lambda x: os.path.join(self.sources_directory, x), sources_files)

        sources = []
        for source_file in sources_files:
            if source_file.endswith(".xml"):
                source = SourceLoader.make_source_with_xml_file(source_file)
            else:
                source = SourceLoader.make_source_with_json_file(source_file)
            sources.append((source_file, source))

        return sources

    def get_active_sources(self):
        """
        Возвращает только активные источники.
        почему внизу [1] - потому что self.sources - tuple === (SourceName, source)
        """
        self.find_sources()
        for source in self.sources:
            if source[1].active or self.run_inactive:
                yield source
