from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, ValidationError

from core.models import WinoseBaseWithTrackingModel
from clients.models import ClientOffice
from bags.models import Bag
from referencegases.models import ReferenceGas
from projects.models import Project


# somewhat shitty sample class which tries to
# be the solution that fits all, that's why loads of
# fields are never required/allowed to be null, edit forms
# take care for validation things
class Sample(WinoseBaseWithTrackingModel):
    class Meta:
        verbose_name = _('sample')
        verbose_name_plural = _('samples')

    overview_url = 'samples_list'
    detail_url = 'sample_detail'
    create_url = 'sample_create'
    update_url = 'sample_update'

    # generic sample identifier
    name = models.CharField(
        max_length=63,
        verbose_name=_('name'),
        unique=True,
    )

    reference = models.CharField(
        max_length=63,
        verbose_name=_('reference'),
        blank=True
    )

    # general purpose description for this particular sample
    comments = models.TextField(
        max_length=1023,
        blank=True,
        verbose_name=_('comments')
    )

    # a samples is always contained in a bag, right? :)
    contained_in = models.ForeignKey(to=Bag,
                                     null=False,
                                     # don't allow selecting inactive bags
                                     limit_choices_to={'is_active': True},
                                     # one2one
                                     unique=True,
                                     related_name='sample',
                                     verbose_name=_('bag'))

    sampled_at = models.DateTimeField(
        blank=False,
        verbose_name=_('time of sampling')
    )

    # the dilution factor implicitly also encodes if the sample is diluted at all (oh really?)
    # having another bool for that around seems handy some times, but alas
    factor = models.FloatField(blank=False,
                               default=1.0,
                               validators=[MinValueValidator(limit_value=1.0)],
                               verbose_name=_('dilution'))

    # source sample this one got pre diluted from
    # enforce not null if factor is != 1.0
    source_sample = models.ForeignKey(to='self',
                                      null=True,
                                      blank=True,
                                      verbose_name=_('source sample'))

    # if client supplied, the form requires this
    supplier = models.ForeignKey(to=ClientOffice,
                                 null=True,
                                 blank=True,
                                 verbose_name=_('supplier'))

    # for reference samples
    cylinder = models.ForeignKey(to=ReferenceGas,
                                 null=True,
                                 blank=True,
                                 related_name='samples',
                                 verbose_name=_('cylinder'))

    # 'odour' sample, optional for reference samples
    project = models.ForeignKey(to=Project,
                                null=True,
                                blank=True,
                                related_name='samples',
                                verbose_name=_('project'))

    @staticmethod
    def has_measurement(pk):
        from measurements.models import Measurement

        return Measurement.objects.filter(sample__pk=pk).count() > 0

    # determine total dilution factor for this sample
    @property
    def total_dilution(self):
        # recursion (n): See recursion :p
        if self.source_sample:
            return self.factor * self.source_sample.total_dilution
        else:
            return self.factor

    @property
    def concentration(self):
        # return concentration for reference sample, so ctrl sw may calculate the ppb value
        # hence this is assumed to be a ppm value
        if self.cylinder:
            return self.cylinder.concentration
        else:
            return None

    def full_clean(self, exclude=None, validate_unique=True):
        super(Sample, self).full_clean(exclude, validate_unique)

        # how should we know?
        if self.supplier is not None and (self.cylinder is not None or self.source_sample is not None):
            raise ValidationError(
                {'supplier': _('client supplied samples may not specify a source sample/cylinder.')})

        if self.cylinder is None and self.project is None:
            raise ValidationError({'project': _('project is only optional for reference samples.')})

        is_predil = self.factor > 1

        # source sample is set, but no factor defined
        if not is_predil and self.source_sample is not None:
            raise ValidationError({'source_sample': _('specified source sample, but no dilution factor.')})

        if self.source_sample and self.source_sample.dilutes_from(self):
            raise ValidationError({'source_sample': _('circular source samples detected.')})

        # a prediluted reference sample must point to the same cylinder as it's
        if self.source_sample and self.source_sample.cylinder is not None:  # ss is a reference
            if self.cylinder != self.source_sample.cylinder:
                raise ValidationError(
                    {'cylinder': _('diluted reference sample must specify the same cylinder as the source.')})

    @property
    def predilution_count_lab(self):
        dil_log = self.get_dilution_back_log([])
        # we're just interested in dilution steps done in the
        # lab, so remove client or source predilution
        # luckily, those steps can only occur once and at
        # the very start
        dil_log = list(reversed(dil_log))
        if dil_log:
            if dil_log[0]['origin'] == 'Source' or dil_log[0]['origin'] == 'Client':
                return len(dil_log) - 1
            else:
                return len(dil_log)
        else:
            return 0

    @property
    def predilution_count_total(self):
        return len(self.get_dilution_back_log([]))

    # lookup the complete chain of dilutions for a sample until we either hit
    # a source diluted sample (no source sample specified, or client provided)
    # or we encounter an error (circular dilutions)... the only important bit
    # here is to detect circular things, which could get us into trouble
    def get_dilution_back_log(self, log_so_far):

        # utility
        def append_sample(log, origin_name, sample):
            log.append({'origin': origin_name, 'factor': sample.factor, 'pk': sample.pk})
            return log

        # nothing to do
        if self.factor == 1:
            return log_so_far
        else:  # diluted from somewhere...
            assert self.factor > 1
            # either client supplied (no source sample possible)
            if self.supplier is not None:
                assert self.source_sample is None
                log_so_far = append_sample(log_so_far, 'Client', self)
                return log_so_far
            # no source sample (and not client supplied) -> sampled at source
            elif self.source_sample is None:
                log_so_far = append_sample(log_so_far, 'Source', self)
                return log_so_far
            else:
                assert self.source_sample is not None
                log_so_far = append_sample(log_so_far, str(self.source_sample), self)
                return self.source_sample.get_dilution_back_log(log_so_far)  # recursion here

    # same as the backlog, but the other way, which other samples
    # are diluted from this
    # we take the lazy route here and show only the next direct steps, else we would/could
    # end up with a tree...
    def get_dilution_forward_log(self):
        prediluted_to = Sample.objects.filter(source_sample=self).values('name', 'pk')
        return prediluted_to

    def toggle_is_active(self, state=None):
        super(Sample, self).toggle_is_active(state)
        # the bag inherits the activation toggle
        # note that the super call does eventually call save on the sample
        # object, while we're not calling save again after altering the bag, since
        # we operated only and directly on the bag object
        self.contained_in.is_active = self.is_active
        self.contained_in.save()  # circumvent call
        for m in self.measurements.all():
            m.toggle_is_active(self.is_active)

    def __str__(self):
        return self.name

    # check if this sample (as a potential source sample) is at some point diluted from
    # the given argument sample, which would form a circular reference, which is bad.
    def dilutes_from(self, sample):

        ss = self.source_sample
        while ss is not None:
            if ss.pk == sample.pk:  # there it is, naughty user!
                return True
            else:
                ss = ss.source_sample  # shift further

        # found Null source sample without finding the queried sample
        return False
