"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/dulcinea/lib/address.py $
$Id: address.py 25248 2004-10-01 20:26:51Z dbinger $

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

class Address (DulcineaPersistent):
    """
    Instance attributes:
      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"]


    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._set_street(street1, street2)
        self._set_city(city)
        self._set_state(state)
        self._set_zip(zip)
        self._set_country_code(country_code)

    def __eq__(self, other):
        return (isinstance(other, self.__class__) and
                other.__dict__ == self.__dict__)

    def __ne__(self, other):
        return not self.__eq__(other)

    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 = []
        for field in ['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"))
                elif state != self.state:
                    self.state = state  # make uppercase

            # Check the Zip code (5 digits, or 5+4 format)
            if not self.zip:
                errors.append(('zip', "missing"))
            elif not self.has_valid_zip():
                errors.append(('zip', "bad"))

        return errors

    def has_valid_zip(self):
        if self.country_code == 'US':
            if not self.zip:
                return False
            return re.match(r'\d{5}(-\d{4})?$', self.zip) is not None
        return True

    def has_valid_state(self):
        if self.country_code == 'US':
            return self.state and self.state in self.STATES
        return True

    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_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:
            typecheck(country_code, str)
            country_code = country_code.upper()
            get_country_name(country_code) # raises KeyError if not found
        self.country_code = country_code

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

    def format(self):
        address = ""
        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

    def get_street1(self):
        return self.street1

    def get_street2(self):
        return self.street2

    def format_street(self):
        result = self.street1 or ''
        if self.street2:
            result += ', ' + self.street2
        return result

    def get_city(self):
        return self.city

    def get_state(self):
        return self.state

    def get_zip(self):
        return self.zip

    def get_country_code(self):
        return self.country_code

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

    Instance attributes:
      address : Address
    """

    address_is = Address

    def __init__(self, address=None, **kwargs):
        self.address = address or Address(**kwargs)

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

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


class ContactAddress(DulcineaPersistent, Addressable):
    """
    This is an address with the additional information normally needed
    for shipping.

    Instance attributes:
      contact_name : string
      contact_phone_number : string
      company_name : string
    """

    def __init__(self, contact_name=None, company_name=None,
                 contact_phone_number=None, **kwargs):
        Addressable.__init__(self, **kwargs)
        self._set_contact_name(contact_name)
        self._set_contact_phone_number(contact_phone_number)
        self._set_company_name(company_name)

    def __eq__(self, other):
        return (isinstance(other, self.__class__) and
                other.__dict__ == self.__dict__)

    def __ne__(self, other):
        return not self.__eq__(other)

    def _set_contact_name(self, name):
        typecheck(name, str, allow_none=1)
        self.contact_name = name

    def _set_contact_phone_number(self, contact_phone_number):
        typecheck(contact_phone_number, str, allow_none=1)
        self.contact_phone_number = contact_phone_number

    def _set_company_name(self, company_name):
        typecheck(company_name, str, allow_none=1)
        self.company_name = company_name

    def is_valid(self):
        return self._check_complete() == []

    def error_message(self):
        return self.address.error_message()

    def get_contact_name(self):
        return self.contact_name

    def get_contact_phone_number(self):
        return self.contact_phone_number

    def set_address(self, address):
        raise RuntimeError("ContactAddress is treated as immutable")

    def get_company_name(self):
        return self.company_name

    def get_street1(self):
        return self.address.get_street1()

    def get_street2(self):
        return self.address.get_street2()

    def get_city(self):
        return self.address.get_city()

    def get_state(self):
        return self.address.get_state()

    def get_zip(self):
        return self.address.get_zip()

    def get_country_code(self):
        return self.address.get_country_code()

    def has_valid_phone_number(self):
        if not self.contact_phone_number:
            return False
        number_of_digits = 0
        for token in self.contact_phone_number:
            if token in '()-.+EeXxTt ':
                continue
            if not token.isdigit():
                return False
            else:
                number_of_digits += 1
        return number_of_digits >= 10

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

        Returns a list of (attrname, errtype) 2-tuples explaining any
        problems.  'errtype' is either "missing" or "bad".
        """
        errors = self.address._check_complete()
        for field in ['contact_name', 'contact_phone_number']:
            if not getattr(self, field):
                errors.append((field, "missing"))
        if self.contact_phone_number and not self.has_valid_phone_number():
            errors.append(('contact_phone_number', 'bad'))
        return errors

    def format(self):
        if self.get_contact_name():
            output = self.get_contact_name() + "\n"
        else:
            output = ""
        if self.company_name:
            output += self.company_name + "\n"
        output += self.address.format()
        return output


class ContactAddressable:
    """
    Mixin for objects that have a contact address.

    Instance attributes:
      contact_address : ContactAddress
    """

    contact_address_is = ContactAddress

    def __init__(self):
        self.contact_address = ContactAddress()

    def set_contact_address(self, address):
        """(address : ContactAddress)
        """
        typecheck(address, ContactAddress)
        self.contact_address = address

    def get_contact_address(self):
        """() -> ContactAddress
        """
        return self.contact_address



