import logging

from django.db import models, transaction
from django.utils.translation import ugettext_lazy as _

from core.models import WinoseBaseModel
from users.models import WinoseUser
from panelists.models import Panelist
from samples.models import Sample
from devices.models import Olfactometer, Calibration

winose_log = logging.getLogger('wnsq')

YES_NO_METHOD = 1
FORCED_CHOICE_METHOD = 2
INTENSITY_METHOD = 3
HEDONICS_METHOD = 4
TFC_METHOD = 5
DFCHED_METHOD = 6
DFCINT_METHOD = 7

METHOD_KEYS = [(YES_NO_METHOD, _('Yes/No'),),
               (FORCED_CHOICE_METHOD, _('Forced Choice'),),
               (INTENSITY_METHOD, _('Intensity'),),
               (HEDONICS_METHOD, _('Hedonic tone'),),
               (TFC_METHOD, _('3-Forced Choice')),
               (DFCHED_METHOD, _('Forced Choice/Hedonics')),
               (DFCINT_METHOD, _('Forced Choice/Intensity'))]


# there it is again, the once size fits all measurement model...
class Measurement(WinoseBaseModel):
    class Meta:
        verbose_name = _('measurement')
        verbose_name_plural = _('measurements')

    overview_url = 'measurements_list'
    detail_url = 'measurement_detail'

    # presentation time (ms) applies to all methods
    # and is enforced to be constant by the control software across rounds
    presentation_time = models.IntegerField(verbose_name=_('presentation time'))

    # device must appear on the reports, but we can't look it up
    # via the calibration (which might change)
    olfactometer = models.ForeignKey(to=Olfactometer,
                                     null=False,
                                     related_name='+',
                                     verbose_name=_('olfactometer'))

    # same thing just reversed, the calibration can't be looked up from
    # the device
    calibration = models.ForeignKey(to=Calibration,
                                    related_name='+',
                                    null=False,
                                    verbose_name=_('calibration'))

    # just to keep track if the en-standard was applied, the
    # number itself might codify revisions of the standard later
    # zero means no standard was applied
    standard_identifier = models.IntegerField(default=0)

    temperature = models.FloatField(blank=False,
                                    null=False,
                                    verbose_name=_('temperature'))

    # note: not quite good, would allow for int/hed in threshold measurements
    used_method = models.IntegerField(choices=METHOD_KEYS,
                                      default=1,
                                      verbose_name=_('method'))

    # room or lab description
    location = models.CharField(max_length=255,
                                blank=False,
                                verbose_name=_('location'))

    # mean of all non excluded ITE including any predilution
    # if the measurement meets all EN things, the flag below is set
    # result = models.FloatField(null=True, blank=True, verbose_name=_('result'))
    # mean_ite = models.FloatField(null=True, blank=True, verbose_name=_('mean ite'))  # just the mean of all ites

    # to avoid storing a value twice we use a simple flag if the result is indeed also the z_ite_pan thingy
    compliant_result = models.BooleanField(null=False,
                                           blank=False,
                                           default=False)

    sample = models.ForeignKey(to=Sample,
                               related_name='measurements',
                               verbose_name=_('sample'))

    # keep this up2date
    finished_at = models.DateTimeField(null=True, blank=True, verbose_name=_('finished at'))

    # the raw analysis result, which is the geometric mean of all included single ites, without
    # taking any predilution into account
    analysis_result = models.FloatField(null=True, blank=True,
                                        verbose_name=_('team ite'))

    @property
    def result(self):
        if self.analysis_result:
            return self.analysis_result * self.sample.total_dilution
        else:
            return None

    # comments made on this analysis
    comments = models.TextField(blank=True,
                                verbose_name=_('comments'))

    @property
    def started_at(self):
        try:
            return self.rounds.all().order_by('started_at').values('started_at')[0]['started_at']
        except:
            winose_log.debug('failed to determine started_at property for measurement pk={0}'.format(self.pk))
            return None

    @property
    def rounds_total(self):
        return self.rounds.count()

    @property
    def rounds_included(self):
        return self.rounds.filter(is_included=True).count()

    @property
    def single_ite_list(self):
        rounds = MeasurementRound.objects.filter(measurement__pk=self.pk)
        return SingleRoundResult.objects.filter(round__in=rounds) \
            .values_list('ite', flat=True)

    # more convenience lookups...
    @property
    def valid_ite_count(self):
        # amount of non excluded/valid its in this measurement
        rounds = self.rounds.filter(is_included=True).prefetch_related("results")
        ite_count = 0
        for rnd in rounds:
            ite_count += rnd.results.filter(valid=True).count()

        return ite_count

    @property
    def unique_panel_count(self):
        rounds = self.rounds.filter(is_included=True).prefetch_related("results")
        panelists = []
        for rnd in rounds:
            # convert QueryFoo to list
            panelists.append(list(rnd.results.filter(valid=True).values_list('panelist', flat=True)))
        from functools import reduce
        # concat list of lists
        list_concat = reduce(lambda x, y: x + y, panelists, [])
        return len(set(list_concat))  # and remove duplicates

    def toggle_is_active(self, state=None):
        # deactivate rounds and srr too
        with transaction.atomic():
            if state is not None:
                self.is_active = state
            else:
                self.is_active = not self.is_active
            for mround in self.rounds.all():
                mround.toggle_is_active(self.is_active)
            self.save()

    def __str__(self):
        return '{0}'.format(self.sample)


class MeasurementRound(WinoseBaseModel):
    class Meta:
        verbose_name = _('round')
        verbose_name_plural = _('rounds')

    detail_url = 'round_detail'

    # the operator is saved on a per round basis, since it is very much possible
    # to have different operators for measurements which spread across the whole day
    operator = models.ForeignKey(to=WinoseUser)

    started_at = models.DateTimeField(verbose_name=_('start of round'))
    finished_at = models.DateTimeField(verbose_name=_('end of round'))

    # is this round discarded (and hence not contributing to the final result)
    # no real meaning for int/hed
    is_included = models.BooleanField(default=True)

    measurement = models.ForeignKey(to=Measurement,
                                    related_name='rounds')

    # store a round number to maintain order within a measurement
    # (instead of storing time again, which would be a bit redundant)
    number = models.SmallIntegerField(verbose_name=_('number'))

    # list of dilution id's which where used as the series in this round
    series = models.CharField(max_length=255,
                              verbose_name=_('dilution series'))

    def toggle_is_active(self, state=None):
        # state is assumed to be never none, as this
        # is only called from the measurement the round belongs to
        self.is_active = state
        for srr in self.results.all():
            # note, lazy circumventing the call here
            srr.is_active = state
            srr.save()
        self.save()

    def __str__(self):
        return '{0} {1}'.format(MeasurementRound._meta.verbose_name, self.number)


# the final result for a round, a panelist and his/hers individual result
# this model takes care for every possible method, that's why most
# things are optional/nullable (think of intensity)
class SingleRoundResult(WinoseBaseModel):
    # panelist that produced this
    panelist = models.ForeignKey(to=Panelist)
    # json magic encoded reply for one panelist, basically a list of tuples, (primary, secondary) replies
    replies = models.CharField(max_length=2047, blank=True)
    # in standard compliant rounds, single results may be flagged invalid, e.g. for too many blank errors
    valid = models.BooleanField(default=True)

    ite = models.FloatField(null=True, blank=True)
    delta_z = models.FloatField(null=True, blank=True)
    round = models.ForeignKey(to=MeasurementRound,
                              related_name='results')

    # save that too, might be handy for something
    blank_error_percentage = models.FloatField(null=True,
                                               blank=True,
                                               default=0.0)

    manually_excluded = models.BooleanField(null=False,
                                            blank=False,
                                            default=False)
