""" The `Best Model Order Equality` chooses the best model (i.e. the one with the highest
`coefficient of determination`) as the representant of the performance of each group of uniquely
identified resources (e.g. corresponding to the same function). Then each pair of baseline and
target models is compared lexicographically (e.g. the `linear` model is lexicographically smaller
than `quadratic` model), and any change in this ordering is detected as either ``Optimization`` or
``Degradation`` if the minimal confidence of the models is above certain threshold.

  - **Detects**: `Order` changes; ``Optimization`` and ``Degradation``
  - **Confidence**: Minimal `coefficient of determination` of best models of baseline and target
    minor versions
  - **Limitations**: Profiles postprocessed by :ref:`postprocessors-regression-analysis`

The example of the output generated by the `BMOE` method is as follows ::

    * 1eb3d6: Fix the degradation of search
    |\
    | * 7813e3: Implement new version of search
    |   > collected by complexity+regression_analysis for cmd: '$ mybin'
    |     > applying 'best_model_order_equality' method
    |       - Optimization         at SLList_search(SLList*, int)
    |           from: power -> to: linear (with confidence r_square = 0.99)
    |
    * 7813e3: Implement new version of search
    |\
    | * 503885: Fix minor issues
    |   > collected by complexity+regression_analysis for cmd: '$ mybin'
    |     > applying 'best_model_order_equality' method
    |       - Degradation          at SLList_search(SLList*, int)
    |           from: linear -> to: power (with confidence r_square = 0.99)
    |
    * 503885: Fix minor issues

In the output above, we detected the ``Optimization`` between commits ``1eb3d6`` (target) and
``7813e3`` (baseline), where the best performance model of running time of ``SLList_search``
function changed from **power** model to **linear**. For the methods based on
:ref:`postprocessors-regression-analysis` we use the `coefficient of determination` (:math:`r^2`)
to represent a confidence, and take the minimal `coefficient of determination` of target and
baseline model as a confidence for this detected change. Since :math:`r^2` is almost close to the
value `1.0` (which would mean, that the model precisely fits the measured values), this signifies
that the best model fit the data tightly and hence the detected optimization is **not spurious**.
"""

import perun.check.factory as check
import perun.check.general_detection as detection

from perun.utils.structs import DegradationInfo

__author__ = 'Tomas Fiedor'

CONFIDENCE_THRESHOLD = 0.9
MODEL_ORDERING = [
    'constant',
    'logarithmic',
    'linear',
    'quadratic',
    'power',
    'exponential'
]


def best_model_order_equality(baseline_profile, target_profile, **_):
    """Checks between pair of (baseline, target) profiles, whether the can be degradation detected

    This is based on simple heuristic, where for the same function models, we only check the order
    of the best fit models. If these differ, we detect the possible degradation.

    :param dict baseline_profile: baseline against which we are checking the degradation
    :param dict target_profile: profile corresponding to the checked minor version
    :param dict _: unification with other detection methods (unused in this method)
    :returns: tuple (degradation result, degradation location, degradation rate)
    """
    best_baseline_models = detection.get_filtered_best_models_of(baseline_profile, group='param')
    best_target_models = detection.get_filtered_best_models_of(target_profile, group='param')

    for uid, best_model in best_target_models.items():
        best_baseline_model = best_baseline_models.get(uid)
        if best_baseline_model:
            confidence = min(best_baseline_model.r_square, best_model.r_square)
            if confidence >= CONFIDENCE_THRESHOLD \
               and best_baseline_model.type != best_model.type:
                baseline_ordering = MODEL_ORDERING.index(best_baseline_model.type)
                target_ordering = MODEL_ORDERING.index(best_model.type)
                if baseline_ordering > target_ordering:
                    change = check.PerformanceChange.Optimization
                else:
                    change = check.PerformanceChange.Degradation
                degradation_rate = target_ordering - baseline_ordering
            else:
                change = check.PerformanceChange.NoChange
                degradation_rate = 0

            yield DegradationInfo(
                res=change,
                t="model",
                loc=uid,
                fb=best_baseline_model.type,
                tt=best_model.type,
                rd=degradation_rate,
                ct="r_square", cr=confidence
            )
