# various methods which generate panelist and lab history on demand
from math import log10, sqrt, fabs
from statistics import mean, stdev
from datetime import datetime

from django import forms
from django.forms import widgets
from django.utils.translation import ugettext_lazy as _

from measurements.models import SingleRoundResult, Measurement
from samples.models import Sample


# centralize the checks against those values so we don't
# repeat them over and over across the code
def qualified_check_mean(val):
    return 0.02 <= val <= 0.08


def qualified_check_stdev(val):
    return val < 2.3


# calculate the standard deviation and mean for a reference value window
# this is intended to be use to check for the current qualified status
def get_sigma_mean(query_set, mincnt=10):
    if len(query_set) < mincnt:
        return None
    else:
        # the ite is assumed to already take any dilution into account
        logvals = [log10(entry['round__measurement__sample__cylinder__concentration'] / entry['ite'])
                   for entry in query_set]
        y_ite_avg = mean(logvals)
        sigma = stdev(logvals, y_ite_avg)
        return {'sigma': sigma, 'mean': y_ite_avg}


# check qualification status for a particular panelist at a given date/time
def panelist_qualification_values(pid, substance='B', when=datetime.now()):
    rm2 = SingleRoundResult.objects.filter(round__measurement__sample__cylinder__isnull=False,  # is a refgas measuremnt
                                           panelist__pk=pid, round__is_included=True,
                                           # panelist took part, not excluded round
                                           # round__finished_at__lte=when,  # before/up to the query date
                                           ite__isnull=False, valid=True,  # ite present and not excluded
                                           round__measurement__sample__cylinder__substance=substance) \
        .order_by('-round__finished_at').prefetch_related('round__measurement__sample'). \
        values('ite', 'round__measurement__sample__cylinder__concentration', 'round__finished_at',
               'round__measurement__sample__pk')

    values_slice = rm2[:20]  # take the first 20 values

    # adjust for prediluted samples
    for v in values_slice:
        s = Sample.objects.get(id=v['round__measurement__sample__pk'])
        v['ite'] = v['ite'] * s.total_dilution

    sigmean = get_sigma_mean(values_slice)
    # if there is something to return, pull in the date/time of the last measurement that guy/girl took part in
    if sigmean is not None:
        sigmean.update({'timestamp': values_slice[0]['round__finished_at']})
        return sigmean
    else:
        return None


# prepare a dictionary with all relevant data for a particular
# panelist history, this gets used from the actual views, while for the current qualified
# status we need much less stuff
def full_history(panelist_pk, substance_key='B', time_key=None):
    history_data = list()

    from measurements.models import SingleRoundResult
    from datetime import datetime, timedelta

    single_results = SingleRoundResult.objects.filter(round__measurement__sample__cylinder__isnull=False,
                                                      # is a refgas measuremnt
                                                      panelist__pk=panelist_pk, round__is_included=True,
                                                      # panelist took part, not excluded round
                                                      round__finished_at__lte=datetime.now(),
                                                      # before/up to the query date
                                                      ite__isnull=False, valid=True,
                                                      round__measurement__sample__cylinder__substance=substance_key) \
        .order_by('-round__finished_at').select_related('round__measurement__sample', 'round', 'round__measurement') \
        .values('round__finished_at', 'round__measurement__sample__cylinder__pk',
                'round__measurement__sample__cylinder__concentration',
                'round__pk', 'ite', 'round__measurement__sample__pk')

    # adjust for prediluted samples
    for sr in single_results:
        sdil = Sample.objects.get(id=sr['round__measurement__sample__pk']).total_dilution
        sr['ite'] = sr['ite'] * sdil

    # start at the top of the list...
    while single_results:
        result = single_results[0]
        history_data.append(dict())

        ppm = result['round__measurement__sample__cylinder__concentration'] / result['ite']
        # grab up to 20 values from the remaining list to get sigma/mean, fill up the rest

        # fill the stuff which does not depend on the other values in
        # this calculation window
        history_data[-1]['date'] = result['round__finished_at'].date()

        history_data[-1]['ite'] = result['ite']

        # link to cylinder
        history_data[-1]['cylinder_pk'] = result['round__measurement__sample__cylinder__pk']
        # show ppm concentration of cylinder
        history_data[-1]['concentration'] = result['round__measurement__sample__cylinder__concentration']
        # allow linking to the round which produced this ite
        history_data[-1]['round_pk'] = result['round__pk']
        history_data[-1]['ppm'] = ppm

        # for the current result, calculate stdev and mean of at most 20 values (this includes the current one too)
        window = single_results[:20]
        sigmean = get_sigma_mean(window)

        # now pop the one we've just processed
        single_results = single_results[1:]

        # implies that y_ite is also not None
        if sigmean is not None:
            history_data[-1]['y_ite'] = sigmean['mean']
            sens = history_data[-1]['y_ite_10'] = 10 ** sigmean['mean']
            history_data[-1]['y_ite_ok'] = qualified_check_mean(sens)

            history_data[-1]['site'] = sigmean['sigma']
            dev = history_data[-1]['site_10'] = 10 ** sigmean['sigma']
            history_data[-1]['site_ok'] = qualified_check_stdev(dev)

    if time_key == 'r':  # most recent, e.g. those who are responsible for determining the qualified status
        return history_data[:20]
    elif time_key == 'y':
        td = timedelta(weeks=52)
        filter_date = (datetime.now() - td).date()
        return filter(lambda x: x['date'] >= filter_date, history_data)
    else:
        return history_data


# update the qualification values of all panelist to reflect the most current state
def update_panel_qualification():
    t0 = datetime.now()
    from panelists.models import Panelist
    from django.db import transaction
    panel = list(Panelist.objects.all())
    with transaction.atomic():
        for p in panel:
            qualvals = panelist_qualification_values(p.pk)
            if qualvals is not None:
                p.qualified = qualified_check_mean(10 ** qualvals['mean']) and qualified_check_stdev(
                    10 ** qualvals['sigma'])
            else:
                p.qualified = False
            p.save()
    print("updating panel qualification values took {0} msecs".format((datetime.now() - t0).microseconds / 1000.0))

# lab stuff

LABHIST_STUDENT_20 = 2.093
LABHIST_AW_20 = sqrt(1.0 / 40.0)
LABHIST_MU = -1.3979  # accepted reference value (roughly log10(40 ppb))
SQR2 = sqrt(2.0)


# like with the panelist history, this central location assesses lab performance
# values. it expects a dictionary with r and A keys and inserts A_ok and r_ok boolean
# key/value pairs
def check_lab_values(lab_perf_dict):
    try:
        r = lab_perf_dict['r']
        A = lab_perf_dict['A']
        lab_perf_dict['A_ok'] = A <= 0.217
        lab_perf_dict['r_ok'] = r <= 0.477
        return lab_perf_dict
    except KeyError:
        return lab_perf_dict  # NOP


# calc sigma/mean/dw/A etc for lab qualification values given a 20+ value slice
def get_sigma_mean_rda_lab(data_slice):
    if len(data_slice) < 20:
        return None
    else:
        log_pbb = [log10(v['sample__cylinder__concentration'] / v['analysis_result'])
                   for v in data_slice]
        y_ite = mean(log_pbb)
        sigma = stdev(log_pbb, y_ite)
        rVal = LABHIST_STUDENT_20 * SQR2 * sigma
        dwVal = y_ite - LABHIST_MU
        aVal = fabs(dwVal) + (LABHIST_AW_20 * rVal)
        return {
            'A': aVal,
            'dw': dwVal,
            'r': rVal,
            'sigma': sigma,
            'mean': y_ite
        }


# stripped down version to query just for the latest value
def lab_qualification_values():
    reference_measurements = Measurement.objects.select_related('sample__cylinder', 'sample') \
        .filter(sample__cylinder__isnull=False, standard_identifier__gt=0, finished_at__lte=datetime.now(),
                compliant_result=True, sample__cylinder__substance='B').order_by('-finished_at')

    if reference_measurements is None or reference_measurements.count() < 20:
        return None
    else:
        reference_measurements = reference_measurements.values('sample__cylinder__concentration', 'sample__pk',
                                                               'analysis_result')[:20]

        for entry in reference_measurements:
            sdil = Sample.objects.get(id=entry['sample__pk']).total_dilution
            entry['analysis_result'] = entry['analysis_result'] * sdil

        lab_perf = get_sigma_mean_rda_lab(reference_measurements)

        return check_lab_values(lab_perf)


# similar to the panel history, this enriches the values for the history
# with stuff like date/time, links to measurements and stuf like that
def full_lab_history(time_span_key=None):
    from datetime import datetime, timedelta

    history_data = list()

    lab_query = Measurement.objects.select_related('sample__cylinder', 'sample') \
        .filter(sample__cylinder__isnull=False, standard_identifier__gt=0, finished_at__lte=datetime.now(),
                compliant_result=True, sample__cylinder__substance='B'). \
        order_by('-finished_at') \
        .values('analysis_result', 'sample__cylinder__concentration', 'sample__cylinder__pk',
                'finished_at', 'pk', 'sample__pk')

    # adjust for diluted samples
    for entry in lab_query:
        sdil = Sample.objects.get(id=entry['sample__pk']).total_dilution
        entry['analysis_result'] = entry['analysis_result'] * sdil

    # start at the top of the list...
    while lab_query:
        result = lab_query[0]

        history_data.append(dict())
        # fill the stuff which does not depend on the other values in
        # this calculation window
        history_data[-1]['date'] = result['finished_at'].date()
        history_data[-1]['ite'] = result['analysis_result']  # team ite of this measurement
        # provide link to measurement
        history_data[-1]['measurement'] = result['pk']

        # link to cylinder
        history_data[-1]['cylinder_pk'] = result['sample__cylinder__pk']
        # show ppm concentration of cylinder
        history_data[-1]['concentration'] = result['sample__cylinder__concentration']
        history_data[-1]['ppm'] = result['sample__cylinder__concentration'] / result['analysis_result']

        sigmean = get_sigma_mean_rda_lab(lab_query[:20])
        if sigmean is not None:
            history_data[-1]['y_ite'] = sigmean['mean']
            history_data[-1]['site'] = sigmean['sigma']
            history_data[-1]['r'] = sigmean['r']
            history_data[-1]['dw'] = sigmean['dw']
            history_data[-1]['A'] = sigmean['A']
            check_lab_values(history_data[-1])

        lab_query = lab_query[1:]

    if time_span_key == 'r':
        history_data = history_data[:20]
    elif time_span_key == 'y':  # last year
        td = timedelta(weeks=52)
        filter_date = (datetime.now() - td).date()
        history_data = filter(lambda x: x['date'] >= filter_date, history_data)

    return history_data


from referencegases.models import REF_GAS_TYPES


class PanelistHistoryForm(forms.Form):
    substance = forms.ChoiceField(choices=REF_GAS_TYPES,
                                  widget=widgets.Select(attrs={'class': 'select2',
                                                               'style': 'width: 200px;',
                                                               'onchange': 'this.form.submit()'}),
                                  label=_('substance'))
