import difflib
import os

from django import forms
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import signals
from django.utils.translation import ugettext_lazy as _
from markdown import markdown

from wiki_settings import *
from util.cache import cache


class ShouldHaveExactlyOneRootSlug(Exception):
    pass

class Article(models.Model):
    """Wiki article referring to Revision model for actual content.
       'slug' and 'parent' field should be maintained centrally, since users
       aren't allowed to change them, anyways.
    """
    
    title = models.CharField(max_length=512, verbose_name=_('Article title'),
                             blank=False)
    slug = models.SlugField(max_length=100, verbose_name=_('slug'),
                            help_text=_('Letters, numbers, underscore and hyphen.'),
                            blank=True)
    created_by = models.ForeignKey(User, verbose_name=_('Created by'), blank=True, null=True)
    created_on = models.DateTimeField(auto_now_add = 1)
    modified_on = models.DateTimeField(auto_now_add = 1)
    parent = models.ForeignKey('self', verbose_name=_('Parent article slug'), 
                               help_text=_('Affects URL structure and possibly inherits permissions'),
                               null=True, blank=True)
    locked = models.BooleanField(default=False, verbose_name=_('Locked for editing'))
    permissions = models.ForeignKey('Permission', verbose_name=_('Permissions'),
                                    blank=True, null=True,
                                    help_text=_('Permission group'))
    current_revision = models.OneToOneField('Revision', related_name='current_rev',
                                            blank=True, null=True, editable=True)
    related = models.ManyToManyField('self', verbose_name=_('Related articles'), symmetrical=True,
                                     help_text=_('Sets a symmetrical relation other articles'),
                                     blank=True, null=True)

    def attachments(self):
        return ArticleAttachment.objects.filter(article__exact = self)

    @classmethod
    def get_root(cls):
        """Return the root article, which should ALWAYS exist..
        except the very first time the wiki is loaded, in which
        case the user is prompted to create this article."""
        try:
            return Article.objects.filter(slug__exact = "")[0]
        except:
            raise ShouldHaveExactlyOneRootSlug()

    def get_url(self):
        """Return the Wiki URL for an article"""    
        url = self.slug + "/"
        if self.parent_id:
            parent_url = cache.get("wiki_url-" + str(self.parent_id))
            if parent_url is None:
                parent_url = self.parent.get_url()
            
            url = parent_url + url
        
        cache.set("wiki_url-" + str(self.id), url, 60*60)
            
        return url

    def get_abs_url(self):
        """Return the absolute path for an article. This is necessary in cases
        where the template system isn't used for generating URLs..."""
        # TODO: Remove and create a reverse() lookup.
        return WIKI_BASE + self.get_url()

    @models.permalink
    def get_absolute_url(self):
        return ('wiki_view', [self.get_url()])

    @classmethod
    def get_url_reverse(cls, path, article, return_list=[]):
        """Lookup a URL and return the corresponding set of articles
        in the path."""
        if path == []:
            return return_list + [article]
        # Lookup next child in path
        try:
            a = Article.objects.get(parent__exact = article, slug__exact=str(path[0]))
            return cls.get_url_reverse(path[1:], a, return_list+[article])
        except Exception, e:
            return None
    
    def can_read(self, user):
        """ Check read permissions and return True/False."""
        if user.is_superuser:
            return True
        if self.permissions:
            perms = self.permissions.can_read.all()
            return perms.count() == 0 or (user in perms)
        else:
            return self.parent.can_read(user) if self.parent else True

    def can_write(self, user):
        """ Check write permissions and return True/False."""
        if user.is_superuser:
            return True
        if self.permissions:
            perms = self.permissions.can_write.all()
            return perms.count() == 0 or (user in perms)
        else:
            return self.parent.can_write(user) if self.parent else True

    def can_write_l(self, user):
        """Check write permissions and locked status"""
        if user.is_superuser:
            return True
        return not self.locked and self.can_write(user)

    def can_attach(self, user):
        return self.can_write_l(user) and (WIKI_ALLOW_ANON_ATTACHMENTS or not user.is_anonymous())

    def __unicode__(self):
        if self.slug == '' and not self.parent:
            return unicode(_('Root article'))
        else:
            return self.get_url()
    
    class Meta:
        unique_together = (('slug', 'parent'),)
        verbose_name = _('Article')
        verbose_name_plural = _('Articles')

def get_attachment_filepath(instance, filename):
    """Store file, appending new extension for added security"""
    dir_ = WIKI_ATTACHMENTS + instance.article.get_url()
    dir_ = '/'.join(filter(lambda x: x!='', dir_.split('/')))
    if not os.path.exists(WIKI_ATTACHMENTS_ROOT + dir_):
        os.makedirs(WIKI_ATTACHMENTS_ROOT + dir_)
    return dir_ + '/' + filename + '.upload'

class ArticleAttachment(models.Model):
    article = models.ForeignKey(Article, verbose_name=_('Article'))
    file = models.FileField(max_length=255, upload_to=get_attachment_filepath, verbose_name=_('Attachment'))
    uploaded_by = models.ForeignKey(User, blank=True, verbose_name=_('Uploaded by'), null=True)
    uploaded_on = models.DateTimeField(auto_now_add = True, verbose_name=_('Upload date'))
    
    def download_url(self):
        return reverse('wiki_view_attachment', args=(self.article.get_url(), self.filename()))
    
    def filename(self):
        return '.'.join(self.file.name.split('/')[-1].split('.')[:-1])
    
    def get_size(self):
        try:
            size = self.file.size
        except OSError:
            size = 0
        return size

    def filename(self):
        return '.'.join(self.file.name.split('/')[-1].split('.')[:-1])
    
    def is_image(self):
        fname = self.filename().split('.')
        if len(fname) > 1 and fname[-1].lower() in WIKI_IMAGE_EXTENSIONS:
            return True
        return False
    
    def get_thumb(self):
        return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE)

    def get_thumb_small(self):
        return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE_SMALL)

    def mk_thumbs(self):
        self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE, **{'force':True})
        self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE_SMALL, **{'force':True})

    def mk_thumb(self, width, height, force=False):
        """Requires Python Imaging Library (PIL)"""
        if not self.get_size():
            return False
        
        if not self.is_image():
            return False
    
        base_path = os.path.dirname(self.file.path)
        orig_name = self.filename().split('.')
        thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1])
        thumb_filepath = "%s%s%s" % (base_path, os.sep, thumb_filename)
    
        if force or not os.path.exists(thumb_filepath):
            try:
                import Image
                img = Image.open(self.file.path)            
                img.thumbnail((width,height), Image.ANTIALIAS)
                img.save(thumb_filepath)
            except IOError:
                return False

        return True

    def get_thumb_impl(self, width, height):
        """Requires Python Imaging Library (PIL)"""
        
        if not self.get_size():
            return False
        
        if not self.is_image():
            return False
    
        self.mk_thumb(width, height)
        
        orig_name = self.filename().split('.')
        thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1])
        thumb_url = settings.MEDIA_URL + WIKI_ATTACHMENTS + self.article.get_url() +'/' + thumb_filename
    
        return thumb_url

    def __unicode__(self):
        return self.filename()
    
class Revision(models.Model):
    
    article = models.ForeignKey(Article, verbose_name=_('Article'))
    revision_text = models.CharField(max_length=255, blank=True, null=True, 
                                     verbose_name=_('Description of change'))
    revision_user = models.ForeignKey(User, verbose_name=_('Modified by'), 
                                      blank=True, null=True, related_name='wiki_revision_user')
    revision_date = models.DateTimeField(auto_now_add = True, verbose_name=_('Revision date'))
    contents = models.TextField(verbose_name=_('Contents (Use MarkDown format)'))
    contents_parsed = models.TextField(editable=False, blank=True, null=True)
    counter = models.IntegerField(verbose_name=_('Revision#'), default=1, editable=False)
    previous_revision = models.ForeignKey('self', blank=True, null=True, editable=False)
    
    # Deleted has three values. 0 is normal, non-deleted. 1 is if it was deleted by a normal user. It should
    # be a NEW revision, so that it appears in the history. 2 is a special flag that can be applied or removed
    # from a normal revision. It means it has been admin-deleted, and can only been seen by an admin. It doesn't
    # show up in the history.
    deleted = models.IntegerField(verbose_name=_('Deleted group'), default=0)
    
    def get_user(self):
        return self.revision_user if self.revision_user else _('Anonymous')
        
    # Called after the deleted fied has been changed (between 0 and 2). This bypasses the normal checks put in
    # save that update the revision or reject the save if contents haven't changed    
    def adminSetDeleted(self, deleted):
        self.deleted = deleted
        super(Revision, self).save()
    
    def save(self, **kwargs):
        # Check if contents have changed... if not, silently ignore save
        if self.article and self.article.current_revision:
            if self.deleted == 0 and self.article.current_revision.contents == self.contents:
                return
            else:
                import datetime
                self.article.modified_on = datetime.datetime.now()
                self.article.save()
        
        # Increment counter according to previous revision
        previous_revision = Revision.objects.filter(article=self.article).order_by('-counter')
        if previous_revision.count() > 0:
            if previous_revision.count() > previous_revision[0].counter:
                self.counter = previous_revision.count() + 1
            else:
                self.counter = previous_revision[0].counter + 1
        else:
            self.counter = 1
        if (self.article.current_revision and self.article.current_revision.deleted == 0):
            self.previous_revision = self.article.current_revision

        # Create pre-parsed contents - no need to parse on-the-fly
        ext = WIKI_MARKDOWN_EXTENSIONS
        ext += ["wikipath(base_url=%s)" % reverse('wiki_view', args=('/',))]
        self.contents_parsed = markdown(self.contents,
                                        extensions=ext,
                                        safe_mode='escape',)
        super(Revision, self).save(**kwargs)
        
    def delete(self, **kwargs):
        """If a current revision is deleted, then regress to the previous
        revision or insert a stub, if no other revisions are available"""
        article = self.article
        if article.current_revision == self:
            prev_revision = Revision.objects.filter(article__exact = article,
                                                    pk__not = self.pk).order_by('-counter')
            if prev_revision:
                article.current_revision = prev_revision[0]
                article.save()
            else:
                r = Revision(article=article, 
                             revision_user = article.created_by)
                r.contents = unicode(_('Auto-generated stub'))
                r.revision_text= unicode(_('Auto-generated stub'))
                r.save()
                article.current_revision = r
                article.save()
        super(Revision, self).delete(**kwargs)
    
    def get_diff(self):
        if (self.deleted == 1):
            yield "Article Deletion"
            return
        
        if self.previous_revision:
            previous = self.previous_revision.contents.splitlines(1)
        else:
            previous = []
        
        # Todo: difflib.HtmlDiff would look pretty for our history pages!
        diff = difflib.unified_diff(previous, self.contents.splitlines(1))
        # let's skip the preamble
        diff.next(); diff.next(); diff.next()
        
        for d in diff:
            yield d
    
    def __unicode__(self):
        return "r%d" % self.counter

    class Meta:
        verbose_name = _('article revision')
        verbose_name_plural = _('article revisions')

class Permission(models.Model):
    permission_name = models.CharField(max_length = 255, verbose_name=_('Permission name'))
    can_write = models.ManyToManyField(User, blank=True, null=True, related_name='write',
                                       help_text=_('Select none to grant anonymous access.'))
    can_read = models.ManyToManyField(User, blank=True, null=True, related_name='read',
                                       help_text=_('Select none to grant anonymous access.'))
    def __unicode__(self):
        return self.permission_name
    class Meta:
        verbose_name = _('Article permission')
        verbose_name_plural = _('Article permissions')

class RevisionForm(forms.ModelForm):
    contents = forms.CharField(label=_('Contents'), widget=forms.Textarea(attrs={'rows':8, 'cols':50}))
    class Meta:
        model = Revision
        fields = ['contents', 'revision_text']
class RevisionFormWithTitle(forms.ModelForm):
    title = forms.CharField(label=_('Title'))
    class Meta:
        model = Revision
        fields = ['title', 'contents', 'revision_text']
class CreateArticleForm(RevisionForm):
    title = forms.CharField(label=_('Title'))
    class Meta:
        model = Revision
        fields = ['title', 'contents',]

def set_revision(sender, *args, **kwargs):
    """Signal handler to ensure that a new revision is always chosen as the
    current revision - automatically. It simplifies stuff greatly. Also
    stores previous revision for diff-purposes"""
    instance = kwargs['instance']
    created = kwargs['created']
    if created and instance.article:
        instance.article.current_revision = instance
        instance.article.save()

signals.post_save.connect(set_revision, Revision)
