#!/usr/bin/env python
#
#   Razer device QT configuration tool
#
#   Copyright (C) 2007-2011 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
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from pyrazer import *


class OneButtonConfig(QWidget):
	def __init__(self, id, name, supportedFunctions, buttonConfWidget):
		QWidget.__init__(self, buttonConfWidget)
		self.buttonConfWidget = buttonConfWidget

		self.id = id
		self.name = name

		self.setLayout(QHBoxLayout(self))

		self.nameLabel = QLabel(name, self)
		self.layout().addWidget(self.nameLabel)
		self.layout().addStretch()
		l = QLabel(self.tr("button is assigned to function"), self)
		self.layout().addWidget(l)

		self.funcCombo = QComboBox(self)
		for func in supportedFunctions:
			self.funcCombo.addItem(func[1], QVariant(func[0]))
		curFunc = razer.getButtonFunction(buttonConfWidget.profileWidget.mouseWidget.mouse,
						  buttonConfWidget.profileWidget.profileId,
						  id)
		self.initialFunc = curFunc[0]
		index = self.funcCombo.findData(QVariant(curFunc[0]))
		if index >= 0:
			self.funcCombo.setCurrentIndex(index)
		self.layout().addWidget(self.funcCombo)

	def getId(self):
		return self.id

	def getSelectedFunction(self):
		index = self.funcCombo.currentIndex()
		return self.funcCombo.itemData(index).toUInt()[0]

	def getInitialFunction(self):
		return self.initialFunc

class ButtonConfDialog(QDialog):
	def __init__(self, profileWidget):
		QDialog.__init__(self, profileWidget)
		self.profileWidget = profileWidget
		self.setWindowTitle(self.tr("Configure buttons"))

		self.setLayout(QVBoxLayout(self))

		h = QHBoxLayout()
		l = QLabel(self.tr("Physical button"), self)
		h.addWidget(l)
		h.addStretch()
		l = QLabel(self.tr("Assigned function"), self)
		h.addWidget(l)
		self.layout().addLayout(h)

		funcs = razer.getSupportedButtonFunctions(profileWidget.mouseWidget.mouse)
		self.buttons = []
		for b in razer.getSupportedButtons(profileWidget.mouseWidget.mouse):
			button = OneButtonConfig(b[0], b[1], funcs, self)
			self.layout().addWidget(button)
			self.buttons.append(button)

		h = QHBoxLayout()
		self.applyButton = QPushButton(self.tr("Apply"), self)
		self.connect(self.applyButton, SIGNAL("clicked()"), self.apply)
		h.addWidget(self.applyButton)
		self.cancelButton = QPushButton(self.tr("Cancel"), self)
		self.connect(self.cancelButton, SIGNAL("clicked()"), self.cancel)
		h.addWidget(self.cancelButton)
		self.layout().addLayout(h)

	def cancel(self):
		self.done(0)

	def apply(self):
		for button in self.buttons:
			func = button.getSelectedFunction()
			if func != button.getInitialFunction():
				razer.setButtonFunction(self.profileWidget.mouseWidget.mouse,
							self.profileWidget.profileId,
							button.getId(),
							func)
		self.done(1)

class LedCheckBox(QCheckBox):
	def __init__(self, ledsWidget, ledname, ledstate):
		text = ledname
		text += ledsWidget.tr(" LED enabled")
		QCheckBox.__init__(self, text, ledsWidget)
		self.ledsWidget = ledsWidget

		self.led = ledname
		if ledstate:
			self.setCheckState(Qt.Checked)
		else:
			self.setCheckState(Qt.Unchecked)
		self.connect(self, SIGNAL("stateChanged(int)"), self.toggled)

	def toggled(self, state):
		razer.setLed(self.ledsWidget.mouseWidget.mouse, self.led, bool(state))

class LedsWidget(QGroupBox):
	def __init__(self, mouseWidget):
		QGroupBox.__init__(self, "LEDs", mouseWidget)
		self.mouseWidget = mouseWidget

		self.setLayout(QVBoxLayout(self))
		self.leds = []

	def clear(self):
		for led in self.leds:
			led.deleteLater()
		self.leds = []
		self.hide()

	def add(self, name, state):
		led = LedCheckBox(self, name, state)
		self.layout().addWidget(led)
		self.leds.append(led)

class MouseProfileWidget(QWidget):
	def __init__(self, mouseWidget, profileId):
		QWidget.__init__(self, mouseWidget)
		self.mouseWidget = mouseWidget
		self.profileId = profileId
		self.recurseProtect = False

		self.setLayout(QGridLayout(self))
		yoff = 0

		self.profileActive = QRadioButton(self.tr("Profile active"), self)
		self.connect(self.profileActive, SIGNAL("toggled(bool)"), self.activeChanged)
		self.layout().addWidget(self.profileActive, 0 + yoff, 0)

		self.layout().addWidget(QLabel(self.tr("Mouse scan frequency:"), self), 1 + yoff, 0)
		self.freqSel = QComboBox(self)
		self.connect(self.freqSel, SIGNAL("currentIndexChanged(int)"), self.freqChanged)
		self.layout().addWidget(self.freqSel, 1 + yoff, 1)

		self.resSel = []
		axes = self.__getIndependentAxes()
		for axis in axes:
			self.layout().addWidget(QLabel(self.tr("%s scan resolution:" % axis[1]), self), 2 + yoff, 0)
			resSel = QComboBox(self)
			self.connect(resSel, SIGNAL("currentIndexChanged(int)"), self.resChanged)
			self.layout().addWidget(resSel, 2 + yoff, 1)
			self.resSel.append(resSel)
			yoff += 1
		self.resIndependent = QCheckBox(self.tr("Independent resolutions"), self)
		self.connect(self.resIndependent, SIGNAL("stateChanged(int)"), self.resIndependentChanged)
		self.layout().addWidget(self.resIndependent, 2 + yoff, 1)
		yoff += 1
		if len(axes) <= 1:
			self.resIndependent.hide()

		funcs = razer.getSupportedButtonFunctions(self.mouseWidget.mouse)
		if funcs:
			self.buttonConf = QPushButton(self.tr("Configure buttons"), self)
			self.connect(self.buttonConf, SIGNAL("clicked(bool)"), self.showButtonConf)
			self.layout().addWidget(self.buttonConf, 2 + yoff, 0, 1, 2)

	def __getIndependentAxes(self):
		axes = razer.getSupportedAxes(self.mouseWidget.mouse)
		axes = filter(lambda axis: (axis[2] & Razer.RAZER_AXIS_INDEPENDENT_DPIMAPPING),
			      axes)
		if not axes:
			axes = [ (0, self.tr("All axes"), 0) ]
		return axes

	def reload(self):
		# Refetch the data from the daemon
		self.recurseProtect = True

		# Frequency selection
		self.freqSel.clear()
		supportedFreqs = razer.getSupportedFreqs(self.mouseWidget.mouse)
		curFreq = razer.getCurrentFreq(self.mouseWidget.mouse, self.profileId)
		self.freqSel.addItem(self.tr("Unknown Hz"), QVariant(0))
		for freq in supportedFreqs:
			self.freqSel.addItem(self.tr("%u Hz" % freq), QVariant(freq))
		index = self.freqSel.findData(QVariant(curFreq))
		if index >= 0:
			self.freqSel.setCurrentIndex(index)

		# Resolution selection
		for resSel in self.resSel:
			resSel.clear()
		supportedMappings = razer.getSupportedDpiMappings(self.mouseWidget.mouse)
		axisMappings = []
		for axis in self.__getIndependentAxes():
			axisMappings.append(razer.getDpiMapping(self.mouseWidget.mouse,
								self.profileId,
								axis[0]))
		i = 0
		for resSel in self.resSel:
			resSel.addItem(self.tr("Unknown mapping"), QVariant(0xFFFFFFFF))
			for mapping in supportedMappings:
				mid = mapping[0]
				res = mapping[1]
				if res:
					res = str(res)
				else:
					res = self.tr("Unknown")
				resSel.addItem(self.tr("Scan resolution %u   (%s DPI)" % (mid + 1, res)),
						QVariant(mid))
			index = resSel.findData(QVariant(axisMappings[i]))
			if index >= 0:
				resSel.setCurrentIndex(index)
			i += 1
		independent = bool(filter(lambda x: x != axisMappings[0], axisMappings))
		if independent:
			self.resIndependent.setCheckState(Qt.Checked)
		else:
			self.resIndependent.setCheckState(Qt.Unchecked)
		self.resIndependentChanged(self.resIndependent.checkState())

		# Profile selection
		activeProf = razer.getActiveProfile(self.mouseWidget.mouse)
		self.profileActive.setChecked(activeProf == self.profileId)

		self.recurseProtect = False

	def activeChanged(self, checked):
		if self.recurseProtect:
			return
		if not checked:
			# Cannot disable
			self.recurseProtect = True
			self.profileActive.setChecked(1)
			self.recurseProtect = False
			return
		razer.setActiveProfile(self.mouseWidget.mouse, self.profileId)
		self.mouseWidget.reloadProfiles()

	def freqChanged(self, index):
		if self.recurseProtect or index <= 0:
			return
		freq = self.freqSel.itemData(index).toUInt()[0]
		razer.setFrequency(self.mouseWidget.mouse, self.profileId, freq)

	def resChanged(self, unused=None):
		if self.recurseProtect:
			return
		if self.resIndependent.checkState() == Qt.Checked:
			axisId = 0
			for resSel in self.resSel:
				index = resSel.currentIndex()
				res = resSel.itemData(index).toUInt()[0]
				razer.setDpiMapping(self.mouseWidget.mouse, self.profileId,
						    res, axisId)
				axisId += 1
		else:
			index = self.resSel[0].currentIndex()
			res = self.resSel[0].itemData(index).toUInt()[0]
			razer.setDpiMapping(self.mouseWidget.mouse, self.profileId, res)
			for resSel in self.resSel[1:]:
				self.recurseProtect = True
				resSel.setCurrentIndex(index)
				self.recurseProtect = False

	def resIndependentChanged(self, newState):
		if newState == Qt.Checked:
			for resSel in self.resSel[1:]:
				resSel.setEnabled(True)
		else:
			for resSel in self.resSel[1:]:
				resSel.setEnabled(False)
		self.resChanged()

	def showButtonConf(self, checked):
		bconf = ButtonConfDialog(self)
		bconf.exec_()

class OneDpiMapping(QWidget):
	def __init__(self, dpiMappingsWidget, id, resolution, isMutable):
		QWidget.__init__(self, dpiMappingsWidget)
		self.dpiMappingsWidget = dpiMappingsWidget
		self.mappingId = id

		self.setLayout(QHBoxLayout(self))
		self.layout().addWidget(QLabel(self.tr("Scan resolution %u" % (id+1)), self))
		supportedRes = razer.getSupportedRes(self.dpiMappingsWidget.mouseWidget.mouse)
		self.mappingSel = QComboBox(self)
		self.mappingSel.addItem(self.tr("Unknown DPI"), QVariant(0))
		for res in supportedRes:
			self.mappingSel.addItem(self.tr("%u DPI" % res), QVariant(res))
		index = self.mappingSel.findData(QVariant(resolution))
		if index >= 0:
			self.mappingSel.setCurrentIndex(index)
		self.connect(self.mappingSel, SIGNAL("currentIndexChanged(int)"),
			     self.changed)
		self.mappingSel.setEnabled(isMutable)
		self.layout().addWidget(self.mappingSel)

	def changed(self, index):
		if index <= 0:
			return
		resolution = self.mappingSel.itemData(index).toUInt()[0]
		razer.changeDpiMapping(self.dpiMappingsWidget.mouseWidget.mouse,
				       self.mappingId, resolution)
		self.dpiMappingsWidget.mouseWidget.reloadProfiles()

class MouseDpiMappingsWidget(QGroupBox):
	def __init__(self, mouseWidget):
		QGroupBox.__init__(self, mouseWidget.tr("Possible scan resolutions"), mouseWidget)
		self.mouseWidget = mouseWidget

		self.setLayout(QVBoxLayout(self))
		self.mappings = []

	def clear(self):
		for mapping in self.mappings:
			mapping.deleteLater()
		self.mappings = []
		self.hide()

	def add(self, id, resolution, isMutable):
		mapping = OneDpiMapping(self, id, resolution, isMutable)
		self.mappings.append(mapping)
		self.layout().addWidget(mapping)
		if isMutable:
			self.show()

class MouseWidget(QWidget):
	def __init__(self, parent=None):
		QWidget.__init__(self, parent)

		self.mainwnd = parent

		self.setLayout(QVBoxLayout(self))

		self.mousesel = QComboBox(self)
		self.connect(self.mousesel, SIGNAL("currentIndexChanged(int)"), self.mouseChanged)
		self.layout().addWidget(self.mousesel)
		self.layout().addSpacing(15)

		self.profiletab = QTabWidget(self)
		self.layout().addWidget(self.profiletab)
		self.profileWidgets = []

		self.dpimappings = MouseDpiMappingsWidget(self)
		self.layout().addWidget(self.dpimappings)

		self.leds = LedsWidget(self)
		self.layout().addWidget(self.leds)

		self.layout().addStretch()
		self.fwVer = QLabel(self)
		self.layout().addWidget(self.fwVer)

	def update(self, mice):
		self.mice = mice
		self.mousesel.clear()
		for mouse in mice:
			id = RazerDevId(mouse)
			self.mousesel.addItem("%s   %s-%s %s" % \
				(id.getDevName(), id.getBusType(),
				 id.getBusPosition(), id.getDevId()))

	def mouseChanged(self, index):
		self.profiletab.clear()
		self.profileWidgets = []
		self.dpimappings.clear()
		self.leds.clear()
		self.profiletab.setEnabled(index > -1)
		self.dpimappings.setEnabled(index > -1)
		self.leds.setEnabled(index > -1)
		if index == -1:
			self.fwVer.clear()
			return
		self.mouse = self.mice[index]

		profileIds = razer.getProfiles(self.mouse)
		activeProfileId = razer.getActiveProfile(self.mouse)
		activeWidget = None
		for profileId in profileIds:
			widget = MouseProfileWidget(self, profileId)
			if profileId == activeProfileId:
				activeWidget = widget
			self.profiletab.addTab(widget, str(profileId + 1))
			self.profileWidgets.append(widget)
		self.reloadProfiles()
		if activeWidget:
			self.profiletab.setCurrentWidget(activeWidget)

		dpimappings = razer.getSupportedDpiMappings(self.mouse)
		for dpimapping in dpimappings:
			id = dpimapping[0]
			resolution = dpimapping[1]
			isMutable = dpimapping[2]
			self.dpimappings.add(id, resolution, isMutable)

		leds = razer.getLeds(self.mouse)
		for led in leds:
			name = led[0]
			state = led[1]
			self.leds.add(name, state)
			self.leds.show()

		ver = razer.getFwVer(self.mouse)
		if (ver[0] == 0xFF and ver[1] == 0xFF) or\
		   (ver[0] == 0 and ver[1] == 0):
			self.fwVer.hide()
		else:
			self.fwVer.setText(self.tr("Firmware version: %u.%u" % (ver[0], ver[1])))
			self.fwVer.show()

	def reloadProfiles(self):
		for prof in self.profileWidgets:
			prof.reload()
		activeProf = razer.getActiveProfile(self.mouse)
		for i in range(0, self.profiletab.count()):
			id = self.profiletab.widget(i).profileId
			name = "Profile " + str(id + 1)
			if activeProf == id:
				name = ">" + name + "<"
			self.profiletab.setTabText(i, name)

class StatusBar(QStatusBar):
	def showMessage(self, msg):
		QStatusBar.showMessage(self, msg, 10000)

class MainWindow(QMainWindow):
	def __init__(self, parent=None):
		QMainWindow.__init__(self, parent)
		self.setWindowTitle(self.tr("Razer device configuration"))

		mb = QMenuBar(self)
		rzrmen = QMenu(self.tr("Razer"), mb)
		rzrmen.addAction(self.tr("Rescan devices"), self.scan)
		rzrmen.addSeparator()
		rzrmen.addAction(self.tr("Exit"), self.close)
		mb.addMenu(rzrmen)
		helpmen = QMenu(self.tr("Help"), mb)
		helpmen.addAction(self.tr("About"), self.about)
		mb.addMenu(helpmen)
		self.setMenuBar(mb)

		tab = QTabWidget(self)
		self.mousewidget = MouseWidget(self)
		tab.addTab(self.mousewidget, self.tr("Mice"))
		self.setCentralWidget(tab)

		self.setStatusBar(StatusBar())

		self.mice = []
		self.scan()
		self.pokeNotificationTimer()

		self.resize(450, 350)

	def pokeNotificationTimer(self):
		QTimer.singleShot(300, self.pollNotifications)

	def pollNotifications(self):
		n = razer.pollNotifications()
		if n:
			self.scan()
		self.pokeNotificationTimer()

	# Rescan for new devices
	def scan(self):
		razer.rescanMice()
		mice = razer.getMice()
		if len(mice) != len(self.mice):
			if (len(mice) == 1):
				self.statusBar().showMessage(self.tr("Found one Razer mouse"))
			elif (len(mice) > 1):
				self.statusBar().showMessage(self.tr("Found %d Razer mice" % len(mice)))
		self.mice = mice
		self.mousewidget.update(mice)

	def about(self):
		QMessageBox.information(self, self.tr("About"),
					self.tr("Razer device configuration tool\n"
						"Version %s\n"
						"Copyright (c) 2007-2011 Michael Buesch"
						% RAZER_VERSION))

def exit(exitcode):
	sys.exit(exitcode)

def main():
	try:
		global razer
		razer = Razer(enableNotifications=True)
	except RazerEx, e:
		print e
		print "Is razerd running?"
		sys.exit(1)

	app = QApplication(sys.argv)
	mainwnd = MainWindow()
	mainwnd.show()

	return app.exec_()

if __name__ == "__main__":
	exit(main())
