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

"""
Специальный калькулятор, который рассчитывает что-то для связанных значений
(например, дельты).
"""
from __future__ import print_function
from stocks3.core.calculator import Calculator
from stocks3.core import factories
from stocks3.core.stock import Quote
from stocks3.core.exception import S3Exception, throw_only
from stocks3.export.exporter import Exporter
from stocks3.core.stock import safeRelativePriceDelta
from stocks3.core.stock import Price


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


class S3QCalculatorError(S3Exception):
    STAGE = "qcalc"


def sign(x):
    if x == 0:
        return 0
    elif x < 0:
        return -1
    else:
        return 1


class QCalculator(Calculator):
    """
    Если нам надо рассчитать что-то для связанных значений, то используем этот
    калькулятор. В отличии от обычного калькулятора он принимает не данные
    котировки (L{Price}), а котировку (L{Quote}).
    """
    HISTORY_LENGTH = 10  # обычно экспортируется 10 точек.
    QUANTUM_JUMP_VALUE = 100.0  # Значение кватового скачка - L{safeRelativePriceDelta}

    def __init__(self, tree, node, exporter, output_category_list):
        """
        @param exporter: Экспортер.
        @param output_category_list: Тип выводимого содержимого (csv, item, ...).
        """
        assert isinstance(exporter, Exporter), "Expected QCalculator (in QCalculator constructor)."
        self.exporter = exporter
        self.reader = self.exporter.reader
        self.output_category_list = output_category_list # Этот список влияет на вывод!
        # FIXME: доступ к default из двух мест возможен, хотя используется только exporter.default
        Calculator.__init__(self, tree, node, exporter.default)

    def makeConfig(self):
        """
        Конфигурация калькуляторо описывает список calculators (с
        калькуляторами для конкретных котировок) и templaters (с шаблонизаторами).

        Параметр quantum-jump-value содержит значение квантового скачка (см.
        L{safeRelativePriceDelta) и по умолчанию - 100.

        Параметр additional-precisions содержит список точностей через запятую, для которых будут
        вычислены значения котировок.

        Параметр history_length - число экспортируемых точек. Может содержать all.
        """
        Calculator.makeConfig(self)
        self.calculators = self.createObjects(factories.calculators, "calculators/calculator", self.exporter.default)
        self.templaters = self.createObjects(factories.templaters, "templaters/templater", self.exporter, self)
        self.quantum_jump_value = self.readFloat("", "quantum-jump-value", self.QUANTUM_JUMP_VALUE)
        precisions = self.readString("", "additional-precisions", "").split(",")
        self.precisions = [self.exporter.precision]
        for precision in filter(lambda x: x != "", precisions):
            self.precisions.append(int(precision))
        self.history_length = self.readString("", "history-length", "") or None
        if self.history_length is None:
            self.history_length = self.HISTORY_LENGTH
        else:
            if self.history_length != "all":
                self.history_length = int(self.history_length)
            else:
                self.history_length = None
        self.desc = self.readBool("", "desc", True)

    def calc_deltas(self, previous, current):
        """
        Вычисляет дельты. Если предыдущего значения нет - выдаются нули.
        Эта функция работает только с одиночными котировками (двойная котировка
        разбивается на buy и sell и каждая часть вычисляется отдельно).

        Котировки previous и current должны быть одной точности.
        """
        assert isinstance(current, Price), "for calc_deltas expected Price."
        assert isinstance(previous, Price), "for calc_deltas expected Price."
        assert (previous.pc == current.pc and current.pc is not None ), "calc_deltas expected refined objects."
        absolute = current.absoluteDelta(previous)
        relative = safeRelativePriceDelta(current, previous, self.quantum_jump_value)
        return absolute, relative

    def calc_deltas_and_save(self, previous, current):
        """
        Вычисляет дельты для котировки current. Вычисленные дельты прицепляются
        к котировке в поля absolute и relative (L{QCalculator.save_deltas).
        """
        absolute, relative = self.calc_deltas(previous, current)
        self.save_deltas(current.buy, absolute, relative)
        return absolute, relative

    def save_deltas(self, price, absolute, relative):
        """
        Прицепляет дельты в объекту price.
        """
        self.save_value(price, "absolute", price.pc.format_(absolute))
        self.save_value(price, "relative", price.pc.format_(relative))
        self.save_value(price, "relative_frmt", "%.2f" % float(relative))

    def calc_deltas_with_precision(self, price, precision):
        """
        Принимает на вход значение котировки price и вычисляет для неё дельты с
        заданной точностью (precision).

        У объекта price должно быть поле previous, содержащее предыдущее
        значение этой котировки. previous может содержать None и тогда дельты
        считаются нулевыми.

        @param price: Исходные данные котировки.
        @param precision: Точность вычислений.
        @returns: Значение котировки с заданной точностью и рассчитанными
                  дельтами.
        """
        assert type(precision) == int, "Expected float precision for calc_deltas_with_precision."
        assert hasattr(price, "previous"), "Attribute error: price.previous"
        price_with_precision = price.toPrecision(precision)
        previous = price.previous

        if previous is None:
            # Нет предыдущего значения - устанавливаем нулевые дельты
            self.save_deltas(price_with_precision.buy, 0.0, 0.0)
            if price_with_precision.is_dual_price:
                self.save_deltas(price_with_precision.sell, 0.0, 0.0)
        else:
            previous_with_precision = previous.toPrecision(precision)
            self.calc_deltas_and_save(previous_with_precision.buy,
                                      price_with_precision.buy)
            if price_with_precision.is_dual_price:
                # Однако может случиться так, что предыдущее значение не
                # содержит sell-значения.
                if previous_with_precision.is_dual_price:
                    self.calc_deltas_and_save(previous_with_precision.sell,
                                              price_with_precision.sell)
                else:
                    self.save_deltas(price_with_precision.sell, 0.0, 0.0)

        # Форматируем значение
        self.save_value(price_with_precision.buy, "formatted_value",
                        price_with_precision.pc.format_(price_with_precision.buy.value))
        if price_with_precision.is_dual_price:
            self.save_value(price_with_precision.sell, "formatted_value",
                            price_with_precision.pc.format_(price_with_precision.sell.value))

        return price_with_precision

    def _normalize_if_need(self, price):
        assert hasattr(price, "previous"), "Attribute error: price.previous"
        if self.exporter.is_need_normalization(price):
            price.normalize()
            # Нормализируем также предыдущее значение.
            if price.previous:
                # Кстати, значение нельзя нормализовать два раза :)
                price.previous.normalize()
        return price

    def scale_price(self, price):
        scale = self.exporter.get_scale_factor(price)
        return price * scale

    def calc_is_hot(self, quote, price, value, precision, is_first_value):
        """
        Вычисляет значение флага is_hot. Это флаг устанавливается в 1, что
        говорит верстке о том, что значение, помеченное этим флагом, нужно
        отрисовать каким-то цветом.
        """
        # В какую сторону изменяется значение.
        if precision == self.exporter.hot_precision:
            # FIXME: ненужное преобразование из строки во float.
            relative = value.buy.relative if value.is_dual_price else value.relative
            absolute = value.buy.absolute if value.is_dual_price else value.absolute
            self.save_value(price, "delta_flag", sign(float(relative)))
            klimit = quote.klimit
            if klimit is not None and is_first_value:
                if quote.klimit_percent:
                    is_hot = 1 if abs(float(relative)) > klimit else 0
                else:
                    is_hot = 1 if abs(float(absolute)) > klimit else 0
                self.save_value(price, "is_hot", str(is_hot))
            else:
                self.save_value(price, "is_hot", "0")

    def construct_price(self, quote, orig_price, precisions, is_first_value=False):
        """
        Конструирует котировку с заданной точностью.

        @param quote: Котировка
        @param orig_price: Значение котировки со связанным с ней значением previous.
        @param precisions: Список целых чисел. Котировка orig_price будет
                           пересчитана с заданными значениями точности.
        @returns: Пересчитанная котировка с дельтами и с дополнительными полями
                  precision_X, где X - точность вычислений.
        """
        assert len(precisions) != 0
        first = True

        # Выполняем нормализацию значения, если это необходимо.
        self._normalize_if_need(orig_price)

        for precision in precisions:
            new_price = self.calc_deltas_with_precision(orig_price,
                                                        precision)
            if first:
                price = new_price
                first = False
            else:
                self.save_value(price, "precision_%s" % precision, new_price)
            # Вычисляем флаг is_hot
            self.calc_is_hot(quote, price, new_price, precision, is_first_value)

        # Запускаем калькуляторы
        price = self.run_calculators(quote.source, price)
        return price

    def get_history_length_plus_one(self):
        if self.history_length is None:
            return None
        else:
            return self.history_length + 1

    def read(self, quote):
        return self.reader.read(self.get_history_length_plus_one(), quote, self.desc)

    def calc(self, quote, price=None):
        assert isinstance(quote, Quote), "Expected Quote, but received %s" % quote.__class__

    def is_active_templater(self, templater):
        return len(self.output_category_list) == 0 or templater.flagName in self.output_category_list

    def flush(self):
        for templater in self.templaters:
            # Не записываем данные, если шаблонизатор неактивен
            if self.is_active_templater(templater):
                templater.run_flush()

    def run_calculators(self, source, price):
        """
        Запускает цепочку калькуляторов.
        """
        for calc in self.calculators:
            price = calc.run_calc(source, price)
        return price

    def run_templaters(self, quote, prices):
        """
        Передает значения котировок prices шаблонизаторам на обработку.
        """
        for templater in self.templaters:
            # Передаем управление только достойным шаблонизаторам
            if self.is_active_templater(templater):
                templater.run_push(quote, prices)

    @throw_only(S3QCalculatorError)
    def run_calc(self, quote, price=None):
        # Если некому передавать данные - вообще не запускаемся
        if len(tuple(filter(self.is_active_templater, self.templaters))) == 0:
            return None
        return self.calc(quote)

    @throw_only(S3QCalculatorError)
    def run_flush(self):
        return self.flush()

