# -*- coding: UTF-8 -*-
from __future__ import unicode_literals

from numbers import Number

from django.utils import timezone
import datetime
import os

from django.contrib.gis.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
from django.utils.text import slugify
from django.utils.dateformat import format

def _timestamp(dt):
    return int(format(dt, 'U'))

def _temp_conv(temp, unit):
    if (not (isinstance(unit, str) or isinstance(unit, unicode))) or \
            not isinstance(temp, Number):
        return None

    if unit != _('Celsius') and unit != _('Fahrenheit'):
        raise ValueError(_('Invalid unit'))

    if unit == _('Celsius'):
        return temp * 9 / 5 + 32

    return temp

class SiteManager(models.Manager):
    """
    Manager for the site class - creates a site to be used in tests
    """
    default = 'POINT(-121.3846841 44.0612385)'

    def create_site(self, site_name, location=default,
                    description='', image=None, active=True):

        site = self.create(site_name=site_name, location=location,
                           description=description, image=image, active=active)
        return site

def validate_Site_location(location):
    if location.x > 180 or location.x < -180:
        raise ValidationError(_('Longitude is not within valid range.'))
    if location.y > 90 or location.y < -90:
        raise ValidationError(_('Latitude is not within valid range.'))

@python_2_unicode_compatible
class Site(models.Model):
    """
    The Sites model holds the information of a site, including the geographic
    location as a pair of latitudinal/longitudinal coordinates and an optional
    text description of entry.
    """

    site_name = models.CharField(max_length=250, verbose_name=_('site name'))
    description = models.TextField(blank=True,
                                   verbose_name=_('site description'))
    site_slug = models.SlugField(unique=True, max_length=50, editable=False)

    # Geo Django fields to store a point
    location = models.PointField(default='POINT(-121.3846841 44.0612385)',
                                 verbose_name=_('location'),
                                 validators=[validate_Site_location])
    image = models.ImageField(null=True, blank=True, verbose_name=_('image'),
                              upload_to='site_photos/')
    active = models.BooleanField(default=True)

    created = models.DateTimeField(default=timezone.now)
    modified = models.DateTimeField(default=timezone.now)

    objects = models.Manager()  # default manager
    test_objects = SiteManager()  # custom manager for use in writing tests

    def __str__(self):
        return self.site_name

    def to_dict(self):
        return {
            'site_name': self.site_name,
            'site_slug': self.site_slug,
            'description': self.description,
            'location': {
                'x': self.location.x,
                'y': self.location.y
            },
            'created': _timestamp(self.created),
            'modified': _timestamp(self.modified)
        }

    def save(self, **kwargs):
        'Ensure site_slug is a unique, not-null field'
        if not self.site_slug:
            self.site_slug = origSlug = slugify(self.site_name)[:50]
            # If the generated slug is not unique, append a number
            for i in xrange(1, 99):
                if not Site.objects.filter(site_slug=self.site_slug).exists():
                    break
                # Ensure the slug is never longer than it's field's maxiumum
                self.site_slug = "%s%d" % (origSlug[:50-len(str(i))], i)
        super(Site, self).save()

class SchoolManager(models.Manager):
    """
    Manager for the school class - creates a school to be used in tests
    """
    def create_school(self, name, school_type='Test', address='12345 Foo St',
                      city='Bar', province='Baz', zipcode='54321',
                      active=True):

        school = self.create(name=name, school_type=school_type,
                             address=address, city=city, province=province,
                             zipcode=zipcode, active=active)
        return school

@python_2_unicode_compatible
class School(models.Model):
    OPTIONS = (
            ('Elementary School', _('Elementary School')),
            ('Middle School', _('Middle School')),
            ('High School', _('High School')),
            ('Organization', _('Organization')),
            ('Community College', _('Community College')),
            ('State College', _('State College')),
            )

    name = models.CharField(max_length=250)
    school_type = models.CharField(
        max_length=250, choices=OPTIONS, default=0, null=True,
        verbose_name=_('school type'))
    address = models.CharField(max_length=250)
    city = models.CharField(max_length=250)
    province = models.CharField(max_length=250)
    zipcode = models.CharField(max_length=250)
    active = models.BooleanField(default=False)

    created = models.DateTimeField(default=timezone.now)
    modified = models.DateTimeField(default=timezone.now)

    # nid and assoc_uid are only used by the data_scripts to make relations
    # between the models
    # nid = models.PositiveIntegerField(blank=True, null=True)
    # assoc_uid = models.PositiveIntegerField(blank=True, null=True)

    test_objects = SchoolManager()
    objects = models.Manager()

    def __str__(self):
        return self.name

@python_2_unicode_compatible
class SchoolRelations(models.Model):
    uid = models.PositiveIntegerField(blank=True, null=True)
    school = models.ForeignKey(
        School, null=True, on_delete=models.CASCADE,
        limit_choices_to={'active': True}
    )

    objects = models.Manager()

    def __str__(self):
        return self.school.name

def validate_UserProfile_birthdate(birthdate):
    today = datetime.datetime.now()
    if birthdate.year > today.year - 13:
        raise ValidationError(_('You must be at least 13'))
    elif birthdate.year == today.year - 13:
        if birthdate.month > today.month:
            raise ValidationError(_('You must be at least 13'))
        elif birthdate.month == today.month:
            if birthdate.day > today.day:
                raise ValidationError(_('You must be at least 13'))

class GalleryAlbum(models.Model):
    title = models.CharField(max_length=250,
                             verbose_name=_('title'))
    description = models.TextField(blank=True, verbose_name=_('description'))

    site = models.ForeignKey(
        Site, null=True, on_delete=models.CASCADE,
        verbose_name=_('Stream/Site name'), limit_choices_to={'active': True}
    )
    school = models.ForeignKey(
        School, null=True, on_delete=models.CASCADE,
        verbose_name=_('school'), limit_choices_to={'active': True}
    )
    date_time = models.DateTimeField(
        default=timezone.now, verbose_name=_('date and time')
    )

    def __str__(self):
        if self.site is not None:
            return self.site.site_name + ' album ' + str(self.id)
        else:
            return _('Unspecified site for album ') + str(self.id)

    class Meta:
        verbose_name = _('gallery album')
        verbose_name_plural = _('gallery albums')

class GalleryImage(models.Model):
    title = models.CharField(max_length=250,
                             verbose_name=_('title'))
    description = models.TextField(blank=True, verbose_name=_('description'))
    site = models.ForeignKey(
        Site, null=True, on_delete=models.CASCADE,
        verbose_name=_('Stream/Site name'), limit_choices_to={'active': True}
    )
    school = models.ForeignKey(
        School, null=True, on_delete=models.CASCADE,
        verbose_name=_('school'), limit_choices_to={'active': True}
    )
    user = models.ForeignKey(User, null=True, verbose_name=_('user'))
    album = models.ForeignKey(
        GalleryAlbum, null=True, blank=True, on_delete=models.CASCADE,
        verbose_name=_('album')
    )
    image = models.ImageField(null=True,
                              upload_to='gallery_images/',
                              verbose_name=_('image'))
    date_time = models.DateTimeField(
        default=timezone.now, verbose_name=_('date and time')
    )

    def filename(self):
        return os.path.basename(self.image.name)

    def __str__(self):
        if self.site is not None:
            if self.album is not None:
                return self.site.site_name + ' gallery image ' + str(self.id)\
                       + ' (Album ' + str(self.album.id) + ')'
            else:
                return self.site.site_name + ' gallery image ' + str(self.id)
        else:
            return _('Unspecified site for gallery image ') + str(self.id)

    class Meta:
        verbose_name = _('gallery image')
        verbose_name_plural = _('gallery images')

class GalleryFile(models.Model):
    title = models.CharField(max_length=250,
                             verbose_name=_('title'))
    description = models.TextField(blank=True, verbose_name=_('description'))
    site = models.ForeignKey(
        Site, null=True, on_delete=models.CASCADE,
        verbose_name=_('Stream/Site name'), limit_choices_to={'active': True}
    )
    school = models.ForeignKey(
        School, null=True, on_delete=models.CASCADE,
        verbose_name=_('school'), limit_choices_to={'active': True}
    )
    user = models.ForeignKey(User, null=True, verbose_name=_('user'))
    gallery_file = models.FileField(null=True, upload_to='gallery_files/',
                                    verbose_name=_('file'))
    date_time = models.DateTimeField(
        default=timezone.now, verbose_name=_('date and time')
    )

    def filename(self):
        return os.path.basename(self.gallery_file.name)

    def __str__(self):
        if self.site is not None:
            return self.site.site_name + ' file ' + str(self.id) +\
                    '(' + self.filename() + ')'
        else:
            return _('Unspecified site for file ') + str(self.id)

    class Meta:
        verbose_name = _('gallery file')
        verbose_name_plural = _('gallery files')

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    school = models.ForeignKey(School, on_delete=models.CASCADE)
    approved = models.BooleanField(default=False)

    def __unicode__(self):
        return self.user.username

class WaterQualityManager(models.Manager):
    """
    Manager for the Water_Quality model/datasheet.
    """
    def create_water_quality(self, site, date_time, school, DEQ_dq_level,
                             latitude, longitude, fish_present,
                             live_fish, dead_fish, air_temp_unit,
                             water_temp_unit, notes=''):

        wq_info = self.create(site=site,
                              date_time=date_time,
                              school=school,
                              DEQ_dq_level=DEQ_dq_level,
                              latitude=latitude,
                              longitude=longitude,
                              fish_present=fish_present,
                              live_fish=live_fish,
                              dead_fish=dead_fish,
                              air_temp_unit=air_temp_unit,
                              water_temp_unit=water_temp_unit,
                              notes=notes)
        return wq_info

def validate_WaterQuality_latitude(latitude):
    if abs(latitude) > 90:
        raise ValidationError(_('Latitude is not within valid range.'))

def validate_WaterQuality_longitude(longitude):
    if abs(longitude) > 180:
        raise ValidationError(_('Longitude is not within valid range.'))

@python_2_unicode_compatible
class Water_Quality(models.Model):
    """
    The Water Quality model corresponds to the Water Quality datasheet. Each
    object has a one-to-one relationship with its specified Site.
    """
    LEVEL_A = 'A'   # Define DEQ water quality levels
    LEVEL_B = 'B'
    LEVEL_C = 'C'
    LEVEL_D = 'D'
    LEVEL_E = 'E'
    FAHRENHEIT = _('Fahrenheit')
    CELSIUS = _('Celsius')

    DEQ_DQ_CHOICES = (
        (None, '-----'),
        (LEVEL_A, _('Level A')),
        (LEVEL_B, _('Level B')),
        (LEVEL_C, _('Level C')),
        (LEVEL_D, _('Level D')),
        (LEVEL_E, _('Level E')),
    )

    BOOL_CHOICES = (('True', _('Yes')), ('False', _('No')))
    UNIT_CHOICES = ((FAHRENHEIT, _('Fahrenheit')), (CELSIUS, _('Celsius')))

    site = models.ForeignKey(
        Site, null=True, on_delete=models.CASCADE,
        verbose_name=_('Stream/Site name'), limit_choices_to={'active': True}
    )
    date_time = models.DateTimeField(
        default=timezone.now, verbose_name=_('date and time')
    )
    DEQ_dq_level = models.CharField(
        max_length=10, choices=DEQ_DQ_CHOICES, blank=True,
        default=None, null=True, verbose_name=_('DEQ data quality level')
    )
    school = models.ForeignKey(
        School, null=True, on_delete=models.CASCADE,
        verbose_name=_('school'), limit_choices_to={'active': True}
    )
    latitude = models.DecimalField(
        null=True, max_digits=9, decimal_places=6,
        verbose_name=_('latitude'), validators=[validate_WaterQuality_latitude]
    )
    longitude = models.DecimalField(
        null=True, max_digits=9, decimal_places=6,
        verbose_name=_('longitude'),
        validators=[validate_WaterQuality_longitude]
    )
    fish_present = models.CharField(
        max_length=255, choices=BOOL_CHOICES, default=0, null=True,
        verbose_name=_('any fish present?')
    )
    live_fish = models.PositiveSmallIntegerField(
        default=0, null=True, verbose_name=_('number of live fish')
    )
    dead_fish = models.PositiveSmallIntegerField(
        default=0, null=True, verbose_name=_('number of dead fish')
    )
    water_temp_unit = models.CharField(
        max_length=255, choices=UNIT_CHOICES, default=0, null=True,
        verbose_name=_('water temperature units')
    )
    air_temp_unit = models.CharField(
        max_length=255, choices=UNIT_CHOICES, default=0, null=True,
        verbose_name=_('air temperature units')
    )
    notes = models.TextField(blank=True, verbose_name=_('notes'))
    nid = models.PositiveIntegerField(blank=True, null=True)

    # Uid used to make relations between school and data sheet
    uid = models.PositiveIntegerField(blank=True, null=True)

    test_objects = WaterQualityManager()
    objects = models.Manager()

    def __str__(self):
        if self.site is not None:
            return self.site.site_name + ' data sheet ' + str(self.id)
        else:
            return _('Unspecified site for data sheet ') + str(self.id)

    def to_dict(self):
        samples = WQ_Sample.objects.filter(water_quality=self)
        return {
            'DEQ_dq_level': self.DEQ_dq_level,
            'date': _timestamp(self.date_time),
            'school': self.school,
            'location': {
                'x': str(self.longitude),
                'y': str(self.latitude)
            },
            'fish_present': self.fish_present,
            'live_fish': self.live_fish,
            'dead_fish': self.dead_fish,
            'air_temp_unit': self.air_temp_unit,
            'water_temp_unit': self.water_temp_unit,
            'notes': self.notes,
            'samples': [m.to_dict() for m in samples]
        }

    class Meta:
        verbose_name = 'water quality'
        verbose_name_plural = 'water quality'

class WQSampleManager(models.Manager):
    """
    Manager for the water quality samples - creates both the required and
    additional field data for the Water Quality datasheet tests
    """
    def create_sample(
        self, water_quality, sample, water_temperature, water_temp_tool,
        air_temperature, air_temp_tool, dissolved_oxygen, oxygen_tool, pH,
        pH_tool, turbidity, turbid_tool, salinity, salt_tool,
        conductivity=None, total_solids=None, bod=None, ammonia=None,
        nitrite=None, nitrate=None, phosphates=None, fecal_coliform=None
    ):
        info = self.create(
            water_quality=water_quality,
            sample=sample,
            water_temperature=water_temperature,
            water_temp_tool=water_temp_tool,
            air_temperature=air_temperature,
            air_temp_tool=air_temp_tool,
            dissolved_oxygen=dissolved_oxygen,
            oxygen_tool=oxygen_tool,
            pH=pH,
            pH_tool=pH_tool,
            turbidity=turbidity,
            turbid_tool=turbid_tool,
            salinity=salinity,
            salt_tool=salt_tool,
            conductivity=conductivity,
            total_solids=total_solids,
            bod=bod,
            ammonia=ammonia,
            nitrite=nitrite,
            nitrate=nitrate,
            phosphates=phosphates,
            fecal_coliform=fecal_coliform
        )
        return info

def validate_pH(ph):
    if not(0 <= ph <= 14):
        raise ValidationError(
            '%(ph)s is not 0-14.',
            params={'ph': ph},
        )

@python_2_unicode_compatible
class WQ_Sample(models.Model):
    """
    The WQ_Sample model describes each water quality sample
    """
    NOT_ACCESSED = 'N/A'
    VERNIER = _('Vernier')
    MANUAL = _('Manual')
    TOOL_CHOICES = ((MANUAL, _('Manual')), (VERNIER, _('Vernier')),)

    # These are required fields
    water_quality = models.ForeignKey(
        Water_Quality,
        on_delete=models.CASCADE,
        related_name='water_quality',
        null=True
    )
    DEFAULT = '(Select a sample number)'
    ONE = 1
    TWO = 2
    THREE = 3
    FOUR = 4
    TOOL_CHOICES = ((NOT_ACCESSED, None),
                    (MANUAL, _('Manual')),
                    (VERNIER, _('Vernier')),)
    SAMPLE_CHOICES = ((DEFAULT, _('(Select a sample number)')),
                      (ONE, 1),
                      (TWO, 2),
                      (THREE, 3),
                      (FOUR, 4),)

    # These are required fields
    water_quality = models.ForeignKey(Water_Quality, on_delete=models.CASCADE,
                                      related_name='water_quality', null=True)
    sample = models.CharField(max_length=255, choices=SAMPLE_CHOICES,
                              null=True, default=SAMPLE_CHOICES[0])
    water_temperature = models.DecimalField(
        default=None, max_digits=5, decimal_places=2, null=True,
        verbose_name=_('water temperature')
    )
    water_temp_tool = models.CharField(
        max_length=255, choices=TOOL_CHOICES, default=NOT_ACCESSED, null=True,
    )
    air_temperature = models.DecimalField(
        default=None, null=True, max_digits=5, decimal_places=2,
        verbose_name=_('air temperature')
    )
    air_temp_tool = models.CharField(
        max_length=255, choices=TOOL_CHOICES, default=NOT_ACCESSED, null=True,
    )
    dissolved_oxygen = models.DecimalField(
        default=None, max_digits=5, decimal_places=2, null=True,
        verbose_name=_('dissolved oxygen (mg/L)')
    )
    oxygen_tool = models.CharField(
        max_length=255, choices=TOOL_CHOICES, default=NOT_ACCESSED, null=True,
    )
    pH = models.DecimalField(
        validators=[validate_pH], default=None, null=True,
        max_digits=5, decimal_places=2, verbose_name=_('pH')
    )
    pH_tool = models.CharField(
        max_length=255, choices=TOOL_CHOICES, default=NOT_ACCESSED, null=True
    )
    turbidity = models.DecimalField(
        default=None, null=True, max_digits=5, decimal_places=2,
        verbose_name=_('turbidity (NTU)')
    )
    turbid_tool = models.CharField(
        max_length=255, choices=TOOL_CHOICES, default=NOT_ACCESSED, null=True,
    )
    salinity = models.DecimalField(
        default=None, null=True, max_digits=5, decimal_places=2,
        verbose_name=_('salinity (PSU) PPT')
    )
    salt_tool = models.CharField(
        max_length=255, choices=TOOL_CHOICES, default=NOT_ACCESSED, null=True
    )
    # The following are optional fields
    conductivity = models.DecimalField(
        max_digits=5, decimal_places=2, blank=True,
        null=True, verbose_name=_('conductivity (µS/cm)')
    )
    total_solids = models.DecimalField(
        max_digits=5, decimal_places=2, blank=True,
        null=True, verbose_name=_('total solids (mg/L)')
    )
    bod = models.DecimalField(
        max_digits=5, decimal_places=2, blank=True,
        null=True, verbose_name=_('BOD (mg/L)')
    )
    ammonia = models.DecimalField(
        max_digits=5, decimal_places=2, blank=True,
        null=True, verbose_name=_('ammonia (mg/L)')
    )
    nitrite = models.DecimalField(
        max_digits=5, decimal_places=2, blank=True,
        null=True, verbose_name=_('nitrite (mg/L)')
    )
    nitrate = models.DecimalField(
        max_digits=5, decimal_places=2, blank=True,
        null=True, verbose_name=_('nitrate (mg/L)')
    )
    phosphates = models.DecimalField(
        max_digits=5, decimal_places=2, blank=True,
        null=True, verbose_name=_('phosphates (mg/L)')
    )
    fecal_coliform = models.DecimalField(
        max_digits=5,
        decimal_places=2, blank=True,
        null=True,
        verbose_name=_('fecal coliform (CFU/100 mL)')
    )
    nid = models.PositiveIntegerField(blank=True, null=True)

    test_objects = WQSampleManager()
    objects = models.Manager()

    def __str__(self):
        if self.water_quality.site is not None:
            return self.water_quality.site.site_name + ' sheet ' + \
                str(self.water_quality.id) + ' sample ' + str(self.sample)
        else:
            return ' Unspecified site: sheet ' + \
                str(self.water_quality.id) + ' sample ' + str(self.sample)

    def to_dict(self):
        return {
            'water_temperature': str(_temp_conv(self.water_temperature,
                                     self.water_quality.water_temp_unit)),
            'water_temp_tool': self.water_temp_tool,
            'air_temperature': str(_temp_conv(self.air_temperature,
                                   self.water_quality.air_temp_unit)),
            'air_temp_tool': self.air_temp_tool,
            'dissolved_oxygen': str(self.dissolved_oxygen),
            'oxygen_tool': self.oxygen_tool,
            'pH': str(self.pH),
            'pH_tool': self.pH_tool,
            'turbidity': str(self.turbidity),
            'turbid_tool': self.turbid_tool,
            'salinity': str(self.salinity),
            'salt_tool': self.salt_tool,
            'conductivity': str(self.conductivity),
            'total_solids': str(self.total_solids),
            'bod': str(self.bod),
            'ammonia': str(self.ammonia),
            'nitrite': str(self.nitrite),
            'nitrate': str(self.nitrate),
            'phosphates': str(self.phosphates),
            'fecal_coliform': str(self.fecal_coliform)
        }

    class Meta:
        verbose_name = 'water quality sample'
        verbose_name_plural = 'water quality samples'

class CameraPointManager(models.Manager):

    def create_camera_point(self, site, cp_date, location, map_datum='',
                            description=''):

        return self.create(site=site, cp_date=cp_date, location=location,
                           map_datum=map_datum, description=description)

class CameraPoint(models.Model):
    map_datum_choices = [
        (None, 'Not Recorded'),
        ('NAD27', _('NAD 27 CONUS')),
        ('NAD83', _('NAD 83')),
        ('WGS84', _('WGS84')),
    ]

    site = models.ForeignKey(Site, on_delete=models.CASCADE, null=True)
    letter = models.CharField(null=True, max_length=5, editable=False)
    cp_date = models.DateField(default=datetime.date.today,
                               verbose_name=_('date established'))
    school = models.ForeignKey(School, null=True, on_delete=models.CASCADE,
                               verbose_name=_('school'))
    location = models.PointField(null=True, verbose_name=_('location'))
    map_datum = models.CharField(
        max_length=5, null=True, blank=True, verbose_name=_('map datum'),
        choices=map_datum_choices, default=None)
    description = models.TextField(blank=True, verbose_name=_('description'))
    created = models.DateTimeField(default=timezone.now)

    test_objects = CameraPointManager()
    objects = models.Manager()

    def __str__(self):
        return (_('Camera point ') + self.letter + _(' for site ') +
                self.site.site_name)

    def save(self, **kwargs):
        if not self.letter:
            site_cps = CameraPoint.objects.filter(site_id=self.site.id)
            if not site_cps.exists():
                self.letter = 'A'
            else:
                prev_letter = site_cps.latest('created').letter
                max_len = len(prev_letter)
                if prev_letter[0] == 'Z':
                    self.letter = (max_len + 1) * 'A'
                else:
                    self.letter = max_len * chr(ord(prev_letter[0]) + 1)
        super(CameraPoint, self).save()

    class Meta:
        verbose_name = _('camera point')
        verbose_name_plural = _('camera points')

class PhotoPointManager(models.Manager):

    def create_photo_point(self, camera_point, pp_date, compass_bearing,
                           distance, camera_height, notes=''):

        return self.create(camera_point=camera_point, pp_date=pp_date,
                           compass_bearing=compass_bearing, distance=distance,
                           camera_height=camera_height, notes=notes)

class PhotoPoint(models.Model):
    camera_point = models.ForeignKey(CameraPoint, on_delete=models.CASCADE,
                                     null=True, related_name='camera_point')
    number = models.PositiveSmallIntegerField(null=True, editable=False)
    pp_date = models.DateField(default=datetime.date.today,
                               verbose_name=_('date established'))
    compass_bearing = models.PositiveSmallIntegerField(
        verbose_name=_('compass bearing'))
    distance = models.DecimalField(
        max_digits=3, decimal_places=0, null=True, blank=True,
        verbose_name=_('distance from camera point'))
    camera_height = models.DecimalField(
        max_digits=3, decimal_places=0, null=True, blank=True,
        verbose_name=_('camera height'))
    notes = models.TextField(blank=True, verbose_name=_('notes'))

    test_objects = PhotoPointManager()
    objects = models.Manager()

    def __str__(self):
        return (_('Photo point ') + str(self.number) + _(' for camera point ')
                + self.camera_point.letter)

    def save(self, **kwargs):
        if not self.number:
            p = PhotoPoint.objects.filter(camera_point_id=self.camera_point.id)
            if not p.exists():
                self.number = 1
            else:
                prev_num = p.latest('number').number
                self.number = prev_num + 1
        super(PhotoPoint, self).save()

    class Meta:
        verbose_name = _('photo point')
        verbose_name_plural = _('photo points')

class PhotoPointImage(models.Model):
    photo_point = models.ForeignKey(PhotoPoint, on_delete=models.CASCADE,
                                    null=True, related_name='photo_point')
    image = models.ImageField(null=True, upload_to='pp_photos/',
                              verbose_name=_('photo'))
    date = models.DateField(default=datetime.date.today,
                            verbose_name=_('date taken'))

    def __str__(self):
        return (str(self.date) + _(' for photo point ') +
                str(self.photo_point.number))

    class Meta:
        verbose_name = _('photo point image')
        verbose_name_plural = _('photo point images')

class MacroinvertebratesManager(models.Manager):
    """
    Manager for the Macroinvertebrates model.
    """
    def create_macro(self, site, school, time_spent=0, num_people=0,
                     water_type='riff', caddisfly=0, mayfly=0, riffle_beetle=0,
                     stonefly=0, water_penny=0, dobsonfly=0, clam_or_mussel=0,
                     crane_fly=0, crayfish=0, damselfly=0, dragonfly=0, scud=0,
                     fishfly=0, alderfly=0, mite=0, aquatic_worm=0, blackfly=0,
                     leech=0, midge=0, snail=0, mosquito_larva=0, notes=''):

        info = self.create(school=school,
                           date_time='2016-07-11 14:09',
                           weather='bbbb',
                           site=site,
                           time_spent=time_spent,
                           num_people=num_people,
                           water_type=water_type,
                           caddisfly=caddisfly,
                           mayfly=mayfly,
                           riffle_beetle=riffle_beetle,
                           stonefly=stonefly,
                           water_penny=water_penny,
                           dobsonfly=dobsonfly,
                           clam_or_mussel=clam_or_mussel,
                           crane_fly=crane_fly,
                           crayfish=crayfish,
                           damselfly=damselfly,
                           dragonfly=dragonfly,
                           scud=scud,
                           fishfly=fishfly,
                           alderfly=alderfly,
                           mite=mite,
                           aquatic_worm=aquatic_worm,
                           blackfly=blackfly,
                           leech=leech,
                           midge=midge,
                           snail=snail,
                           mosquito_larva=mosquito_larva,
                           notes=notes)
        return info

@python_2_unicode_compatible
class Macroinvertebrates(models.Model):
    RIFFLE = 'riff'
    POOL = 'pool'

    WATER_TYPE_CHOICES = (
        (None, '-----'),
        (RIFFLE, _('riffle')),
        (POOL, _('pool')),
    )

    school = models.ForeignKey(School, null=True, on_delete=models.CASCADE,
                               verbose_name=_('school'))
    date_time = models.DateTimeField(default=timezone.now,
                                     verbose_name=_('date and time'))
    weather = models.CharField(max_length=250,
                               verbose_name=_('weather'))
    site = models.ForeignKey(Site, null=True, on_delete=models.CASCADE,
                             limit_choices_to={'active': True})
    time_spent = models.PositiveIntegerField(
        default=None, null=True,
        verbose_name=_('Time spent sorting/identifying')
        )
    num_people = models.PositiveIntegerField(
        default=None, null=True,
        verbose_name=_('Number of people sorting/identifying')
        )
    water_type = models.CharField(max_length=4, verbose_name=_('water type'),
                                  choices=WATER_TYPE_CHOICES, default=None,
                                  blank=True)
    notes = models.TextField(blank=True, verbose_name=_('field notes'))

    # Sensitive/intolerant to pollution
    caddisfly = models.PositiveIntegerField(default=0,
                                            verbose_name=_('caddisfly'))
    mayfly = models.PositiveIntegerField(default=0,
                                         verbose_name=_('mayfly'))
    riffle_beetle = models.PositiveIntegerField(
        default=0, verbose_name=_('riffle beetle')
        )
    stonefly = models.PositiveIntegerField(default=0,
                                           verbose_name=_('stonefly'))
    water_penny = models.PositiveIntegerField(default=0,
                                              verbose_name=_('water penny'))
    dobsonfly = models.PositiveIntegerField(default=0,
                                            verbose_name=_('dobsonfly'))
    sensitive_total = models.PositiveIntegerField(
        default=0, verbose_name=_('sensitive total')
        )

    # Somewhat sensitive
    clam_or_mussel = models.PositiveIntegerField(default=0,
                                                 verbose_name=_('clam/mussel'))
    crane_fly = models.PositiveIntegerField(default=0,
                                            verbose_name=_('crane fly'))
    crayfish = models.PositiveIntegerField(default=0,
                                           verbose_name=_('crayfish'))
    damselfly = models.PositiveIntegerField(default=0,
                                            verbose_name=_('damselfly'))
    dragonfly = models.PositiveIntegerField(default=0,
                                            verbose_name=_('dragonfly'))
    scud = models.PositiveIntegerField(default=0, verbose_name=_('scud'))
    fishfly = models.PositiveIntegerField(default=0, verbose_name=_('fishfly'))
    alderfly = models.PositiveIntegerField(default=0,
                                           verbose_name=_('alderfly'))
    mite = models.PositiveIntegerField(default=0, verbose_name=_('mite'))
    somewhat_sensitive_total = models.PositiveIntegerField(
        default=0, verbose_name=_('somewhat sensitive total')
        )

    # Tolerant
    aquatic_worm = models.PositiveIntegerField(default=0,
                                               verbose_name=_('aquatic worm'))
    blackfly = models.PositiveIntegerField(default=0,
                                           verbose_name=_('blackfly'))
    leech = models.PositiveIntegerField(default=0, verbose_name=_('leech'))
    midge = models.PositiveIntegerField(default=0, verbose_name=_('midge'))
    snail = models.PositiveIntegerField(default=0, verbose_name=_('snail'))
    mosquito_larva = models.PositiveIntegerField(
        default=0, verbose_name=_('mosquito larva')
        )
    tolerant_total = models.PositiveIntegerField(
        default=0, verbose_name=_('tolerant total')
        )

    # Water quality rating
    wq_rating = models.PositiveIntegerField(
        default=0, verbose_name=_('water quality rating')
        )

    # Uid used to make relations between school and data sheet
    uid = models.PositiveIntegerField(blank=True, null=True)

    objects = models.Manager()
    test_objects = MacroinvertebratesManager()

    def __str__(self):
        return self.site.site_name + ' sheet ' + str(self.id)

    def calculate_scores(self):
        self.sensitive_total = 0
        self.somewhat_sensitive_total = 0
        self.tolerant_total = 0

        # divvy up indiv count values into three arrays
        sensitive = [self.caddisfly, self.mayfly, self.riffle_beetle,
                     self.stonefly, self.water_penny, self.dobsonfly]
        somewhat = [self.clam_or_mussel, self.crane_fly, self.crayfish,
                    self.damselfly, self.dragonfly, self.scud, self.fishfly,
                    self.alderfly, self.mite]
        tolerant = [self.aquatic_worm, self.blackfly, self.leech, self.midge,
                    self.snail, self.mosquito_larva]

        # loop thru each array-- if a bug's count is at least one, that bug
        # gets n points towards its category's final score (n = 3 for
        # sensitive, n = 2 for somewhat sensitive, n = 1 for tolerant)
        for count in sensitive:
            if count > 0:
                self.sensitive_total += 3

        for count in somewhat:
            if count > 0:
                self.somewhat_sensitive_total += 2

        for count in tolerant:
            if count > 0:
                self.tolerant_total += 1

    def save(self, **kwargs):
        self.calculate_scores()

        # the water quality rating is just the sum of the three scores
        self.wq_rating = (self.sensitive_total +
                          self.somewhat_sensitive_total + self.tolerant_total)

        super(Macroinvertebrates, self).save()

    class Meta:
        verbose_name = _('macroinvertebrate')
        verbose_name_plural = _('macroinvertebrates')

    def get_tolerant_counts(self):
        return [
            {'name': _('Aquatic Worm'), 'value': self.aquatic_worm},
            {'name': _('Blackfly'), 'value': self.blackfly},
            {'name': _('Leech'), 'value': self.leech},
            {'name': _('Midge'), 'value': self.midge},
            {'name': _('Snail'), 'value': self.snail},
            {'name': _('Mosquito Larva'), 'value': self.mosquito_larva}
        ]

    def get_somewhat_sensitive_counts(self):
        return [
            {'name': _('Mussel/Clam'), 'value': self.clam_or_mussel},
            {'name': _('Crane Fly'), 'value': self.crane_fly},
            {'name': _('Crayfish'), 'value': self.crayfish},
            {'name': _('Damselfly'), 'value': self.damselfly},
            {'name': _('Dragonfly'), 'value': self.dragonfly},
            {'name': _('Scud'), 'value': self.scud},
            {'name': _('Fishfly'), 'value': self.fishfly},
            {'name': _('Alderfly'), 'value': self.alderfly},
            {'name': _('Mite'), 'value': self.mite}
        ]

    def get_sensitive_counts(self):
        return [
            {'name': _('Riffle Beetle'), 'value': self.riffle_beetle},
            {'name': _('Mayfly'), 'value': self.mayfly},
            {'name': _('Water Penny'), 'value': self.water_penny},
            {'name': _('Stonefly'), 'value': self.stonefly},
            {'name': _('Caddisfly'), 'value': self.caddisfly},
            {'name': _('Dobsonfly'), 'value': self.dobsonfly}
        ]

    def get_totals(self):
        # total number of somewhat sensitive macros found:
        somewhat = (int(self.clam_or_mussel) + int(self.crane_fly) +
                    int(self.crayfish) + int(self.damselfly) +
                    int(self.dragonfly) + int(self.scud) + int(self.fishfly) +
                    int(self.alderfly) + int(self.mite))

        # total number of sensitive macros found:
        sensitive = (int(self.caddisfly) + int(self.mayfly) +
                     int(self.riffle_beetle) + int(self.stonefly) +
                     int(self.water_penny) + int(self.dobsonfly))

        # total number of tolerant macros found:
        tolerant = (int(self.aquatic_worm) + int(self.blackfly) +
                    int(self.leech) + int(self.midge) + int(self.snail) +
                    int(self.mosquito_larva))

        return {
            _('Tolerant'): tolerant,
            _('Somewhat Sensitive'): somewhat,
            _('Sensitive'): sensitive
        }

class RipTransectManager(models.Manager):
    """
    Manager for the RiparianTransect model/datasheet.
    """
    def create_transect(self, school, date_time, site, weather='', slope=None,
                        notes=''):
        return self.create(school=school, date_time=date_time, site=site,
                           weather=weather, slope=slope, notes=notes)

def validate_slope(slope):
    if not(0 <= slope):
        raise ValidationError(
            '%(slope)s is not positive.',
            params={'slope': slope},
            )

class RiparianTransect(models.Model):
    """
    This model corresponds to the Riparian Transect data sheet and has a one-to
    -one relationship with its specified Site.
    """
    school = models.ForeignKey(School, null=True, on_delete=models.CASCADE,
                               verbose_name=_('school'))
    date_time = models.DateTimeField(default=timezone.now,
                                     verbose_name=_('date and time'))
    weather = models.CharField(max_length=255, blank=True,
                               verbose_name=_('weather'))
    site = models.ForeignKey(Site, null=True, on_delete=models.CASCADE,
                             verbose_name=_('site'),
                             limit_choices_to={'active': True})
    slope = models.DecimalField(
        blank=True, null=True, max_digits=5, decimal_places=3,
        verbose_name=_('slope of stream bank (rise over run)')
    )
    notes = models.TextField(blank=True, verbose_name=_('notes'))
    nid = models.PositiveIntegerField(blank=True, null=True)

    # Uid used to make relations between school and data sheet
    uid = models.PositiveIntegerField(blank=True, null=True)

    test_objects = RipTransectManager()
    objects = models.Manager()

    def __str__(self):
        return (_('Transect ') + str(self.id) + _(' for site ')
                + self.site.site_name)

    class Meta:
        verbose_name = _('riparian transect')
        verbose_name_plural = _('riparian transects')

class TransectZoneManager(models.Manager):
    """
    Manager for the TransectZone model.
    """
    def create_zone(self, transect, conifers=0, hardwoods=0, shrubs=0,
                    comments=''):
        return self.create(transect=transect, conifers=conifers,
                           hardwoods=hardwoods, shrubs=shrubs,
                           comments=comments)

class TransectZone(models.Model):
    """
    Each Riparian Transect datasheet requires five zones.
    """
    ZONE_1 = 1
    ZONE_2 = 2
    ZONE_3 = 3
    ZONE_4 = 4
    ZONE_5 = 5

    ZONES = ((ZONE_1, 1), (ZONE_2, 2), (ZONE_3, 3), (ZONE_4, 4), (ZONE_5, 5))

    transect = models.ForeignKey(RiparianTransect, on_delete=models.CASCADE,
                                 related_name='transect', null=True)
    zone_num = models.CharField(max_length=1, default=0, choices=ZONES)
    conifers = models.PositiveSmallIntegerField(default=0, null=True,
                                                verbose_name=_('conifers'))
    hardwoods = models.PositiveSmallIntegerField(default=0, null=True,
                                                 verbose_name=_('hardwoods'))
    shrubs = models.PositiveSmallIntegerField(default=0, null=True,
                                              verbose_name=_('shrubs'))
    comments = models.TextField(blank=True, default='',
                                verbose_name=_('additional comments'))

    test_objects = TransectZoneManager()
    objects = models.Manager()

    def __str__(self):
        return ('Zone ' + str(self.zone_num) + ' for transect ' +
                str(self.transect.id))

    class Meta:
        verbose_name = 'zone'
        verbose_name_plural = 'zones'

def validate_cover(canopy_cover):
    if canopy_cover & 0b11111111000000000000000000000000 != 0:
        raise ValidationError(
            '%(canopy_cover)s has bad bits set.',
            params={'canopy_cover': canopy_cover},
            )

def validate_total_cover(est_canopy_cover):
    if not(0 <= est_canopy_cover and est_canopy_cover <= 96):
        raise ValidationError(
            '%(est_canopy_cover)s is not 0-96.',
            params={'est_canopy_cover': est_canopy_cover},
            )

@python_2_unicode_compatible
class Canopy_Cover(models.Model):
    school = models.ForeignKey(School, null=True, on_delete=models.CASCADE,
                               verbose_name=_('school'))
    date_time = models.DateTimeField(default=timezone.now,
                                     verbose_name=_('date and time'))
    site = models.ForeignKey(Site, null=True, on_delete=models.CASCADE,
                             verbose_name=_('site'))
    weather = models.CharField(max_length=250, verbose_name=_('weather'))
    north_cc = models.IntegerField(
        default=0, validators=[validate_cover],
        verbose_name=_('north canopy cover')
        )
    east_cc = models.IntegerField(
        default=0, validators=[validate_cover],
        verbose_name=_('east canopy cover')
        )
    south_cc = models.IntegerField(
        default=0, validators=[validate_cover],
        verbose_name=_('south canopy cover')
        )
    west_cc = models.IntegerField(
        default=0, validators=[validate_cover],
        verbose_name=_('west canopy cover')
        )
    est_canopy_cover = models.PositiveIntegerField(
        default=0, validators=[validate_total_cover],
        verbose_name=_('estimated canopy cover')
        )

    # Uid used to make relations between school and data sheet
    uid = models.PositiveIntegerField(blank=True, null=True)

    def __str__(self):
        return(str(self.date_time) + ' ' + self.site.site_name)

    class Meta:
        verbose_name = _('canopy cover survey')
        verbose_name_plural = _('canopy cover surveys')

@python_2_unicode_compatible
class Soil_Survey(models.Model):
    school = models.ForeignKey(School, null=True, on_delete=models.CASCADE,
                               verbose_name=_('school'))
    date_time = models.DateTimeField(default=timezone.now,
                                     verbose_name=_('date and time'))
    weather = models.CharField(max_length=250, verbose_name=_('weather'),
                               blank=True)
    site = models.ForeignKey(Site, null=True, on_delete=models.CASCADE,
                             verbose_name=_('site'))
    notes = models.TextField(blank=True, verbose_name=_("Field notes"))

    landscape_pos_choices = [
        ('summit', _('Summit')),
        ('slope', _('Slope')),
        ('depression', _('Depression')),
        ('large_flat', _('Large Flat Area')),
        ('stream_bank', _('Stream Bank'))
    ]

    cover_type_choices = [
        ('bare_soil', _('Bare Soil')),
        ('rocks', _('Rocks')),
        ('grass', _('Grass')),
        ('shrubs', _('Shrubs')),
        ('trees', _('Trees'))
    ]

    land_use_choices = [
        ('urban', _('Urban')),
        ('agricultural', _('Agricultural')),
        ('recreation', _('Recreation')),
        ('wilderness', _('Wilderness')),
        ('other', _('Other'))
    ]

    landscape_pos = models.CharField(max_length=11, default=None,
                                     choices=landscape_pos_choices,
                                     verbose_name='landscape position')

    cover_type = models.CharField(max_length=9, default=None,
                                  choices=cover_type_choices)

    land_use = models.CharField(max_length=12, default=None,
                                choices=land_use_choices)

    distance = models.DecimalField(max_digits=5, decimal_places=2, null=True,
                                   verbose_name=_('distance from stream'))

    site_char = models.TextField(blank=True,
                                 verbose_name=_('distinguishing site \
                                 characteristics'))

    soil_type_choices = [
        ('sand', _('Sand')),
        ('loamy_sand', _('Loamy Sand')),
        ('silt_loam', _('Silt Loam')),
        ('loam', _('Loam')),
        ('clay_loam', _('Clay Loam')),
        ('light_clay', _('Light Clay')),
        ('heavy_clay', _('Heavy Clay')),
        ('n/a', _('N/A')),
        ('other', _('Other'))
    ]

    soil_type = models.CharField(max_length=10, default=None, blank=False,
                                 choices=soil_type_choices)

    # Uid used to make relations between school and data sheet
    uid = models.PositiveIntegerField(blank=True, null=True)

    def __str__(self):
        return self.site.site_name

    class Meta:
        verbose_name = _('soil survey')
        verbose_name_plural = _('soil surveys')

class ResourceManager(models.Model):
    """ Manager for the Resources model """
    def create_resource(
        self, name, res_type, downloadable, thumbnail, sort_order
    ):
        return self.create(
            name=name, res_type=res_type, downloadable=downloadable,
            thumbnail=thumbnail, sort_order=sort_order
        )

class Resource(models.Model):
    """ This model organizes Resources like pdfs or videos """
    TYPE_CHOICES = (
        (None, '-----'),
        ('data_sheet', _('Data Sheet')),
        ('publication', _('Publication')),
        ('tutorial_video', _('Tutorial Video')),
    )
    name = models.CharField(max_length=255, blank=False)
    res_type = models.CharField(
        max_length=255, blank=False, choices=TYPE_CHOICES
    )
    # bad way to handle upload_to!  It should be based on res_type
    downloadable = models.FileField(upload_to='assets/', blank=True)
    thumbnail = models.ImageField(upload_to='assets/thumbnails/', blank=True)
    sort_order = models.PositiveSmallIntegerField(default=1000)

    objects = models.Manager()
    test_objects = ResourceManager()

    class Meta:
        verbose_name = _('resource')
        verbose_name_plural = _('resources')

    def __str__(self):
        return self.name

    @property
    def thumbnail_url(self):
        if self.thumbnail and hasattr(self.thumbnail, 'url'):
            return self.thumbnail.url

class RipAquaticSurveyManager(models.Manager):

    def create_aqua(self, site, school, date_time='2016-04-03 12:00',
                    riffle_count=3, pool_count=3, weather='RAAIIN'):

        info = self.create(school=school,
                           site=site,
                           weather=weather,
                           riffle_count=riffle_count,
                           pool_count=pool_count)
        return info

class RipAquaticSurvey(models.Model):
    OPTIONS = (
            ('Very Little', _('Very Little')),
            ('Some', _('Some')),
            ('A Lot', _('A Lot'))
            )
    school = models.ForeignKey(School, null=True, on_delete=models.CASCADE,
                               verbose_name=_('school'),
                               blank=False, default='',
                               limit_choices_to={'active': True}
                               )
    site = models.ForeignKey(Site, null=True, on_delete=models.CASCADE,
                             verbose_name=_('site'),
                             limit_choices_to={'active': True}
                             )
    date_time = models.DateTimeField(default=timezone.now,
                                     verbose_name=_('date and time'))
    weather = models.CharField(
        max_length=20, verbose_name=_('weather'), blank=True, null=True
    )
    stream_length = models.PositiveIntegerField(
        blank=True, null=True, verbose_name=_('length of stream')
    )
    riffle_count = models.PositiveIntegerField(
        blank=True, null=True, verbose_name=_('number of riffles')
    )
    pool_count = models.PositiveIntegerField(
        blank=True, null=True, verbose_name=_('number of pools')
    )
    silt = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('silt')
    )
    sand = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('sand')
    )
    gravel = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('gravel')
    )
    cobble = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('cobble')
    )
    boulders = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('boulders')
    )
    bedrock = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('bedrock')
    )
    small_debris = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('small_debris')
    )
    medium_debris = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('medium_debris')
    )
    large_debris = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('large_debris')
    )
    comments = models.CharField(
        max_length=250, null=True, verbose_name=_('comments'), blank=True
    )
    coniferous_trees = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('coniferous_trees')
    )
    deciduous_trees = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('deciduous_trees')
    )
    shrubs = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('shrubs')
    )
    small_plants = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('small_plants')
    )
    ferns = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('ferns')
    )
    grasses = models.CharField(
        max_length=250, choices=OPTIONS, null=True, blank=True,
        verbose_name=_('grasses')
    )
    species1 = models.CharField(
        max_length=250, null=True, blank=True,
        verbose_name=_('species')
    )
    species2 = models.CharField(
        max_length=250, null=True, blank=True,
        verbose_name=_('species')
    )
    species3 = models.CharField(
        max_length=250, null=True, blank=True,
        verbose_name=_('species')
    )
    species4 = models.CharField(
        max_length=250, null=True, blank=True,
        verbose_name=_('species')
    )
    species5 = models.CharField(
        max_length=250, null=True, blank=True,
        verbose_name=_('species')
    )
    species6 = models.CharField(
        max_length=250, null=True, blank=True,
        verbose_name=_('species')
    )
    significance1 = models.CharField(
        max_length=250, null=True, blank=True,
        verbose_name=_('significance to riparian area')
    )
    significance2 = models.CharField(
        max_length=250, null=True, blank=True,
        verbose_name=_('significance to riparian area')
    )
    significance3 = models.CharField(
        max_length=250, null=True, blank=True,
        verbose_name=_('significance to riparian area')
    )
    significance4 = models.CharField(
        max_length=250, null=True, blank=True,
        verbose_name=_('significance to riparian area')
    )
    significance5 = models.CharField(
        max_length=250, null=True, blank=True,
        verbose_name=_('significance to riparian area')
    )
    significance6 = models.CharField(
        max_length=250, null=True, blank=True,
        verbose_name=_('significance to riparian area')
    )
    wildlife_type1 = models.CharField(
        max_length=500, null=True, blank=True,
        verbose_name=_('type, species, or track/sign')
    )
    wildlife_type2 = models.CharField(
        max_length=500, null=True, blank=True,
        verbose_name=_('type, species, or track/sign')
    )
    wildlife_type3 = models.CharField(
        max_length=500, null=True, blank=True,
        verbose_name=_('type, species, or track/sign')
    )
    wildlife_type4 = models.CharField(
        max_length=500, null=True, blank=True,
        verbose_name=_('type, species, or track/sign')
    )
    wildlife_type5 = models.CharField(
        max_length=500, null=True, blank=True,
        verbose_name=_('type, species, or track/sign')
    )
    wildlife_type6 = models.CharField(
        max_length=500, null=True, blank=True,
        verbose_name=_('type, species, or track/sign')
    )
    wildlife_comments1 = models.CharField(
        max_length=500, null=True, blank=True,
        verbose_name=_('# or comments')
    )
    wildlife_comments2 = models.CharField(
        max_length=500, null=True, blank=True,
        verbose_name=_('# or comments')
    )
    wildlife_comments3 = models.CharField(
        max_length=500, null=True, blank=True,
        verbose_name=_('# or comments')
    )
    wildlife_comments4 = models.CharField(
        max_length=500, null=True, blank=True,
        verbose_name=_('# or comments')
    )
    wildlife_comments5 = models.CharField(
        max_length=500, null=True, blank=True,
        verbose_name=_('# or comments')
    )
    wildlife_comments6 = models.CharField(
        max_length=500, null=True, blank=True,
        verbose_name=_('# or comments')
    )
    notes = models.TextField(blank=True, verbose_name=_("Field notes"))
    objects = models.Manager()
    test_objects = RipAquaticSurveyManager()


# -*- coding: utf-8 -*-
import unittest, math

def zad1():
  """
  Proszę do ciała tej funkcji wpisać kod który policzy sumę liczb całkowitych
  z zakresu 0..100, za pomocą funkcji for.

  Test:

  >>> zad1() == 5050
  True
  >>> "for" in helper_getsource(zad1)
  True
  """
  sum = 0
  for i in range(101):
    sum += i
  return sum

def zad2():
  """
  Proszę w ciele tej funkcji wyznaczyć wartość całki z sinusa na zakresie od
  zera do dwa pi korzystając z 1E5 przedziałów załkowania.
  """
  start = 0
  end = math.pi*2
  sum = 0
  ran = 100000
  step = (end-start)/ran
  f = math.sin
  for i in range(ran):
    sum += f(i*step)
  return sum * step

def zad3_arange(start, stop, step):
  """
  Napisz funkcję która jest lepszą wersją funkcji range, funkcja ta dziala również
  dla liczb zmiennoprzecinkowych.

  Powinna ona przyjmować trzy argumenty:

  * start --- początek iteracji
  * stop --- koniec iteracji
  * step --- krok iteracji

  i zwracać listę która zawiera wszystkie liczby w postaci start + i*step
  mniejsze od stop.

  Np. zad3_arange(0, 5, 1) zwraca: [0, 1, 2, 3, 4]
  zad3_arange(0, 5, .1) zwraca: [0, .1, .2 ..., 3, ... 4.9]
  """
  ran = []
  while start < stop:
    ran.append(start)
    start += step
  return ran

ZAD3_DATA  = [
  {
    "imie": "Ziemniak",
    "rasa": "Pies",
    "wiek": 5
  },
  {
    "imie": "Marchewka",
    "rasa": "Kot",
    "wiek": 3
  },
  {
    "imie": "Seler",
    "rasa": "Chomik",
    "wiek": 0.5
  },
  {
    "imie": "Brokuł",
    "rasa": "Pająk",
    "wiek": 25
  },
  {
    "imie": "Szpinak nowozelandzki",
    "rasa": "Gżegżółka",
    "wiek": 1
  },
]
"""
Podmień definicję zmiennej DATA tak by:

Była to lista słowników, każdy słownik reprezentuje jedno zwierze.
Lista powinna mieć 5 elementów, a każde zwierze przynajmniej atrybuty
3 w tym imię.

Do pobrania imienia trzeciego zwierzęcia powinno mi starczyć
wywołania data[2]['imie'].
"""

#### TESTY

class TestBlok1Zaj1(unittest.TestCase):

  def test_result(self):
    self.assertEqual(zad1(), 5050)

  def test_for(self):
    self.assertIn("for", helper_getsource(zad1))

class TestBlok1Zaj2(unittest.TestCase):

  def test_result(self):
    self.assertAlmostEqual(zad2(), 0)

  def test_sin(self):
    self.assertIn("sin", helper_getsource(zad2))

  def test_for(self):
    self.assertIn("for", helper_getsource(zad2))

class TestBlok1Zaj3(unittest.TestCase):

  def test_result_type(self):
    self.assertIsInstance(zad3_arange(1, 2, 3), list)

  def test_result(self):
    self.assertEqual(zad3_arange(1, 2, 3), [1])

  def test_result_2(self):
    expected = [1, 1.3, 1.6, 1.9]
    result = zad3_arange(1, 2, .3)
    for ii, (left, right) in enumerate(zip(expected, result)):
      with self.subTest("Element {}".format(ii)):
        self.assertAlmostEqual(left,right)

class TestBlok1Zad4(unittest.TestCase):

  def test_result_type(self):
    self.assertIsInstance(ZAD3_DATA, list)

  def test_contents_type(self):
    for e in ZAD3_DATA:
      self.assertIsInstance(e, dict)

  def test_keys(self):
    for e in ZAD3_DATA:
      self.assertIn('imie', e)

  def test_len(self):
    self.assertEqual(5, len(ZAD3_DATA))

def helper_getsource(func):
  import inspect, re
  lines = []
  var = 0
  for line in inspect.getsourcelines(func)[0][1:]:
    if var >= 2:
      lines.append(line)
    if '''"""''' in line:
      var+=1
  code = "\n".join(lines)
  return re.sub('"""(^["])+"""', '', code, flags=re.MULTILINE)


from django.db import migrations
from rankings.models import GAME_OPTIONS

def populate_ranking_type(apps, schema_editor):
    if schema_editor.connection.alias != "default":
        return

    model = apps.get_model("rankings", "RankingType")

    for ranking_type in GAME_OPTIONS:
        new_ranking_type = model.objects.create(value=ranking_type.value)
        new_ranking_type.save()

class Migration(migrations.Migration):
    dependencies = [
        ("rankings", "0004_auto_20210715_1933"),
    ]

    operations = [
        migrations.RunPython(populate_ranking_type, reverse_code=migrations.RunPython.noop)
    ]


import time
import tushare as ts
from tushare.pro import client

# pro = ts.pro_api('4d7357aee9bef99c3b5d61f37a3451535f4cdd6a63fe45e3b0080c4e')
# 设置timeout更长
pro = client.DataApi('4d7357aee9bef99c3b5d61f37a3451535f4cdd6a63fe45e3b0080c4e', timeout=1000)

def get_daily_trade_to_csv(start_date='', end_date='', file_save_path=''):
	trade_cal = get_trade_cal(start_date, end_date)
	write_count = 0
	for index, row in trade_cal.iterrows():
		if write_count == 0:
			mode = 'w'
		else:
			mode = 'a'
		if row[-1] == 1:
			write_count += 1
			daily = pro.daily(trade_date=row[-2])
			print(row[-2])
			daily.to_csv(file_save_path, encoding='utf-8', index=False, header=False, mode=mode)

def get_dailt_trade_util_today(start_date='', file_save_path=''):
	now = time.strftime("%Y%m%d", time.localtime())
	get_daily_trade_to_csv(start_date, now, file_save_path)

def get_trade_cal(start_date='', end_date=''):
	return pro.trade_cal(exchange='', start_date=start_date, end_date=end_date)

# 获取某天的日交易数据
def get_oneday_trade(date=''):
	return pro.daily(trade_date=date)

if __name__ == '__main__':
	# get_today_trade()
	print(get_oneday_trade('20190417'))


import yfinance as yf
import numpy as np

def getTicker(stockList, dataList):
    for i in stockList:
        yfObj = yf.Ticker(i)
        data = yfObj.history(start='2000-01-01', end='2022-12-31')
        dataList.append(data)
    return dataList

def logReturnList(dataList, returnList):
    for i in dataList:
        data = i
        returns = np.log(data['Close'] / data['Close'].shift(1)).dropna()
        returnList.append(returns)
    return returnList

def newTSMAlgo(returnList, performanceList, returns, period=1, short=False):
    if short:
        position1 = returnList[0].rolling(period).mean().map(
            lambda x: -1 if x <= 0 else 1)
        position2 = returnList[1].rolling(period).mean().map(
            lambda x: -1 if x <= 0 else 1)
        performanceList.append(position1, position2)
    else:
        position1 = returnList[0].rolling(period).mean().map(
            lambda x: 0 if x <= 0 else 1)
        position2 = returnList[1].rolling(period).mean().map(
            lambda x: 0 if x <= 0 else 1)
    performance1 = position1.shift(1) * returns
    performance2 = position2.shift(1) * returns
    return [performance1, performance2]


from django.urls import path, include
from rest_framework.routers import DefaultRouter
from rest_framework_simplejwt.views import TokenVerifyView
from rest_framework.routers import DefaultRouter

from .views import (
    UserApi, Token, UsernameCheck,
    showuser, getuser, show, showuserid,
    getfollowing, getfollower, removefollow,
    addfollow, Editaccount, usernameuniqe,
    emailuniqe, topuser, NotiPost, notification
)

router = DefaultRouter()
router.register('edit', Editaccount)
router.register('notification', NotiPost)

urlpatterns = [
    path('', include(router.urls)),
    path('noti/<int:id>/', notification),
    path('topuser/', topuser),
    path('createuser/', UserApi.as_view()),
    path('token/', Token.as_view()),
    path('tokenV/', TokenVerifyView.as_view()),
    path('check/', UsernameCheck.as_view()),
    path('showuser/<str:username>/', showuser),
    path('showuserid/<int:id>/', showuserid),
    path('getuser/<int:id>/', getuser),
    path('getfollowing/<int:id>/', getfollowing),
    path('getfollower/<int:id>/', getfollower),
    path('addfollow/<int:id>/<int:add>/', addfollow),
    path('removefollow/<int:id>/<int:remove>/', removefollow),
    path('usernameuniqe/<str:username>/', usernameuniqe),
    path('emailuniqe/<str:email>/', emailuniqe),
    path('show/', show)
]


import matplotlib.pyplot as plt
import numpy as np
from itertools import chain

def seq2list(seq, mode="wave"):
    out = list(map(int, seq))
    if mode == "wave":
        out = list(chain.from_iterable(zip(out, out)))
    return out

class Plot(object):
    # t = range(40)
    # _t = list(chain.from_iterable(zip(t, t)))
    _t = [0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7,
          8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14,
          15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21,
          21, 22, 22, 23, 23, 24, 24, 25, 25, 26, 26, 27, 27,
          28, 28, 29, 29, 30, 30, 31, 31, 32, 32, 33, 33, 34,
          34, 35, 35, 36, 36, 37, 37, 38, 38, 39, 39]

    def __init__(self, name="$Q_{{{}}}$"):
        self.name = name
        self.n = 0
        self.loc = 0

    def __call__(self, time_series, name=None):
        if name is None:
            name = self.name.format(self.n)
            self.n += 1
        _y = np.array(seq2list(time_series))
        _t = self._t[:len(_y)]
        plt.text(-1.5, self.loc + 0.5, name)
        plt.plot(_t, _y + self.loc)
        self.loc -= 1.2

class Calculate(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, mode="str"):
        inp = map(seq2list, args, [None, None])
        out = map(self.func, *inp)
        if mode == "str":
            out = "".join(map(str, out))
        return out

if __name__ == '__main__':
    cal = Calculate(lambda x, y: x | y)
    print(cal("01010", "01110"))


import numpy as np
import cv2, random, sys, math, cProfile

### Problem 1 code following

def clip(val, minimum, maximum):
    return sorted((minimum, val, maximum))[1]

def leak_filter(img, bright_factor = 2, blend_factor = 0.8, rainbow = False):
    # Now we need to generate the light streak - randomly generating some y = mx + c
    # To start with I'll pick two x values in the central 1/3 of the image for the line to go between

    image_y, image_x, image_channels = img.shape # Get the dimensions of the image

    point_1 = (0, random.randint(2*image_x//5, 3*image_x//5))
    point_2 = (image_y, random.randint(2*image_x//5, 3*image_x//5)) # Pick two random points at the top and bottom of the image

    m = sys.maxsize & 2 + 1
    if point_1[1] != point_2[1]:
        m = image_y/(point_1[1]-point_2[1])
    c = image_y - m*point_2[1] # Work out the equation of the line between them

    # Compare each pixel against the line

    width = 30
    attenuation = 5 # Good values for making this work
    max_hue = 160

    mask = np.zeros(img.shape, np.uint8) # Generate a blank image to act as a mask

    img = np.copy(img)

    for y in range(image_y):
        for x in range(image_x): # Iterate through the image
            dist = abs(c + m*x - y)/math.sqrt(1 + math.pow(m, 2)) # Calculate the distance of between the point (x,y) and the line
            img.itemset((y, x, 0), max(img.item(y, x, 0)/bright_factor, 0))
            img.itemset((y, x, 1), max(img.item(y, x, 1)/bright_factor, 0))
            img.itemset((y, x, 2), max(img.item(y, x, 2)/bright_factor, 0)) # Darken it accordingly
            if dist < width: # If the resulting distance is less than the width of the beam
                new_bright_factor = bright_factor # Get ready to adjust the brightness factor depending on position in the beam
                if not rainbow:
                    if abs(dist) < width/1.5:
                        new_bright_factor *= 2-(1.5*abs(dist)/width) # This makes the centre of the beam brighter
                    mask.itemset((y, x, 0), min(img.item(y, x, 0)*new_bright_factor, 255))
                    mask.itemset((y, x, 1), min(img.item(y, x, 1)*new_bright_factor, 255))
                    mask.itemset((y, x, 2), min(img.item(y, x, 2)*new_bright_factor, 255)) # Copy brightness data into mask, capping at 255
                else: # If we're generating a rainbow, it's a bit more complicated
                    sat_factor = 1
                    if abs(dist) < width/3: # We use a narrower bright streak for this
                        new_bright_factor *= 2-(3*abs(dist)/width)
                        sat_factor = 0.1+0.9*(3*abs(dist)/width)
                    if m*x - y + c < 0:
                        dist = -dist # Work out which side of the line we're on (important for working out hue)
                    pix_bgr = np.uint8([[[img.item(y, x, 0), img.item(y, x, 1), img.item(y, x , 2)]]]) # Get the value of the pixel
                    pix_hsv = cv2.cvtColor(pix_bgr, cv2.COLOR_BGR2HSV) # Convert to HSV (rainbows are easier to make by varying hue)
                    pix_hsv.itemset((0, 0, 0), ((dist+width)/(2*width))*max_hue) # Calculate value for hue
                    pix_hsv.itemset((0, 0, 1), sat_factor*max(150, pix_hsv.item(0, 0, 1))) # Saturation should be at least 150, but also no less than the existing saturation
                    pix_hsv.itemset((0, 0, 2), clip(pix_hsv.item(0, 0, 2)*new_bright_factor, 50, 255)) # Apply brightening effect
                    pix_bgr = cv2.cvtColor(pix_hsv, cv2.COLOR_HSV2BGR) # Convert back to BGR
                    mask.itemset((y, x, 0), pix_bgr.item(0, 0, 0))
                    mask.itemset((y, x, 1), pix_bgr.item(0, 0, 1))
                    mask.itemset((y, x, 2), pix_bgr.item(0, 0, 2)) # Copy pixel data into mask
            elif dist < width + attenuation: # Smooth transition into normal image makes it look more natural
                att_factor = math.pow(bright_factor, 1 - 2*(dist-width)/attenuation) # Function to transition from bright to dark as we move further out
                if not rainbow:
                    mask.itemset((y, x, 0), min(img.item(y, x, 0)*att_factor, 255))
                    mask.itemset((y, x, 1), min(img.item(y, x, 1)*att_factor, 255))
                    mask.itemset((y, x, 2), min(img.item(y, x, 2)*att_factor, 255)) # Copy image data to mask
                else:
                    hue = 0 # Min hue at one end
                    if m*x - y + c > 0:
                        hue = max_hue # Otherwise max hue
                    pix_bgr = np.uint8([[[img.item(y, x, 0), img.item(y, x, 1), img.item(y, x , 2)]]]) # Get pixel data from image
                    pix_hsv = pix_hsv = cv2.cvtColor(pix_bgr, cv2.COLOR_BGR2HSV) # Convert to hsv
                    pix_hsv.itemset((0, 0, 0), hue) # Set hue
                    pix_hsv.itemset((0, 0, 1), max((150/bright_factor)*att_factor, pix_hsv.item(0, 0, 1))) # Saturation decided as before but also attenuating
                    pix_hsv.itemset((0 ,0 ,2), pix_hsv.item(0, 0, 2)*att_factor) # Brightness also attenuates
                    pix_bgr = cv2.cvtColor(pix_hsv, cv2.COLOR_HSV2BGR) # Convert back to BGR
                    mask.itemset((y, x, 0), pix_bgr.item(0, 0, 0))
                    mask.itemset((y, x, 1), pix_bgr.item(0, 0, 1))
                    mask.itemset((y, x, 2), pix_bgr.item(0, 0, 2)) # Copy pixel data into mask
            else:
                mask.itemset((y, x, 0), max(img.item(y, x, 0)/bright_factor, 0))
                mask.itemset((y, x, 1), max(img.item(y, x, 1)/bright_factor, 0))
                mask.itemset((y, x, 2), max(img.item(y, x, 2)/bright_factor, 0)) # Otherwise continue to copy image data through

    #img = cv2.addWeighted(img, 1-blend_factor, mask, blend_factor, 0) # Blend the two images together

    new_img = np.zeros(img.shape, np.uint8)
    for y in range(image_y):
        for x in range(image_x):
            new_img.itemset((y, x, 0), min(255, img.item(y, x, 0)*(1-blend_factor) + mask.item(y, x, 0)*blend_factor))
            new_img.itemset((y, x, 1), min(255, img.item(y, x, 1)*(1-blend_factor) + mask.item(y, x, 1)*blend_factor))
            new_img.itemset((y, x, 2), min(255, img.item(y, x, 2)*(1-blend_factor) + mask.item(y, x, 2)*blend_factor))

    return new_img

def problem1(img, bright_factor = 2, blend_factor = 0.8, rainbow = False):
    output = leak_filter(img, bright_factor, blend_factor, rainbow)
    cv2.imshow("Output - problem 1", output)
    key = cv2.waitKey(0)
    if key == ord('x'):
        cv2.destroyAllWindows()

### Problem 2 code following

def applyMask(img, mask): # Function to apply square mask to a greyscale image
    image_y, image_x = img.shape
    mask_y, mask_x = mask.shape # Get image and mask dimensions
    new_img = np.zeros(img.shape, dtype=np.uint8) # Create array for output image
    for y in range(image_y):
        for x in range(image_x): # Iterate through image pixels
            val = 0 # Variable to store sum as we go through the neighbourhood
            for j in range(mask_y):
                for i in range(mask_x): # Iterate through the size of the mask
                    offset = mask_y//2 # Calculate how far we are from the centre of the mask
                    pixel_y = y + j - offset
                    pixel_x = x + i - offset # Convert that into pixel coords

                    if pixel_y < 0:
                        pixel_y = -pixel_y
                    elif pixel_y > image_y - 1:
                        pixel_y = 2*(image_y-1) - pixel_y
                    if pixel_x < 0:
                        pixel_x = -pixel_x
                    elif pixel_x > image_x -1:
                        pixel_x = 2*(image_x-1) - pixel_x # If the pixel falls outside the image, "reflect" it back into valid coords

                    val += img.item(pixel_y,pixel_x)*mask.item(j,i) # Multiply the pixel by the mask and add it to the su
            new_img.itemset((y,x), int(clip(val, 0, 255))) # Copy the result of the sum into the output image
    return new_img

def pencilFilter(img, blend_factor): # Function to apply a pencil filter to a greyscale image (output image will also be greyscale)
    y_dim, x_dim = img.shape # Get dimensions of the shape

    noise = np.zeros(img.shape, dtype=np.uint8) # Generate empty image to use as noise mask

    for y in range(y_dim):
        for x in range(x_dim):
            noise.itemset((y, x), np.clip((1+np.random.normal(0, 0.1))*img.item(y, x), 0, 255)) # Generate random gaussian noise and merge with source

    # motion_blur_filter = np.array([[0, 0, 0, 0, 1/5],
    #                             [0, 0, 0, 1/5, 0],
    #                             [0, 0, 1/5, 0, 0],
    #                             [0, 1/5, 0, 0, 0],
    #                             [1/5, 0, 0, 0, 0]]) # Image filter for motion blur (just blurs along a line)

    motion_blur_filter = np.array([[0, 0, 0, 0, 0, 0, 1./14.],
                                   [0, 0, 0, 0, 0, 1./7., 0],
                                   [0, 0, 0, 0, 1./7., 0, 0],
                                   [0, 0, 0, 2./7., 0, 0, 0],
                                   [0, 0, 1./7., 0, 0, 0, 0],
                                   [0, 1./7., 0, 0, 0 ,0, 0],
                                   [1./14., 0, 0, 0, 0, 0, 0]], dtype=np.float64) # Image filter for motion blur (just blurs along a line)

    final_mask = cv2.filter2D(noise, 0, motion_blur_filter) # Convolves noisy image with motion blur to create effect

    new_img = np.zeros(img.shape, np.uint8) # Create new output image
    for y in range(y_dim):
        for x in range(x_dim): # Iterate through dimensions of source image
            new_img.itemset((y, x), min(255, img.item(y, x)*(1-blend_factor) + final_mask.item(y, x)*blend_factor))
            # Blend the images based on a blending factor

    return new_img

def final_pencil_filter(img, blend_factor=0.6, colour=False):
    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    y_dim, x_dim = img.shape

    if colour:
        channels = random.sample([0, 1, 2], 2)
        channel_1 = pencilFilter(img, blend_factor)
        channel_2 = pencilFilter(img, blend_factor)
        imgs = [channel_1, channel_2]
        new_img = np.zeros(img.shape + (3,), np.uint8)
        for y in range(y_dim):
            for x in range(x_dim):
                for c in range(2):
                    new_img.itemset((y, x, channels[c]), imgs[c].item(y, x))
    else:
        new_img = pencilFilter(img, blend_factor)

    return new_img

def problem2(img, blend_factor=0.6, colour=False):
    output = final_pencil_filter(img, blend_factor, colour)
    cv2.imshow("Output - problem 2", output)
    key = cv2.waitKey(0)
    if key == ord('x'):
        cv2.destroyAllWindows()

### Problem 3 code following

gaussians = {}

def gaussian(x, sigma=1, mu=0):
    if (x, sigma, mu) in gaussians:
        return gaussians[(x, sigma, mu)]
    else:
        value = (1/(sigma*((2*math.pi)**0.5)))*math.e**(-0.5*((x-mu)**2)/(sigma**2))
        gaussians[(x, sigma, mu)] = value
        return value

def bilateralFilter(img, colour_sigma, space_sigma):
    output = np.zeros(img.shape, np.uint8) # Create new image for output (we can't do this in place)
    y_dim = img.shape[0]
    x_dim = img.shape[1] # Get dimensions of image
    #d = max(colour_sigma, space_sigma)
    d = 3
    for y in range(y_dim):
        for x in range(x_dim): # Iterate through every pixel
            bilateral_numerator_BGR = [0, 0, 0]
            bilateral_denominator_BGR = [0, 0, 0] # Variables for numerator and denominator (per lecture 5 slide 31)
            pixel = [img.item(y, x, 0), img.item(y, x, 1), img.item(y, x, 2)] # Grab the pixel data here (no need to redo this for every neighbour)
            for j in range(y-d, y+d):
                for i in range(x-d, x+d): # Get neighbourhood
                    if j < 0: # Series of if statements "reflect" coords outside of the image back within the bounds of the image
                        j = -j
                    if i < 0:
                        i = -i
                    if j > y_dim-1:
                        j = 2*(y_dim-1) - j # Equivalent to j = y_dim - (j - y_dim)
                    if i > x_dim-1:
                        i = 2*(x_dim-1) - i
                    cart_dist = ((y-j)**2 + (x-i)**2)**0.5 # Calculate the cartesian distance between pixels (same for all channels)
                    gauss_dist = gaussian(cart_dist, space_sigma) # Get the output of the gaussian function for the distance component
                    for channel in range(3): # Handle each colour channel separately
                        coords = (j, i, channel)
                        weight = gauss_dist*gaussian(abs(img.item(coords)-pixel[channel]), colour_sigma) # Get the gaussian value for the intensity difference
                        bilateral_denominator_BGR[channel] += weight
                        bilateral_numerator_BGR[channel] += weight*img.item(coords) # Add to the summations on the top and bottom of the fraction for I_output
            for channel in range(3): # Loop to copy calculated data into the new image
                output.itemset((y, x, channel), bilateral_numerator_BGR[channel]//bilateral_denominator_BGR[channel])
    return output

def contrast_stretch(img, a=255, b=0):
    img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    h, s, v = cv2.split(img_hsv)
    c = np.amax(v)
    d = np.amin(v)
    for y in range(v.shape[0]):
        for x in range(v.shape[1]):
            v.itemset((y, x), (v.item(y, x)-c)*((a-b)/(c-d))+a)
    img_hsv = cv2.merge([h, s, v])
    return cv2.cvtColor(img_hsv, cv2.COLOR_HSV2BGR)

def beauty_filter(img, colour_sigma=20, space_sigma=10, mode="warm"):

    image_y, image_x, image_channels = img.shape # Get dimensions of image to work with

    new_img = bilateralFilter(img, colour_sigma, space_sigma) # Apply bilaterial filtering with given parameters

    new_img = contrast_stretch(new_img)

    red_adjust = {}
    blue_adjust = {} # Create lookup dictionaries for colour values

    if mode == "cold": # Same as warm, but drop red and increase blue
        red_pow = 0.96
        blue_pow = 1.04
        for i in range(256):
            red_adjust[i] = max(math.pow(i, red_pow), 0)
            blue_adjust[i] = min(math.pow(i, blue_pow), 255)
    else: # Generate colour values to "warm" image i.e. drop blue and increase red
        red_pow = 1.04
        blue_pow = 0.96 # Some powers for altering values
        for i in range(256): # For every possible red value
            red_adjust[i] = min(math.pow(i, red_pow), 255) # Raise to power and cap at 255
            blue_adjust[i] = max(math.pow(i, blue_pow), 0) # Raise to power and cap at 0

    for y in range(image_y):
        for x in range(image_x): # Iterate over the entire image
            new_img.itemset((y, x, 0), blue_adjust[new_img.item(y, x, 0)])
            new_img.itemset((y, x, 2), red_adjust[new_img.item(y, x, 2)]) # Replace the pixel value from the lookup table

    return new_img

def problem3(img, colour_sigma=20, space_sigma=10, mode="warm"):
    output = beauty_filter(img, colour_sigma, space_sigma, mode)
    cv2.imshow("Output - problem 3", output)
    key = cv2.waitKey(0)
    if key == ord('x'):
        cv2.destroyAllWindows()

### Problem 4 code following

def xy_to_polar(xy, centre):
    dx = xy[0] - centre[0]
    dy = xy[1] - centre[1]
    r = math.sqrt(dx**2 + dy**2)
    theta = math.atan2(dy, dx)
    return [r, theta]

def polar_to_xy(rtheta, centre):
    dx = rtheta[0] * math.cos(rtheta[1])
    dy = rtheta[0] * math.sin(rtheta[1])
    x = dx + centre[0]
    y = dy + centre[1]
    return [x, y]

def gaussian_low_pass(img, sigma):
    channels = cv2.split(img)
    for i in range(len(channels)):
        c = channels[i]
        c_fourier = np.fft.fftshift(np.fft.fft2(c))
        img_centre = (c_fourier.shape[0]//2, c_fourier.shape[1]//2)
        for y in range(c_fourier.shape[0]):
            for x in range(c_fourier.shape[1]):
                dx = x - img_centre[1]
                dy = y - img_centre[0]
                G = math.exp(-(dx**2 + dy**2)/2*(sigma**2))
                c_fourier.itemset((y, x), c_fourier.item(y, x)*G)
        channels[i] = np.uint8(np.clip(abs(np.fft.ifft2(np.fft.ifftshift(c_fourier))), 0, 255))
    return cv2.merge([channels[0], channels[1], channels[2]])

def subtract_image(img1, img2):
    return np.uint8(abs(np.subtract(img1, img2)))

def swirl_filter(img, angle=(3*math.pi/4), radius=150, interpolation="nn", prefilter=False):
    image_y, image_x, image_channels = img.shape
    output = np.zeros(img.shape, dtype=np.uint8)
    img_centre = (image_x//2, image_y//2)
    if prefilter:
        img = gaussian_low_pass(img, 0.0125)
    for y in range(image_y):
        for x in range(image_x):
            src_coords = [x,y]
            dist_from_centre = math.sqrt((x-img_centre[0])**2 + (y-img_centre[1])**2)
            if dist_from_centre < radius:
                as_polar = xy_to_polar((x, y), img_centre)
                as_polar[1] += angle*((1-(dist_from_centre/radius))**1.5)
                src_coords = polar_to_xy(as_polar, img_centre)
            for c in range(image_channels):
                if interpolation == "nn":
                    input_x = clip(round(src_coords[0]), 0, image_x-1)
                    input_y = clip(round(src_coords[1]), 0, image_y-1)
                    output.itemset((y, x, c), img.item(int(input_y), int(input_x), c))
                else:
                    x1 = int(clip(math.floor(src_coords[0]), 0, image_x-1))
                    y1 = int(clip(math.floor(src_coords[1]), 0, image_y-1))
                    x2 = int(clip(math.ceil(src_coords[0]), 0, image_x-1))
                    y2 = int(clip(math.ceil(src_coords[1]), 0, image_y-1))
                    input_x = clip(src_coords[0], 0, image_x-1)
                    input_y = clip(src_coords[1], 0, image_y-1)
                    if x2 != x1:
                        I_y1 = img.item(y1, x1, c)*(x2-input_x)/(x2-x1) + img.item(y1, x2, c)*(input_x-x1)/(x2-x1)
                        I_y2 = img.item(y2, x1, c)*(x2-input_x)/(x2-x1) + img.item(y2, x2, c)*(input_x-x1)/(x2-x1)
                    else:
                        I_y1 = img.item(y1, x1, c)
                        I_y2 = img.item(y2, x1, c)
                    if y2 != y1:
                        I = I_y1*(y2-input_y)/(y2-y1) + I_y2*(input_y-y1)/(y2-y1)
                    else:
                        I = I_y1
                    output.itemset((y, x, c), I)

    return output

def problem4(img, angle=(3*math.pi/4), radius=150):
    filter_nn = swirl_filter(img, angle, radius)
    cv2.imshow("Swirl filter - NN interpolation", filter_nn)
    key = cv2.waitKey(0)
    if key == ord('x'):
        cv2.destroyAllWindows()

    filter_bi = swirl_filter(img, angle, radius, interpolation='bi')
    cv2.imshow("Swirl filter - bilinear interpolation", filter_bi)
    key = cv2.waitKey(0)
    if key == ord('x'):
        cv2.destroyAllWindows()

    filter_nn_pre = swirl_filter(img, angle, radius, prefilter=True)
    cv2.imshow("Swirl filter - NN interpolation + prefiltering", filter_nn_pre)
    key = cv2.waitKey(0)
    if key == ord('x'):
        cv2.destroyAllWindows()

    filter_bi_pre = swirl_filter(img, angle, radius, interpolation='bi', prefilter=True)
    cv2.imshow("Swirl filter - bilinear interpolation + prefiltering", filter_bi_pre)
    key = cv2.waitKey(0)
    if key == ord('x'):
        cv2.destroyAllWindows()

    filter_invert = swirl_filter(filter_bi, -angle, radius, interpolation='bi', prefilter=True)
    cv2.imshow("Swirl filter - bilinear interpolation (inversed)", filter_invert)
    key = cv2.waitKey(0)
    if key == ord('x'):
        cv2.destroyAllWindows()

    filter_invert_diff = subtract_image(img, filter_invert)
    cv2.imshow("Swirl filter - difference between inverse and original", filter_invert_diff)
    key = cv2.waitKey(0)
    if key == ord('x'):
        cv2.destroyAllWindows()

if __name__ == "__main__":
    problem = int(sys.argv[1])
    output = np.zeros((1,1,3), np.uint8)
    if len(sys.argv) < 3:
        print("Specify image file!")
    args = sys.argv[2:]
    img = cv2.imread(args[0], cv2.IMREAD_COLOR)
    if problem == 1:
        if len(args) == 1:
            print("No filter params given, using defaults.")
            output = leak_filter(img)
        elif len(args) == 2:
            print("Only rainbow param given, using defaults for others.")
            output = leak_filter(img, rainbow=(args[1]=="rainbow"))
        elif len(args) < 4:
            print("Please give both brightness/blending parameters or none at all.")
            sys.exit()
        else:
            output = leak_filter(img, float(args[2]), float(args[3]), (args[1]=="rainbow"))
    elif problem == 2:
        if len(args) == 1:
            print("No filter params given, using defaults.")
            output = final_pencil_filter(img)
        elif len(args) == 2:
            print("Only blend factor given, defaulting to b/w.")
            output = final_pencil_filter(img, float(args[1]))
        else:
            output = final_pencil_filter(img, float(args[1]), args[2] == "colour")
    elif problem == 3:
        if len(args) == 1:
            print("No filter params given, using defaults.")
            output = beauty_filter(img)
        elif len(args) == 2:
            print("Give both sigma values or neither.")
            sys.exit()
        elif len(args) == 3:
            print("No colour temp params given, defaulting to warm")
            output = beauty_filter(img, int(args[1]), int(args[2]))
        else:
            output = beauty_filter(img, int(args[1]), int(args[2]), args[3])
    elif problem == 4:
        if len(args) == 1:
            print("No filter params given, using defaults.")
            output = swirl_filter(img)
        elif len(args) == 2:
            print("Provide both swirl parameters or none at all")
            sys.exit()
        elif len(args) == 3:
            print("No interpolation/prefilter params given, defaulting to nearest neighbour and no prefiltering")
            output = swirl_filter(img, float(args[1]), int(args[2]))
        elif len(args) == 4:
            print("Defaulting to no prefiltering")
            output = swirl_filter(img, float(args[1]), int(args[2]), args[3])
        else:
            output = swirl_filter(img, float(args[1]), int(args[2]), args[3], args[4] == "prefilter")

    cv2.imshow("Output - final image", output)
    key = cv2.waitKey(0)
    if key == ord('x'):
        cv2.destroyAllWindows()