#!/usr/bin/env python

import operator
import ctypes
import datetime
from ctypes.wintypes import WORD, WCHAR, BOOL, LONG

from jaraco.windows.util import Extended
from jaraco.collections import RangeMap

class AnyDict(object):
	"A dictionary that returns the same value regardless of key"

	def __init__(self, value):
		self.value = value

	def __getitem__(self, key):
		return self.value

class SYSTEMTIME(Extended, ctypes.Structure):
	_fields_ = [
		('year', WORD),
		('month', WORD),
		('day_of_week', WORD),
		('day', WORD),
		('hour', WORD),
		('minute', WORD),
		('second', WORD),
		('millisecond', WORD),
	]

class REG_TZI_FORMAT(Extended, ctypes.Structure):
	_fields_ = [
		('bias', LONG),
		('standard_bias', LONG),
		('daylight_bias', LONG),
		('standard_start', SYSTEMTIME),
		('daylight_start', SYSTEMTIME),
	]

class TIME_ZONE_INFORMATION(Extended, ctypes.Structure):
	_fields_ = [
		('bias', LONG),
		('standard_name', WCHAR*32),
		('standard_start', SYSTEMTIME),
		('standard_bias', LONG),
		('daylight_name', WCHAR*32),
		('daylight_start', SYSTEMTIME),
		('daylight_bias', LONG),
	]

class DYNAMIC_TIME_ZONE_INFORMATION(TIME_ZONE_INFORMATION):
	"""
	Because the structure of the DYNAMIC_TIME_ZONE_INFORMATION extends
	the structure of the TIME_ZONE_INFORMATION, this structure
	can be used as a drop-in replacement for calls where the
	structure is passed by reference.

	For example,
	dynamic_tzi = DYNAMIC_TIME_ZONE_INFORMATION()
	ctypes.windll.kernel32.GetTimeZoneInformation(ctypes.byref(dynamic_tzi))

	(although the key_name and dynamic_daylight_time_disabled flags will be
	set to the default (null)).

	>>> isinstance(DYNAMIC_TIME_ZONE_INFORMATION(), TIME_ZONE_INFORMATION)
	True


	"""
	_fields_ = [
		# ctypes automatically includes the fields from the parent
		('key_name', WCHAR*128),
		('dynamic_daylight_time_disabled', BOOL),
	]

	def __init__(self, *args, **kwargs):
		"""Allow initialization from args from both this class and
		its superclass.  Default ctypes implementation seems to
		assume that this class is only initialized with its own
		_fields_ (for non-keyword-args)."""
		super_self = super(DYNAMIC_TIME_ZONE_INFORMATION, self)
		super_fields = super_self._fields_
		super_args = args[:len(super_fields)]
		self_args = args[len(super_fields):]
		# convert the super args to keyword args so they're also handled
		for field, arg in zip(super_fields, super_args):
			field_name, spec = field
			kwargs[field_name] = arg
		super(DYNAMIC_TIME_ZONE_INFORMATION, self).__init__(*self_args, **kwargs)

class Info(DYNAMIC_TIME_ZONE_INFORMATION):
	"""
	A time zone definition class based on the win32
	DYNAMIC_TIME_ZONE_INFORMATION structure.

	Describes a bias against UTC (bias), and two dates at which a separate
	additional bias applies (standard_bias and daylight_bias).
	"""

	def field_names(self):
		return map(operator.itemgetter(0), self._fields_)

	def __init__(self, *args, **kwargs):
		"""
		Try to construct a timezone.Info from
		a) [DYNAMIC_]TIME_ZONE_INFORMATION args
		b) another Info
		c) a REG_TZI_FORMAT
		d) a byte structure
		"""
		funcs = (
			super(Info, self).__init__,
			self.__init_from_other,
			self.__init_from_reg_tzi,
			self.__init_from_bytes,
			)
		for func in funcs:
			try:
				func(*args, **kwargs)
				return
			except TypeError:
				pass
		raise TypeError("Invalid arguments for %s" % self.__class__)

	def __init_from_bytes(self, bytes, **kwargs):
		reg_tzi = REG_TZI_FORMAT()
		# todo: use buffer API in Python 3
		buffer = buffer(bytes)
		ctypes.memmove(ctypes.addressof(reg_tzi), buffer, len(buffer))
		self.__init_from_reg_tzi(self, reg_tzi, **kwargs)

	def __init_from_reg_tzi(self, reg_tzi, **kwargs):
		if not isinstance(reg_tzi, REG_TZI_FORMAT):
			raise TypeError("Not a REG_TZI_FORMAT")
		for field_name, type in reg_tzi._fields_:
			setattr(self, field_name, getattr(reg_tzi, field_name))
		for name, value in kwargs.items():
			setattr(self, name, value)

	def __init_from_other(self, other):
		if not isinstance(other, TIME_ZONE_INFORMATION):
			raise TypeError("Not a TIME_ZONE_INFORMATION")
		for name in other.field_names():
			# explicitly get the value from the underlying structure
			value = super(Info, other).__getattribute__(other, name)
			setattr(self, name, value)
		# consider instead of the loop above just copying the memory directly
		#size = max(ctypes.sizeof(DYNAMIC_TIME_ZONE_INFO), ctypes.sizeof(other))
		#ctypes.memmove(ctypes.addressof(self), other, size)

	def __getattribute__(self, attr):
		value = super(Info, self).__getattribute__(attr)
		make_minute_timedelta = lambda m: datetime.timedelta(minutes = m)
		if 'bias' in attr:
			value = make_minute_timedelta(value)
		return value

	@classmethod
	def current(class_):
		"Windows Platform SDK GetTimeZoneInformation"
		tzi = class_()
		kernel32 = ctypes.windll.kernel32
		getter = kernel32.GetTimeZoneInformation
		getter = getattr(kernel32, 'GetDynamicTimeZoneInformation', getter)
		code = getter(ctypes.byref(tzi))
		return code, tzi

	def set(self):
		kernel32 = ctypes.windll.kernel32
		setter = kernel32.SetTimeZoneInformation
		setter = getattr(kernel32, 'SetDynamicTimeZoneInformation', setter)
		return setter(ctypes.byref(self))

	def copy(self):
		return self.__class__(self)

	def locate_daylight_start(self, year):
		info = self.get_info_for_year(year)
		return self._locate_day(year, info.daylight_start)

	def locate_standard_start(self, year):
		info = self.get_info_for_year(year)
		return self._locate_day(year, info.standard_start)

	def get_info_for_year(self, year):
		return self.dynamic_info[year]

	@property
	def dynamic_info(self):
		"Return a map that for a given year will return the correct Info"
		if self.key_name:
			dyn_key = self.get_key().subkey('Dynamic DST')
			del dyn_key['FirstEntry']
			del dyn_key['LastEntry']
			years = map(int, dyn_key.keys())
			values = map(Info, dyn_key.values())
			# create a range mapping that searches by descending year and matches
			# if the target year is greater or equal.
			return RangeMap(zip(years, values), RangeMap.descending, operator.ge)
		else:
			return AnyDict(self)

	@staticmethod
	def _locate_day(year, cutoff):
		"""
		Takes a SYSTEMTIME object, such as retrieved from a TIME_ZONE_INFORMATION
		structure or call to GetTimeZoneInformation 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 and week of the month (e.g. 4th saturday in March).

		>>> SATURDAY = 6
		>>> MARCH = 3
		>>> st = SYSTEMTIME(2000, MARCH, SATURDAY, 4, 0, 0, 0, 0)

		# according to my calendar, the 4th Saturday in March in 2009 was the 28th
		>>> expected_date = datetime.datetime(2009, 3, 28)
		>>> Info._locate_day(2009, st) == expected_date
		True
		"""
		# MS stores Sunday as 0, Python datetime stores Monday as zero
		target_weekday = (cutoff.day_of_week + 6) % 7
		# For SYSTEMTIMEs relating to time zone inforamtion, cutoff.day
		#  is the week of the month
		week_of_month = cutoff.day
		# so the following is the first day of that week
		day = (week_of_month - 1) * 7 + 1
		result = datetime.datetime(year, cutoff.month, day,
			cutoff.hour, cutoff.minute, cutoff.second, cutoff.millisecond)
		# now the result is the correct week, but not necessarily the correct day of the week
		days_to_go = (target_weekday - result.weekday()) % 7
		result += datetime.timedelta(days_to_go)
		# 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 == cutoff.month + 1:
			result -= datetime.timedelta(weeks = 1)
		return result
