#!/usr/bin/env python
"""Module for calling Geo related last.fm web services API methods"""

__author__ = "Abhinav Sarkar <abhinav@abhinavsarkar.net>"
__version__ = "0.2"
__license__ = "GNU Lesser General Public License"
__package__ = "lastfm"

from functools import reduce
from lastfm.base import LastfmBase
from lastfm.mixin import mixin
from lastfm.decorators import cached_property, top_property, depaginate

class Geo(object):
    """A class representing an geographic location"""
    @staticmethod
    @depaginate
    def get_events(api,
                  location,
                  latitude = None,
                  longitude = None,
                  distance = None,
                  page = None):
        """
        Get the events for a location.
        
        @param api:          an instance of L{Api}
        @type api:           L{Api}
        @param location:     location to retrieve events for (optional)
        @type location:      L{str}
        @param latitude:     latitude value to retrieve events for (optional)
        @type latitude:      L{float}
        @param longitude:    longitude value to retrieve events for (optional)
        @type longitude:     L{float}
        @param distance:     find events within a specified distance (optional)
        @type distance:      L{float}
        
        @return:             events for the location
        @rtype:              L{lazylist} of L{Event}
        
        @raise InvalidParametersError: Either location or latitude and longitude
                                       has to be provided. Otherwise exception is
                                       raised.
        
        @note: Use L{Location.events} instead of using this method directly.
        """
        if reduce(lambda x,y: x and y is None, [location, latitude, longitude], True):
            raise InvalidParametersError(
                "Either location or latitude and longitude has to be provided")
        
        params = {'method': 'geo.getEvents', 'location': location}
        if distance is not None:
            params.update({'distance': distance})

        if latitude is not None and longitude is not None:
            params.update({'lat': latitude, 'long': longitude})
        if page is not None:
            params.update({'page': page})
            
        data = api._fetch_data(params).find('events')
        total_pages = int(data.attrib['totalpages'])
        yield total_pages
        
        for e in data.findall('event'):
            yield Event.create_from_data(api, e)

    @staticmethod
    def get_top_artists(api, country):
        """
        Get the most popular artists on Last.fm by country
        
        @param api:          an instance of L{Api}
        @type api:           L{Api}
        @param country:      a country name, as defined by 
                             the ISO 3166-1 country names standard
        @type country:       L{str}
        
        @return:             most popular artists of the country
        @rtype:              L{list} of L{Artist}
        
        @note: Use L{Country.top_artists} instead of using this method directly.
        """
        params = {'method': 'geo.getTopArtists', 'country': country}
        data = api._fetch_data(params).find('topartists')
        return [
                Artist(
                       api,
                       name = a.findtext('name'),
                       mbid = a.findtext('mbid'),
                       stats = Stats(
                                     subject = a.findtext('name'),
                                     rank = int(a.attrib['rank']),
                                     playcount = int(a.findtext('playcount')) if a.findtext('playcount') else None,
                                     listeners = int(a.findtext('listeners')) if a.findtext('listeners') else None
                                     ),
                       url = 'http://' + a.findtext('url'),
                       image = {'large': a.findtext('image')}
                       )
                for a in data.findall('artist')
                ]

    @staticmethod
    def get_top_tracks(api, country, location = None):
        """
        Get the most popular tracks on Last.fm by country
        
        @param api:          an instance of L{Api}
        @type api:           L{Api}
        @param country:      a country name, as defined by 
                             the ISO 3166-1 country names standard
        @type country:       L{str}
        @param location:     a metro name, to fetch the charts for 
                            (must be within the country specified) (optional)
        
        @return:             most popular tracks of the country
        @rtype:              L{list} of L{Track}
        
        @note: Use L{Country.top_tracks} and L{Country.get_top_tracks}
               instead of using this method directly.
        """
        params = {'method': 'geo.getTopTracks', 'country': country}
        if location is not None:
            params.update({'location': location})

        data = api._fetch_data(params).find('toptracks')
        return [
                Track(
                       api,
                       name = t.findtext('name'),
                       mbid = t.findtext('mbid'),
                       artist = Artist(
                                       api,
                                       name = t.findtext('artist/name'),
                                       mbid = t.findtext('artist/mbid'),
                                       url = t.findtext('artist/url')
                                       ),
                       stats = Stats(
                                     subject = t.findtext('name'),
                                     rank = int(t.attrib['rank']),
                                     playcount = int(t.findtext('playcount')) if t.findtext('playcount') else None,
                                     listeners = int(t.findtext('listeners')) if t.findtext('listeners') else None
                                     ),
                       streamable = (t.findtext('streamable') == '1'),
                       full_track = (t.find('streamable').attrib['fulltrack'] == '1'),
                       url = 'http://' + t.findtext('url'),
                       image = {'large': t.findtext('image')}
                       )
                for t in data.findall('track')
                ]

@mixin("crawlable", "cacheable", "property_adder")
class Location(LastfmBase):
    """A class representing a location of an event"""
    XMLNS = "http://www.w3.org/2003/01/geo/wgs84_pos#"
    
    class Meta(object):
        properties = ["city", "country", "street",
            "postal_code", "latitude", "longitude",
            "timezone"]
        
    def init(self, api, **kwargs):
        """
        Create a Location object by providing all the data related to it.
        
        @param api:          an instance of L{Api}
        @type api:           L{Api}
        @param city:         city in which the location is situated
        @type city:          L{str}
        @param country:      country in which the location is situated
        @type country:       L{Country}
        @param street:       street in which the location is situated
        @type street:        L{str}
        @param postal_code:  postal code of the location
        @type postal_code:   L{str}
        @param latitude:     latitude of the location
        @type latitude:      L{float}
        @param longitude:    longitude of the location
        @type longitude:     L{float}
        @param timezone:     timezone in which the location is situated
        @type timezone:      L{str}
        
        @raise InvalidParametersError: If an instance of L{Api} is not provided as the first
                                       parameter then an Exception is raised.
        """
        if not isinstance(api, Api):
            raise InvalidParametersError("api reference must be supplied as an argument")
        self._api = api
        super(Location, self).init(**kwargs)

    @cached_property
    def top_tracks(self):
        """
        top tracks for the location
        @rtype: L{list} of L{Track}
        """
        if self.country is None or self.city is None:
            raise InvalidParametersError("country and city of this location are required for calling this method")
        return Geo.get_top_tracks(self._api, self.country.name, self.city)

    @top_property("top_tracks")
    def top_track(self):
        """
        top track for the location
        @rtype: L{Track}
        """
        pass

    def get_events(self, distance = None):
        """
        Get the events taking place at the location.
        
        @param distance:    find events within a specified distance (optional)
        @type distance:     L{float}
        
        @return:            events taking place at the location
        @rtype:             L{lazylist} of L{Event}
        """
        return Geo.get_events(self._api, self.city,
            self.latitude, self.longitude, distance)

    @cached_property
    def events(self):
        """
        events taking place at/around the location
        @rtype: L{lazylist} of L{Event}
        """
        return self.get_events()

    @staticmethod
    def _get_all(seed_location):
        def gen():
            for event in Event.get_all(seed_location.events[0]):
                yield event.venue.location
        
        return (seed_location, ['city', 'country'], lambda api, hsh: gen())
        
    @staticmethod
    def _hash_func(*args, **kwds):
        try:
            return hash("latlong%s%s" % (kwds['latitude'], kwds['longitude']))
        except KeyError:
            try:
                return hash("name%s" % kwds['city'])
            except KeyError:
                raise InvalidParametersError("either latitude and longitude or city has to be provided for hashing")

    def __hash__(self):
        if not self.city:
            return self.__class__._hash_func(
                                           latitude = self.latitude,
                                           longitude = self.longitude)
        else:
            return self.__class__._hash_func(name = self.city)

    def __eq__(self, other):
        return self.latitude == other.latitude and self.longitude == other.longitude

    def __lt__(self, other):
        if self.country != other.country:
            return self.country < other.country
        else:
            return self.city < other.city

    def __repr__(self):
        if self.city is None:
            return "<lastfm.geo.Location: (%s, %s)>" % (self.latitude, self.longitude)
        else:
            return "<lastfm.geo.Location: %s>" % self.city

@mixin("crawlable", "cacheable", "property_adder")
class Country(LastfmBase):
    """A class representing a country."""
    ISO_CODES = {
         'AD': 'Andorra',
         'AE': 'United Arab Emirates',
         'AF': 'Afghanistan',
         'AG': 'Antigua and Barbuda',
         'AI': 'Anguilla',
         'AL': 'Albania',
         'AM': 'Armenia',
         'AN': 'Netherlands Antilles',
         'AO': 'Angola',
         'AQ': 'Antarctica',
         'AR': 'Argentina',
         'AS': 'American Samoa',
         'AT': 'Austria',
         'AU': 'Australia',
         'AW': 'Aruba',
         'AX': 'land Islands',
         'AZ': 'Azerbaijan',
         'BA': 'Bosnia and Herzegovina',
         'BB': 'Barbados',
         'BD': 'Bangladesh',
         'BE': 'Belgium',
         'BF': 'Burkina Faso',
         'BG': 'Bulgaria',
         'BH': 'Bahrain',
         'BI': 'Burundi',
         'BJ': 'Benin',
         'BL': 'Saint Barthlemy',
         'BM': 'Bermuda',
         'BN': 'Brunei Darussalam',
         'BO': 'Bolivia',
         'BR': 'Brazil',
         'BS': 'Bahamas',
         'BT': 'Bhutan',
         'BV': 'Bouvet Island',
         'BW': 'Botswana',
         'BY': 'Belarus',
         'BZ': 'Belize',
         'CA': 'Canada',
         'CC': 'Cocos (Keeling) Islands',
         'CD': 'Congo, The Democratic Republic of the',
         'CF': 'Central African Republic',
         'CG': 'Congo',
         'CH': 'Switzerland',
         'CI': "Cte d'Ivoire",
         'CK': 'Cook Islands',
         'CL': 'Chile',
         'CM': 'Cameroon',
         'CN': 'China',
         'CO': 'Colombia',
         'CR': 'Costa Rica',
         'CU': 'Cuba',
         'CV': 'Cape Verde',
         'CX': 'Christmas Island',
         'CY': 'Cyprus',
         'CZ': 'Czech Republic',
         'DE': 'Germany',
         'DJ': 'Djibouti',
         'DK': 'Denmark',
         'DM': 'Dominica',
         'DO': 'Dominican Republic',
         'DZ': 'Algeria',
         'EC': 'Ecuador',
         'EE': 'Estonia',
         'EG': 'Egypt',
         'EH': 'Western Sahara',
         'ER': 'Eritrea',
         'ES': 'Spain',
         'ET': 'Ethiopia',
         'FI': 'Finland',
         'FJ': 'Fiji',
         'FK': 'Falkland Islands (Malvinas)',
         'FM': 'Micronesia, Federated States of',
         'FO': 'Faroe Islands',
         'FR': 'France',
         'GA': 'Gabon',
         'GB': 'United Kingdom',
         'GD': 'Grenada',
         'GE': 'Georgia',
         'GF': 'French Guiana',
         'GG': 'Guernsey',
         'GH': 'Ghana',
         'GI': 'Gibraltar',
         'GL': 'Greenland',
         'GM': 'Gambia',
         'GN': 'Guinea',
         'GP': 'Guadeloupe',
         'GQ': 'Equatorial Guinea',
         'GR': 'Greece',
         'GS': 'South Georgia and the South Sandwich Islands',
         'GT': 'Guatemala',
         'GU': 'Guam',
         'GW': 'Guinea-Bissau',
         'GY': 'Guyana',
         'HK': 'Hong Kong',
         'HM': 'Heard Island and McDonald Islands',
         'HN': 'Honduras',
         'HR': 'Croatia',
         'HT': 'Haiti',
         'HU': 'Hungary',
         'ID': 'Indonesia',
         'IE': 'Ireland',
         'IL': 'Israel',
         'IM': 'Isle of Man',
         'IN': 'India',
         'IO': 'British Indian Ocean Territory',
         'IQ': 'Iraq',
         'IR': 'Iran, Islamic Republic of',
         'IS': 'Iceland',
         'IT': 'Italy',
         'JE': 'Jersey',
         'JM': 'Jamaica',
         'JO': 'Jordan',
         'JP': 'Japan',
         'KE': 'Kenya',
         'KG': 'Kyrgyzstan',
         'KH': 'Cambodia',
         'KI': 'Kiribati',
         'KM': 'Comoros',
         'KN': 'Saint Kitts and Nevis',
         'KP': "Korea, Democratic People's Republic of",
         'KR': 'Korea, Republic of',
         'KW': 'Kuwait',
         'KY': 'Cayman Islands',
         'KZ': 'Kazakhstan',
         'LA': "Lao People's Democratic Republic",
         'LB': 'Lebanon',
         'LC': 'Saint Lucia',
         'LI': 'Liechtenstein',
         'LK': 'Sri Lanka',
         'LR': 'Liberia',
         'LS': 'Lesotho',
         'LT': 'Lithuania',
         'LU': 'Luxembourg',
         'LV': 'Latvia',
         'LY': 'Libyan Arab Jamahiriya',
         'MA': 'Morocco',
         'MC': 'Monaco',
         'MD': 'Moldova',
         'ME': 'Montenegro',
         'MF': 'Saint Martin',
         'MG': 'Madagascar',
         'MH': 'Marshall Islands',
         'MK': 'Macedonia, The Former Yugoslav Republic of',
         'ML': 'Mali',
         'MM': 'Myanmar',
         'MN': 'Mongolia',
         'MO': 'Macao',
         'MP': 'Northern Mariana Islands',
         'MQ': 'Martinique',
         'MR': 'Mauritania',
         'MS': 'Montserrat',
         'MT': 'Malta',
         'MU': 'Mauritius',
         'MV': 'Maldives',
         'MW': 'Malawi',
         'MX': 'Mexico',
         'MY': 'Malaysia',
         'MZ': 'Mozambique',
         'NA': 'Namibia',
         'NC': 'New Caledonia',
         'NE': 'Niger',
         'NF': 'Norfolk Island',
         'NG': 'Nigeria',
         'NI': 'Nicaragua',
         'NL': 'Netherlands',
         'NO': 'Norway',
         'NP': 'Nepal',
         'NR': 'Nauru',
         'NU': 'Niue',
         'NZ': 'New Zealand',
         'OM': 'Oman',
         'PA': 'Panama',
         'PE': 'Peru',
         'PF': 'French Polynesia',
         'PG': 'Papua New Guinea',
         'PH': 'Philippines',
         'PK': 'Pakistan',
         'PL': 'Poland',
         'PM': 'Saint Pierre and Miquelon',
         'PN': 'Pitcairn',
         'PR': 'Puerto Rico',
         'PS': 'Palestinian Territory, Occupied',
         'PT': 'Portugal',
         'PW': 'Palau',
         'PY': 'Paraguay',
         'QA': 'Qatar',
         'RE': 'Runion',
         'RO': 'Romania',
         'RS': 'Serbia',
         'RU': 'Russian Federation',
         'RW': 'Rwanda',
         'SA': 'Saudi Arabia',
         'SB': 'Solomon Islands',
         'SC': 'Seychelles',
         'SD': 'Sudan',
         'SE': 'Sweden',
         'SG': 'Singapore',
         'SH': 'Saint Helena',
         'SI': 'Slovenia',
         'SJ': 'Svalbard and Jan Mayen',
         'SK': 'Slovakia',
         'SL': 'Sierra Leone',
         'SM': 'San Marino',
         'SN': 'Senegal',
         'SO': 'Somalia',
         'SR': 'Suriname',
         'ST': 'Sao Tome and Principe',
         'SV': 'El Salvador',
         'SY': 'Syrian Arab Republic',
         'SZ': 'Swaziland',
         'TC': 'Turks and Caicos Islands',
         'TD': 'Chad',
         'TF': 'French Southern Territories',
         'TG': 'Togo',
         'TH': 'Thailand',
         'TJ': 'Tajikistan',
         'TK': 'Tokelau',
         'TL': 'Timor-Leste',
         'TM': 'Turkmenistan',
         'TN': 'Tunisia',
         'TO': 'Tonga',
         'TR': 'Turkey',
         'TT': 'Trinidad and Tobago',
         'TV': 'Tuvalu',
         'TW': 'Taiwan, Province of China',
         'TZ': 'Tanzania, United Republic of',
         'UA': 'Ukraine',
         'UG': 'Uganda',
         'UK': 'United Kingdom',
         'UM': 'United States Minor Outlying Islands',
         'US': 'United States',
         'UY': 'Uruguay',
         'UZ': 'Uzbekistan',
         'VA': 'Holy See (Vatican City State)',
         'VC': 'Saint Vincent and the Grenadines',
         'VE': 'Venezuela',
         'VG': 'Virgin Islands, British',
         'VI': 'Virgin Islands, U.S.',
         'VN': 'Viet Nam',
         'VU': 'Vanuatu',
         'WF': 'Wallis and Futuna',
         'WS': 'Samoa',
         'YE': 'Yemen',
         'YT': 'Mayotte',
         'ZA': 'South Africa',
         'ZM': 'Zambia',
         'ZW': 'Zimbabwe'}
    """ISO Codes of the countries"""
    
    class Meta(object):
        properties = ["name"]
        
    def init(self, api, **kwargs):
        """
        Create a Country object by providing all the data related to it.
        @param api:    an instance of L{Api}
        @type api:     L{Api}
        @param name:   name of the country
        @type name:    L{str}
        
        @raise InvalidParametersError: If an instance of L{Api} is not provided as the first
                                       parameter then an Exception is raised.
        """
        if not isinstance(api, Api):
            raise InvalidParametersError("api reference must be supplied as an argument")
        self._api = api
        super(Country, self).init(**kwargs)

    @cached_property
    def top_artists(self):
        """
        top artists of the country
        @rtype: L{list} of L{Artist}
        """
        return Geo.get_top_artists(self._api, self.name)

    @top_property("top_artists")
    def top_artist(self):
        """
        top artist of the country
        @rtype: L{Artist}
        """
        pass

    def get_top_tracks(self, location = None):
        """
        Get the top tracks for country.
        
        @param location:     a metro name, to fetch the charts for 
                            (must be within the country specified) (optional)
        
        @return:             most popular tracks of the country
        @rtype:              L{list} of L{Track}
        """
        return Geo.get_top_tracks(self._api, self.name, location)

    @cached_property
    def top_tracks(self):
        """
        top tracks of the country
        @rtype: L{list} of L{Track}
        """
        return self.get_top_tracks()

    @top_property("top_tracks")
    def top_track(self):
        """
        top track of the country
        @rtype: L{Track}
        """
        pass

    @cached_property
    def events(self):
        """
        events taking place in the country
        @rtype: L{lazylist} of L{Event}
        """
        return Geo.get_events(self._api, self.name)

    @staticmethod
    def _get_all(seed_country):
        return (seed_country, ['name'],
            lambda api, hsh: (Country(api, name = c) for c in Country.ISO_CODES.itervalues()))
        
    @staticmethod
    def _hash_func(*args, **kwds):
        try:
            return hash(kwds['name'].lower())
        except KeyError:
            raise InvalidParametersError("name has to be provided for hashing")

    def __hash__(self):
        return self.__class__._hash_func(name = self.name)

    def __eq__(self, other):
        return self.name.lower() == other.name.lower()

    def __lt__(self, other):
        return self.name < other.name

    def __repr__(self):
        return "<lastfm.geo.Country: %s>" % self.name

from lastfm.api import Api
from lastfm.artist import Artist
from lastfm.error import InvalidParametersError
from lastfm.event import Event
from lastfm.stats import Stats
from lastfm.track import Track
