# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import math

import six

"""
data filters:
takes a series of run data and applies statistical transforms to it

Each filter is a simple function, but it also have attached a special
`prepare` method that create a tuple with one instance of a
:class:`Filter`; this allow to write stuff like::

  from talos import filter
  filters = filter.ignore_first.prepare(1) + filter.median.prepare()

  for filter in filters:
      data = filter(data)
  # data is filtered
"""

_FILTERS = {}


class Filter(object):
    def __init__(self, func, *args, **kwargs):
        """
        Takes a filter function, and save args and kwargs that
        should be used when the filter is used.
        """
        self.func = func
        self.args = args
        self.kwargs = kwargs

    def apply(self, data):
        """
        Apply the filter on the data, and return the new data
        """
        return self.func(data, *self.args, **self.kwargs)


def define_filter(func):
    """
    decorator to attach the prepare method.
    """

    def prepare(*args, **kwargs):
        return (Filter(func, *args, **kwargs),)

    func.prepare = prepare
    return func


def register_filter(func):
    """
    all filters defined in this module
    should be registered
    """
    global _FILTERS

    _FILTERS[func.__name__] = func
    return func


def filters(*args):
    global _FILTERS

    filters_ = [_FILTERS[filter] for filter in args]
    return filters_


def apply(data, filters):
    for filter in filters:
        data = filter(data)

    return data


def parse(string_):
    def to_number(string_number):
        try:
            return int(string_number)
        except ValueError:
            return float(string_number)

    tokens = string_.split(":")

    func = tokens[0]
    digits = []
    if len(tokens) > 1:
        digits.extend(tokens[1].split(","))
        digits = [to_number(digit) for digit in digits]

    return [func, digits]


# filters that return a scalar


@register_filter
@define_filter
def mean(series):
    """
    mean of data; needs at least one data point
    """
    return sum(series) / float(len(series))


@register_filter
@define_filter
def median(series):
    """
    median of data; needs at least one data point
    """
    series = sorted(series)
    if len(series) % 2:
        # odd
        return series[int(len(series) / 2)]
    else:
        # even
        middle = int(len(series) / 2)  # the higher of the middle 2, actually
        return 0.5 * (series[middle - 1] + series[middle])


@register_filter
@define_filter
def variance(series):
    """
    variance: http://en.wikipedia.org/wiki/Variance
    """

    _mean = mean(series)
    variance = sum([(i - _mean) ** 2 for i in series]) / float(len(series))
    return variance


@register_filter
@define_filter
def stddev(series):
    """
    standard deviation: http://en.wikipedia.org/wiki/Standard_deviation
    """
    return variance(series) ** 0.5


@register_filter
@define_filter
def dromaeo(series):
    """
    dromaeo: https://wiki.mozilla.org/Dromaeo, pull the internal calculation
    out
      * This is for 'runs/s' based tests, not 'ms' tests.
      * chunksize: defined in dromaeo: tests/dromaeo/webrunner.js#l8
    """
    means = []
    chunksize = 5
    series = list(dromaeo_chunks(series, chunksize))
    for i in series:
        means.append(mean(i))
    return geometric_mean(means)


@register_filter
@define_filter
def dromaeo_chunks(series, size):
    for i in six.moves.range(0, len(series), size):
        yield series[i : i + size]


@register_filter
@define_filter
def geometric_mean(series):
    """
    geometric_mean: http://en.wikipedia.org/wiki/Geometric_mean
    """
    total = 0
    for i in series:
        total += math.log(i + 1)
    # pylint --py3k W1619
    return math.exp(total / len(series)) - 1


# filters that return a list


@register_filter
@define_filter
def ignore_first(series, number=1):
    """
    ignore first datapoint
    """
    if len(series) <= number:
        # don't modify short series
        return series
    return series[number:]


@register_filter
@define_filter
def ignore(series, function):
    """
    ignore the first value of a list given by function
    """
    if len(series) <= 1:
        # don't modify short series
        return series
    series = series[:]  # do not mutate the original series
    value = function(series)
    series.remove(value)
    return series


@register_filter
@define_filter
def ignore_max(series):
    """
    ignore maximum data point
    """
    return ignore(series, max)


@register_filter
@define_filter
def ignore_min(series):
    """
    ignore minimum data point
    """
    return ignore(series, min)


@register_filter
@define_filter
def v8_subtest(series, name):
    """
    v8 benchmark score - modified for no sub benchmarks.
    * removed Crypto and kept Encrypt/Decrypt standalone
    * removed EarlyBoyer and kept Earley/Boyer standalone

    this is not 100% in parity but within .3%
    """
    reference = {
        "Encrypt": 266181.0,
        "Decrypt": 266181.0,
        "DeltaBlue": 66118.0,
        "Earley": 666463.0,
        "Boyer": 666463.0,
        "NavierStokes": 1484000.0,
        "RayTrace": 739989.0,
        "RegExp": 910985.0,
        "Richards": 35302.0,
        "Splay": 81491.0,
    }

    # pylint --py3k W1619
    return reference[name] / geometric_mean(series)


@register_filter
@define_filter
def responsiveness_Metric(val_list):
    return sum([float(x) * float(x) / 1000000.0 for x in val_list])
