''' database schema for books and shelves '''
import re

from django.db import models
from model_utils.managers import InheritanceManager

from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN

from .base_model import BookWyrmModel
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
from . import fields

class BookDataModel(ActivitypubMixin, BookWyrmModel):
    ''' fields shared between editable book data (books, works, authors) '''
    origin_id = models.CharField(max_length=255, null=True, blank=True)
    openlibrary_key = fields.CharField(
        max_length=255, blank=True, null=True, deduplication_field=True)
    librarything_key = fields.CharField(
        max_length=255, blank=True, null=True, deduplication_field=True)
    goodreads_key = fields.CharField(
        max_length=255, blank=True, null=True, deduplication_field=True)

    last_edited_by = models.ForeignKey(
        'User', on_delete=models.PROTECT, null=True)

    class Meta:
        ''' can't initialize this model, that wouldn't make sense '''
        abstract = True

    def save(self, *args, **kwargs):
        ''' ensure that the remote_id is within this instance '''
        if self.id:
            self.remote_id = self.get_remote_id()
        else:
            self.origin_id = self.remote_id
            self.remote_id = None
        return super().save(*args, **kwargs)


class Book(BookDataModel):
    ''' a generic book, which can mean either an edition or a work '''
    connector = models.ForeignKey(
        'Connector', on_delete=models.PROTECT, null=True)

    # book/work metadata
    title = fields.CharField(max_length=255)
    sort_title = fields.CharField(max_length=255, blank=True, null=True)
    subtitle = fields.CharField(max_length=255, blank=True, null=True)
    description = fields.HtmlField(blank=True, null=True)
    languages = fields.ArrayField(
        models.CharField(max_length=255), blank=True, default=list
    )
    series = fields.CharField(max_length=255, blank=True, null=True)
    series_number = fields.CharField(max_length=255, blank=True, null=True)
    subjects = fields.ArrayField(
        models.CharField(max_length=255), blank=True, null=True, default=list
    )
    subject_places = fields.ArrayField(
        models.CharField(max_length=255), blank=True, null=True, default=list
    )
    authors = fields.ManyToManyField('Author')
    cover = fields.ImageField(
        upload_to='covers/', blank=True, null=True, alt_field='alt_text')
    first_published_date = fields.DateTimeField(blank=True, null=True)
    published_date = fields.DateTimeField(blank=True, null=True)

    objects = InheritanceManager()

    @property
    def author_text(self):
        ''' format a list of authors '''
        return ', '.join(a.name for a in self.authors.all())

    @property
    def edition_info(self):
        ''' properties of this edition, as a string '''
        items = [
            self.physical_format if hasattr(self, 'physical_format') else None,
            self.languages[0] + ' language' if self.languages and \
                    self.languages[0] != 'English' else None,
            str(self.published_date.year) if self.published_date else None,
        ]
        return ', '.join(i for i in items if i)

    @property
    def alt_text(self):
        ''' image alt test '''
        text = '%s cover' % self.title
        if self.edition_info:
            text += ' (%s)' % self.edition_info
        return text

    def save(self, *args, **kwargs):
        ''' can't be abstract for query reasons, but you shouldn't USE it '''
        if not isinstance(self, Edition) and not isinstance(self, Work):
            raise ValueError('Books should be added as Editions or Works')
        return super().save(*args, **kwargs)

    def get_remote_id(self):
        ''' editions and works both use "book" instead of model_name '''
        return 'https://%s/book/%d' % (DOMAIN, self.id)

    def __repr__(self):
        return "<{} key={!r} title={!r}>".format(
            self.__class__,
            self.openlibrary_key,
            self.title,
        )


class Work(OrderedCollectionPageMixin, Book):
    ''' a work (an abstract concept of a book that manifests in an edition) '''
    # library of congress catalog control number
    lccn = fields.CharField(
        max_length=255, blank=True, null=True, deduplication_field=True)
    # this has to be nullable but should never be null
    default_edition = fields.ForeignKey(
        'Edition',
        on_delete=models.PROTECT,
        null=True,
        load_remote=False
    )

    def save(self, *args, **kwargs):
        ''' set some fields on the edition object '''
        # set rank
        for edition in self.editions.all():
            edition.save()
        return super().save(*args, **kwargs)

    def get_default_edition(self):
        ''' in case the default edition is not set '''
        return self.default_edition or self.editions.order_by(
            '-edition_rank'
        ).first()

    def to_edition_list(self, **kwargs):
        ''' an ordered collection of editions '''
        return self.to_ordered_collection(
            self.editions.order_by('-edition_rank').all(),
            remote_id='%s/editions' % self.remote_id,
            **kwargs
        )

    activity_serializer = activitypub.Work
    serialize_reverse_fields = [('editions', 'editions', '-edition_rank')]
    deserialize_reverse_fields = [('editions', 'editions')]


class Edition(Book):
    ''' an edition of a book '''
    # these identifiers only apply to editions, not works
    isbn_10 = fields.CharField(
        max_length=255, blank=True, null=True, deduplication_field=True)
    isbn_13 = fields.CharField(
        max_length=255, blank=True, null=True, deduplication_field=True)
    oclc_number = fields.CharField(
        max_length=255, blank=True, null=True, deduplication_field=True)
    asin = fields.CharField(
        max_length=255, blank=True, null=True, deduplication_field=True)
    pages = fields.IntegerField(blank=True, null=True)
    physical_format = fields.CharField(max_length=255, blank=True, null=True)
    publishers = fields.ArrayField(
        models.CharField(max_length=255), blank=True, default=list
    )
    shelves = models.ManyToManyField(
        'Shelf',
        symmetrical=False,
        through='ShelfBook',
        through_fields=('book', 'shelf')
    )
    parent_work = fields.ForeignKey(
        'Work', on_delete=models.PROTECT, null=True,
        related_name='editions', activitypub_field='work')
    edition_rank = fields.IntegerField(default=0)

    activity_serializer = activitypub.Edition
    name_field = 'title'

    @property
    def get_rank(self):
        ''' calculate how complete the data is on this edition '''
        if self.parent_work and self.parent_work.default_edition == self:
            # default edition has the highest rank
            return 20
        rank = 0
        rank += int(bool(self.cover)) * 3
        rank += int(bool(self.isbn_13))
        rank += int(bool(self.isbn_10))
        rank += int(bool(self.oclc_number))
        rank += int(bool(self.pages))
        rank += int(bool(self.physical_format))
        rank += int(bool(self.description))
        # max rank is 9
        return rank

    def save(self, *args, **kwargs):
        ''' set some fields on the edition object '''
        # calculate isbn 10/13
        if self.isbn_13 and self.isbn_13[:3] == '978' and not self.isbn_10:
            self.isbn_10 = isbn_13_to_10(self.isbn_13)
        if self.isbn_10 and not self.isbn_13:
            self.isbn_13 = isbn_10_to_13(self.isbn_10)

        # set rank
        self.edition_rank = self.get_rank

        return super().save(*args, **kwargs)


def isbn_10_to_13(isbn_10):
    ''' convert an isbn 10 into an isbn 13 '''
    isbn_10 = re.sub(r'[^0-9X]', '', isbn_10)
    # drop the last character of the isbn 10 number (the original checkdigit)
    converted = isbn_10[:9]
    # add "978" to the front
    converted = '978' + converted
    # add a check digit to the end
    # multiply the odd digits by 1 and the even digits by 3 and sum them
    try:
        checksum = sum(int(i) for i in converted[::2]) + \
               sum(int(i) * 3 for i in converted[1::2])
    except ValueError:
        return None
    # add the checksum mod 10 to the end
    checkdigit = checksum % 10
    if checkdigit != 0:
        checkdigit = 10 - checkdigit
    return converted + str(checkdigit)


def isbn_13_to_10(isbn_13):
    ''' convert isbn 13 to 10, if possible '''
    if isbn_13[:3] != '978':
        return None

    isbn_13 = re.sub(r'[^0-9X]', '', isbn_13)

    # remove '978' and old checkdigit
    converted = isbn_13[3:-1]
    # calculate checkdigit
    # multiple each digit by 10,9,8.. successively and sum them
    try:
        checksum = sum(int(d) * (10 - idx) for (idx, d) in enumerate(converted))
    except ValueError:
        return None
    checkdigit = checksum % 11
    checkdigit = 11 - checkdigit
    if checkdigit == 10:
        checkdigit = 'X'
    return converted + str(checkdigit)