import logging
import sys

from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.fields.related import ForeignObject
from django.utils.encoding import python_2_unicode_compatible

try:
    from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor
except ImportError:
    from django.db.models.fields.related import ReverseSingleRelatedObjectDescriptor as ForwardManyToOneDescriptor

logger = logging.getLogger(__name__)

if sys.version > '3':
    long = int
    basestring = (str, bytes)
    unicode = str

__all__ = ['Country', 'State', 'Locality', 'Address', 'AddressField']


class InconsistentDictError(Exception):
    pass


def _to_python(value):
    raw = value.get('raw', '')
    country = value.get('country', '')
    country_code = value.get('country_code', '')
    state = value.get('state', '')
    state_code = value.get('state_code', '')
    locality = value.get('locality', '')
    sublocality = value.get('sublocality', '')
    postal_code = value.get('postal_code', '')
    street_number = value.get('street_number', '')
    route = value.get('route', '')
    formatted = value.get('formatted', '')
    latitude = value.get('latitude', None)
    longitude = value.get('longitude', None)

    # If there is no value (empty raw) then return None.
    if not raw:
        return None

    # Fix issue with NYC boroughs (https://code.google.com/p/gmaps-api-issues/issues/detail?id=635)
    if not locality and sublocality:
        locality = sublocality

    # If we have an inconsistent set of value bail out now.
    if (country or state or locality) and not (country and state and locality):
        raise InconsistentDictError

    # Handle the country.
    try:
        country_obj = Country.objects.get(name=country)
    except Country.DoesNotExist:
        if country:
            if len(country_code) > Country._meta.get_field('code').max_length:
                if country_code != country:
                    raise ValueError('Invalid country code (too long): %s' % country_code)
                country_code = ''
            country_obj = Country.objects.create(name=country, code=country_code)
        else:
            country_obj = None

    # Handle the state.
    try:
        state_obj = State.objects.get(name=state, country=country_obj)
    except State.DoesNotExist:
        if state:
            if len(state_code) > State._meta.get_field('code').max_length:
                if state_code != state:
                    raise ValueError('Invalid state code (too long): %s' % state_code)
                state_code = ''
            state_obj = State.objects.create(name=state, code=state_code, country=country_obj)
        else:
            state_obj = None

    # Handle the locality.
    try:
        locality_obj = Locality.objects.get(name=locality, postal_code=postal_code, state=state_obj)
    except Locality.DoesNotExist:
        if locality:
            locality_obj = Locality.objects.create(name=locality, postal_code=postal_code, state=state_obj)
        else:
            locality_obj = None

    # Handle the address.
    try:
        if not (street_number or route or locality):
            address_obj = Address.objects.get(raw=raw)
        else:
            address_obj = Address.objects.get(
                street_number=street_number,
                route=route,
                locality=locality_obj
            )
    except Address.DoesNotExist:
        address_obj = Address(
            street_number=street_number,
            route=route,
            raw=raw,
            locality=locality_obj,
            formatted=formatted,
            latitude=latitude,
            longitude=longitude,
        )

        # If "formatted" is empty try to construct it from other values.
        if not address_obj.formatted:
            address_obj.formatted = unicode(address_obj)

        # Need to save.
        address_obj.save()

    # Done.
    return address_obj

##
# Convert a dictionary to an address.
##


def to_python(value):

    # Keep `None`s.
    if value is None:
        return None

    # Is it already an address object?
    if isinstance(value, Address):
        return value

    # If we have an integer, assume it is a model primary key. This is mostly for
    # Django being a cunt.
    elif isinstance(value, (int, long)):
        return value

    # A string is considered a raw value.
    elif isinstance(value, basestring):
        obj = Address(raw=value)
        obj.save()
        return obj

    # A dictionary of named address components.
    elif isinstance(value, dict):

        # Attempt a conversion.
        try:
            return _to_python(value)
        except InconsistentDictError:
            return Address.objects.create(raw=value['raw'])

    # Not in any of the formats I recognise.
    raise ValidationError('Invalid address value.')

##
# A country.
##


@python_2_unicode_compatible
class Country(models.Model):
    name = models.CharField(max_length=40, unique=True, blank=True)
    code = models.CharField(max_length=2, blank=True)  # not unique as there are duplicates (IT)

    class Meta:
        verbose_name_plural = 'Countries'
        ordering = ('name',)

    def __str__(self):
        return '%s' % (self.name or self.code)

##
# A state. Google refers to this as `administration_level_1`.
##


@python_2_unicode_compatible
class State(models.Model):
    name = models.CharField(max_length=165, blank=True)
    code = models.CharField(max_length=3, blank=True)
    country = models.ForeignKey(Country, on_delete=models.CASCADE, related_name='states')

    class Meta:
        unique_together = ('name', 'country')
        ordering = ('country', 'name')

    def __str__(self):
        txt = self.to_str()
        country = '%s' % self.country
        if country and txt:
            txt += ', '
        txt += country
        return txt

    def to_str(self):
        return '%s' % (self.name or self.code)

##
# A locality (suburb).
##


@python_2_unicode_compatible
class Locality(models.Model):
    name = models.CharField(max_length=165, blank=True)
    postal_code = models.CharField(max_length=10, blank=True)
    state = models.ForeignKey(State, on_delete=models.CASCADE, related_name='localities')

    class Meta:
        verbose_name_plural = 'Localities'
        unique_together = ('name', 'postal_code', 'state')
        ordering = ('state', 'name')

    def __str__(self):
        txt = '%s' % self.name
        state = self.state.to_str() if self.state else ''
        if txt and state:
            txt += ', '
        txt += state
        if self.postal_code:
            txt += ' %s' % self.postal_code
        cntry = '%s' % (self.state.country if self.state and self.state.country else '')
        if cntry:
            txt += ', %s' % cntry
        return txt

##
# An address. If for any reason we are unable to find a matching
# decomposed address we will store the raw address string in `raw`.
##


@python_2_unicode_compatible
class Address(models.Model):
    street_number = models.CharField(max_length=20, blank=True)
    route = models.CharField(max_length=100, blank=True)
    locality = models.ForeignKey(Locality, on_delete=models.CASCADE, related_name='addresses', blank=True, null=True)
    raw = models.CharField(max_length=200)
    formatted = models.CharField(max_length=200, blank=True)
    latitude = models.FloatField(blank=True, null=True)
    longitude = models.FloatField(blank=True, null=True)

    class Meta:
        verbose_name_plural = 'Addresses'
        ordering = ('locality', 'route', 'street_number')
        # unique_together = ('locality', 'route', 'street_number')

    def __str__(self):
        if self.formatted != '':
            txt = '%s' % self.formatted
        elif self.locality:
            txt = ''
            if self.street_number:
                txt = '%s' % self.street_number
            if self.route:
                if txt:
                    txt += ' %s' % self.route
            locality = '%s' % self.locality
            if txt and locality:
                txt += ', '
            txt += locality
        else:
            txt = '%s' % self.raw
        return txt

    def clean(self):
        if not self.raw:
            raise ValidationError('Addresses may not have a blank `raw` field.')

    def as_dict(self):
        ad = dict(
            street_number=self.street_number,
            route=self.route,
            raw=self.raw,
            formatted=self.formatted,
            latitude=self.latitude if self.latitude else '',
            longitude=self.longitude if self.longitude else '',
        )
        if self.locality:
            ad['locality'] = self.locality.name
            ad['postal_code'] = self.locality.postal_code
            if self.locality.state:
                ad['state'] = self.locality.state.name
                ad['state_code'] = self.locality.state.code
                if self.locality.state.country:
                    ad['country'] = self.locality.state.country.name
                    ad['country_code'] = self.locality.state.country.code
        return ad


class AddressDescriptor(ForwardManyToOneDescriptor):

    def __set__(self, inst, value):
        super(AddressDescriptor, self).__set__(inst, to_python(value))

##
# A field for addresses in other models.
##


class AddressField(models.ForeignKey):
    description = 'An address'

    def __init__(self, *args, **kwargs):
        kwargs['to'] = 'address.Address'
        super(AddressField, self).__init__(*args, **kwargs)

    def contribute_to_class(self, cls, name, virtual_only=False):
        from address.compat import compat_contribute_to_class

        compat_contribute_to_class(self, cls, name, virtual_only)
        # super(ForeignObject, self).contribute_to_class(cls, name, virtual_only=virtual_only)

        setattr(cls, self.name, AddressDescriptor(self))

    # def deconstruct(self):
    #     name, path, args, kwargs = super(AddressField, self).deconstruct()
    #     del kwargs['to']
    #     return name, path, args, kwargs

    def formfield(self, **kwargs):
        from .forms import AddressField as AddressFormField
        defaults = dict(form_class=AddressFormField)
        defaults.update(kwargs)
        return super(AddressField, self).formfield(**defaults)

[((14, 9, 14, 36), 'logging.getLogger', 'logging.getLogger', ({(14, 27, 14, 35): '__name__'}, {}), '(__name__)', False, 'import logging\n'), ((157, 10, 157, 51), 'django.core.exceptions.ValidationError', 'ValidationError', ({(157, 26, 157, 50): '"""Invalid address value."""'}, {}), "('Invalid address value.')", False, 'from django.core.exceptions import ValidationError\n'), ((166, 11, 166, 67), 'django.db.models.CharField', 'models.CharField', (), '', False, 'from django.db import models\n'), ((167, 11, 167, 53), 'django.db.models.CharField', 'models.CharField', (), '', False, 'from django.db import models\n'), ((183, 11, 183, 55), 'django.db.models.CharField', 'models.CharField', (), '', False, 'from django.db import models\n'), ((184, 11, 184, 53), 'django.db.models.CharField', 'models.CharField', (), '', False, 'from django.db import models\n'), ((185, 14, 185, 89), 'django.db.models.ForeignKey', 'models.ForeignKey', (), '', False, 'from django.db import models\n'), ((209, 11, 209, 55), 'django.db.models.CharField', 'models.CharField', (), '', False, 'from django.db import models\n'), ((210, 18, 210, 61), 'django.db.models.CharField', 'models.CharField', (), '', False, 'from django.db import models\n'), ((211, 12, 211, 89), 'django.db.models.ForeignKey', 'models.ForeignKey', (), '', False, 'from django.db import models\n'), ((239, 20, 239, 63), 'django.db.models.CharField', 'models.CharField', (), '', False, 'from django.db import models\n'), ((240, 12, 240, 56), 'django.db.models.CharField', 'models.CharField', (), '', False, 'from django.db import models\n'), ((241, 15, 241, 117), 'django.db.models.ForeignKey', 'models.ForeignKey', (), '', False, 'from django.db import models\n'), ((242, 10, 242, 42), 'django.db.models.CharField', 'models.CharField', (), '', False, 'from django.db import models\n'), ((243, 16, 243, 60), 'django.db.models.CharField', 'models.CharField', (), '', False, 'from django.db import models\n'), ((244, 15, 244, 55), 'django.db.models.FloatField', 'models.FloatField', (), '', False, 'from django.db import models\n'), ((245, 16, 245, 56), 'django.db.models.FloatField', 'models.FloatField', (), '', False, 'from django.db import models\n'), ((315, 8, 315, 65), 'address.compat.compat_contribute_to_class', 'compat_contribute_to_class', ({(315, 35, 315, 39): 'self', (315, 41, 315, 44): 'cls', (315, 46, 315, 50): 'name', (315, 52, 315, 64): 'virtual_only'}, {}), '(self, cls, name, virtual_only)', False, 'from address.compat import compat_contribute_to_class\n'), ((272, 18, 272, 80), 'django.core.exceptions.ValidationError', 'ValidationError', ({(272, 34, 272, 79): '"""Addresses may not have a blank `raw` field."""'}, {}), "('Addresses may not have a blank `raw` field.')", False, 'from django.core.exceptions import ValidationError\n')]