"""
#   Razer device configuration
#   High level user interface library
#
#   This library connects to the lowlevel 'razerd' system daemon.
#
#   Copyright (C) 2008-2018 Michael Buesch <m@bues.ch>
#
#   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.
"""

import sys
if sys.version_info[0] != 3:
	print("Python %d is not supported by razercfg." % sys.version_info[0])
	print("Please install Python 3.x")
	sys.exit(1)

import socket
import select
import hashlib
import struct

RAZER_VERSION	= "0.42"


class RazerEx(Exception):
	"Exception thrown by pyrazer code."

__be32_struct = struct.Struct(">I")
__be16_struct = struct.Struct(">H")

def razer_be32_to_int(be32, offset=0):
	return __be32_struct.unpack_from(be32, offset)[0]

def razer_be16_to_int(be16, offset=0):
	return __be16_struct.unpack_from(be16, offset)[0]

def razer_int_to_be32(integer):
	return __be32_struct.pack(integer)

def razer_int_to_be16(integer):
	return __be16_struct.pack(integer)

def razer_str2bool(string):
	string = string.lower().strip()
	if string in ["no", "off", "false"]:
		return False
	if string in ["yes", "on", "true"]:
		return True
	try:
		return bool(int(string))
	except ValueError as e:
		pass
	raise ValueError

class RazerDevId(object):
	"devid parser"

	DEVTYPE_UNKNOWN = "Unknown"
	DEVTYPE_MOUSE = "Mouse"

	BUSTYPE_UNKNOWN = "Unknown"
	BUSTYPE_USB = "USB"

	def __init__(self, devid):
		self.devtype = self.DEVTYPE_UNKNOWN
		self.bustype = self.BUSTYPE_UNKNOWN
		self.buspos = ""
		self.devname = ""
		self.devid = ""
		try:
			id = devid.split(':')
			self.devtype = id[0]
			self.devname = id[1]
			bus = id[2].split('-')
			self.bustype = bus[0]
			self.buspos = bus[1]
			if len(bus) >= 3:
				self.buspos += "-" + bus[2]
			self.devid = id[3]
		except IndexError:
			pass

	def getDevType(self):
		"Returns DEVTYPE_..."
		return self.devtype
	def getBusType(self):
		"Returns BUSTYPE_..."
		return self.bustype
	def getBusPosition(self):
		"Returns the bus position ID string"
		return self.buspos
	def getDevName(self):
		"Returns the device name string"
		return self.devname
	def getDevId(self):
		"Returns the device ID string"
		return self.devid

class RazerRGB(object):
	"An RGB color"

	def __init__(self, r, g, b):
		self.r = r
		self.g = g
		self.b = b

	@classmethod
	def fromU32(cls, u32):
		return cls(r=(u32 >> 16) & 0xFF,
			   g=(u32 >> 8) & 0xFF,
			   b=(u32 >> 0) & 0xFF)

	def toU32(self):
		return ((self.r & 0xFF) << 16) |\
		       ((self.g & 0xFF) << 8) |\
		       ((self.b & 0xFF) << 0)

	@classmethod
	def fromString(cls, string):
		string = string.strip().lstrip("#")
		if len(string) != 6:
			raise ValueError
		return cls(r=int(string[0:2], 16),
			   g=int(string[2:4], 16),
			   b=int(string[4:6], 16))

class RazerLEDMode(object):
	"Representation of LED mode"

	LED_MODE_STATIC				= 0
	LED_MODE_SPECTRUM			= 1
	LED_MODE_BREATHING			= 2
	LED_MODE_WAVE               = 3 
	LED_MODE_REACTION           = 4
	def __init__(self, val):
		self.val = val

	def toString(self):
		return {
			self.LED_MODE_STATIC:    'static',
			self.LED_MODE_SPECTRUM:  'spectrum',
			self.LED_MODE_BREATHING: 'breathing',
			self.LED_MODE_WAVE:      'wave',
			self.LED_MODE_REACTION:  'reaction'
		}[self.val]

	@classmethod
	def listFromSupportedModes(cls, mask):
		modes = []
		for mode in (cls.LED_MODE_STATIC, cls.LED_MODE_SPECTRUM, cls.LED_MODE_BREATHING, cls.LED_MODE_WAVE, cls.LED_MODE_REACTION):
			if mask & (1 << mode):
				modes.append(cls(mode))

		return modes

	@classmethod
	def fromString(cls, string):
		return {
			'static':    cls(cls.LED_MODE_STATIC),
			'spectrum':  cls(cls.LED_MODE_SPECTRUM),
			'breathing': cls(cls.LED_MODE_BREATHING),
			'wave':      cls(cls.LED_MODE_WAVE),
			'reaction':  cls(cls.LED_MODE_REACTION)
			
		}[string]


class RazerLED(object):
	"LED representation"

	def __init__(self, profileId, name, state, mode, supported_modes, color, canChangeColor):
		self.profileId = profileId
		self.name = name
		self.state = state
		self.mode = mode
		self.supported_modes = supported_modes
		self.color = color
		self.canChangeColor = canChangeColor

class RazerDpiMapping(object):
	"DPI mapping"

	def __init__(self, id, res, profileMask, mutable):
		self.id = id
		self.res = res
		self.profileMask = profileMask
		self.mutable = mutable

class Razer(object):
	SOCKET_PATH	= "/run/razerd/socket"
	PRIVSOCKET_PATH	= "/run/razerd/socket.privileged"

	INTERFACE_REVISION = 6

	COMMAND_MAX_SIZE = 512
	COMMAND_HDR_SIZE = 1
	BULK_CHUNK_SIZE = 128
	RAZER_IDSTR_MAX_SIZE = 128
	RAZER_LEDNAME_MAX_SIZE = 64
	RAZER_NR_DIMS = 3

	COMMAND_ID_GETREV = 0		# Get the revision number of the socket interface.
	COMMAND_ID_RESCANMICE = 1	# Rescan mice.
	COMMAND_ID_GETMICE = 2		# Get a list of detected mice.
	COMMAND_ID_GETFWVER = 3		# Get the firmware rev of a mouse.
	COMMAND_ID_SUPPFREQS = 4	# Get a list of supported frequencies.
	COMMAND_ID_SUPPRESOL = 5	# Get a list of supported resolutions.
	COMMAND_ID_SUPPDPIMAPPINGS = 6	# Get a list of supported DPI mappings.
	COMMAND_ID_CHANGEDPIMAPPING = 7	# Modify a DPI mapping.
	COMMAND_ID_GETDPIMAPPING = 8	# Get the active DPI mapping for a profile.
	COMMAND_ID_SETDPIMAPPING = 9	# Set the active DPI mapping for a profile.
	COMMAND_ID_GETLEDS = 10		# Get a list of LEDs on the device.
	COMMAND_ID_SETLED = 11		# Set the state of a LED.
	COMMAND_ID_GETFREQ = 12		# Get the current frequency.
	COMMAND_ID_SETFREQ = 13		# Set the frequency.
	COMMAND_ID_GETPROFILES = 14	# Get a list of supported profiles.
	COMMAND_ID_GETACTIVEPROF = 15	# Get the active profile.
	COMMAND_ID_SETACTIVEPROF = 16	# Set the active profile.
	COMMAND_ID_SUPPBUTTONS = 17	# Get a list of physical buttons.
	COMMAND_ID_SUPPBUTFUNCS = 18	# Get a list of supported button functions.
	COMMAND_ID_GETBUTFUNC = 19	# Get the current function of a button.
	COMMAND_ID_SETBUTFUNC = 20	# Set the current function of a button.
	COMMAND_ID_SUPPAXES = 21	# Get a list of supported axes.
	COMMAND_ID_RECONFIGMICE = 22	# Reconfigure all mice
	COMMAND_ID_GETMOUSEINFO = 23	# Get detailed information about a mouse
	COMMAND_ID_GETPROFNAME = 24	# Get a profile name.
	COMMAND_ID_SETPROFNAME = 25	# Set a profile name.

	COMMAND_PRIV_FLASHFW = 128	# Upload and flash a firmware image
	COMMAND_PRIV_CLAIM = 129	# Claim the device.
	COMMAND_PRIV_RELEASE = 130	# Release the device.

	# Replies to commands
	REPLY_ID_U32 = 0		# An unsigned 32bit integer.
	REPLY_ID_STR = 1		# A string
	# Notifications. These go through the reply channel.
	__NOTIFY_ID_FIRST = 128
	NOTIFY_ID_NEWMOUSE = 128	# New mouse was connected.
	NOTIFY_ID_DELMOUSE = 129	# A mouse was removed.

	# String encodings
	STRING_ENC_ASCII = 0
	STRING_ENC_UTF8 = 1
	STRING_ENC_UTF16BE = 2

	ERR_NONE = 0
	ERR_CMDSIZE = 1
	ERR_NOMEM = 2
	ERR_NOMOUSE = 3
	ERR_NOLED = 4
	ERR_CLAIM = 5
	ERR_FAIL = 6
	ERR_PAYLOAD = 7
	ERR_NOTSUPP = 8

	errorToStringMap = {
		ERR_NONE	: "Success",
		ERR_CMDSIZE	: "Invalid command size",
		ERR_NOMEM	: "Out of memory",
		ERR_NOMOUSE	: "Could not find mouse",
		ERR_NOLED	: "Could not find LED",
		ERR_CLAIM	: "Failed to claim device",
		ERR_FAIL	: "Failure",
		ERR_PAYLOAD	: "Payload error",
		ERR_NOTSUPP	: "Operation not supported",
	}

	# Axis flags
	RAZER_AXIS_INDEPENDENT_DPIMAPPING	= (1 << 0)

	# Mouseinfo flags
	MOUSEINFOFLG_RESULTOK		= (1 << 0) # Other flags are ok, if this is set.
	MOUSEINFOFLG_GLOBAL_LEDS	= (1 << 1) # The device has global LEDs.
	MOUSEINFOFLG_PROFILE_LEDS	= (1 << 2) # The device has per-profile LEDs.
	MOUSEINFOFLG_GLOBAL_FREQ	= (1 << 3) # The device has global frequency settings.
	MOUSEINFOFLG_PROFILE_FREQ	= (1 << 4) # The device has per-profile frequency settings.
	MOUSEINFOFLG_PROFNAMEMUTABLE	= (1 << 5) # Profile names can be changed.
	MOUSEINFOFLG_SUGGESTFWUP	= (1 << 6) # A firmware update is suggested.

	# LED flags
	LED_FLAG_HAVECOLOR		= (1 << 0)
	LED_FLAG_CHANGECOLOR		= (1 << 1)

	# Special profile ID
	PROFILE_INVALID			= 0xFFFFFFFF

	@staticmethod
	def strerror(errno):
		try:
			errstr = Razer.errorToStringMap[errno]
		except KeyError:
			errstr = "Unknown error"
		return "Errorcode %d: %s" % (errno, errstr)

	def __init__(self, enableNotifications=False):
		"Connect to razerd."
		self.enableNotifications = enableNotifications
		self.notifications = []
		try:
			self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
			self.sock.connect(self.SOCKET_PATH)
		except socket.error as e:
			raise RazerEx("Failed to connect to razerd socket: %s" % e)
		try:
			self.privsock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
			self.privsock.connect(self.PRIVSOCKET_PATH)
		except socket.error as e:
			self.privsock = None # No privileged access

		self.__sendCommand(self.COMMAND_ID_GETREV)
		rev = self.__recvU32()
		if (rev != self.INTERFACE_REVISION):
			additional = ""
			if rev < self.INTERFACE_REVISION:
				additional = "\nThe running razerd is too old. " \
					"Try to delete all razerd binaries and " \
					"re-install the razercfg package."
			raise RazerEx("Incompatible razerd daemon socket interface revision.\n"
				      "razerd reported revision %u, but we expected revision %u."
				      "%s" %\
					(rev, self.INTERFACE_REVISION, additional))

	def __constructCommand(self, commandId, idstr, payload):
		cmd = bytes((commandId,))
		idstr = idstr.encode("UTF-8")
		idstr += b'\0' * (self.RAZER_IDSTR_MAX_SIZE - len(idstr))
		cmd += idstr
		cmd += payload
		cmd += b'\0' * (self.COMMAND_MAX_SIZE - len(cmd))
		return cmd

	def __send(self, data):
		self.sock.sendall(data)

	def __sendPrivileged(self, data):
		try:
			self.privsock.sendall(data)
		except (socket.error, AttributeError) as e:
			raise RazerEx("Privileged command failed. Do you have permission?")

	def __sendBulkPrivileged(self, data):
		for i in range(0, len(data), self.BULK_CHUNK_SIZE):
			chunk = data[i : i + self.BULK_CHUNK_SIZE]
			self.__sendPrivileged(chunk)
			result = self.__recvU32Privileged()
			if result != 0:
				raise RazerEx("Privileged bulk write failed. %u" % result)

	def __sendCommand(self, commandId, idstr="", payload=b""):
		cmd = self.__constructCommand(commandId, idstr, payload)
		self.__send(cmd)

	def __sendPrivilegedCommand(self, commandId, idstr="", payload=b""):
		cmd = self.__constructCommand(commandId, idstr, payload)
		self.__sendPrivileged(cmd)

	def __handleReceivedMessage(self, packet):
		id = packet[0]
		if id < self.__NOTIFY_ID_FIRST:
			raise RazerEx("Received unhandled packet %u" % id)
		if self.enableNotifications:
			self.notifications.append(packet)

	def __receive(self, sock):
		"Receive the next message. This will block until a message arrives."
		hdrlen = 1
		hdr = sock.recv(hdrlen)
		id = hdr[0]
		payload = None
		if id == self.REPLY_ID_U32:
			payload = razer_be32_to_int(sock.recv(4))
		elif id == self.REPLY_ID_STR:
			encoding = sock.recv(1)[0]
			strlen = razer_be16_to_int(sock.recv(2))
			if encoding == self.STRING_ENC_ASCII:
				nrbytes = strlen
				decode = lambda pl: pl.decode("ASCII")
			elif encoding == self.STRING_ENC_UTF8:
				nrbytes = strlen
				decode = lambda pl: pl.decode("UTF-8")
			elif encoding == self.STRING_ENC_UTF16BE:
				nrbytes = strlen * 2
				decode = lambda pl: pl.decode("UTF-16-BE")
			else:
				raise RazerEx("Received invalid string encoding %d" %\
					      encoding)
			payload = sock.recv(nrbytes) if nrbytes else b""
			try:
				payload = decode(payload)
			except UnicodeError as e:
				raise RazerEx("Unicode decode error in received payload")
		elif id == self.NOTIFY_ID_NEWMOUSE:
			pass
		elif id == self.NOTIFY_ID_DELMOUSE:
			pass
		else:
			raise RazerEx("Received unknown message (id=%u)" % id)

		return (id, payload)

	def __receiveExpectedMessage(self, sock, expectedId):
		"""Receive messages until the expected one appears.
		Unexpected messages will be handled by __handleReceivedMessage.
		This function returns the payload of the expected message."""
		while 1:
			id, payload = self.__receive(sock)
			if id == expectedId:
				break
			else:
				self.__handleReceivedMessage((id, payload))
		return payload

	def __recvU32(self):
		"Receive an expected REPLY_ID_U32"
		return self.__receiveExpectedMessage(self.sock, self.REPLY_ID_U32)

	def __recvU32Privileged(self):
		"Receive an expected REPLY_ID_U32 on the privileged socket"
		try:
			return self.__receiveExpectedMessage(self.privsock, self.REPLY_ID_U32)
		except (socket.error, AttributeError) as e:
			raise RazerEx("Privileged recvU32 failed. Do you have permission?")

	def __recvString(self):
		"Receive an expected REPLY_ID_STR"
		return self.__receiveExpectedMessage(self.sock, self.REPLY_ID_STR)

	def pollNotifications(self):
		"Returns a list of pending notifications (id, payload)"
		if not self.enableNotifications:
			raise RazerEx("Polled notifications while notifications were disabled")
		while 1:
			res = select.select([self.sock], [], [], 0.001)
			if not res[0]:
				break
			pack = self.__receive(self.sock)
			self.__handleReceivedMessage(pack)
		notifications = self.notifications
		self.notifications = []
		return notifications

	def rescanMice(self):
		"Send the command to rescan for mice to the daemon."
		self.__sendCommand(self.COMMAND_ID_RESCANMICE)

	def rescanDevices(self):
		"Rescan for new devices."
		self.rescanMice()

	def getMice(self):
		"Returns a list of ID-strings for the detected mice."
		self.__sendCommand(self.COMMAND_ID_GETMICE)
		count = self.__recvU32()
		mice = []
		for i in range(0, count):
			mice.append(self.__recvString())
		return mice

	def getMouseInfo(self, idstr):
		"Get detailed information about a mouse"
		self.__sendCommand(self.COMMAND_ID_GETMOUSEINFO, idstr)
		flags = self.__recvU32()
		if (flags & self.MOUSEINFOFLG_RESULTOK) == 0:
			raise RazerEx("Failed to get mouseinfo for " + idstr)
		return flags

	def reconfigureMice(self):
		"Reconfigure all mice."
		self.__sendCommand(self.COMMAND_ID_RECONFIGMICE)

	def reconfigureDevices(self):
		"Reconfigure all devices."
		self.reconfigureMice()

	def getFwVer(self, idstr):
		"Returns the firmware version. The returned value is a tuple (major, minor)."
		self.__sendCommand(self.COMMAND_ID_GETFWVER, idstr)
		rawVer = self.__recvU32()
		return ((rawVer >> 8) & 0xFF, rawVer & 0xFF)

	def getSupportedFreqs(self, idstr):
		"Returns a list of supported frequencies for a mouse."
		self.__sendCommand(self.COMMAND_ID_SUPPFREQS, idstr)
		count = self.__recvU32()
		freqs = []
		for i in range(0, count):
			freqs.append(self.__recvU32())
		return freqs

	def getCurrentFreq(self, idstr, profileId=PROFILE_INVALID):
		"Returns the currently selected frequency for a mouse."
		payload = razer_int_to_be32(profileId)
		self.__sendCommand(self.COMMAND_ID_GETFREQ, idstr, payload)
		return self.__recvU32()

	def getSupportedRes(self, idstr):
		"Returns a list of supported resolutions for a mouse."
		self.__sendCommand(self.COMMAND_ID_SUPPRESOL, idstr)
		count = self.__recvU32()
		res = []
		for i in range(0, count):
			res.append(self.__recvU32())
		return res

	def getLeds(self, idstr, profileId=PROFILE_INVALID):
		"""Returns a list of RazerLED instances for the given profile,
		or the global LEDs, if no profile given"""
		payload = razer_int_to_be32(profileId)
		self.__sendCommand(self.COMMAND_ID_GETLEDS, idstr, payload)
		count = self.__recvU32()
		leds = []
		for i in range(0, count):
			flags = self.__recvU32()
			name = self.__recvString()
			state = self.__recvU32()
			mode = RazerLEDMode(self.__recvU32())
			supported_modes = RazerLEDMode.listFromSupportedModes(self.__recvU32())
			color = self.__recvU32()
			if (flags & self.LED_FLAG_HAVECOLOR) == 0:
				color = None
			else:
				color = RazerRGB.fromU32(color)
			canChangeColor = bool(flags & self.LED_FLAG_CHANGECOLOR)
			leds.append(RazerLED(profileId, name, state, mode, supported_modes, color, canChangeColor))
		return leds

	def setLed(self, idstr, led):
		"Set a LED to a new state."
		if len(led.name) > self.RAZER_LEDNAME_MAX_SIZE:
			raise RazerEx("LED name string too long")
		payload = razer_int_to_be32(led.profileId)
		led_name = led.name.encode("UTF-8")
		payload += led_name
		payload += b'\0' * (self.RAZER_LEDNAME_MAX_SIZE - len(led_name))
		payload += b'\x01' if led.state else b'\x00'
		payload += bytes([led.mode.val])
		if led.color:
			payload += razer_int_to_be32(led.color.toU32())
		else:
			payload += razer_int_to_be32(0)
		self.__sendCommand(self.COMMAND_ID_SETLED, idstr, payload)
		return self.__recvU32()

	def setFrequency(self, idstr, profileId, newFrequency):
		"Set a new scan frequency (in Hz)."
		payload = razer_int_to_be32(profileId) + razer_int_to_be32(newFrequency)
		self.__sendCommand(self.COMMAND_ID_SETFREQ, idstr, payload)
		return self.__recvU32()

	def getSupportedDpiMappings(self, idstr):
		"Returns a list of supported DPI mappings. Each entry is a RazerDpiMapping() instance."
		self.__sendCommand(self.COMMAND_ID_SUPPDPIMAPPINGS, idstr)
		count = self.__recvU32()
		mappings = []
		for i in range(0, count):
			id = self.__recvU32()
			dimMask = self.__recvU32()
			res = []
			for i in range(0, self.RAZER_NR_DIMS):
				rVal = self.__recvU32()
				if (dimMask & (1 << i)) == 0:
					rVal = None
				res.append(rVal)
			profileMaskHigh = self.__recvU32()
			profileMaskLow = self.__recvU32()
			profileMask = (profileMaskHigh << 32) | profileMaskLow
			mutable = self.__recvU32()
			mappings.append(RazerDpiMapping(
				id, res, profileMask, mutable))
		return mappings

	def changeDpiMapping(self, idstr, mappingId, dimensionId, newResolution):
		"Changes the resolution value of a DPI mapping."
		payload = razer_int_to_be32(mappingId) +\
			  razer_int_to_be32(dimensionId) +\
			  razer_int_to_be32(newResolution)
		self.__sendCommand(self.COMMAND_ID_CHANGEDPIMAPPING, idstr, payload)
		return self.__recvU32()

	def getDpiMapping(self, idstr, profileId, axisId=None):
		"Gets the resolution mapping of a profile."
		if axisId is None:
			axisId = 0xFFFFFFFF
		payload = razer_int_to_be32(profileId) +\
			  razer_int_to_be32(axisId)
		self.__sendCommand(self.COMMAND_ID_GETDPIMAPPING, idstr, payload)
		return self.__recvU32()

	def setDpiMapping(self, idstr, profileId, mappingId, axisId=None):
		"Sets the resolution mapping of a profile."
		if axisId is None:
			axisId = 0xFFFFFFFF
		payload = razer_int_to_be32(profileId) +\
			  razer_int_to_be32(axisId) +\
			  razer_int_to_be32(mappingId)
		self.__sendCommand(self.COMMAND_ID_SETDPIMAPPING, idstr, payload)
		return self.__recvU32()

	def getProfiles(self, idstr):
		"Returns a list of profiles. Each entry is the profile ID."
		self.__sendCommand(self.COMMAND_ID_GETPROFILES, idstr)
		count = self.__recvU32()
		profiles = []
		for i in range(0, count):
			profiles.append(self.__recvU32())
		return profiles

	def getActiveProfile(self, idstr):
		"Returns the ID of the active profile."
		self.__sendCommand(self.COMMAND_ID_GETACTIVEPROF, idstr)
		return self.__recvU32()

	def setActiveProfile(self, idstr, profileId):
		"Selects the active profile."
		payload = razer_int_to_be32(profileId)
		self.__sendCommand(self.COMMAND_ID_SETACTIVEPROF, idstr, payload)
		return self.__recvU32()

	def getProfileName(self, idstr, profileId):
		"Get a profile name."
		payload = razer_int_to_be32(profileId)
		self.__sendCommand(self.COMMAND_ID_GETPROFNAME, idstr, payload)
		return self.__recvString()

	def setProfileName(self, idstr, profileId, newName):
		"Set a profile name. newName is expected to be unicode."
		payload = razer_int_to_be32(profileId)
		rawstr = newName.encode("UTF-16-BE")
		rawstr = rawstr[:min(len(rawstr), 64 * 2)]
		rawstr += b'\0' * (64 * 2 - len(rawstr))
		payload += rawstr
		self.__sendCommand(self.COMMAND_ID_SETPROFNAME, idstr, payload)
		return self.__recvU32()

	def flashFirmware(self, idstr, image):
		"Flash a new firmware on the device. Needs high privileges!"
		payload = razer_int_to_be32(len(image))
		self.__sendPrivilegedCommand(self.COMMAND_PRIV_FLASHFW, idstr, payload)
		self.__sendBulkPrivileged(image)
		return self.__recvU32Privileged()

	def getSupportedButtons(self, idstr):
		"Get a list of supported buttons. Each entry is a tuple (id, name)."
		self.__sendCommand(self.COMMAND_ID_SUPPBUTTONS, idstr)
		buttons = []
		count = self.__recvU32()
		for i in range(0, count):
			id = self.__recvU32()
			name = self.__recvString()
			buttons.append( (id, name) )
		return buttons

	def getSupportedButtonFunctions(self, idstr):
		"Get a list of possible button functions. Each entry is a tuple (id, name)."
		self.__sendCommand(self.COMMAND_ID_SUPPBUTFUNCS, idstr)
		funcs = []
		count = self.__recvU32()
		for i in range(0, count):
			id = self.__recvU32()
			name = self.__recvString()
			funcs.append( (id, name) )
		return funcs

	def getButtonFunction(self, idstr, profileId, buttonId):
		"Get a button function. Returns a tuple (id, name)."
		payload = razer_int_to_be32(profileId) + razer_int_to_be32(buttonId)
		self.__sendCommand(self.COMMAND_ID_GETBUTFUNC, idstr, payload)
		id = self.__recvU32()
		name = self.__recvString()
		return (id, name)

	def setButtonFunction(self, idstr, profileId, buttonId, functionId):
		"Set a button function."
		payload = razer_int_to_be32(profileId) +\
			  razer_int_to_be32(buttonId) +\
			  razer_int_to_be32(functionId)
		self.__sendCommand(self.COMMAND_ID_SETBUTFUNC, idstr, payload)
		return self.__recvU32()

	def getSupportedAxes(self, idstr):
		"Get a list of axes on the device. Each entry is a tuple (id, name, flags)."
		self.__sendCommand(self.COMMAND_ID_SUPPAXES, idstr)
		axes = []
		count = self.__recvU32()
		for i in range(0, count):
			id = self.__recvU32()
			name = self.__recvString()
			flags = self.__recvU32()
			axes.append( (id, name, flags) )
		return axes

class IHEXParser(object):
	TYPE_DATA = 0
	TYPE_EOF  = 1
	TYPE_ESAR = 2
	TYPE_SSAR = 3
	TYPE_ELAR = 4
	TYPE_SLAR = 5

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

	def parse(self):
		bin = []
		try:
			lines = self.ihex.decode("ASCII").splitlines()
			hiAddr = 0
			for line in lines:
				line = line.strip()
				if len(line) == 0:
					continue
				if len(line) < 11 or (len(line) - 1) % 2 != 0:
					raise RazerEx("Invalid firmware file format (IHEX length error)")
				if line[0] != ':':
					raise RazerEx("Invalid firmware file format (IHEX magic error)")
				count = int(line[1:3], 16)
				if len(line) != count * 2 + 11:
					raise RazerEx("Invalid firmware file format (IHEX count error)")
				addr = (int(line[3:5], 16) << 8) | int(line[5:7], 16)
				addr |= hiAddr << 16
				type = int(line[7:9], 16)
				checksum = 0
				for i in range(1, len(line), 2):
					byte = int(line[i:i+2], 16)
					checksum = (checksum + byte) & 0xFF
				checksum = checksum & 0xFF
				if checksum != 0:
					raise RazerEx("Invalid firmware file format (IHEX checksum error)")

				if type == self.TYPE_EOF:
					break
				if type == self.TYPE_ELAR:
					if count != 2:
						raise RazerEx("Invalid firmware file format (IHEX inval ELAR)")
					hiAddr = (int(line[9:11], 16) << 8) | int(line[11:13], 16)
					continue
				if type == self.TYPE_DATA:
					if len(bin) < addr + count: # Reallocate
						bin += [b'\0'] * (addr + count - len(bin))
					for i in range(9, 9 + count * 2, 2):
						byte = bytes( (int(line[i:i+2], 16), ) )
						if bin[(i - 9) // 2 + addr] != b'\0':
							raise RazerEx("Invalid firmware file format (IHEX corruption)")
						bin[(i - 9) // 2 + addr] = byte
					continue
				raise RazerEx("Invalid firmware file format (IHEX unsup type %d)" % type)
		except (ValueError, UnicodeError) as e:
			raise RazerEx("Invalid firmware file format (IHEX digit format)")
		return b"".join(bin)

class RazerFirmwareParser(object):
	class Descriptor:
		def __init__(self, startOffset, endOffset, parser, binTruncate):
			# startOffset: The offset where the ihex/srec/etc starts
			# endOffset: The offset where the ihex/srec/etc ends
			# parser: ihex/srec/etc parser
			# binTruncate: Number of bytes to truncate the binary to
			self.start = startOffset
			self.len = endOffset - startOffset + 1
			self.parser = parser
			self.binTruncate = binTruncate

	DUMP = 0 # Set to 1 to dump all images to /tmp

	FWLIST = {
		# Deathadder 1.27
		"92d7f44637858405a83c0f192c61388c" : Descriptor(0x14B28, 0x1D8F4, IHEXParser, 0x4000)
	}

	def __init__(self, filepath):
		try:
			self.data = open(filepath, "rb").read()
		except IOError as e:
			raise RazerEx("Could not read file: %s" % e.strerror)
		md5sum = hashlib.md5(self.data).hexdigest().lower()
		try:
			descriptor = self.FWLIST[md5sum]
		except KeyError:
			raise RazerEx("Unsupported firmware file")
		try:
			rawFwData = self.data[descriptor.start : descriptor.start+descriptor.len]
			if self.DUMP:
				open("/tmp/razer.dump", "wb").write(rawFwData)
			fwImage = descriptor.parser(rawFwData).parse()
			if self.DUMP:
				open("/tmp/razer.dump.image", "wb").write(fwImage)
			if descriptor.binTruncate:
				fwImage = fwImage[:descriptor.binTruncate]
			if self.DUMP:
				open("/tmp/razer.dump.image.trunc", "wb").write(fwImage)
		except IndexError:
			raise RazerEx("Invalid firmware file format")
		self.fwImage = fwImage

	def getImage(self):
		return self.fwImage
