import os
import random
import shutil
import zipfile

from datetime import datetime
from inspect import isclass

from django.db import models
from django.db.models.signals import post_init
from django.conf import settings
from django.core.files.base import ContentFile
from django.core.urlresolvers import reverse
from django.template.defaultfilters import slugify
from django.utils.functional import curry
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from django.contrib.sites.managers import CurrentSiteManager
from django.contrib.sites.models import Site
from django_extensions.db.models import TimeStampedModel
from django_extensions.db.fields import AutoSlugField

from darkroom.managers import *
from directory.models import Town, Place
from taggit.managers import TaggableManager
from taggit.models import Tag

# Required PIL classes may or may not be available from the root namespace
# depending on the installation method used.
try:
    import Image
    import ImageFile
    import ImageFilter
    import ImageEnhance
except ImportError:
    try:
        from PIL import Image
        from PIL import ImageFile
        from PIL import ImageFilter
        from PIL import ImageEnhance
    except ImportError:
        raise ImportError(
            _(
                "Photologue was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path."
            )
        )

from utils import EXIF
from utils.image_entropy import image_entropy
from utils.reflection import add_reflection
from utils.watermark import apply_watermark

# Path to sample image
SAMPLE_IMAGE_PATH = getattr(
    settings,
    "SAMPLE_IMAGE_PATH",
    os.path.join(os.path.dirname(__file__), "res", "sample.jpg"),
)  # os.path.join(settings.PROJECT_PATH, 'darkroom', 'res', 'sample.jpg'

# Modify image file buffer size.
ImageFile.MAXBLOCK = getattr(settings, "DARKROOM_MAXBLOCK", 256 * 2 ** 10)

# Photologue image path relative to media root
DARKROOM_DIR = getattr(settings, "DARKROOM_DIR", "darkroom/")

# Look for user function to define file paths
DARKROOM_PATH = getattr(settings, "DARKROOM_PATH", None)
if DARKROOM_PATH is not None:
    if callable(DARKROOM_PATH):
        get_storage_path = DARKROOM_PATH
    else:
        parts = DARKROOM_PATH.split(".")
        module_name = ".".join(parts[:-1])
        module = __import__(module_name)
        get_storage_path = getattr(module, parts[-1])
else:

    def get_storage_path(instance, filename):
        return os.path.join(DARKROOM_DIR, "photos", filename)


# Quality options for JPEG images
JPEG_QUALITY_CHOICES = (
    (30, _("Very Low")),
    (40, _("Low")),
    (50, _("Medium-Low")),
    (60, _("Medium")),
    (70, _("Medium-High")),
    (80, _("High")),
    (90, _("Very High")),
    (100, _("Unchanged")),
)

# choices for new crop_anchor field in Photo
CROP_ANCHOR_CHOICES = (
    ("top", _("Top")),
    ("right", _("Right")),
    ("bottom", _("Bottom")),
    ("left", _("Left")),
    ("center", _("Center")),
    ("smart", _("Smart (Default)")),
)

IMAGE_TRANSPOSE_CHOICES = (
    ("FLIP_LEFT_RIGHT", _("Flip left to right")),
    ("FLIP_TOP_BOTTOM", _("Flip top to bottom")),
    ("ROTATE_90", _("Rotate 90 degrees counter-clockwise")),
    ("ROTATE_270", _("Rotate 90 degrees clockwise")),
    ("ROTATE_180", _("Rotate 180 degrees")),
)

WATERMARK_STYLE_CHOICES = (
    ("tile", _("Tile")),
    ("scale", _("Scale")),
)

IMAGE_ORIENTATION_CHOICES = (
    (0, _("no rotation")),
    (2, _("Rotate 90 degrees counter-clockwise")),
    (3, _("Rotate 180 degrees")),
    (4, _("Rotate 90 degrees clockwise")),
)

# see http://www.impulseadventure.com/photo/exif-orientation.html
IMAGE_EXIF_ORIENTATION_MAP = {
    1: 0,
    8: 2,
    3: 3,
    6: 4,
}

# Slideshow size choices, as dictated by the Soundslides application
# default ouputs.
SLIDESHOW_SIZE_CHOICES = (
    ("425x356", _("Small")),
    ("620x533", _("Standard")),
    ("800x596", _("Large")),
)


def _compare_entropy(start_slice, end_slice, slice, difference):
    """
    Calculate the entropy of two slices (from the start and end of an axis),
    returning a tuple containing the amount that should be added to the start
    and removed from the end of the axis.

    """
    start_entropy = image_entropy(start_slice)
    end_entropy = image_entropy(end_slice)
    if end_entropy and abs(start_entropy / end_entropy - 1) < 0.01:
        # Less than 1% difference, remove from both sides.
        if difference >= slice * 2:
            return slice, slice
        half_slice = slice // 2
        return half_slice, slice - half_slice
    if start_entropy > end_entropy:
        return 0, slice
    else:
        return slice, 0


# Prepare a list of image filters
filter_names = []
for n in dir(ImageFilter):
    klass = getattr(ImageFilter, n)
    if (
        isclass(klass)
        and issubclass(klass, ImageFilter.BuiltinFilter)
        and hasattr(klass, "name")
    ):
        filter_names.append(klass.__name__)
IMAGE_FILTERS_HELP_TEXT = _(
    'Chain multiple filters using the following pattern "FILTER_ONE->FILTER_TWO->FILTER_THREE". Image filters will be applied in order. The following filters are available: %s.'
    % (", ".join(filter_names))
)

# Next five functions streamline expanding zip files to folders
def path_name(name):
    return os.path.split(name)[0]


def file_name(name):
    return os.path.split(name)[1]


def path_names(names):
    return (name for name in names if path_name(name) and not file_name(name))


def file_names(names):
    return (name for name in names if file_name(name))


def extract(zipfile):
    SLIDE_PATH = settings.MEDIA_ROOT + DARKROOM_DIR + "slideshows/"
    names = zipfile.namelist()
    for name in path_names(names):
        if not os.path.exists(SLIDE_PATH + name):
            if not name.find("__MACOSX"):  # do not process meta or hidden files
                continue
            else:
                os.mkdir(SLIDE_PATH + name)
    for name in file_names(names):
        if name.startswith("__MACOSX"):  # do not process meta or hidden files
            continue
        else:
            outfile = file(SLIDE_PATH + name, "wb")
            outfile.write(zipfile.read(name))
        outfile.close()
    return names[0].split("/")[0]  # Return the root folder for the model's sake


# End zip file to folder functions


class MediaMetadata(TimeStampedModel):
    title = models.CharField(_("title"), max_length=100)
    slug = AutoSlugField(_("slug"), populate_from="title", editable=True)
    description = models.TextField(_("description"), blank=True, null=True)
    published = models.BooleanField(
        _("published"),
        default=False,
        help_text=_("Published items will appear on all parts of the site."),
    )
    published_on = models.DateTimeField(_("date published"), blank=True)
    weight = models.IntegerField(_("Weight"), blank=True, null=True, max_length=2)
    towns = models.ManyToManyField(Town, blank=True, null=True)
    tags = TaggableManager(blank=True)
    auto_tag = models.BooleanField(_("auto-tag"), default=True)

    class Meta:
        unique_together = (("title", "slug"),)
        abstract = True

    def do_auto_tag(self):
        """
        Performs the auto-tagging work if necessary.
        
        Returns True if an additional save is required, False otherwise.
        """

        found = False
        if self.auto_tag:
            import re

            # don't clobber any existing tags!
            try:
                existing_ids = [t.id for t in self.tags.all()]
                unused = Tag.objects.all()
                unused = unused.exclude(id__in=existing_ids)
            except:
                unused = Tag.objects.all()

            for tag in unused:
                regex = re.compile(r"\b%s\b" % tag.name, re.I)
                if regex.search(self.description) or regex.search(self.title):
                    self.tags.add(tag)
                    found = True

        return found

    def save(self, *args, **kwargs):

        super(TimeStampedModel, self).save(*args, **kwargs)

        requires_save = self.do_auto_tag()

        if requires_save:
            super(TimeStampedModel, self).save()


class Photographer(TimeStampedModel):
    name = models.CharField(_("name"), max_length=100)
    title = models.CharField(_("Title"), blank=True, null=True, max_length=100)
    slug = AutoSlugField(_("slug"), populate_from="name", editable=True)
    user = models.ForeignKey(User, blank=True, null=True)
    date_added = models.DateTimeField(
        _("date added"), default=datetime.now(), editable=False
    )

    class Meta:
        ordering = ("name",)
        get_latest_by = "date_added"

    def __unicode__(self):
        return self.name

    def __str__(self):
        return self.__unicode__()

    def get_absolute_url(self):
        return reverse("dr-photographer", args=[self.slug])


class Slideshow(MediaMetadata):
    preview = models.ImageField(
        _("preview"),
        upload_to=get_storage_path,
        help_text="Slideshow previews are a standard 410 pixels wide by 276 pixels high",
    )
    folder = models.CharField(_("folder"), max_length=200, null=True, blank=True)
    size = models.CharField(
        _("size"), max_length=9, default="620x533", choices=SLIDESHOW_SIZE_CHOICES
    )

    class Meta:
        ordering = ["created"]
        get_latest_by = "created"
        verbose_name = _("slideshow")
        verbose_name_plural = _("slideshows")

    def __unicode__(self):
        return self.title

    def __str__(self):
        return self.__unicode__()

    def get_absolute_url(self):
        return reverse("dr-slideshow-detail", args=[self.slug])

    def path(self):
        return

    def delete(self):
        try:
            os.remove(self.folder)
        except:
            pass
        super(Slideshow, self).delete()


class SlideshowUpload(models.Model):
    zip_file = models.FileField(
        _("slideshow file (.zip)"),
        upload_to=DARKROOM_DIR + "/slideshows",
        help_text=_("Select a .zip file with files to create a new Slideshow."),
    )
    slideshow = models.ForeignKey(Slideshow, null=True, blank=True)

    class Meta:
        verbose_name = _("slideshow upload")
        verbose_name_plural = _("slideshow uploads")

    def save(self, *args, **kwargs):
        super(SlideshowUpload, self).save(*args, **kwargs)
        self.process_zipfile()
        super(SlideshowUpload, self).delete()

    def process_zipfile(self):
        if os.path.isfile(self.zip_file.path):
            # TODO: implement try-except here
            zip = zipfile.ZipFile(self.zip_file.path)
            bad_file = zip.testzip()
            if bad_file:
                raise Exception('"%s" in the .zip archive is corrupt.' % bad_file)
            folder = extract(zip)
            slideshow = self.slideshow
            slideshow.folder = extract(zip)  # Extract the zip contents,
            # save the folder name to the model
            slideshow.save()
            zip.close()


class Webcam(models.Model):
    title = models.CharField(_("title"), max_length=80)
    slug = AutoSlugField(_("slug"), populate_from="title", editable=True)
    town = models.ForeignKey(Town, blank=True, null=True)
    image_file = models.CharField(_("webcam image location"), max_length=255)
    thumb_file = models.CharField(
        _("webcam thumbnail location"), max_length=255, blank=True, null=True
    )
    image_width = models.IntegerField(
        _("image width"), max_length=5, blank=True, null=True
    )
    image_height = models.IntegerField(
        _("image height"), max_length=5, blank=True, null=True
    )
    alt_text = models.CharField(_("alt text"), max_length=255, blank=True, null=True)
    description = models.TextField(_("description"), blank=True, null=True)
    ordering = models.IntegerField(_("order"), blank=True, null=True, max_length=3)

    class Meta:
        verbose_name = _("webcam")
        verbose_name_plural = _("webcams")
        ordering = ["ordering"]

    def __unicode__(self):
        return u"%s webcam" % self.title

    def get_absolute_url(self):
        return reverse("dr-webcam-detail", args=[self.slug])


class Gallery(MediaMetadata):
    preview = models.ImageField(
        _("graphic"),
        upload_to=DARKROOM_DIR + "/gallery-graphics",
        help_text="Gallery previews are a displayed at the top of the page if they exist.",
        null=True,
        blank=True,
    )
    photos = models.ManyToManyField(
        "Photo",
        related_name="gallery_photos",
        verbose_name=_("photos"),
        null=True,
        blank=True,
    )
    slideshow = models.ForeignKey(Slideshow, null=True, blank=True)
    photographer = models.ForeignKey(Photographer, null=True, blank=True)
    sites = models.ManyToManyField(Site)

    objects = MediaManager()
    on_site = CurrentSiteManager("sites")

    class Meta:
        ordering = ["-created"]
        get_latest_by = "created"
        verbose_name = _("gallery")
        verbose_name_plural = _("galleries")

    def __unicode__(self):
        return self.title

    @models.permalink
    def get_absolute_url(self, site=""):
        return (
            "dr-gallery-detail",
            (),
            {
                "year": self.published_on.year,
                "month": self.published_on.strftime("%b").lower(),
                "day": self.published_on.day,
                "slug": self.slug,
            },
        )

    def last_photo(self):
        return self.photos.latest()

    def latest(self, limit=0, published=True):
        if limit == 0:
            limit = self.photo_count()
        if published:
            return self.published()[:limit]
        else:
            return self.photos.all()[:limit]

    def sample(self, count=0, published=True):
        if count == 0 or count > self.photo_count():
            count = self.photo_count()
        if published:
            photo_set = self.published()
        else:
            photo_set = self.photos.all()
        return random.sample(photo_set, count)

    def photo_count(self, published=True):
        if published:
            return self.published_photos().count()
        else:
            return self.photos.all().count()

    photo_count.short_description = _("count")

    def published_photos(self):
        return self.photos.filter(published=True).order_by("created")

    def save(self, *args, **kwargs):
        super(Gallery, self).save(*args, **kwargs)
        if self.photos:
            if self.published:
                for p in self.photos.all():
                    p.published = True
                    p.save()
            else:
                for p in self.photos.all():
                    p.published = False
                    p.save()


class GalleryUpload(models.Model):
    zip_file = models.FileField(
        _("images file (.zip)"),
        upload_to=DARKROOM_DIR + "/temp",
        help_text=_("Select a .zip file of images to upload into a new Gallery."),
    )
    gallery = models.ForeignKey(
        Gallery,
        null=True,
        blank=True,
        help_text=_(
            "Select a gallery to add these images to. leave this empty to create a new gallery from the supplied title."
        ),
    )
    title = models.CharField(
        _("title"),
        blank=True,
        null=True,
        help_text=_("Used to create a gallery if none is specified."),
        max_length=140,
    )
    description = models.TextField(
        _("description"),
        blank=True,
        help_text=_("Descrpition will be added to all photos."),
    )
    photographer = models.ForeignKey(Photographer, null=True, blank=True)

    class Meta:
        verbose_name = _("gallery upload")
        verbose_name_plural = _("gallery uploads")

    def save(self):
        super(GalleryUpload, self).save(*args, **kwargs)
        self.process_zipfile()
        super(GalleryUpload, self).delete()

    def process_zipfile(self):
        if os.path.isfile(self.zip_file.path):
            # TODO: implement try-except here
            zip = zipfile.ZipFile(self.zip_file.path)
            bad_file = zip.testzip()
            if bad_file:
                raise Exception('"%s" in the .zip archive is corrupt.' % bad_file)
            count = 1
            if self.gallery:
                gallery = self.gallery
            else:
                gallery = Gallery.objects.create(
                    title=self.title,
                    slug=slugify(self.title),
                    description=self.description,
                    photographer=self.photographer,
                )
            from cStringIO import StringIO

            for filename in zip.namelist():
                if filename.startswith("__"):  # do not process meta files
                    continue
                data = zip.read(filename)
                if len(data):
                    try:
                        # the following is taken from django.newforms.fields.ImageField:
                        #  load() is the only method that can spot a truncated JPEG,
                        #  but it cannot be called sanely after verify()
                        trial_image = Image.open(StringIO(data))
                        trial_image.load()
                        # verify() is the only method that can spot a corrupt PNG,
                        #  but it must be called immediately after the constructor
                        trial_image = Image.open(StringIO(data))
                        trial_image.verify()
                    except Exception:
                        # if a "bad" file is found we just skip it.
                        continue
                    while 1:
                        title = " ".join([gallery.title, str(count)])
                        slug = slugify(title)
                        try:
                            p = Photo.objects.get(slug=slug)
                        except Photo.DoesNotExist:
                            photo = Photo(
                                title=title,
                                slug=slug,
                                published_on=gallery.published_on,
                                description=self.description,
                                published=gallery.published,
                                photographer=self.photographer,
                            )
                            photo.image.save(filename, ContentFile(data))
                            # If EXIF data exists for comment and headline, set appropriate image info
                            # if photo.EXIF['Image ImageDescription']:
                            #    photo.description=unicode(photo.EXIF['Image ImageDescription'])
                            # else:
                            #    pass
                            # if photo.EXIF['Image DocumentName']:
                            #    photo.title=photo.EXIFunicode(['Image DocumentName'])
                            #    photo.slug=slugify(photo.EXIF['Image DocumentName'])
                            # else:
                            #    pass
                            photo.save()
                            gallery.photos.add(photo)
                            count = count + 1
                            break
                        count = count + 1
            zip.close()


class ImageModel(MediaMetadata):
    view_count = models.PositiveIntegerField(default=0, editable=False)
    crop_from = models.CharField(
        _("crop from"),
        blank=True,
        max_length=10,
        default="center",
        choices=CROP_ANCHOR_CHOICES,
    )
    effect = models.ForeignKey(
        "PhotoEffect",
        null=True,
        blank=True,
        related_name="%(class)s_related",
        verbose_name=_("effect"),
    )
    orientation = models.IntegerField(
        _("Orientation"), choices=IMAGE_ORIENTATION_CHOICES, null=True, blank=True
    )

    class Meta:
        abstract = True

    @property
    def EXIF(self):
        try:
            return EXIF.process_file(open(self.image.path, "rb"))
        except:
            try:
                return EXIF.process_file(open(self.image.path, "rb"), details=False)
            except:
                return {}

    def admin_thumbnail(self):
        func = getattr(self, "get_admin_thumbnail_url", None)
        if func is None:
            return _('An "admin_thumbnail" photo size has not been defined.')
        else:
            if hasattr(self, "get_absolute_url"):
                return u'<a href="%s"><img src="%s"></a>' % (
                    self.get_absolute_url(),
                    func(),
                )
            else:
                return u'<a href="%s"><img src="%s"></a>' % (self.image.url, func())

    admin_thumbnail.short_description = _("Thumbnail")
    admin_thumbnail.allow_tags = True

    def cache_path(self):
        return os.path.join(os.path.dirname(self.image.path), "cache")

    def cache_url(self):
        return "/".join([os.path.dirname(self.image.url), "cache"])

    def image_filename(self):
        return os.path.basename(self.image.path)

    def _get_filename_for_size(self, size):
        size = getattr(size, "name", size)
        base, ext = os.path.splitext(self.image_filename())
        return "".join([base, "_", size, ext])

    def _get_SIZE_photosize(self, size):
        return PhotoSizeCache().sizes.get(size)

    def _get_SIZE_size(self, size):
        photosize = PhotoSizeCache().sizes.get(size)
        if not self.size_exists(photosize):
            self.create_size(photosize)
        return Image.open(self._get_SIZE_filename(size)).size

    def _get_SIZE_url(self, size):
        photosize = PhotoSizeCache().sizes.get(size)
        if not self.size_exists(photosize):
            self.create_size(photosize)
        if photosize.increment_count:
            self.view_count += 1
            self.save(update=True)
        return "/".join([self.cache_url(), self._get_filename_for_size(photosize.name)])

    def _get_SIZE_filename(self, size):
        photosize = PhotoSizeCache().sizes.get(size)
        return os.path.join(
            self.cache_path(), self._get_filename_for_size(photosize.name)
        )

    def add_accessor_methods(self, *args, **kwargs):
        for size in PhotoSizeCache().sizes.keys():
            setattr(self, "get_%s_size" % size, curry(self._get_SIZE_size, size=size))
            setattr(
                self,
                "get_%s_photosize" % size,
                curry(self._get_SIZE_photosize, size=size),
            )
            setattr(self, "get_%s_url" % size, curry(self._get_SIZE_url, size=size))
            setattr(
                self,
                "get_%s_filename" % size,
                curry(self._get_SIZE_filename, size=size),
            )

    def size_exists(self, photosize):
        func = getattr(self, "get_%s_filename" % photosize.name, None)
        if func is not None:
            if os.path.isfile(func()):
                return True
        return False

    def resize_image(self, im, photosize):
        cur_width, cur_height = im.size
        new_width, new_height = photosize.size
        if photosize.crop:
            ratio = max(float(new_width) / cur_width, float(new_height) / cur_height)
            x = cur_width * ratio
            y = cur_height * ratio
            xd = abs(new_width - x)
            yd = abs(new_height - y)
            x_diff = int(xd / 2)
            y_diff = int(yd / 2)
            if self.crop_from == "top":
                box = (int(x_diff), 0, int(x_diff + new_width), new_height)
            elif self.crop_from == "left":
                box = (0, int(y_diff), new_width, int(y_diff + new_height))
            elif self.crop_from == "bottom":
                box = (
                    int(x_diff),
                    int(yd),
                    int(x_diff + new_width),
                    int(y),
                )  # y - yd = new_height
            elif self.crop_from == "right":
                box = (
                    int(xd),
                    int(y_diff),
                    int(x),
                    int(y_diff + new_height),
                )  # x - xd = new_width
            elif self.crop_from == "smart":
                left = top = 0
                right, bottom = x, y
                while xd:
                    slice = min(xd, max(xd // 5, 10))
                    start = im.crop((left, 0, left + slice, y))
                    end = im.crop((right - slice, 0, right, y))
                    add, remove = _compare_entropy(start, end, slice, xd)
                    left += add
                    right -= remove
                    xd = xd - add - remove
                while yd:
                    slice = min(yd, max(yd // 5, 10))
                    start = im.crop((0, top, x, top + slice))
                    end = im.crop((0, bottom - slice, x, bottom))
                    add, remove = _compare_entropy(start, end, slice, yd)
                    top += add
                    bottom -= remove
                    yd = yd - add - remove
                box = (left, top, right, bottom)
            else:
                box = (
                    int(x_diff),
                    int(y_diff),
                    int(x_diff + new_width),
                    int(y_diff + new_height),
                )
            im = im.resize((int(x), int(y)), Image.ANTIALIAS).crop(box)
        else:
            if not new_width == 0 and not new_height == 0:
                ratio = min(
                    float(new_width) / cur_width, float(new_height) / cur_height
                )
            else:
                if new_width == 0:
                    ratio = float(new_height) / cur_height
                else:
                    ratio = float(new_width) / cur_width
            new_dimensions = (
                int(round(cur_width * ratio)),
                int(round(cur_height * ratio)),
            )
            if new_dimensions[0] > cur_width or new_dimensions[1] > cur_height:
                if not photosize.upscale:
                    return im
            im = im.resize(new_dimensions, Image.ANTIALIAS)
        return im

    def create_size(self, photosize):
        if self.size_exists(photosize):
            return
        if not os.path.isdir(self.cache_path()):
            os.makedirs(self.cache_path())
        try:
            im = Image.open(self.image.path)
        except IOError:
            return
        # Apply effect if found
        if self.effect is not None:
            im = self.effect.pre_process(im)
        elif photosize.effect is not None:
            im = photosize.effect.pre_process(im)
        # Resize/crop image
        if im.size != photosize.size:
            im = self.resize_image(im, photosize)
        # Apply watermark if found
        if photosize.watermark is not None:
            im = photosize.watermark.post_process(im)
        # Apply effect if found
        if self.effect is not None:
            im = self.effect.post_process(im)
        elif photosize.effect is not None:
            im = photosize.effect.post_process(im)
        if self.orientation is not None and self.orientation > 0:
            im = im.transpose(self.orientation)
        # Save file
        im_filename = getattr(self, "get_%s_filename" % photosize.name)()
        try:
            if im.format == "JPEG":
                im.save(
                    im_filename, "JPEG", quality=int(photosize.quality), optimize=True
                )
            else:
                im.save(im_filename)
        except IOError, e:
            if os.path.isfile(im_filename):
                os.unlink(im_filename)
            raise e

    def remove_size(self, photosize, remove_dirs=True):
        if not self.size_exists(photosize):
            return
        filename = getattr(self, "get_%s_filename" % photosize.name)()
        if os.path.isfile(filename):
            os.remove(filename)
        if remove_dirs:
            self.remove_cache_dirs()

    def clear_cache(self):
        cache = PhotoSizeCache()
        for photosize in cache.sizes.values():
            self.remove_size(photosize, False)
        self.remove_cache_dirs()

    def pre_cache(self):
        cache = PhotoSizeCache()
        for photosize in cache.sizes.values():
            if photosize.pre_cache:
                self.create_size(photosize)

    def remove_cache_dirs(self):
        try:
            os.removedirs(self.cache_path())
        except:
            pass

    def save(self, update=False, *args, **kwargs):
        if update:
            models.Model.save(self)
            return
        exif_title = self.EXIF.get("Image DocumentName", None)
        exif_description = self.EXIF.get("Image UserComment", None)
        if exif_title is not None:
            self.title = exif_title
            self.slug = slugify(exif_title)
        if exif_description is not None:
            self.description = exif_description
        if self.date_taken:
            if self.date_taken is None:
                try:
                    exif_date = self.EXIF.get("EXIF DateTimeOriginal", None)

                    if exif_date is not None:
                        d, t = str.split(exif_date.values)
                        year, month, day = d.split(":")
                        hour, minute, second = t.split(":")
                        self.date_taken = datetime(
                            int(year),
                            int(month),
                            int(day),
                            int(hour),
                            int(minute),
                            int(second),
                        )
                except:
                    self.date_taken = datetime.now()
        if self.orientation is None:
            try:
                exif_orientation = self.EXIF.get("Image Orientation", 1).values[0]
                self.orientation = IMAGE_EXIF_ORIENTATION_MAP[exif_orientation]
            except:
                self.orientation = 0
        if self._get_pk_val():
            self.clear_cache()
        super(ImageModel, self).save(*args, **kwargs)
        self.pre_cache()

    def delete(self):
        self.clear_cache()
        super(ImageModel, self).delete()


class Movie(MediaMetadata):
    flv_video = models.FileField(
        _("video file (.flv)"), upload_to=DARKROOM_DIR + "/movies"
    )
    original_video = models.FileField(
        _("original file"),
        upload_to=DARKROOM_DIR + "/movies",
        blank=True,
        null=True,
        help_text=_("The original video preferably in a standard format like H.264."),
    )
    flash_skin = models.CharField(
        _("flash player skin"),
        blank=False,
        null=True,
        max_length=100,
        help_text=_("Do not include the .swf extension with the skin name."),
    )
    photographer = models.ForeignKey(Photographer, blank=True, null=True)
    preview = models.ImageField(
        _("preview"),
        upload_to=get_storage_path,
        help_text="Movie previews are a standard 70 pixels wide by 50 pixels high",
    )

    objects = MediaManager()

    class Meta:
        ordering = ["-created"]
        get_latest_by = "created"
        verbose_name = _("movie")
        verbose_name_plural = _("movies")

    def __unicode__(self):
        return self.title

    def __str__(self):
        return self.__unicode__()

    def get_absolute_url(self):
        return reverse("dr-movie-detail", args=[self.slug])


class Photo(ImageModel):
    image = models.ImageField(_("Image"), upload_to=get_storage_path)
    cropped_image = models.ImageField(
        _("Cropped image"), blank=True, null=True, upload_to="darkroom/cropped/"
    )
    date_taken = models.DateTimeField(
        _("Date taken"), null=True, blank=True, editable=False
    )
    photographer = models.ForeignKey(Photographer, blank=True, null=True)
    courtesy = models.CharField(_("Courtesy"), max_length=140, blank=True, null=True)
    file_photo = models.BooleanField(_("File photo?"), default=False)
    sites = models.ManyToManyField(Site)

    objects = MediaManager()
    on_site = CurrentSiteManager("sites")

    class Meta:
        ordering = ["-created"]
        get_latest_by = "published_on"
        verbose_name = _("photo")
        verbose_name_plural = _("photos")

    def __unicode__(self):
        return self.title

    def save(self, update=False):
        if self.slug is None:
            self.slug = slugify(self.title)
        super(Photo, self).save(update)

    @models.permalink
    def get_absolute_url(self, site=""):
        return (
            "dr-photo-detail",
            (),
            {
                "year": self.published_on.year,
                "month": self.published_on.strftime("%b").lower(),
                "day": self.published_on.day,
                "slug": self.slug,
            },
        )

    def get_lb_url(self):
        return reverse("dr-photo-lightbox", args=[self.slug])

    def published_galleries(self):
        """Return the published galleries to which this photo belongs."""
        return self.galleries.filter(published=True)

    def get_previous_in_gallery(self, gallery):
        try:
            return self.get_previous_by_published_on(galleries__exact=gallery)
        except Photo.DoesNotExist:
            return None

    def get_next_in_gallery(self, gallery):
        try:
            return self.get_next_by_published_on(galleries__exact=gallery)
        except Photo.DoesNotExist:
            return None


class Graphic(ImageModel):
    image = models.ImageField(_("image"), upload_to=DARKROOM_DIR + "/graphics")
    date_taken = models.DateTimeField(
        _("date created"), null=True, blank=True, editable=False
    )
    data_source = models.CharField(
        _("data source"), blank=True, null=True, max_length=150
    )
    source = models.ForeignKey(Place, blank=True, null=True)

    class Meta:
        ordering = ["-created"]
        get_latest_by = "created"
        verbose_name = _("graphic")
        verbose_name_plural = _("graphics")

    def __unicode__(self):
        return self.title

    def __str__(self):
        return self.__unicode__()

    @models.permalink
    def get_absolute_url(self, site=""):
        return (
            "dr-graphic-detail",
            (),
            {
                "year": self.published_on.year,
                "month": self.published_on.strftime("%b").lower(),
                "day": self.published_on.day,
                "slug": self.slug,
            },
        )


class BaseEffect(models.Model):
    name = models.CharField(_("name"), max_length=30, unique=True)
    description = models.TextField(_("description"), blank=True)

    class Meta:
        abstract = True

    def sample_dir(self):
        return os.path.join(settings.MEDIA_ROOT, DARKROOM_DIR, "samples")

    def sample_url(self):
        return settings.MEDIA_URL + "/".join(
            [DARKROOM_DIR, "samples", "%s %s.jpg" % (self.name.lower(), "sample")]
        )

    def sample_filename(self):
        return os.path.join(
            self.sample_dir(), "%s %s.jpg" % (self.name.lower(), "sample")
        )

    def create_sample(self):
        if not os.path.isdir(self.sample_dir()):
            os.makedirs(self.sample_dir())
        try:
            im = Image.open(SAMPLE_IMAGE_PATH)
        except IOError:
            raise IOError(
                "Photologue was unable to open the sample image: %s."
                % SAMPLE_IMAGE_PATH
            )
        im = self.process(im)
        im.save(self.sample_filename(), "JPEG", quality=90, optimize=True)

    def admin_sample(self):
        return u'<img src="%s">' % self.sample_url()

    admin_sample.short_description = "Sample"
    admin_sample.allow_tags = True

    def pre_process(self, im):
        return im

    def post_process(self, im):
        return im

    def process(self, im):
        im = self.pre_process(im)
        im = self.post_process(im)
        return im

    def __unicode__(self):
        return self.name

    def __str__(self):
        return self.__unicode__()

    def save(self):
        try:
            os.remove(self.sample_filename())
        except:
            pass
        models.Model.save(self)
        self.create_sample()
        for size in self.photo_sizes.all():
            size.clear_cache()
        # try to clear all related subclasses of ImageModel
        for prop in [prop for prop in dir(self) if prop[-8:] == "_related"]:
            for obj in getattr(self, prop).all():
                obj.clear_cache()
                obj.pre_cache()

    def delete(self):
        try:
            os.remove(self.sample_filename())
        except:
            pass
        super(PhotoEffect, self).delete()


class PhotoEffect(BaseEffect):
    """ A pre-defined effect to apply to photos """

    transpose_method = models.CharField(
        _("rotate or flip"), max_length=15, blank=True, choices=IMAGE_TRANSPOSE_CHOICES
    )
    color = models.FloatField(
        _("color"),
        default=1.0,
        help_text=_(
            "A factor of 0.0 gives a black and white image, a factor of 1.0 gives the original image."
        ),
    )
    brightness = models.FloatField(
        _("brightness"),
        default=1.0,
        help_text=_(
            "A factor of 0.0 gives a black image, a factor of 1.0 gives the original image."
        ),
    )
    contrast = models.FloatField(
        _("contrast"),
        default=1.0,
        help_text=_(
            "A factor of 0.0 gives a solid grey image, a factor of 1.0 gives the original image."
        ),
    )
    sharpness = models.FloatField(
        _("sharpness"),
        default=1.0,
        help_text=_(
            "A factor of 0.0 gives a blurred image, a factor of 1.0 gives the original image."
        ),
    )
    filters = models.CharField(
        _("filters"), max_length=200, blank=True, help_text=_(IMAGE_FILTERS_HELP_TEXT)
    )
    reflection_size = models.FloatField(
        _("size"),
        default=0,
        help_text=_(
            "The height of the reflection as a percentage of the orignal image. A factor of 0.0 adds no reflection, a factor of 1.0 adds a reflection equal to the height of the orignal image."
        ),
    )
    reflection_strength = models.FloatField(
        _("strength"),
        default=0.6,
        help_text="The initial opacity of the reflection gradient.",
    )
    background_color = models.CharField(
        _("color"),
        max_length=7,
        default="#FFFFFF",
        help_text="The background color of the reflection gradient. Set this to match the background color of your page.",
    )

    class Meta:
        verbose_name = _("photo effect")
        verbose_name_plural = _("photo effects")

    def pre_process(self, im):
        if self.transpose_method != "":
            method = getattr(Image, self.transpose_method)
            im = im.transpose(method)
        if im.mode != "RGB" and im.mode != "RGBA":
            return im
        for name in ["Color", "Brightness", "Contrast", "Sharpness"]:
            factor = getattr(self, name.lower())
            if factor != 1.0:
                im = getattr(ImageEnhance, name)(im).enhance(factor)
        for name in self.filters.split("->"):
            image_filter = getattr(ImageFilter, name.upper(), None)
            if image_filter is not None:
                try:
                    im = im.filter(image_filter)
                except ValueError:
                    pass
        return im

    def post_process(self, im):
        if self.reflection_size != 0.0:
            im = add_reflection(
                im,
                bgcolor=self.background_color,
                amount=self.reflection_size,
                opacity=self.reflection_strength,
            )
        return im


class Watermark(BaseEffect):
    image = models.ImageField(_("image"), upload_to=DARKROOM_DIR + "/watermarks")
    style = models.CharField(
        _("style"), max_length=5, choices=WATERMARK_STYLE_CHOICES, default="scale"
    )
    opacity = models.FloatField(
        _("opacity"), default=1, help_text=_("The opacity of the overlay.")
    )

    class Meta:
        verbose_name = _("watermark")
        verbose_name_plural = _("watermarks")

    def post_process(self, im):
        mark = Image.open(self.image.path)
        return apply_watermark(im, mark, self.style, self.opacity)


class PhotoSize(models.Model):
    name = models.CharField(
        _("name"),
        max_length=20,
        unique=True,
        help_text=_(
            'Photo size name should contain only letters, numbers and underscores. Examples: "thumbnail", "display", "small", "main_page_widget".'
        ),
    )
    width = models.PositiveIntegerField(
        _("width"),
        default=0,
        help_text=_(
            'If width is set to "0" the image will be scaled to the supplied height.'
        ),
    )
    height = models.PositiveIntegerField(
        _("height"),
        default=0,
        help_text=_(
            'If height is set to "0" the image will be scaled to the supplied width'
        ),
    )
    quality = models.PositiveIntegerField(
        _("quality"),
        choices=JPEG_QUALITY_CHOICES,
        default=70,
        help_text=_("JPEG image quality."),
    )
    upscale = models.BooleanField(
        _("upscale images?"),
        default=False,
        help_text=_(
            "If selected the image will be scaled up if necessary to fit the supplied dimensions. Cropped sizes will be upscaled regardless of this setting."
        ),
    )
    crop = models.BooleanField(
        _("crop to fit?"),
        default=False,
        help_text=_(
            "If selected the image will be scaled and cropped to fit the supplied dimensions."
        ),
    )
    pre_cache = models.BooleanField(
        _("pre-cache?"),
        default=False,
        help_text=_(
            "If selected this photo size will be pre-cached as photos are added."
        ),
    )
    increment_count = models.BooleanField(
        _("increment view count?"),
        default=False,
        help_text=_(
            'If selected the image\'s "view_count" will be incremented when this photo size is displayed.'
        ),
    )
    effect = models.ForeignKey(
        "PhotoEffect",
        null=True,
        blank=True,
        related_name="photo_sizes",
        verbose_name=_("photo effect"),
    )
    watermark = models.ForeignKey(
        "Watermark",
        null=True,
        blank=True,
        related_name="photo_sizes",
        verbose_name=_("watermark image"),
    )

    class Meta:
        ordering = ["width", "height"]
        verbose_name = _("photo size")
        verbose_name_plural = _("photo sizes")

    def __unicode__(self):
        return self.name

    def __str__(self):
        return self.__unicode__()

    def clear_cache(self):
        for cls in ImageModel.__subclasses__():
            for obj in cls.objects.all():
                obj.remove_size(self)
                if self.pre_cache:
                    obj.create_size(self)
        PhotoSizeCache().reset()

    def save(self, *args, **kwargs):
        if self.width + self.height <= 0:
            raise ValueError(_("A PhotoSize must have a positive height or width."))
        super(PhotoSize, self).save(*args, **kwargs)
        PhotoSizeCache().reset()
        self.clear_cache()

    def delete(self):
        self.clear_cache()
        super(PhotoSize, self).delete()

    def _get_size(self):
        return (self.width, self.height)

    def _set_size(self, value):
        self.width, self.height = value

    size = property(_get_size, _set_size)


class PhotoSizeCache(object):
    __state = {"sizes": {}}

    def __init__(self):
        self.__dict__ = self.__state
        if not len(self.sizes):
            sizes = PhotoSize.objects.all()
            for size in sizes:
                self.sizes[size.name] = size

    def reset(self):
        self.sizes = {}


# Set up the accessor methods
def add_methods(sender, instance, signal, *args, **kwargs):
    """ Adds methods to access sized images (urls, paths)

    after the Photo model's __init__ function completes,
    this method calls "add_accessor_methods" on each instance.
    """
    if hasattr(instance, "add_accessor_methods"):
        instance.add_accessor_methods()


# connect the add_accessor_methods function to the post_init signal
post_init.connect(add_methods)
