import bibtexparser
import jsonfield
import logging
import re
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _
from wagtail.core.models import CollectionMember, Orderable
from wagtail.search import index
from wagtail.search.queryset import SearchableQuerySetMixin
from wagtail.snippets.models import register_snippet
from modelcluster.models import ClusterableModel
from bibtexparser.bparser import BibTexParser
from bibtexparser.bwriter import BibTexWriter
from bibtexparser.customization import homogenize_latex_encoding, convert_to_unicode, author, type
from wagtail_references.bibjson import record_from_entry


logger = logging.getLogger(__name__)


class ReferenceQuerySet(SearchableQuerySetMixin, models.QuerySet):
    pass


class AbstractReference(ClusterableModel, CollectionMember, Orderable, index.Indexed, models.Model):
    slug = models.SlugField(
        verbose_name=_('slug'),
        allow_unicode=False,
        unique=True,
        max_length=255,
        help_text=_("A short key to cite the reference by. Determined from the BibTeX entry key. Must be unique.")
    )
    bibtex = models.TextField(help_text=_('The reference, in bibtex format.'))
    bibtype = models.CharField(max_length=255, verbose_name=_('Bibliography entry type'), default='article', help_text=_('The entry type, detected from the BibTeX entry.'))
    created_at = models.DateTimeField(verbose_name=_('created at'), auto_now_add=True, db_index=True)
    bibjson = jsonfield.JSONField()
    created_by_user = models.ForeignKey(
        settings.AUTH_USER_MODEL, verbose_name=_('created by user'),
        null=True, blank=True, editable=False, on_delete=models.SET_NULL
    )

    objects = ReferenceQuerySet.as_manager()

    search_fields = CollectionMember.search_fields + [
        index.SearchField('slug', partial_match=True, boost=10),
        index.SearchField('bibtex', partial_match=True, boost=10),
        index.AutocompleteField('slug'),
        index.FilterField('bibtype'),
        index.FilterField('created_by_user'),
    ]

    def __str__(self):
        return self.bibtex

    def is_editable_by_user(self, user):
        from wagtail.images.permissions import permission_policy
        return permission_policy.user_has_permission_for_instance(user, 'change', self)

    class Meta:
        abstract = True


@register_snippet
class Reference(AbstractReference):

    # Note slug field is added into the edit form only, not for creation
    admin_form_fields = (
        'bibtex',
        'collection',
    )

    class Meta:
        verbose_name = _('BibTeX Reference')
        verbose_name_plural = _('BibTeX References')

    @staticmethod
    def _slug_is_available(slug):
        """
        Determine whether the given slug is available for use on a wagtail reference.
        """
        return not Reference.objects.filter(slug=slug).exists()

    def slug_is_available(self):
        """
        Determine whether the given slug is used by a wagtail reference other than this one.
        """
        return not Reference.objects.exclude(id=self.id).filter(slug=self.slug).exists()

    @staticmethod
    def _get_autogenerated_slug(base_slug):
        """ Auto-generate an available slug, incrementing suffix digit unit slug is available
        """
        candidate_slug = base_slug
        suffix = 1
        while not Reference._slug_is_available(candidate_slug):
            suffix += 1
            candidate_slug = "{}-{}".format(base_slug, suffix)

        return candidate_slug

    def set_slug_from_entry(self, entry):
        """ Sets the slug field from a combination of author and year where those fields are available in the bibtex
        entry. If neither field is available, or slugify cannot create a slug, it is created based on the entry type.
        """

        def surnames(authors):
            """ Clean authors string into whitespace separated surname only
                So "Clark, Tom and de Angelo, Michel" becomes "Clark deAngelo"
            """
            return ' '.join([i.split(',')[0].strip() for i in authors.replace('\n', ' ').split(" and ")])

        # Get the attributes used to build the slug
        # Yes, this is ugly. No, you can't replace this with more idiomatic python because bibtexparser makes it
        # possible for there to be an attribute (e.g. 'author') whose value is None, as well as simply not having that
        # attribute. This covers both cases.
        surnames = surnames(entry['author']) if entry.get('author', None) is not None else ''
        year = entry['year'] if entry.get('year', None) is not None else ''

        base_slug = slugify(surnames + ' ' + year, allow_unicode=False)
        if (base_slug is None) or (len(base_slug) == 0):
            base_slug = slugify(entry['ENTRYTYPE'], allow_unicode=False)
        self.slug = self._get_autogenerated_slug(base_slug)

    def update_from_bibtex(self):
        """ Perform (optional) parsing and conversion of bibtex to ensure validity and force to a uniform type.

        Uses BibtexParser library. Your bibtex may contain accents and specific characters. They are sometimes coded like
        this \'{e} but this is not the correct way. {\'e} is preferred by latex... but for purposes other than using latex,
        you may wish to clean up such entries to unicode e.g. é.

        If WAGTAILREFERENCES_CONVERT_BIBTEX is True (default), clean and convert the input bibtex.
        If 'WAGTAILREFERENCES_ENABLE_UNICODE', is True (default False), convert to unicode, otherwise convert to homogenised LaTeX.

        The entry ID key is always forced to lower case to avoid confusing duplicates.

        """

        def bibtex_parser_customization(record):
            if getattr(settings, 'WAGTAILREFERENCES_ENABLE_UNICODE', True):
                record = convert_to_unicode(record)
            if getattr(settings, 'WAGTAILREFERENCES_CONVERT_BIBTEX', False):
                record = homogenize_latex_encoding(record)
            return type(record)

        parser = BibTexParser()
        parser.customization = bibtex_parser_customization
        bib_database = bibtexparser.loads(self.bibtex, parser=parser)

        assert len(bib_database.entries) > 0
        if len(bib_database.entries) > 1:
            bib_database.entries = [bib_database.entries[0]]
            logger.warning('More than one entry in submitted bibtex string. Removing all but the first.')

        writer = BibTexWriter()
        writer.indent = '    '

        if (not self.slug) or (len(self.slug) == 0):
            self.set_slug_from_entry(bib_database.entries[0])
        bib_database.entries[0]['ID'] = self.slug
        self.bibtype = bib_database.entries[0]['ENTRYTYPE']
        self.bibtex = bibtexparser.dumps(bib_database, writer=writer)
        self.bibjson = record_from_entry(self.slug, author(bib_database.entries[0]), getattr(settings, 'WAGTAILREFERENCES_COLLECTION_NAME', None))

    # TODO Add an update_from_bibjson method, allowing us to edit the json, not the bibtex, in the admin:edit view

    def full_clean(self, *args, **kwargs):
        """ Slug creation and bibtex parsing needs to happen before full-field validation occurs
        """
        self.update_from_bibtex()
        super().full_clean(*args, **kwargs)

    def clean(self):
        super().clean()
        if not self.slug_is_available():
            raise ValidationError({'slug': _("This slug is already in use")})

    @property
    def bibtex_string(self):
        """ Returns the bibtex as a single string with no line breaks
        """
        return re.sub(r'[\n\r]+', '', self.bibtex)
