"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/dulcinea/lib/address.py $
$Id: address.py 23424 2004-02-05 16:28:17Z nascheme $

Address-related classes, functions, and constants.
"""
import re

from dulcinea.base import DulcineaPersistent
from dulcinea.country import get_country_name
from dulcinea.typeutils import typecheck
from dulcinea.spec import nspec

def is_valid_phone_number(phone_number):
    """(phone_number:str) -> bool
    """
    typecheck(phone_number, str, allow_none=1)
    if not phone_number:
        return False
    number_of_digits = 0
    for token in phone_number:
        if token in '()-.+EeXxTt ':
            continue
        if not token.isdigit():
            return False
        else:
            number_of_digits += 1
    if number_of_digits < 10:
        return False
    return True

class Address (DulcineaPersistent):
    """A mailing address, which consists of a bunch of attributes.
    You can also check the validity of an address.

    Instance attributes:
      contact_name : string
      contact_phone_number : string
      company_name : string
      street1 : string
        1st line of the street address
      street2 : string
        2nd line of the street address
      city : string
      state : string
      zip : string
      country_code : string
        ISO-3166 two-letter country code (see dulcinea.country for
        tools to map between country code and country name)
    """

    STATES = [
        "AL", "AK", "AR", "AZ", "CA", "CO", "CT", "DE", "FL", "GA",
        "HI", "IA", "ID", "IL", "IN", "KS", "KY", "LA", "MA", "MD",
        "ME", "MI", "MN", "MO", "MS", "MT", "NC", "ND", "NE", "NH",
        "NJ", "NM", "NV", "NY", "OH", "OK", "OR", "PA", "RI", "SC",
        "SD", "TN", "TX", "UT", "VA", "VT", "WA", "WI", "WV", "WY",

        # Commonwealths & territories
        "AS", "DC", "FM", "GU", "MP", "PR", "VI",

        # Armed forces codes
        "AA", "AE", "AP"]

    FRIENDLY_ATTR = { 'street1': "street address",
                      'street2': "street address",
                      'state': "state/province",
                      'zip': "postal/ZIP code",
                      'country_code': "country",
                      'contact_name': "name",
                      'company_name': "company",
                      'contact_phone_number': "phone number"}

    def __init__(self, street1=None, street2=None, city=None,
                 state=None, zip=None, country_code=None):
        """(street1:str=None, street2:str=None, city:str=None, state:str=None,
            zip:str=None, country_code:str=None)
        """
        self.contact_name = None
        self.contact_phone_number = None
        self.company_name = None
        self.set_street(street1, street2)
        self.set_city(city)
        self.set_state(state)
        self.set_zip(zip)
        self.set_country_code(country_code)

    # This should really be __eq__ -- lt/gt/etc don't make sense here.
    def __cmp__(self, other):
        # Return the first non-zero cmp()
        return (cmp(self.contact_name, other.contact_name) or
                cmp(self.contact_phone_number, other.contact_phone_number) or
                cmp(self.company_name, other.company_name) or
                cmp(self.street1, other.street1) or
                cmp(self.street2, other.street2) or
                cmp(self.city, other.city) or
                cmp(self.state, other.state) or
                cmp(self.zip, other.zip) or
                cmp(self.country_code, other.country_code))

    def get_friendly_attr(self, attr):
        """(attr : str) -> str

        Expand the attribute names returned by check_complete()
        to human-readable form.
        """
        if not hasattr(self, attr):
            raise AttributeError, attr
        fr_attr = self.FRIENDLY_ATTR.get(attr)
        if fr_attr is None:
            return " ".join(attr.split("_"))
        else:
            return fr_attr

    def is_empty(self):
        """() -> bool

        Return true if this address is empty.
        """
        values = filter(None, self.__dict__.values())
        return len(values) == 0

    def is_valid(self):
        """() -> bool

        Return true if this address is valid: either it's empty or
        it's complete (and correct!).
        """
        return self.check_complete() == []

    def check_complete(self):
        """() -> [(attrname:str, errtype:str)]

        Check whether this address is complete: must have at least one
        line of street address, city, and country; if country is US,
        must have a valid state and ZIP code too.

        Returns a list of (attrname, errtype) 2-tuples explaining any
        problems.  'errtype' is either "missing" or "bad".
        """
        errors = []

        # Check that the contact_name, contact_phone_number
        # and various critical fields in the address aren't blank.
        for field in ['contact_name', 'contact_phone_number',
                      'street1', 'city', 'country_code']:
            if not getattr(self, field):
                errors.append((field, "missing"))

        # US-specific checks
        if self.country_code == 'US':
            if not self.state:
                errors.append(('state', "missing"))
            else:
                state = self.state.upper()
                if state not in self.STATES:
                    errors.append(('state', "bad"))
                else:
                    self.state = state  # ensure uppercase

            # Check the Zip code (5 digits, or 5+4 format)
            if not self.zip:
                errors.append(('zip', "missing"))
            elif re.match(r'\d{5}(-\d{4})?$', self.zip) is None:
                errors.append(('zip', "bad"))

            # Check Phone number (10 digits, some special chars ok)
            if self.contact_phone_number and not self.has_valid_phone_number():
                errors.append(('contact_phone_number', 'bad'))
        return errors

    def has_valid_phone_number(self):
        return is_valid_phone_number(self.contact_phone_number)

    def error_message(self):
        msg = ""
        for errors in self.check_complete():
            msg = msg + "%s is %s, " % (
                " ".join([word.capitalize() for word in errors[0].split("_")]),
                errors[1])
        return (msg and msg[:-2] + '.') or msg

    def set_contact_name(self, contact_name):
        """(contact_name:str)
        """
        typecheck(contact_name, str, allow_none=1)
        self.contact_name = contact_name

    def get_contact_name(self):
        return self.contact_name
    
    def set_contact_phone_number(self, contact_phone_number):
        """(contact_phone_number:str)
        """
        typecheck(contact_phone_number, str, allow_none=1)
        self.contact_phone_number = contact_phone_number

    def get_contact_phone_number(self):
        return self.contact_phone_number
    
    def set_company_name(self, company_name):
        """(company_name:str)
        """
        typecheck(company_name, str, allow_none=1)
        self.company_name = company_name

    def set_street(self, street1, street2):
        """(street1:str, street2:str)
        """
        typecheck(street1, str, allow_none=1)
        typecheck(street2, str, allow_none=1)
        self.street1 = street1
        self.street2 = street2

    def set_city(self, city):
        """(city:str)
        """
        typecheck(city, str, allow_none=1)
        self.city = city

    def set_state(self, state):
        """(state:str)
        """
        typecheck(state, str, allow_none=1)
        self.state = state

    def set_country_code(self, country_code):
        """(country_code:str)
        """
        if country_code is not None:
            typecheck(country_code, str)
            country_code = country_code.upper()
            if not get_country_name(country_code):
                raise ValueError("invalid country code: %r." % country_code)
        self.country_code = country_code

    def set_zip(self, zip):
        """(zip:str)
        """
        typecheck(zip, str, allow_none=1)
        self.zip = zip

    def format(self):
        if self.contact_name:
            address = self.contact_name + "\n"
        else:
            address = ""
        if self.company_name:
            address += self.company_name + "\n"
        if self.street1:
            address += self.street1 + "\n"
        if self.street2:
            address += self.street2 + "\n"
        if self.city:
            address += self.city + ", "
        if self.state:
            address += self.state + " "
        if self.zip:
            address += self.zip
        if address[-1:] != "\n":
            address += "\n"
        if self.country_code:
            address += self.country_code
        return address


class Addressable:
    """
    Mixin for objects that contain addresses

    Instance attributes:
      address : Address
    """

    address_is = nspec(Address)

    def __init__(self):
        self.address = None

    def set_address(self, address):
        """(address : Address)
        """
        typecheck(address, Address)
        self.address = address

    def get_address(self):
        """() -> Address
        """
        return self.address

