# -*- coding: iso-8859-1
#
#  weather.py
#
# Classes for storing and converting weather observation and forecast data
#
# Copyright Heikki Lehvaslaiho, heikki@ebi.ac.uk
#
# This software is destributed under the terms of the GNU General Public License.
# This software is free software. You may distribute and modify this module freely
#
# Attributes may be accessed directly (this python after all), or through methods
# which take care of unit conversions and decorations.

# The default input and putput is done in metric units. If you need
# the output in non-metric units, set parameter metric to false. If
# set in the Observation instance, the setting will be recognised by
# linked Forecast objects.

#


import re

class Weather:
    "Abstract base class for holding weather observation and forecast data"
    
    _icon = '36'
    _temp = 0
    _sky  = 'N/A'
    _heat = None
    _precip = 0
    _rain_poss = 0
    _uv = 'N/A'
    _pollution = '0'
    _dew = 0
    _hum = 0
    _vis = 'N/A'
    _numericvis = 0
    _baro = 0
    _pressure_change = 'N/A'
    _wind_speed = 0
    _wind = 'Calm'
    _metric = 1
    _parent = None

    # time and date
    _hour = '00'
    _min = '00'
    _zone = 'N/A'
    _weekday = 'N/A'
    _day = 'N/A'
    _mon = 'N/A'
    _year = 'N/A'
    _date = ''

    def metric(self, value=None):
        "set/get if the output is to be given in metric units or not"
        if value != None : self._metric = value
        return self._metric

    def icon(self, value=None):
        "set/get icon code number"
        if value != None : self._icon = str(value)
        return self._icon

    def sky(self, value=None):
        "set/get description of the sky"
        if value != None : self._sky = value.capitalize()
        return self._sky

    def date(self, value=None):
        "set/get generic, nonformatted date string"
        if value != None : self._date = str(value)
        return self._date

    def uv(self, value=None):
        "set/get sun UV index string"
        if value != None : self._uv = str(value)
        return self._uv

    def pollution(self, value=None):
        "set/get air pollution index string"
        if value != None : self._pollution = str(value)
        return self._pollution

    def parent(self, value=None):
        '''set/get parent reference for this object
        Parent has to be weather Observation object
        Mostly internal method
        '''
        if value != None :
            if isinstance(value, Observation):
                self._parent = value
            else: raise TypeError ('Not a weather Observation object ['+ value +']')
        return self._parent

    def temperature(self, value=None, unit='C'):
        '''get and set the temperature
        The output units depend on metric
        if metric equals '0' method  returns Fahrenheits instead of Celcius
        Returns  : string with the temperature numeral and unit abbreviation
        Arguments: 1. temperature numeral in Celcius
                   2. unit, optional, values: 'C' (default), 'F'
        '''
        unit = unit.upper()
        if value:
            if unit == 'C': self._temp = value
            elif unit == 'F': self._temp = self.F2C(value)
            else: raise ValueError ('Unknown unit ['+ unit +']')
        return self.temperature_out(self._temp)

    def dewpoint(self, value=None, unit='C'):
        unit = unit.upper()
        if value:
            if unit == 'C': self._dew = value
            elif unit == 'F': self._dew = self.F2C(value)
            else: raise ValueError ('Unknown unit ['+ unit +']')
        return self.temperature_out(self._dew)

    def relative_heat(self, value=None, unit='C'):
        'if not set, return current temperature'
        unit = unit.upper()
        if value:
            if unit == 'C': self._heat = value
            elif unit == 'F': self._heat = self.F2C(value)
            else: raise ValueError ('Unknown unit ['+ unit +']')
        if self._heat: return self.temperature_out(self._heat)
        return self.temperature_out(self._temp)

    def temperature_out(self, value):
        if self.parent(): self.metric(self.parent().metric())
        if self.metric() :
            return str(value) + ' &#176;C'
        return str(self.C2F(value))  + ' &#176;F'

    def visibility(self, value=None, unit='km'):
        ''' This method is capable of stripping numerals from the beginning of
        the string. It does not try to guess the unit, so pay attention when using!
        '''
        unit = unit.lower()
        if value:
            self._numericvis = 0
            numvalue = 0
            p_numstart = re.compile('([\d\.]+)')
            match = re.match(p_numstart, value)
            if match: numvalue = match.group(1)
            if numvalue:
                if unit == 'km': self._vis = numvalue
                elif unit == 'mi': self._vis = self.mph2kph(numvalue)
                else: raise ValueError ('Unknown unit ['+ unit +']')
                self._numericvis = 1
            else: self._vis = value
        if self._numericvis: return self.distance_out(self._vis)
        else: return self._vis

    def distance_out(self, t):
        if self.parent(): self.metric(self.parent().metric())
        if self.metric(): return str(t) + ' km'
        return str(self.kph2mph(t))  + ' mi'

    def wind_speed(self, value=None, unit='kph'):
        unit = unit.lower()
        if value:
            if unit == 'kph': self._wind_speed = value
            elif unit == 'mph': self._wind_speed = self.mph2kph(value)
            else: raise ValueError ('Unknown unit ['+ unit +']')
        return self.speed_out(self._wind_speed)

    def speed_out(self, t):
        if self.parent(): self.metric(self.parent().metric)
        if self.metric(): return str(t) + ' kph'
        return str(self.kph2mph(t))  + ' mph'

    def wind(self, value=None):
        """set/get the string describing the wind direction
        The string is normailized into max three letter abbreviation unless
        it is 'Calm' or 'Variable' """
        if value != None :
            w = str(value)

            if w != 'Calm' and w != 'calm' and w != 'Variable' and w != 'variable':
                # capitalize
                def _capit(match):
                    return match.group().capitalize()
                p_compass = re.compile('(north|south|east|west)')
                w2 = p_compass.sub(_capit, w)

                # remove lower case characters
                p_abr = re.compile('[a-z ]')
                w = p_abr.sub('',w2)

            self._wind = w
        return self._wind

    def precipitation(self, value=None, unit='mm'):
        unit = unit.lower()
        if value:
            if unit == 'mm': self._precip = value
            elif unit == 'in': self._precip = self.in2mm(value)
            else: raise ValueError ('Unknown unit ['+ unit +']')
        return self.precipitation_out(self._precip)

    def precipitation_out(self, value):
        if self.metric(): return str(value) + ' mm'
        return self.mm2in(value)  + ' in'

    def pressure(self, h=None, unit='mbar'):
        if h:
            if unit == 'mbar': self._baro = h
            elif unit == 'inHg': self._baro = self.inHg2mbar(h)
            else: raise ValueError ('Unknown unit ['+ unit +']')
        if self.metric(): return str(self._baro) + ' mbar'
        return '%.2f' % self.mbar2inHg(self._baro)  + ' inHg'

    def pressure_change(self, value=None):
        "set/get the string describing the air pressure change"
        if value != None : self._pressure_change = str(value)
        return self._pressure_change

    def icon_path(self):
        "return path to the icon"
        pass

    #
    # per cent (%)
    #
    def humidity(self, value=None):
        if value: self._hum = value
        return str(self._hum) + ' %'

    def rain(self, value=None):
        if value: self._rain_poss = int(value)
        return str(self._rain_poss) + ' %'

    #
    # Unit conversions
    #
    def F2C (self, f):
        'Convert a Fahrenheit numeral into of a Celcius numeral'
        return int(round(int(f) - 32) / 1.8 )
    def C2F (self, c):
        'Convert a Celcius numeral into of a Fahrenheit numeral'
        return int(round(1.8 * int(c) + 32))


    def in2mm (self, inches):
        'Convert an inch numeral into of a millimeter numeral'
        return round( float(inches) * 25.4, 1)
    def mm2in (self, mm):
        'Convert a millimeter numeral into of an inch numeral'
        return int(round(int(mm) / 25.4))


    def mph2kph (self, mph):
        'Convert a miles per hour  numeral into of a kilometers per hour numeral'
        return int(round(float(mph) * 1.609344))
    def kph2mph (self, kph):
        'Convert a kilometers per hour numeral into of a miles per hour numeral'
        return int(round(float(kph) / 1.609344))


    def inHg2mbar (self, inHg):
        'Convert am inches of mercury numeral into of a millibar numeral'
        return int(round(float(inHg) / 0.02953))
    def mbar2inHg (self, mbar):
        'Convert a millibar numeral into of an inches of mercury numeral'
        #print mbar
        #print round(mbar*0.002)
        #print int(round(mbar*0.002))
        return float(mbar) * 0.02953


    #
    # Time and date methods
    #
    def year(self, value=None):
        "set/get year string"
        if value != None : self._year = str(value)
        return self._year
    
    def month(self, m=None):
        '''get and set the month
        Returns  : three letter abbreviation of a month name
        Arguments: month as an interger or a three letter abbreviation
        '''
        months = {0:"N/A", 1:"Jan", 2:"Feb", 3:"Mar", 4:"Apr", 5:"May", 6:"Jun",
                  7:"Jul", 8:"Aug", 9:"Sep", 10:"Oct", 11:"Nov", 12:"Dec" }
        if m != None:
            if len(str(m)) < 3: self._mon = months[int(m)]
            else: self._mon = str(m)[:3]
        if len(str(self._mon)) != 3 : self._mon = months[int(self._mon)]
        return self._mon

    def weekday(self, value=None):
        "set/get weekday string"
        if value != None : self._weekday = str(value)
        return self._weekday

    def day(self, value=None):
        "set/get day string"
        if value != None : self._day = str(value)
        return self._day

    def time (self, t=None, m=None):
        '''Get and set time
        Accepts
        Returns: time string in 24h format
        '''
        if t :
            t = str(t)
            seploc = t.find(':')
            if seploc >= 0 :
                self._hour = t[:(seploc)]
                self._min = t[(seploc+1):]
            elif len(str(t)) == 4:
                self._hour = t[:2]
                self._min = t[2:]
            else:
                raise ValueError ('Not a valid time [t]')
        if m :
            #print m
            if m.upper() == 'AM':
                if int(self._hour) > 12:
                    raise ValueError ('AM hour can not be over 12 ['+ self._hour +']')
	        if int(self._hour) == 12: self._hour = str(0)
            if m.upper() == 'PM':
                if int(self._hour) > 12:
                    raise ValueError ('PM hour can not be over 12 ['+ self._hour +']')
                if int(self._hour) != 12: self._hour = str(int(self._hour) + 12)
        if len(self._hour) < 2 : self._hour = self._hour.zfill(2)
        if len(self._min) < 2 : self._min = self._hour.zfill(2)
        if int(self._hour) > 24: raise ValueError ('Hour can not be over 24 ['+ self._hour +']')
        if int(self._min) > 60: raise ValueError ('Min can not be over 60 ['+ self._min +']')
        if self._hour == '24' and int(self._min) > 0:
            raise ValueError ('Time can not be over 24:00 ['+ self._hour + ':' + self._min +']')
        return self._hour + ':' + self._min

    def get_12h_time(self):
        if int(self._hour) < 12 : return self._hour + ':' + self._min + ' AM'
        elif int(self._hour) == 12 : return '12:' + self._min + ' PM'
        else : return str(int(self._hour)-12) + ':' + self._min + ' PM'

    def timezone (self, z=None):
        if z: self._zone = z
        if len(self._zone) != 3 :
            p_capitals = re.compile('[^A-Z\\\/]')
            self._zone = p_capitals.sub('', self._zone)
        return self._zone

class Observation(Weather):
    "The main weather class with location and forecasts"
    _place = 'N/A'
    _reverse_wind_arrow = 0
    def __init__(self, place='N/A'):
        self.place(place)
        self.forecasts = []
        self.large_icon_path = 'large_icons/'
        self.small_icon_path = 'small_icons/'
    def __str__ (self):
        return 'Observation: ' + ' ( @' + self.location() + ' )' \
               + '\n  ( with ' + str(len(self.forecasts)) + ' forecasts' + ' )' \
               + '\n  icon            : '  + self.icon() \
               + "\n  sky             : " + self.sky() \
               + "\n  temperature     : " + self.temperature() \
               + "\n  relative_heat   : " + self.relative_heat() \
               + "\n  dewpoint        : " + self.dewpoint() \
               + "\n  visibility      : " + self.visibility() \
               + "\n  uv              : " + self.uv() \
               + "\n  pollution       : " + self.pollution() \
               + "\n  wind            : " + self.wind() \
               + "\n  wind_speed      : " + self.wind_speed() \
               + "\n  reverse_wind_arrows : " + str(self.reverse_wind_arrows()) \
               + "\n  wind_icon       : " + self.wind_icon_path() \
               + "\n  humidity        : " + self.humidity() \
               + "\n  pressure        : " + self.pressure() \
               + "\n  pressure_change : " + self.pressure_change()

    def location (self):
        return self.place() + ', ' + self.time() + ' ' + self.timezone() + \
               ' ' + self.weekday() + ' '  + self.day() + ' ' +  self.month() + \
               ' ' + self.year()

    def place(self, value=None):
        "set/get place string"
        if value != None : self._place = str(value)
        return self._place

    def add_forecast(self, value):
        '''Add a new weather object into an ordered list
        The first object in the list should be the forecast for today
        Use obj.forecasts to access the list
        Arguments: 1. weather.Forecast object
        '''
        if isinstance(value, Forecast):
            value.parent(self)
            self.forecasts.append(value)
        else: raise TypeError ('Not a weather Forecast object ['+ value +']')

    def get_forecast(self, value):
        return self.forecasts[value]

    def icon_path(self):
        '''Get the path to the icon
        Returns  : string
        '''
        return self.large_icon_path + self.icon() + '.png'

    def reverse_wind_arrows (self, value=None):
        """set/get wind arrow direction.
        Some people prefer to see arrow point to where wind is blowing FROM
        """
        if value != None :
            if value : self._reverse_wind_arrow = 1
            else : self._reverse_wind_arrow = 0
        return self._reverse_wind_arrow

    def wind_icon_path(self):

	speed = self._wind_speed
	direction = self.wind()
	if int(speed) == 0:
		return "wind_icons/calm.png"
	elif direction == "Variable":
		return "wind_icons/variable.png"

	if self.reverse_wind_arrows():
                direction = direction.replace('N','s')
                direction = direction.replace('S','n')
                direction = direction.replace('E','w')
                direction = direction.replace('W','e')
                direction = direction.upper()

        # threshold values for different arrow types (in kpm)
        thresh_one = 20
        thresh_two = 39

        if int(speed) < thresh_one:
            prefix = "weak"
        elif int(speed) < thresh_two:
            prefix = "medium"
        else:
            prefix = "strong"

	suffix = '/'
        icon_path = "wind_icons/" + prefix + suffix + direction + '.png'
	return icon_path

class Forecast(Weather):
    _temp_low = 0
    _temp_high = 0
    _parent = ''
    _sunrise = 'N/A'
    _sunset = 'N/A'

    def __init__ (self, wday = 'N/A'):
        "Constructor with weekday string"
        self.weekday(wday)
    def __str__ (self):
        #return 'Forecast:  '  + ' ( date=' + self.date() + ' )' 
        return 'Forecast:  ' + self.weekday() + ' ( date=' + self.date() + ' )'  \
               + '\n  icon            : '  + self.icon() \
               + "\n  sky             : " + self.sky() \
               + "\n  temperature_low : " + self.temperature_low() \
               + "\n  temperature_high: " + self.temperature_high() \
               + "\n  uv              : " + self.uv() \
               + "\n  pollution       : " + self.pollution() \
               + "\n  wind_speed      : " + self.wind_speed() \
               + "\n  wind            : " + self.wind() \
               + "\n  humidity        : " + self.humidity() \
               + "\n  rain            : " + self.rain() \
               + "\n  pressure        : " + self.pressure() \
               + "\n  pressure_change : " + self.pressure_change() \
               + "\n  sunrise         : " + self.sunrise() \
               + "\n  sunset          : " + self.sunset() 


    def temperature_low(self, value=None, unit='C'):
        '''Get the expected lower limit of the daily temperature.
        Returns  : string, temperature in unit depending on the attribute metric
        Arguments: 1. temperature numeral in Celcius
                   2. unit, optional, values: 'C' (default), 'F'
        '''
        unit = unit.upper()
        if value:
            if unit == 'C': self._temp_low = value
            elif unit == 'F': self._temp_low = self.F2C(value)
            else: raise ValueError ('Unknown unit ['+ unit +']')
        return self.temperature_out(self._temp_low)

    def temperature_high(self, value=None, unit='C'):
        '''Get the expected higher limit of the daily temperature.
        Returns  : string, temperature in unit depending on the attribute metric
        Arguments: 1. temperature numeral in Celcius
                   2. unit, optional, values: 'C' (default), 'F'
        '''
        unit = unit.upper()
        if value:
            if unit == 'C': self._temp_high = value
            elif unit == 'F': self._temp_high = self.F2C(value)
            else: raise ValueError ('Unknown unit ['+ unit +']')
        return self.temperature_out(self._temp_high)

    def temp_range(self, order=None, separator='/'):
        '''Get forecasted temperature range with the unit. Unit depends on the attribute metric.
        Returns  :  string
        Arguments: 1. boolean, reverses the order
                   2. character, separator
        '''
        if order: return self.temperature_high()[:-7] + separator + self.temperature_low()[:-7]
        return self.temperature_low()[:-7] + separator + self.temperature_high()[:-7]

    def temperature_range(self, order=None, separator='-'):
        '''Get forecasted temperature range with the unit. Unit depends on the attribute metric.
        Returns  : string
        Arguments: 1. boolean, reverses the order
                   2. character, separator
        '''
        if order: return self.temperature_high()[:-3] + separator + self.temperature_low()
        return self.temperature_low()[:-3] + separator + self.temperature_high()

    def icon_path(self):
        '''Get the path to the icon
        Returns  : string
        '''
        if self.parent(): return self.parent().small_icon_path + self.icon() + '.png'
        return ''

    def sunrise(self, value=None):
        '''set/get sunrise string
        Note that this a redundant method. Location has one already'''
        if value != None : self._sunrise = self.time(value)
        return self._sunrise

    def sunset(self, value=None):
        '''set/get sunset string
        Note that this a redundant method. Location has one already'''
        if value != None : self._sunset = self.time(value)
        return self._sunset
