# Copyright 2015 Cloudbase Solutions Srl
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import ctypes
from ctypes import wintypes
import os
import struct
import winreg

import win32security

from cloudbaseinit import exception
from cloudbaseinit.utils.windows import privilege


REG_TIME_ZONES = "Software\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones"
NOT_FOUND = 2
kernel32 = ctypes.windll.kernel32


class SYSTEMTIME(ctypes.Structure):
    _fields_ = [
        ('wYear', wintypes.WORD),
        ('wMonth', wintypes.WORD),
        ('wDayOfWeek', wintypes.WORD),
        ('wDay', wintypes.WORD),
        ('wHour', wintypes.WORD),
        ('wMinute', wintypes.WORD),
        ('wMilliseconds', wintypes.WORD),
    ]


class TIME_ZONE_INFORMATION(ctypes.Structure):
    _fields_ = [
        ('Bias', wintypes.LONG),
        ('StandardName', wintypes.WCHAR * 32),
        ('StandardDate', SYSTEMTIME),
        ('StandardBias', wintypes.LONG),
        ('DaylightName', wintypes.WCHAR * 32),
        ('DaylightDate', SYSTEMTIME),
        ('DaylightBias', wintypes.LONG),
    ]


class DYNAMIC_TIME_ZONE_INFORMATION(ctypes.Structure):
    _fields_ = [
        ('Bias', wintypes.LONG),
        ('StandardName', wintypes.WCHAR * 32),
        ('StandardDate', SYSTEMTIME),
        ('StandardBias', wintypes.LONG),
        ('DaylightName', wintypes.WCHAR * 32),
        ('DaylightDate', SYSTEMTIME),
        ('DaylightBias', wintypes.LONG),
        ('TimeZoneKeyName', wintypes.WCHAR * 128),
        ('DynamicDaylightTimeDisabled', wintypes.BOOLEAN),
    ]


class Timezone(object):
    """Class which holds details about a particular timezone.

    It also can be used to change the current timezone,
    by calling the :meth:`~set`. The supported time zone names
    are the ones found here:
    https://technet.microsoft.com/en-us/library/cc749073%28v=ws.10%29.aspx
    """

    def __init__(self, name):
        self._name = name
        self._timezone_info = self._get_timezone_info()

        # Public API.
        self.bias = self._timezone_info[0]
        self.standard_name = self._timezone_info[1]
        self.standard_date = self._timezone_info[2]
        self.standard_bias = self._timezone_info[3]
        self.daylight_name = self._timezone_info[4]
        self.daylight_date = self._timezone_info[5]
        self.daylight_bias = self._timezone_info[6]

    @staticmethod
    def _create_system_time(values):
        mtime = SYSTEMTIME()
        mtime.wYear = values[0]
        mtime.wMonth = values[1]
        mtime.wDayOfWeek = values[2]
        mtime.wDay = values[3]
        mtime.wHour = values[4]
        mtime.wMinute = values[5]
        mtime.wSecond = values[6]
        mtime.wMilliseconds = values[7]
        return mtime

    def _get_timezone_struct(self):
        info = TIME_ZONE_INFORMATION()
        info.Bias = self.bias
        info.StandardName = self.standard_name
        info.StandardDate = self._create_system_time(self.standard_date)
        info.StandardBias = self.standard_bias
        info.DaylightName = self.daylight_name
        info.DaylightBias = self.daylight_bias
        info.DaylightDate = self._create_system_time(self.daylight_date)
        return info

    def _get_dynamic_timezone_struct(self):
        info = DYNAMIC_TIME_ZONE_INFORMATION()
        info.Bias = self.bias
        info.StandardName = self.standard_name
        info.StandardDate = self._create_system_time(self.standard_date)
        info.StandardBias = self.standard_bias
        info.DaylightName = self.daylight_name
        info.DaylightBias = self.daylight_bias
        info.DaylightDate = self._create_system_time(self.daylight_date)
        # TODO(cpopa): should this flag be controllable?
        info.DynamicDaylightTimeDisabled = False
        info.TimeZoneKeyName = self._name
        return info

    def _get_timezone_info(self):
        keyname = os.path.join(REG_TIME_ZONES, self._name)
        try:
            with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, keyname) as key:
                return self._unpack_timezone_info(key)
        except WindowsError as exc:
            if exc.errno == NOT_FOUND:
                raise exception.CloudbaseInitException(
                    "Timezone %r not found" % self._name)
            else:
                raise

    @staticmethod
    def _unpack_system_time(tzi, offset):
        # Unpack the values of a TIME_ZONE_INFORMATION structure
        # from the given blob, starting at the given offset.
        return [struct.unpack("H", tzi[index: index + 2])[0]
                for index in range(offset, offset + 16, 2)]

    @staticmethod
    def _query_tz_key(key):
        tzi = winreg.QueryValueEx(key, "TZI")[0]
        daylight_name = winreg.QueryValueEx(key, "Dlt")[0]
        standard_name = winreg.QueryValueEx(key, "Std")[0]
        return tzi, standard_name, daylight_name

    def _unpack_timezone_info(self, key):
        # Get information about the current timezone from the given
        # registry key.
        tzi, standard_name, daylight_name = self._query_tz_key(key)
        bias, = struct.unpack("l", tzi[:4])
        standard_bias, = struct.unpack("l", tzi[4:8])
        daylight_bias, = struct.unpack("l", tzi[8:12])
        standard_date = self._unpack_system_time(tzi, 12)
        daylight_date = self._unpack_system_time(tzi, 12 + 16)

        return (bias, standard_name, tuple(standard_date),
                standard_bias, daylight_name,
                tuple(daylight_date), daylight_bias)

    def _set_time_zone_information(self):
        info = self._get_timezone_struct()
        with privilege.acquire_privilege(win32security.SE_SYSTEMTIME_NAME):
            kernel32.SetTimeZoneInformation(ctypes.byref(info))

    def _set_dynamic_time_zone_information(self):
        info = self._get_dynamic_timezone_struct()
        with privilege.acquire_privilege(win32security.SE_TIME_ZONE_NAME):
            kernel32.SetDynamicTimeZoneInformation(ctypes.byref(info))

    def set(self, osutils):
        """Change the underlying timezone with this one.

        This will use SetDynamicTimeZoneInformation on Windows Vista+ and
        for Windows 2003 it will fallback to SetTimeZoneInformation, which
        doesn't handle Daylight Saving Time.
        """
        if osutils.check_os_version(6, 0):
            self._set_dynamic_time_zone_information()
        else:
            self._set_time_zone_information()
