# -*- python-mode -*-
# -*- coding: UTF-8 -*-

## Copyright (C) 2012-2013  Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

# Logitech Unifying Receiver API.

from __future__ import absolute_import, division, print_function, unicode_literals

from logging import getLogger, DEBUG as _DEBUG
_log = getLogger(__name__)
del getLogger


from .common import (FirmwareInfo as _FirmwareInfo,
					ReprogrammableKeyInfo as _ReprogrammableKeyInfo,
					ReprogrammableKeyInfoV4 as _ReprogrammableKeyInfoV4,
					KwException as _KwException,
					NamedInts as _NamedInts,
					pack as _pack,
					unpack as _unpack)
from . import special_keys

#
#
#

# <FeaturesSupported.xml sed '/LD_FID_/{s/.*LD_FID_/\t/;s/"[ \t]*Id="/=/;s/" \/>/,/p}' | sort -t= -k2
"""Possible features available on a Logitech device.

A particular device might not support all these features, and may support other
unknown features as well.
"""
FEATURE = _NamedInts(
	ROOT=0x0000,
	FEATURE_SET=0x0001,
	FEATURE_INFO=0x0002,
	DEVICE_FW_VERSION=0x0003,
	DEVICE_NAME=0x0005,
	DEVICE_GROUPS=0x0006,
	RESET=0x0020, # "Config Change"
	DFUCONTROL=0x00C0,
	DFUCONTROL_2=0x00C1,
	DFU=0x00D0,
	BATTERY_STATUS=0x1000,
	LED_CONTROL=0x1300,
	CHANGE_HOST=0x1814,
	BACKLIGHT=0x1981,
	REPROG_CONTROLS=0x1B00,
	REPROG_CONTROLS_V2=0x1B01,
	REPROG_CONTROLS_V2_2=0x1B02, # LogiOptions 2.10.73 features.xml
	REPROG_CONTROLS_V3=0x1B03,
	REPROG_CONTROLS_V4=0x1B04,
	WIRELESS_DEVICE_STATUS=0x1D4B,
	LEFT_RIGHT_SWAP=0x2001,
	SWAP_BUTTON=0x2005,
	VERTICAL_SCROLLING=0x2100,
	SMART_SHIFT=0x2110,
	HI_RES_SCROLLING=0x2120,
	HIRES_WHEEL=0x2121,
	LOWRES_WHEEL=0x2130,
	MOUSE_POINTER=0x2200,
	ADJUSTABLE_DPI=0x2201,
	POINTER_SPEED=0x2205,
	ANGLE_SNAPPING=0x2230,
	SURFACE_TUNING=0x2240,
	HYBRID_TRACKING=0x2400,
	FN_INVERSION=0x40A0,
	NEW_FN_INVERSION=0x40A2,
	K375S_FN_INVERSION=0x40A3,
	ENCRYPTION=0x4100,
	LOCK_KEY_STATE=0x4220,
	SOLAR_DASHBOARD=0x4301,
	KEYBOARD_LAYOUT=0x4520,
	KEYBOARD_DISABLE=0x4521,
	DUALPLATFORM=0x4530,
	KEYBOARD_LAYOUT_2=0x4540,
	TOUCHPAD_FW_ITEMS=0x6010,
	TOUCHPAD_SW_ITEMS=0x6011,
	TOUCHPAD_WIN8_FW_ITEMS=0x6012,
	TOUCHPAD_RAW_XY=0x6100,
	TOUCHMOUSE_RAW_POINTS=0x6110,
	TOUCHMOUSE_6120=0x6120,
	GESTURE=0x6500,
	GESTURE_2=0x6501,
	GKEY=0x8010,
	MKEYS=0x8020,
	MR=0x8030,
	REPORT_RATE=0x8060,
	COLOR_LED_EFECTS=0x8070,
	PER_KEY_LIGHTING=0x8080,
	ONBOARD_PROFILES=0x8100,
	MOUSE_BUTTON_SPY=0x8110,
)
FEATURE._fallback = lambda x: 'unknown:%04X' % x

FEATURE_FLAG = _NamedInts(
				internal=0x20,
				hidden=0x40,
				obsolete=0x80)

DEVICE_KIND = _NamedInts(
				keyboard=0x00,
				remote_control=0x01,
				numpad=0x02,
				mouse=0x03,
				touchpad=0x04,
				trackball=0x05,
				presenter=0x06,
				receiver=0x07)

FIRMWARE_KIND = _NamedInts(
				Firmware=0x00,
				Bootloader=0x01,
				Hardware=0x02,
				Other=0x03)

BATTERY_OK = lambda status: status not in (BATTERY_STATUS.invalid_battery, BATTERY_STATUS.thermal_error)

BATTERY_STATUS = _NamedInts(
				discharging=0x00,
				recharging=0x01,
				almost_full=0x02,
				full=0x03,
				slow_recharge=0x04,
				invalid_battery=0x05,
				thermal_error=0x06)

ERROR = _NamedInts(
				unknown=0x01,
				invalid_argument=0x02,
				out_of_range=0x03,
				hardware_error=0x04,
				logitech_internal=0x05,
				invalid_feature_index=0x06,
				invalid_function=0x07,
				busy=0x08,
				unsupported=0x09)

#
#
#

class FeatureNotSupported(_KwException):
	"""Raised when trying to request a feature not supported by the device."""
	pass

class FeatureCallError(_KwException):
	"""Raised if the device replied to a feature call with an error."""
	pass

#
#
#

class FeaturesArray(object):
	"""A sequence of features supported by a HID++ 2.0 device."""
	__slots__ = ('supported', 'device', 'features')
	assert FEATURE.ROOT == 0x0000

	def __init__(self, device):
		assert device is not None
		self.device = device
		self.supported = True
		self.features = None

	def __del__(self):
		self.supported = False
		self.device = None
		self.features = None

	def _check(self):
		# print (self.device, "check", self.supported, self.features, self.device.protocol)
		if self.supported:
			assert self.device
			if self.features is not None:
				return True

			if not self.device.online:
				# device is not connected right now, will have to try later
				return False

			# I _think_ this is universally true
			if self.device.protocol and self.device.protocol < 2.0:
				self.supported = False
				self.device.features = None
				self.device = None
				return False

			reply = self.device.request(0x0000, _pack('!H', FEATURE.FEATURE_SET))
			if reply is None:
				self.supported = False
			else:
				fs_index = ord(reply[0:1])
				if fs_index:
					count = self.device.request(fs_index << 8)
					if count is None:
						_log.warn("FEATURE_SET found, but failed to read features count")
						# most likely the device is unavailable
						return False
					else:
						count = ord(count[:1])
						assert count >= fs_index
						self.features = [None] * (1 + count)
						self.features[0] = FEATURE.ROOT
						self.features[fs_index] = FEATURE.FEATURE_SET
						return True
				else:
					self.supported = False

		return False

	__bool__ = __nonzero__ = _check

	def __getitem__(self, index):
		if self._check():
			if isinstance(index, int):
				if index < 0 or index >= len(self.features):
					raise IndexError(index)

				if self.features[index] is None:
					feature = self.device.feature_request(FEATURE.FEATURE_SET, 0x10, index)
					if feature:
						feature, = _unpack('!H', feature[:2])
						self.features[index] = FEATURE[feature]

				return self.features[index]

			elif isinstance(index, slice):
				indices = index.indices(len(self.features))
				return [self.__getitem__(i) for i in range(*indices)]

	def __contains__(self, featureId):
		"""Tests whether the list contains given Feature ID"""
		if self._check():
			ivalue = int(featureId)

			may_have = False
			for f in self.features:
				if f is None:
					may_have = True
				elif ivalue == int(f):
					return True

			if may_have:
				reply = self.device.request(0x0000, _pack('!H', ivalue))
				if reply:
					index = ord(reply[0:1])
					if index:
						self.features[index] = FEATURE[ivalue]
						return True

	def index(self, featureId):
		"""Gets the Feature Index for a given Feature ID"""
		if self._check():
			may_have = False
			ivalue = int(featureId)
			for index, f in enumerate(self.features):
				if f is None:
					may_have = True
				elif ivalue == int(f):
					return index

			if may_have:
				reply = self.device.request(0x0000, _pack('!H', ivalue))
				if reply:
					index = ord(reply[0:1])
					self.features[index] = FEATURE[ivalue]
					return index

		raise ValueError("%r not in list" % featureId)

	def __iter__(self):
		if self._check():
			yield FEATURE.ROOT
			index = 1
			last_index = len(self.features)
			while index < last_index:
				yield self.__getitem__(index)
				index += 1

	def __len__(self):
		return len(self.features) if self._check() else 0

#
#
#

class KeysArray(object):
	"""A sequence of key mappings supported by a HID++ 2.0 device."""
	__slots__ = ('device', 'keys', 'keyversion')

	def __init__(self, device, count):
		assert device is not None
		self.device = device
		self.keyversion = 0
		self.keys = [None] * count

	def __getitem__(self, index):
		if isinstance(index, int):
			if index < 0 or index >= len(self.keys):
				raise IndexError(index)

			# TODO: add here additional variants for other REPROG_CONTROLS
			if self.keys[index] is None:
				keydata = feature_request(self.device, FEATURE.REPROG_CONTROLS, 0x10, index)
				self.keyversion=1
				if keydata is None:
					keydata = feature_request(self.device, FEATURE.REPROG_CONTROLS_V4, 0x10, index)
					self.keyversion=4
				if keydata:
					key, key_task, flags, pos, group, gmask = _unpack('!HHBBBB', keydata[:8])
					ctrl_id_text = special_keys.CONTROL[key]
					ctrl_task_text = special_keys.TASK[key_task]
					if self.keyversion == 1:
						self.keys[index] = _ReprogrammableKeyInfo(index, ctrl_id_text, ctrl_task_text, flags)
					if self.keyversion == 4:
						try:
							mapped_data = feature_request(self.device, FEATURE.REPROG_CONTROLS_V4, 0x20, key&0xff00, key&0xff)
							if mapped_data:
								remap_key, remap_flag, remapped = _unpack('!HBH', mapped_data[:5])
								# if key not mapped map it to itself for display
								if remapped == 0:
									remapped = key
						except Exception:
							remapped = key
							remap_key = key
							remap_flag = 0

						remapped_text = special_keys.CONTROL[remapped]
						self.keys[index] = _ReprogrammableKeyInfoV4(index, ctrl_id_text, ctrl_task_text, flags, pos, group, gmask, remapped_text)

			return self.keys[index]

		elif isinstance(index, slice):
			indices = index.indices(len(self.keys))
			return [self.__getitem__(i) for i in range(*indices)]

	def index(self, value):
		for index, k in enumerate(self.keys):
			if k is not None and int(value) == int(k.key):
				return index

		for index, k in enumerate(self.keys):
			if k is None:
				k = self.__getitem__(index)
				if k is not None:
					return index

	def __iter__(self):
		for k in range(0, len(self.keys)):
			yield self.__getitem__(k)

	def __len__(self):
		return len(self.keys)

#
#
#

def feature_request(device, feature, function=0x00, *params):
	if device.online and device.features:
		if feature in device.features:
			feature_index = device.features.index(int(feature))
			return device.request((feature_index << 8) + (function & 0xFF), *params)


def get_firmware(device):
	"""Reads a device's firmware info.

	:returns: a list of FirmwareInfo tuples, ordered by firmware layer.
	"""
	count = feature_request(device, FEATURE.DEVICE_FW_VERSION)
	if count:
		count = ord(count[:1])

		fw = []
		for index in range(0, count):
			fw_info = feature_request(device, FEATURE.DEVICE_FW_VERSION, 0x10, index)
			if fw_info:
				level = ord(fw_info[:1]) & 0x0F
				if level == 0 or level == 1:
					name, version_major, version_minor, build = _unpack('!3sBBH', fw_info[1:8])
					version = '%02X.%02X' % (version_major, version_minor)
					if build:
						version += '.B%04X' % build
					extras = fw_info[9:].rstrip(b'\x00') or None
					fw_info = _FirmwareInfo(FIRMWARE_KIND[level], name.decode('ascii'), version, extras)
				elif level == FIRMWARE_KIND.Hardware:
					fw_info = _FirmwareInfo(FIRMWARE_KIND.Hardware, '', str(ord(fw_info[1:2])), None)
				else:
					fw_info = _FirmwareInfo(FIRMWARE_KIND.Other, '', '', None)

				fw.append(fw_info)
				# if _log.isEnabledFor(_DEBUG):
				# 	_log.debug("device %d firmware %s", devnumber, fw_info)
		return tuple(fw)


def get_kind(device):
	"""Reads a device's type.

	:see DEVICE_KIND:
	:returns: a string describing the device type, or ``None`` if the device is
	not available or does not support the ``DEVICE_NAME`` feature.
	"""
	kind = feature_request(device, FEATURE.DEVICE_NAME, 0x20)
	if kind:
		kind = ord(kind[:1])
		# if _log.isEnabledFor(_DEBUG):
		# 	_log.debug("device %d type %d = %s", devnumber, kind, DEVICE_KIND[kind])
		return DEVICE_KIND[kind]


def get_name(device):
	"""Reads a device's name.

	:returns: a string with the device name, or ``None`` if the device is not
	available or does not support the ``DEVICE_NAME`` feature.
	"""
	name_length = feature_request(device, FEATURE.DEVICE_NAME)
	if name_length:
		name_length = ord(name_length[:1])

		name = b''
		while len(name) < name_length:
			fragment = feature_request(device, FEATURE.DEVICE_NAME, 0x10, len(name))
			if fragment:
				name += fragment[:name_length - len(name)]
			else:
				_log.error("failed to read whole name of %s (expected %d chars)", device, name_length)
				return None

		return name.decode('ascii')


def get_battery(device):
	"""Reads a device's battery level.

	:raises FeatureNotSupported: if the device does not support this feature.
	"""
	battery = feature_request(device, FEATURE.BATTERY_STATUS)
	if battery:
		discharge, dischargeNext, status = _unpack('!BBB', battery[:3])
		if _log.isEnabledFor(_DEBUG):
			_log.debug("device %d battery %d%% charged, next level %d%% charge, status %d = %s",
						device.number, discharge, dischargeNext, status, BATTERY_STATUS[status])
		return discharge, BATTERY_STATUS[status]


def get_keys(device):
	# TODO: add here additional variants for other REPROG_CONTROLS
	count = feature_request(device, FEATURE.REPROG_CONTROLS)
	if count is None:
		count = feature_request(device, FEATURE.REPROG_CONTROLS_V4)
	if count:
		return KeysArray(device, ord(count[:1]))


def get_mouse_pointer_info(device):
	pointer_info = feature_request(device, FEATURE.MOUSE_POINTER)
	if pointer_info:
		dpi, flags = _unpack('!HB', pointer_info[:3])
		acceleration = ('none', 'low', 'med', 'high')[flags & 0x3]
		suggest_os_ballistics = (flags & 0x04) != 0
		suggest_vertical_orientation = (flags & 0x08) != 0
		return {
				'dpi': dpi,
				'acceleration': acceleration,
				'suggest_os_ballistics': suggest_os_ballistics,
				'suggest_vertical_orientation': suggest_vertical_orientation
			}

def get_hires_wheel(device):
	caps = feature_request(device, FEATURE.HIRES_WHEEL, 0x00)
	mode = feature_request(device, FEATURE.HIRES_WHEEL, 0x10)
	ratchet = feature_request(device, FEATURE.HIRES_WHEEL, 0x030)


	if caps and mode and ratchet:
		# Parse caps
		multi, flags = _unpack('!BB', caps[:2])

		has_invert = (flags & 0x08) != 0
		has_ratchet = (flags & 0x04) != 0

		# Parse mode
		wheel_mode, reserved = _unpack('!BB', mode[:2])

		target = (wheel_mode & 0x01) != 0
		res = (wheel_mode & 0x02) != 0
		inv = (wheel_mode & 0x04) != 0

		# Parse Ratchet switch
		ratchet_mode, reserved = _unpack('!BB', ratchet[:2])

		ratchet = (ratchet_mode & 0x01) != 0

		return multi, has_invert, has_ratchet, inv, res, target, ratchet
