# -*- coding: UTF-8 -*-

"""
win32timezone:
	Module for handling datetime.tzinfo time zones using the windows
registry for time zone information.  The time zone names are dependent
on the registry entries defined by the operating system.

	Currently, this module only supports the Windows NT line of products
and not Windows 95/98/Me.

	This module may be tested using the doctest module.

	Written by Jason R. Coombs (jaraco@jaraco.com).
	Copyright © 2003.
	All Rights Reserved.	

	To use this time zone module with the datetime module, simply pass
the TimeZoneInfo object to the datetime constructor.  For example,

>>> import win32timezone, datetime
>>> assert 'Mountain Standard Time' in win32timezone.GetTimeZoneNames()
>>> tzi = TimeZoneInfo( 'Mountain Standard Time' )
>>> now = datetime.datetime.now( tzi )

	The now object is now a time-zone aware object, and daylight savings-
aware methods may be called on it.

>>> now.utcoffset() in ( datetime.timedelta(-1, 61200), datetime.timedelta(-1, 64800) )
True

(note that the result of utcoffset call will be different based on when now was
generated, unless standard time is always used)

>>> now = datetime.datetime.now( TimeZoneInfo( 'Mountain Standard Time', True ) )
>>> now.utcoffset()
datetime.timedelta(-1, 61200)

>>> aug2 = datetime.datetime( 2003, 8, 2, tzinfo = tzi )
>>> aug2.utctimetuple()
(2003, 8, 2, 6, 0, 0, 5, 214, 0)
>>> nov2 = datetime.datetime( 2003, 11, 2, tzinfo = tzi )
>>> nov2.utctimetuple()
(2003, 11, 2, 7, 0, 0, 6, 306, 0)

To convert from one timezone to another, just use the astimezone method.

>>> aug2.isoformat()
'2003-08-02T00:00:00-06:00'
>>> aug2est = aug2.astimezone( win32timezone.TimeZoneInfo( 'Eastern Standard Time' ) )
>>> aug2est.isoformat()
'2003-08-02T02:00:00-04:00'

calling the displayName member will return the display name as set in the
registry.

>>> est = win32timezone.TimeZoneInfo( 'Eastern Standard Time' )
>>> est.displayName
u'(GMT-05:00) Eastern Time (US & Canada)'

>>> gmt = win32timezone.TimeZoneInfo( 'GMT Standard Time', True )
>>> gmt.displayName
u'(GMT) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London'

TimeZoneInfo now supports being pickled and comparison
>>> import pickle
>>> tz = win32timezone.TimeZoneInfo( 'China Standard Time' )
>>> tz == pickle.loads( pickle.dumps( tz ) )
True
"""
from __future__ import generators

__author__ = 'Jason R. Coombs <jaraco@jaraco.com>'
__version__ = '$Revision: 1.5 $'[11:-2]
__vssauthor__ = '$Author: mhammond $'[9:-2]
__date__ = '$Modtime: 04-04-14 10:52 $'[10:-2]

import os, _winreg, struct, datetime

class TimeZoneInfo( datetime.tzinfo ):
	"""
	Main class for handling win32 time zones.
	Usage:
		TimeZoneInfo( <Time Zone Standard Name>, [<Fix Standard Time>] )
	If <Fix Standard Time> evaluates to True, daylight savings time is calculated in the same
		way as standard time.
	"""

	# this key works for WinNT+, but not for the Win95 line.
	tzRegKey = r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones'
		
	def __init__( self, timeZoneName, fixedStandardTime=False ):
		self.timeZoneName = timeZoneName
		key = self._FindTimeZoneKey()
		self._LoadInfoFromKey( key )
		self.fixedStandardTime = fixedStandardTime

	def _FindTimeZoneKey( self ):
		"""Find the registry key for the time zone name (self.timeZoneName)."""
		# for multi-language compatability, match the time zone name in the
		# "Std" key of the time zone key.
		zoneNames = dict( GetIndexedTimeZoneNames( 'Std' ) )
		# Also match the time zone key name itself, to be compatible with
		# English-based hard-coded time zones.
		timeZoneName = zoneNames.get( self.timeZoneName, self.timeZoneName )
		tzRegKeyPath = os.path.join( self.tzRegKey, timeZoneName )
		try:
			key = _winreg.OpenKeyEx( _winreg.HKEY_LOCAL_MACHINE, tzRegKeyPath )
		except:
			raise ValueError, 'Timezone Name %s not found.' % timeZoneName
		return key

	def __getinitargs__( self ):
		return ( self.timeZoneName, )

	def _LoadInfoFromKey( self, key ):
		"""Loads the information from an opened time zone registry key
		into relevant fields of this TZI object"""
		self.displayName = _winreg.QueryValueEx( key, "Display" )[0]
		self.standardName = _winreg.QueryValueEx( key, "Std" )[0]
		self.daylightName = _winreg.QueryValueEx( key, "Dlt" )[0]
		# TZI contains a structure of time zone information and is similar to
		#  TIME_ZONE_INFORMATION described in the Windows Platform SDK
		winTZI, type = _winreg.QueryValueEx( key, "TZI" )
		winTZI = struct.unpack( '3l8h8h', winTZI )
		makeMinuteTimeDelta = lambda x: datetime.timedelta( minutes = x )
		self.bias, self.standardBiasOffset, self.daylightBiasOffset = \
				   map( makeMinuteTimeDelta, winTZI[:3] )
		# daylightEnd and daylightStart are 8-tuples representing a Win32 SYSTEMTIME structure
		self.daylightEnd, self.daylightStart = winTZI[3:11], winTZI[11:19]

	def __repr__( self ):
		result = '%s( %s' % ( self.__class__.__name__, repr( self.timeZoneName ) )
		if self.fixedStandardTime:
			result += ', True'
		result += ' )'
		return result

	def __str__( self ):
		return self.displayName

	def tzname( self, dt ):
		if self.dst( dt ) == self.daylightBiasOffset:
			result = self.daylightName
		elif self.dst( dt ) == self.standardBiasOffset:
			result = self.standardName
		return result
		
	def _getStandardBias( self ):
		return self.bias + self.standardBiasOffset
	standardBias = property( _getStandardBias )

	def _getDaylightBias( self ):
		return self.bias + self.daylightBiasOffset
	daylightBias = property( _getDaylightBias )

	def utcoffset( self, dt ):
		"Calculates the utcoffset according to the datetime.tzinfo spec"
		if dt is None: return
		return -( self.bias + self.dst( dt ) )

	def dst( self, dt ):
		"Calculates the daylight savings offset according to the datetime.tzinfo spec"
		if dt is None: return
		assert dt.tzinfo is self
		result = self.standardBiasOffset

		try:
			dstStart = self.GetDSTStartTime( dt.year )
			dstEnd = self.GetDSTEndTime( dt.year )

			if dstStart <= dt.replace( tzinfo=None ) < dstEnd and not self.fixedStandardTime:
				result = self.daylightBiasOffset
		except ValueError:
			# there was an error parsing the time zone, which is normal when a
			#  start and end time are not specified.
			pass

		return result

	def GetDSTStartTime( self, year ):
		"Given a year, determines the time when daylight savings time starts"
		return self._LocateDay( year, self.daylightStart )

	def GetDSTEndTime( self, year ):
		"Given a year, determines the time when daylight savings ends."
		return self._LocateDay( year, self.daylightEnd )
	
	def _LocateDay( self, year, win32SystemTime ):
		"""
		Takes a SYSTEMTIME structure as retrieved from a TIME_ZONE_INFORMATION
		structure and interprets it based on the given year to identify the actual day.

		This method is necessary because the SYSTEMTIME structure refers to a day by its
		day of the week or week of the month (e.g. 4th saturday in April).

		Refer to the Windows Platform SDK for more information on the SYSTEMTIME
		and TIME_ZONE_INFORMATION structures.
		"""
		month = win32SystemTime[ 1 ]
		# MS stores Sunday as 0, Python datetime stores Monday as zero
		targetWeekday = ( win32SystemTime[ 2 ] + 6 ) % 7
		# win32SystemTime[3] is the week of the month, so the following
		#  is the first day of that week
		day = ( win32SystemTime[ 3 ] - 1 ) * 7 + 1
		hour, min, sec, msec = win32SystemTime[4:]
		result = datetime.datetime( year, month, day, hour, min, sec, msec )
		# now the result is the correct week, but not necessarily the correct day of the week
		daysToGo = targetWeekday - result.weekday()
		result += datetime.timedelta( daysToGo )
		# if we selected a day in the month following the target month,
		#  move back a week or two.
		# This is necessary because Microsoft defines the fifth week in a month
		#  to be the last week in a month and adding the time delta might have
		#  pushed the result into the next month.
		while result.month == month + 1:
			result -= datetime.timedelta( weeks = 1 )
		return result

	def __cmp__( self, other ):
		return cmp( self.__dict__, other.__dict__ )

def _RegKeyEnumerator( key ):
	return _RegEnumerator( key, _winreg.EnumKey )

def _RegValueEnumerator( key ):
	return _RegEnumerator( key, _winreg.EnumValue )

def _RegEnumerator( key, func ):
	"Enumerates an open registry key as an iterable generator"
	index = 0
	try:
		while 1:
			yield func( key, index )
			index += 1
	except WindowsError: pass
	
def _RegKeyDict( key ):
	values = _RegValueEnumerator( key )
	values = tuple( values )
	return dict( map( lambda (name,value,type): (name,value), values ) )

def GetTimeZoneNames( ):
	"Returns the names of the time zones as defined in the registry"
	key = _winreg.OpenKeyEx( _winreg.HKEY_LOCAL_MACHINE, TimeZoneInfo.tzRegKey )
	return _RegKeyEnumerator( key )

def GetIndexedTimeZoneNames( index_key = 'Index' ):
	"""Returns the names of the time zones as defined in the registry, but
	includes an index by which they may be sorted.  Default index is "Index"
	by which they may be sorted longitudinally."""
	for timeZoneName in GetTimeZoneNames():
		tzRegKeyPath = os.path.join( TimeZoneInfo.tzRegKey, timeZoneName )
		key = _winreg.OpenKeyEx( _winreg.HKEY_LOCAL_MACHINE, tzRegKeyPath )
		tzIndex, type = _winreg.QueryValueEx( key, index_key )
		yield ( tzIndex, timeZoneName )

def GetSortedTimeZoneNames( ):
	""" Uses GetIndexedTimeZoneNames to return the time zone names sorted
	longitudinally."""
	tzs = list( GetIndexedTimeZoneNames() )
	tzs.sort()
	return zip( *tzs )[1]

def GetLocalTimeZone( ):
	"""Returns the local time zone as defined by the operating system in the
	registry.
	Note that this will only work if the TimeZone in the registry has not been
	customized.  It should have been selected from the Windows interface.
	>>> localTZ = GetLocalTimeZone()
	>>> nowLoc = datetime.datetime.now( localTZ )
	>>> nowUTC = datetime.datetime.utcnow( )
	>>> ( nowUTC - nowLoc ) < datetime.timedelta( seconds = 5 )
	Traceback (most recent call last):
	  ...
	TypeError: can't subtract offset-naive and offset-aware datetimes

	>>> nowUTC = nowUTC.replace( tzinfo = TimeZoneInfo( 'GMT Standard Time', True ) )

	Now one can compare the results of the two offset aware values	
	>>> ( nowUTC - nowLoc ) < datetime.timedelta( seconds = 5 )
	True
	"""
	tzRegKey = r'SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
	key = _winreg.OpenKeyEx( _winreg.HKEY_LOCAL_MACHINE, tzRegKey )
	local = _RegKeyDict( key )
	# if the user has not checked "Automatically adjust clock for daylight
	# saving changes" in the Date and Time Properties control, the standard
	# and daylight values will be the same.  If this is the case, create a
	# timezone object fixed to standard time.
	fixStandardTime = local['StandardName'] == local['DaylightName'] and \
					local['StandardBias'] == local['DaylightBias']
	return TimeZoneInfo( local['StandardName'], fixStandardTime )
