#!/usr/bin/env python2
# -*- coding: utf-8 -*-
###############################################################################
# conkyForecast.py is a (not so) simple (anymore) python script to gather 
# details of the current weather for use in conky.
#
#  Author: Kaivalagi
# Created: 13/04/2008

from datetime import *
from dateutil.relativedelta import *
from dateutil.tz import *
from optparse import OptionParser
import os
import sys
import traceback
import math

# not sure on these, might create more trouble, but here goes...
reload(sys)
sys.setdefaultencoding('utf-8')

# cPickle is a pickle class implemented in C - so its faster
# in case its not available, use regular pickle
try:
    import cPickle as pickle
except ImportError:
    import pickle

app_name = "conkyForecast-SunsetSunriseCountdown"
app_path = os.path.dirname(os.path.abspath(__file__))
module_name = __file__.replace(os.path.dirname (__file__) + "/", "").replace(".pyc","").replace(".py", "")

class CommandLineParser:

    parser = None

    def __init__(self):

        self.parser = OptionParser()
        self.parser.add_option("-C", "--config", dest="config", default="~/.conkyForecast.config", type="string", metavar="FILE", help=u"[default: %default] The path to the configuration file, allowing multiple config files to be used.")
        self.parser.add_option("-l", "--location", dest="location", type="string", metavar="CODE", help=u"location code for weather data [default set in config]. Use the following url to determine your location code by city name: http://xoap.weather.com/search/search?where=Norwich")
        self.parser.add_option("-o", "--outputformat", dest="outputformat", default="DIFF", type="string", metavar="OPTION", help=u"default:[%default]Specify the format of the output date time difference values, options are either DIFF, HOURS, MINUTES, SECONDS")
        self.parser.add_option("-L", "--longoutput", dest="longoutput", default=False, action="store_true", help=u"Specify if long output is required resulting in plain english output such as '1 hr 2 mins 14 secs' rather than '01:02:14' or '23 secs' rather than '23'")        
        self.parser.add_option("-t", "--textoutput", dest="textoutput", default=False, action="store_true", help=u"Request textual output instead of time to remain to identify whether a sunset or sunrise is due, will either be 'Sunrise' or 'Sunset'")
        self.parser.add_option("-v", "--verbose", dest="verbose", default=False, action="store_true", help=u"Request verbose output, not a good idea when running through conky!")
        self.parser.add_option("-V", "--version", dest="version", default=False, action="store_true", help=u"Displays the version of the script.")
        self.parser.add_option("--errorlogfile", dest="errorlogfile", type="string", metavar="FILE", help=u"If a filepath is set, the script appends errors to the filepath.")
        self.parser.add_option("--infologfile", dest="infologfile", type="string", metavar="FILE", help=u"If a filepath is set, the script appends info to the filepath.")                

    def parse_args(self):
        (options, args) = self.parser.parse_args()
        return (options, args)

    def print_help(self):
        return self.parser.print_help()
    
    
class ForecastConfig:
    CACHE_FOLDERPATH = "/tmp/"
    DEFAULT_LOCATION = "UKXX0103"

class ForecastDataset:
    def __init__(self, last_update, day_of_week, low, high, condition_code, condition_text, precip, humidity, wind_dir, wind_dir_numeric, wind_speed, wind_gusts, timezone, sunrise, sunset, moon_phase, moon_icon, bar_read, bar_desc, uv_index, uv_text, dew_point, observatory, visibility, city, country, weathermap):
        self.last_update = last_update
        self.day_of_week = day_of_week
        self.low = low
        self.high = high
        self.condition_code = condition_code
        self.condition_text = condition_text
        self.precip = precip
        self.humidity = humidity
        self.wind_dir = wind_dir
        self.wind_dir_numeric = wind_dir_numeric
        self.wind_speed = wind_speed
        self.wind_gusts = wind_gusts
        self.timezone = timezone
        self.sunrise = sunrise
        self.sunset = sunset
        self.moon_phase = moon_phase
        self.moon_icon = moon_icon        
        self.bar_read = bar_read
        self.bar_desc = bar_desc
        self.uv_index = uv_index
        self.uv_text = uv_text
        self.dew_point = dew_point
        self.observatory = observatory
        self.visibility = visibility
        self.city = city
        self.country = country
        self.weathermap = weathermap
        
class ForecastLocation:
    timestamp = None
    
    def __init__(self, current, day, night):
        self.current = current
        self.day = day
        self.night = night
        self.timestamp = datetime.today()
        
    def outdated(self, mins):
        if datetime.today() > self.timestamp + timedelta(minutes=mins):
            return True
        else:
            return False

class ForecastInfo:
    
    # design time variables
    options = None
    config = None
    forecast_data = {}
    # a list of locations for which an attempt was made to load them
    # locations in this list are not loaded again (if there was an error,
    # this makes sure it doesn't repeat over and over)
    loaded_locations = []
    
    # design time settings
    CACHE_FILENAME = ".conkyForecast-<LOCATION>.cache"

    def __init__(self, options):

        self.options = options
                                         
        self.loadConfigData()

        # setup location code if not set
        if self.options.location == None:
            self.options.location = self.config.DEFAULT_LOCATION           

        # Check if the location is loaded, if not, load it. If it can't be loaded, there was an error
        if not self.loadLocation(self.options.location):
            self.logError("Failed to load the location cache")
                    
    def loadConfigData(self):
        try:         
            # load .conkyForecast.config from the options setting
            configfilepath = os.path.expanduser(self.options.config)
                                          
            if os.path.exists(configfilepath):
                
                self.config = ForecastConfig()
                
                #load the file
                fileinput = open(configfilepath)
                lines = fileinput.read().split("\n")
                fileinput.close() 

                for line in lines:
                    line = line.strip()
                    if len(line) > 0 and line[0:1] != "#": # ignore commented lines or empty ones

                        splitpos = line.find("=")
                        name = line[:splitpos-1].strip().upper() # config setting name on the left of =
                        value = line[splitpos+1:].split("#")[0].strip()
                        
                        if len(value) > 0:
                            if name == "CACHE_FOLDERPATH":
                                self.config.CACHE_FOLDERPATH = value                               
                            elif name == "DEFAULT_LOCATION":
                                self.config.DEFAULT_LOCATION = value

                if self.options.verbose == True:
                    print >> sys.stdout,"*** CONFIG OPTIONS:"
                    print >> sys.stdout,"    CACHE_FOLDERPATH:", self.config.CACHE_FOLDERPATH      
                    print >> sys.stdout,"    DEFAULT_LOCATION:", self.config.DEFAULT_LOCATION
                    
            else:
                self.logError("Config data file %s not found, using defaults (Registration info is needed though)" % configfilepath)

        except Exception, e:
            self.logError("Error while loading config data, using defaults (Registration info is needed though): " + e.__str__())


    def loadLocation(self, location):

        # if the location was not loaded before (or attempted to load)
        if not location in self.loaded_locations:
            # add it to the list so it doesn't get loaded again (or attempted to load)
            self.loaded_locations.append(location)

            # define CACHE_FILEPATH based on cache folder and location code
            CACHE_FILEPATH = os.path.join(self.config.CACHE_FOLDERPATH, self.CACHE_FILENAME.replace("<LOCATION>", location))

            if not self.forecast_data.has_key(location):
                if os.path.exists(CACHE_FILEPATH):
                    try:
                        self.logInfo("Loading cache file " + CACHE_FILEPATH)
                        file = open(CACHE_FILEPATH, 'rb')
                        self.forecast_data[location] = pickle.load(file)
                        file.close()
                    except Exception, e:
                        self.logError("Unable to read the cache file %s: %s" % (CACHE_FILEPATH, e.__str__()))

        # if the location is still not in cache, print an error and return false to writeOutput()
        if self.forecast_data.has_key(location):
            return True
        else:
            self.logError("Location %s is not in cache, use main script to get the data required." % self.options.location) 
            return False

    def getTimeDeltasByLocation(self, location):
        output = u""

        try:
            
            localtimezoneadjustment = float(datetime.strftime(datetime.now(tzlocal()),"%z"))/100.0
            if localtimezoneadjustment > 0:
                self.logInfo("Local timezone adjustment:+%d hrs"%localtimezoneadjustment)
            else:
                self.logInfo("Local timezone adjustment:%d hrs"%localtimezoneadjustment)            

            locationtimezoneadjustment = int(self.forecast_data[location].current.timezone)
            
            if locationtimezoneadjustment > 0:
                self.logInfo("Location timezone adjustment:+%d hrs"%locationtimezoneadjustment)
            else:
                self.logInfo("Location timezone adjustment:%d hrs"%locationtimezoneadjustment)            
            
            timezonedelta = timedelta(hours=(locationtimezoneadjustment - localtimezoneadjustment))
            self.logInfo("Timezone difference to be used:%d hrs"%(locationtimezoneadjustment - localtimezoneadjustment))
            
            locationdatetime = datetime.now() + timezonedelta
            locationfuturedatetime = locationdatetime + timedelta(days=+1)
                        
            locationdate = "%04d%02d%02d"%(locationdatetime.year,locationdatetime.month,locationdatetime.day)
            locationfuturedate = "%04d%02d%02d"%(locationfuturedatetime.year,locationfuturedatetime.month,locationfuturedatetime.day)
            
            try:
                locationsrtime = datetime.strptime("%s-%s"%(locationdate, self.forecast_data[location].current.sunrise), "%Y%m%d-%I:%M %p")
                locationsstime = datetime.strptime("%s-%s"%(locationdate, self.forecast_data[location].current.sunset), "%Y%m%d-%I:%M %p")                  
                locationfuturesrtime = datetime.strptime("%s-%s"%(locationfuturedate, self.forecast_data[location].day[1].sunrise), "%Y%m%d-%I:%M %p")
            except:
                self.logError("Failed to extract sunrise/sunset from data, python 2.5 or higher is required for this" + traceback.format_exc())

            if locationsrtime != None and locationsstime != None and locationfuturesrtime != None:
                
                if locationdatetime < locationsrtime:
                    tdelta = locationsrtime - locationdatetime
                    rdelta = relativedelta(locationsrtime, locationdatetime)
                    text = "Sunrise"
                elif locationdatetime < locationsstime:
                    tdelta = locationsstime - locationdatetime
                    rdelta = relativedelta(locationsstime, locationdatetime)
                    text = "Sunset"
                else:
                    tdelta = locationfuturesrtime - locationdatetime
                    rdelta = relativedelta(locationfuturesrtime, locationdatetime)
                    text = "Sunrise"
                               
                return rdelta, tdelta, text
            else:
                return None, None, None

        except KeyError, e:
            self.logError("Unknown value %s encountered! Please report this!" % (e.__str__()))
                            
        return output
    
    def getFormattedOutput(self, rdelta, tdelta, text):
    
        try:
            
            output = u""
            
            if self.options.textoutput == True:
                return text
                
            if self.options.outputformat == "DIFF":
                    
                if self.options.longoutput == False:
                
                    output = "%02d:%02d:%02d"%(rdelta.hours,rdelta.minutes,rdelta.seconds)        

                else:
    
                    if rdelta.hours > 0:
                        if rdelta.hours == 1:
                            suffix = " hr  "
                        else:
                            suffix = " hrs "
            
                        output = output + str(rdelta.hours) + suffix
                        
                    if rdelta.minutes > 0:
                        if rdelta.minutes == 1:
                            suffix = " min  "
                        else:
                            suffix = " mins "
            
                        output = output + str(rdelta.minutes) + suffix
                        
                    if rdelta.seconds > 0:
                        if rdelta.seconds == 1:
                            suffix = " sec "
                        else:
                            suffix = " secs"
            
                        output = output + str(rdelta.seconds) + suffix

            elif self.options.outputformat == "HOURS":

                hours = int(math.floor(float(tdelta.seconds + tdelta.days * 24 * 3600)/3600.0))
                
                if self.options.longoutput == True:
                    if hours < 2:
                        suffix = " hr"
                    else:
                        suffix = " hrs"
                else:
                    suffix = ""
                
                output = str(hours) + suffix
                
            elif self.options.outputformat == "MINUTES":

                
                minutes = int(float(tdelta.seconds + tdelta.days * 24 * 3600)/60.0)
                
                if self.options.longoutput == True:
                    if minutes < 2:
                        suffix = " min"
                    else:
                        suffix = " mins"
                else:
                    suffix = ""
                
                output = str(minutes) + suffix
                
            elif self.options.outputformat == "SECONDS":

                seconds = int(float(tdelta.seconds + tdelta.days * 24 * 3600))
                
                if self.options.longoutput == True:
                    if seconds < 2:
                        suffix = " sec"
                    else:
                        suffix = " secs"
                else:
                    suffix = ""
                
                output = str(seconds) + suffix
          
            else:
                self.logError("Invalid output format of %s requested, see help for more info"%self.options.outputformat)

            return output
        
        except Exception, e:
            self.logError(e.__str__())
            
    def writeOutput(self):
        
        rdelta, tdelta, text = self.getTimeDeltasByLocation(self.options.location)
        output = self.getFormattedOutput(rdelta, tdelta, text)
        print output.encode("utf-8")

            
    def logInfo(self, text):
        if self.options.verbose == True:
            print >> sys.stdout, "INFO: " + text

        if self.options.infologfile != None:
            datetimestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 
            fileoutput = open(self.options.infologfile, "ab")
            fileoutput.write(datetimestamp+" INFO: "+text+"\n")
            fileoutput.close()
            
    def logError(self, text):
        print >> sys.stderr, "ERROR: " + text
        
        if self.options.errorlogfile != None:
            datetimestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 
            fileoutput = open(self.options.errorlogfile, "ab")
            fileoutput.write(datetimestamp+" ERROR: "+text+"\n")
            fileoutput.close()
    
def main():

    parser = CommandLineParser()
    (options, args) = parser.parse_args()

    if options.version == True:
        
        print >> sys.stdout,"conkyForecast-TimeToSunRiseSet v.0.2"
        
    else:
        
        if options.verbose == True:
            print >> sys.stdout, "*** INITIAL OPTIONS:"
            print >> sys.stdout, "    config:", options.config
            print >> sys.stdout, "    location:", options.location
            print >> sys.stdout, "    outputformat:", options.outputformat
            print >> sys.stdout, "    longoutput:", options.longoutput
            print >> sys.stdout, "    textoutput:", options.textoutput
            print >> sys.stdout, "    verbose:", options.verbose
            print >> sys.stdout, "    errorlogfile:",options.errorlogfile
            print >> sys.stdout, "    infologfile:",options.infologfile        
    
        forecastinfo = ForecastInfo(options)
        forecastinfo.writeOutput()

if __name__ == '__main__':
    main()
    sys.exit()
    
