import zlib

from django.db import models
from django.utils.translation import ugettext_lazy as _


class AbstractAddress(models.Model):
    """
    Superclass address object

    This is subclassed and extended to provide models for
    user, shipping and billing addresses.
    """
    # @todo: Need a way of making these choice lists configurable
    # per project
    MR, MISS, MRS, MS, DR = ('Mr', 'Miss', 'Mrs', 'Ms', 'Dr')
    TITLE_CHOICES = (
        (MR, _("Mr")),
        (MISS, _("Miss")),
        (MRS, _("Mrs")),
        (MS, _("Ms")),
        (DR, _("Dr")),
    )
    title = models.CharField(_("Title"), max_length=64, choices=TITLE_CHOICES,
                             blank=True, null=True)
    first_name = models.CharField(_("First name"), max_length=255, blank=True,
                                  null=True)
    last_name = models.CharField(_("Last name"), max_length=255, blank=True)

    # We use quite a few lines of an address as they are often quite long and
    # it's easier to just hide the unnecessary ones than add extra ones.
    line1 = models.CharField(_("First line of address"), max_length=255)
    line2 = models.CharField(_("Second line of address"), max_length=255,
                             blank=True, null=True)
    line3 = models.CharField(_("Third line of address"), max_length=255,
                             blank=True, null=True)
    line4 = models.CharField(_("City"), max_length=255, blank=True, null=True)
    state = models.CharField(_("State/County"), max_length=255, blank=True,
                             null=True)
    postcode = models.CharField(_("Post/Zip-code"), max_length=64, blank=True, null=True)
    country = models.ForeignKey('address.Country', verbose_name=_("Country"))

    # A field only used for searching addresses - this contains all the
    # relevant fields
    search_text = models.CharField(
        _("Search text - used only for searching addresses"),
        max_length=1000)

    class Meta:
        abstract = True
        verbose_name = _('Address')
        verbose_name_plural = _('Addresses')

    def save(self, *args, **kwargs):
        self._clean_fields()
        self._update_search_text()
        super(AbstractAddress, self).save(*args, **kwargs)

    @property
    def city(self):
        return self.line4

    def _clean_fields(self):
        """
        Clean up fields
        """
        for field in ['first_name', 'last_name', 'line1', 'line2', 'line3',
                      'line4', 'state', 'postcode']:
            if self.__dict__[field]:
                self.__dict__[field] = self.__dict__[field].strip()

        # Ensure postcodes are always uppercase
        if self.postcode:
            self.postcode = self.postcode.upper()

    def _update_search_text(self):
        search_fields = filter(
            lambda x: x, [self.first_name, self.last_name,
                          self.line1, self.line2, self.line3, self.line4,
                          self.state, self.postcode, self.country.name])
        self.search_text = ' '.join(search_fields)

    @property
    def summary(self):
        """
        Returns a single string summary of the address,
        separating fields using commas.
        """
        return u", ".join(self.active_address_fields())

    def populate_alternative_model(self, address_model):
        """
        For populating an address model using the matching fields
        from this one.

        This is used to convert a user address to a shipping address
        as part of the checkout process.
        """
        destination_field_names = [
            field.name for field in address_model._meta.fields]
        for field_name in [field.name for field in self._meta.fields]:
            if field_name in destination_field_names and field_name != 'id':
                setattr(address_model, field_name, getattr(self, field_name))

    def active_address_fields(self):
        """
        Returns the non-empty components of the address, but merging the
        title, first_name and last_name into a single line.
        """
        self._clean_fields()
        fields = filter(
            lambda x: x, [self.salutation(), self.line1, self.line2,
                          self.line3, self.line4, self.state, self.postcode])
        if self.country:
            fields.append(self.country.name)
        return fields

    def salutation(self):
        """
        Return the salutation
        """
        return u" ".join([part for part in [
            self.title, self.first_name, self.last_name] if part])

    def name(self):
        """
        Return the full name
        """
        return u" ".join([part for part in [
            self.first_name, self.last_name] if part])

    def __unicode__(self):
        return self.summary


class AbstractCountry(models.Model):
    """
    International Organization for Standardization (ISO) 3166-1 Country list.
    """
    iso_3166_1_a2 = models.CharField(_('ISO 3166-1 alpha-2'), max_length=2,
                                     primary_key=True)
    iso_3166_1_a3 = models.CharField(_('ISO 3166-1 alpha-3'), max_length=3,
                                     null=True, db_index=True)
    iso_3166_1_numeric = models.PositiveSmallIntegerField(
        _('ISO 3166-1 numeric'), null=True, db_index=True)
    name = models.CharField(_('Official name (CAPS)'), max_length=128)
    printable_name = models.CharField(_('Country name'), max_length=128)

    is_highlighted = models.BooleanField(_("Is Highlighted"), default=False,
                                         db_index=True)
    is_shipping_country = models.BooleanField(_("Is Shipping Country"),
                                              default=False, db_index=True)

    class Meta:
        abstract = True
        verbose_name = _('Country')
        verbose_name_plural = _('Countries')
        ordering = ('-is_highlighted', 'name',)

    def __unicode__(self):
        return self.printable_name


class AbstractShippingAddress(AbstractAddress):
    """
    A shipping address.

    A shipping address should not be edited once the order has been placed -
    it should be read-only after that.
    """
    phone_number = models.CharField(_("Phone number"), max_length=32,
                                    blank=True, null=True)
    notes = models.TextField(
        blank=True, null=True,
        verbose_name=_('Courier instructions'),
        help_text=_("For example, leave the parcel in the wheelie bin "
                    "if I'm not in."))

    class Meta:
        abstract = True
        verbose_name = _("Shipping address")
        verbose_name_plural = _("Shipping addresses")

    @property
    def order(self):
        """
        Return the order linked to this shipping address
        """
        orders = self.order_set.all()
        if not orders:
            return None
        return orders[0]


class AbstractUserAddress(AbstractShippingAddress):
    """
    A user's address.  A user can have many of these and together they form an
    'address book' of sorts for the user.

    We use a separate model for shipping and billing (even though there will be
    some data duplication) because we don't want shipping/billing addresses
    changed or deleted once an order has been placed.  By having a separate
    model, we allow users the ability to add/edit/delete from their address
    book without affecting orders already placed.
    """
    user = models.ForeignKey('auth.User', related_name='addresses',
                             verbose_name=_("User"))

    # Customers can set defaults
    is_default_for_shipping = models.BooleanField(
        _("Default shipping address?"), default=False)
    #: Whether this address should be the default for billing.
    is_default_for_billing = models.BooleanField(
        _("Default billing address?"), default=False)

    # We keep track of the number of times an address has been used
    # as a shipping address so we can show the most popular ones
    # first at the checkout.
    num_orders = models.PositiveIntegerField(_("Number of Orders"), default=0)

    # A hash is kept to try and avoid duplicate addresses being added
    # to the address book.
    hash = models.CharField(_("Address Hash"), max_length=255, db_index=True)
    date_created = models.DateTimeField(_("Date Created"), auto_now_add=True)

    def generate_hash(self):
        """
        Returns a hash of the address summary
        """
        # We use an upper-case version of the summary
        return zlib.crc32(self.summary.strip().upper().encode('UTF8'))

    def save(self, *args, **kwargs):
        """
        Save a hash of the address fields
        """
        # Save a hash of the address fields so we can check whether two
        # addresses are the same to avoid saving duplicates
        self.hash = self.generate_hash()
        # Ensure that each user only has one default shipping address
        # and billing address
        self._ensure_defaults_integrity()
        super(AbstractUserAddress, self).save(*args, **kwargs)

    def _ensure_defaults_integrity(self):
        if self.is_default_for_shipping:
            self.__class__._default_manager.filter(
                user=self.user,
                is_default_for_shipping=True).update(
                    is_default_for_shipping=False)
        if self.is_default_for_billing:
            self.__class__._default_manager.filter(
                user=self.user,
                is_default_for_billing=True).update(
                    is_default_for_billing=False)

    class Meta:
        abstract = True
        verbose_name = _("User address")
        verbose_name_plural = _("User addresses")
        ordering = ['-num_orders']


class AbstractBillingAddress(AbstractAddress):

    class Meta:
        abstract = True
        verbose_name_plural = _("Billing address")
        verbose_name_plural = _("Billing addresses")

    @property
    def order(self):
        """
        Return the order linked to this shipping address
        """
        orders = self.order_set.all()
        if not orders:
            return None
        return orders[0]
