#!/usr/bin/env python3
#
#   Razer device commandline 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
import os
import getopt
import time
import re
from pyrazer import *
from configparser import *

try:
	PYRAZER_SETUP_PY == True
except:
	print("ERROR: Found an old 'pyrazer' module.")
	print("You should uninstall razercfg from the system (see README)")
	print("and re-install it properly.")
	sys.exit(1)


razer = None

def getRazer():
	global razer
	if razer is None:
		razer = Razer()
	return razer

class Operation:
	def parseProfileValueStr(self, parameter, idstr, nrValues=1):
		# Parse profile:value[:value]... string. Default to active
		# profile, if not given. May raise ValueError.
		split = parameter.split(":")
		if len(split) == nrValues:
			profile = None
			values = split
		elif len(split) == nrValues + 1:
			profile = int(split[0].strip())
			values = split[1:]
			if profile == 0:
				profile = getRazer().getActiveProfile(idstr) + 1
			if profile < 0:
				raise ValueError
		else:
			raise ValueError
		return (profile, values)

class OpSleep(Operation):
	def __init__(self, seconds):
		self.seconds = seconds

	def run(self, idstr):
		time.sleep(self.seconds)

class OpScan(Operation):
	def run(self, idstr):
		scanDevices()

class OpReconfigure(Operation):
	def run(self, idstr):
		reconfigureDevices()

class OpGetFwVer(Operation):
	def run(self, idstr):
		verTuple = getRazer().getFwVer(idstr)
		print("%s: Firmware version %d.%02d" %\
			(idstr, verTuple[0], verTuple[1]))

class OpGetProfile(Operation):
	def run(self, idstr):
		profileId = getRazer().getActiveProfile(idstr)
		print("Active profile: %u" % (profileId + 1))

class OpGetFreq(Operation):
	def run(self, idstr):
		freqs = getRazer().getSupportedFreqs(idstr)
		minfo = getRazer().getMouseInfo(idstr)
		curProf = getRazer().getActiveProfile(idstr)
		if minfo & Razer.MOUSEINFOFLG_GLOBAL_FREQ:
			curFreq = getRazer().getCurrentFreq(idstr)
			self.printFreq(freqs, curFreq)
		if minfo & Razer.MOUSEINFOFLG_PROFILE_FREQ:
			profiles = getRazer().getProfiles(idstr)
			for profile in profiles:
				sys.stdout.write("Profile %2u%s:   " %\
					(profile + 1,
					 "*" if profile == curProf else " "))
				curFreq = getRazer().getCurrentFreq(idstr, profile)
				self.printFreq(freqs, curFreq)

	def printFreq(self, freqs, curFreq):
		output = []
		for freq in freqs:
			pfx = "*" if freq == curFreq else " "
			output.append("%s%u Hz" % (pfx, freq))
		print(", ".join(output))

class OpGetRes(Operation):
	def run(self, idstr):
		mappings = getRazer().getSupportedDpiMappings(idstr)
		profiles = getRazer().getProfiles(idstr)
		curProf = getRazer().getActiveProfile(idstr)
		for profile in profiles:
			sys.stdout.write("Profile %2u%s:   " %\
				(profile + 1,
				 "*" if profile == curProf else " "))
			curMapping = getRazer().getDpiMapping(idstr, profile)
			output = []
			pm = [m for m in mappings if m.profileMask == 0 or\
					      m.profileMask & (1 << profile)]
			for mapping in pm:
				pfx = "*" if mapping.id == curMapping else " "
				r = [ "%u" % r for r in mapping.res if r is not None ]
				rStr = "/".join(r)
				output.append("%s%u (%s DPI)" % (pfx, mapping.id + 1, rStr))
			print(", ".join(output))

class OpPrintLeds(Operation):
	def run(self, idstr):
		minfo = getRazer().getMouseInfo(idstr)
		if minfo & Razer.MOUSEINFOFLG_GLOBAL_LEDS:
			sys.stdout.write("Global LEDs:   ")
			leds = getRazer().getLeds(idstr)
			self.printLeds(leds)
		if minfo & Razer.MOUSEINFOFLG_PROFILE_LEDS:
			profiles = getRazer().getProfiles(idstr)
			curProf = getRazer().getActiveProfile(idstr)
			for profile in profiles:
				sys.stdout.write("Profile %2u%s LEDs:   " %\
					(profile + 1,
					 "*" if profile == curProf else " "))
				leds = getRazer().getLeds(idstr, profile)
				self.printLeds(leds)

	def printLeds(self, leds):
		output = []
		for led in leds:
			state = "on" if led.state else "off"
			mode  = led.mode.toString()
			color = ""
			if led.color is not None:
				color = "color#%02X%02X%02X" %\
					(led.color.r, led.color.g, led.color.b)
			output.append("%s => %s/%s/%s" % (led.name, state, mode, color))
		print(",  ".join(output))

class OpSetProfile(Operation):
	def __init__(self, param):
		self.param = param

	def run(self, idstr):
		try:
			profileId = int(self.param) - 1
			error = getRazer().setActiveProfile(idstr, profileId)
			if error:
				raise RazerEx("Failed to set active profile (%s)" %\
					      Razer.strerror(error))
		except (ValueError) as e:
			raise RazerEx("Invalid parameter to --profile option")

class OpSetLedState(Operation):
	def __init__(self, param):
		self.param = param

	def setLed(self, idstr, led, state):
		led.state = state
		error = getRazer().setLed(idstr, led)
		if error:
			raise RazerEx("Failed to set LED state (%s)" %\
					  Razer.strerror(error))

	def run(self, idstr):
		try:
			(profile, config) = self.parseProfileValueStr(self.param, idstr, 2)
			ledName = config[0].strip().lower()
			newState = razer_str2bool(config[1])
			if profile is None:
				profile = Razer.PROFILE_INVALID
			else:
				profile -= 1
			leds = getRazer().getLeds(idstr, profile)
			if 'all' == ledName:
				for led in leds:
					self.setLed(idstr, led, newState)
			else:
				led = [led for led in leds if led.name.lower() == ledName.lower()][0]
				self.setLed(idstr, led, newState)
		except (IndexError, ValueError):
			raise RazerEx("Invalid parameter to --setled option")

class OpSetLedColor(Operation):
	def __init__(self, param):
		self.param = param

	def run(self, idstr):
		try:
			(profile, config) = self.parseProfileValueStr(self.param, idstr, 2)
			ledName = config[0].strip()
			newColor = RazerRGB.fromString(config[1])
			if profile is None:
				profile = Razer.PROFILE_INVALID
			else:
				profile -= 1
			leds = getRazer().getLeds(idstr, profile)
			led = [led for led in leds if led.name.lower() == ledName.lower()][0]
			led.color = newColor
			error = getRazer().setLed(idstr, led)
			if error:
				raise RazerEx("Failed to set LED color (%s)" %\
					      Razer.strerror(error))
		except (IndexError, ValueError):
			raise RazerEx("Invalid parameter to --setledcolor option")

class OpSetLedMode(Operation):
	def __init__(self, param):
		self.param = param

	def run(self, idstr):
		try:
			(profile, config) = self.parseProfileValueStr(self.param, idstr, 2)
			ledName = config[0].strip().lower()
			newMode = RazerLEDMode.fromString(config[1].lower())
			if profile is None:
				profile = Razer.PROFILE_INVALID
			else:
				profile -= 1
			leds = getRazer().getLeds(idstr, profile)
			led = [led for led in leds if led.name.lower() == ledName.lower()][0]
			led.mode = newMode
			error = getRazer().setLed(idstr, led)
			if error:
				raise RazerEx("Failed to set LED mode (%s)" %\
					      Razer.strerror(error))
		except (KeyError, IndexError, ValueError):
			raise RazerEx("Invalid parameter to --setledmode option")


class OpSetRes(Operation):
	def __init__(self, param):
		self.param = param

	def setDpiMapping(self, idstr, profile, axisId, value):
		# Get profile mappings.
		mappings = [m for m in getRazer().getSupportedDpiMappings(idstr) \
					if m.profileMask == 0 or m.profileMask & (1 << profile)]
		if value >= 100:
			# Value is in DPI.
			mappings = [m for m in mappings if value in m.res]
		else:
			# Value is a mapping ID.
			mappings = [m for m in mappings if (value - 1) == m.id]
		try:
			mappingId = mappings[0].id
		except IndexError:
			raise RazerEx("Invalid resolution %d" % value)
		error = getRazer().setDpiMapping(idstr, profile - 1, mappingId, axisId=axisId)
		if error:
			raise RazerEx("Failed to set resolution to %u (%s)" %\
					(mappingId, Razer.strerror(error)))

	def run(self, idstr):
		try:
			(profile, values) = self.parseProfileValueStr(self.param, idstr)
			resolutions = []
			for arg in values[0].split(','):
				m = re.match(r'^\d+$', arg)
				if m is not None:
					resolutions.append((None, int(arg)))
					continue
				m = re.match(r'^(\d+)x(\d+)$', arg)
				if m is not None:
					resolutions.append((0, int(m.group(1))))
					resolutions.append((1, int(m.group(2))))
					continue
				raise ValueError
		except ValueError:
			raise RazerEx("Invalid parameter to --res option")
		if profile is None:
			# No profile number was specified. Get the current one.
			profile = getRazer().getActiveProfile(idstr) + 1
		for axisId, value in resolutions:
			self.setDpiMapping(idstr, profile, axisId, value)

class OpSetFreq(Operation):
	def __init__(self, param):
		self.param = param

	def run(self, idstr):
		try:
			(profile, freq) = self.parseProfileValueStr(self.param, idstr)
			freq = int(freq[0])
		except ValueError:
			raise RazerEx("Invalid parameter to --freq option")
		if profile is None:
			profile = Razer.PROFILE_INVALID
		else:
			profile -= 1
		error = getRazer().setFrequency(idstr, profile, freq)
		if error:
			raise RazerEx("Failed to set frequency to %d Hz (%s)" %\
					(freq, Razer.strerror(error)))

class OpFlashFw(Operation):
	def __init__(self, filename):
		self.filename = filename

	def run(self, idstr):
		p = RazerFirmwareParser(self.filename)
		data = p.getImage()
		print("Flashing firmware on %s ..." % idstr)
		print("!!! DO NOT DISCONNECT ANY DEVICE !!!")
		print("Sending %d bytes..." % len(data))
		error = getRazer().flashFirmware(idstr, data)
		if error:
			raise RazerEx("Failed to flash firmware (%s)" % Razer.strerror(error))
		print("Firmware successfully flashed.")

# List of operations
class DevOps:
	def __init__(self, idstr):
		self.idstr = idstr
		self.ops = []

	def add(self, op):
		self.ops.append(op)

	def runAll(self):
		for op in self.ops:
			op.run(self.idstr)

def scanDevices():
	getRazer().rescanMice()
	mice = getRazer().getMice()
	for mouse in mice:
		print(mouse)

def reconfigureDevices():
	getRazer().rescanDevices()
	getRazer().reconfigureDevices()

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

def prVersion():
	print("Razer device configuration tool")
	print("Version", RAZER_VERSION)

def usage():
	prVersion()
	print("")
	print("Usage: razercfg [OPTIONS] [-d DEV DEVOPS] [-d DEV DEVOPS]...")
	print("")
	print("-h|--help            Print this help text")
	print("-v|--version         Print the program version number")
	print("-B|--background      Fork into the background")
	print("-s|--scan            Scan for devices and print the bus IDs")
	print("-K|--reconfigure     Force-reconfigure all detected devices")
	print("")
	print("-d|--device DEV      Selects the device with the bus ID \"DEV\"")
	print("    Use the special value \"mouse\" for DEV to select")
	print("    the first razer mouse device found in the system.")
	print("    If this option is omitted, the first Razer device found is selected.")
	print("")
	print("-S|--sleep SECS      Sleep SECS seconds.")
	print("")
	print("Device operations (DEVOPS):")
	print("These options apply to the device that is specified with -d")
	print("")
	print("Options for mice:")
	print("-V|--fwver                          Print the firmware version number")
	print("-p|--profile PROF                   Changes the active profile")
	print("-P|--getprofile                     Prints the active profile")
	print("-r|--res [PROF:]RES[xRES]           Changes the scan resolution")
	print("-R|--getres                         Prints the resolutions")
	print("-f|--freq [PROF:]FREQ               Changes the scan frequency")
	print("-F|--getfreq                        Prints the frequencies")
	print("-L|--leds                           List the device supported LED identifiers")
	print("-l|--setled [PROF:]LED:(on|off)     Toggle the LED with the identifier \"LED\"")
	print("                                    Use the special identifier \"all\"")
	print("                                    to toggle all supported LEDs.")
	print("-c|--setledcolor [PROF:]LED:rrggbb  Set LED color to RGB 'rrggbb'")
	print("-m|--setledmode  [PROF:]LED:MODE    Set LED mode to MODE ('static', 'spectrum'")
	print("                                                               or 'breathing')")
	print("")
	print("-X|--flashfw FILE                   Flash a firmware image to the device")
	print("")
	print("The profile number \"PROF\" may be 0 for the current profile. If omitted,")
	print("the global settings are changed (not possible for every device).")

def findDevice(deviceType=None):
	if deviceType is None or deviceType == "mouse":
		getRazer().rescanMice()
		mice = getRazer().getMice()
		if mice:
			return mice[0] # Return the first idstr
		if deviceType:
			raise RazerEx("No Razer mouse found in the system")
	raise RazerEx("No Razer device found in the system")

def parse_args():
	devOpsList = []
	currentDevOps = None

	try:
		(opts, args) = getopt.getopt(sys.argv[1:],
			"hvBsKd:r:Rf:FLl:VS:X:c:p:Pm:",
			[ "help", "version", "background",
			  "scan", "reconfigure", "device=", "res=",
			  "getres", "freq=", "getfreq", "leds", "setled=",
			  "fwver", "config=", "sleep=", "flashfw=",
			  "setledcolor=", "setledmode=",
			  "profile=", "getprofile", ])
	except getopt.GetoptError:
		usage()
		exit(1)

	for (o, v) in opts:
		if o in ("-h", "--help"):
			usage()
			exit(0)
		if o in ("-v", "--version"):
			prVersion()
			exit(0)
		if o in ("-B", "--background"):
			if os.fork() != 0:
				exit(0) # Exit parent
		if o in ("-s", "--scan"):
			ops = currentDevOps
			if not currentDevOps:
				ops = DevOps(None)
			ops.add(OpScan())
			if not currentDevOps:
				devOpsList.append(ops)
			continue
		if o in ("-K", "--reconfigure"):
			ops = currentDevOps
			if not currentDevOps:
				ops = DevOps(None)
			ops.add(OpReconfigure())
			if not currentDevOps:
				devOpsList.append(ops)
			continue
		if o in ("-d", "--device"):
			if v == "mouse": # magic; select the first mouse
				v = findDevice("mouse")
			if currentDevOps and currentDevOps.ops:
				devOpsList.append(currentDevOps)
			currentDevOps = DevOps(v)
			continue
		if o in ("-p", "--profile"):
			if not currentDevOps:
				currentDevOps = DevOps(findDevice())
			currentDevOps.add(OpSetProfile(v))
			continue
		if o in ("-P", "--getprofile"):
			if not currentDevOps:
				currentDevOps = DevOps(findDevice())
			currentDevOps.add(OpGetProfile())
			continue
		if o in ("-r", "--res"):
			if not currentDevOps:
				currentDevOps = DevOps(findDevice())
			currentDevOps.add(OpSetRes(v))
			continue
		if o in ("-R", "--getres"):
			if not currentDevOps:
				currentDevOps = DevOps(findDevice())
			currentDevOps.add(OpGetRes())
			continue
		if o in ("-f", "--freq"):
			if not currentDevOps:
				currentDevOps = DevOps(findDevice())
			currentDevOps.add(OpSetFreq(v))
			continue
		if o in ("-F", "--getfreq"):
			if not currentDevOps:
				currentDevOps = DevOps(findDevice())
			currentDevOps.add(OpGetFreq())
			continue
		if o in ("-L", "--leds"):
			if not currentDevOps:
				currentDevOps = DevOps(findDevice())
			currentDevOps.add(OpPrintLeds())
			continue
		if o in ("-l", "--setled"):
			if not currentDevOps:
				currentDevOps = DevOps(findDevice())
			currentDevOps.add(OpSetLedState(v))
			continue
		if o in ("-c", "--setledcolor"):
			if not currentDevOps:
				currentDevOps = DevOps(findDevice())
			currentDevOps.add(OpSetLedColor(v))
			continue
		if o in ("-m", "--setledmode"):
			if not currentDevOps:
				currentDevOps = DevOps(findDevice())
			currentDevOps.add(OpSetLedMode(v))
			continue
		if o in ("-V", "--fwver"):
			if not currentDevOps:
				currentDevOps = DevOps(findDevice())
			currentDevOps.add(OpGetFwVer())
			continue
		if o in ("-S", "--sleep"):
			ops = currentDevOps
			if not currentDevOps:
				ops = DevOps(None)
			try:
				v = float(v)
			except ValueError:
				raise RazerEx("Value for -S|--sleep must be a floating point value")
			ops.add(OpSleep(v))
			if not currentDevOps:
				devOpsList.append(ops)
			continue
		if o in ("-X", "--flashfw"):
			if not currentDevOps:
				raise RazerEx("Must specify a device (-d) before -X|--flashfw")
			currentDevOps.add(OpFlashFw(v))
			continue
	if currentDevOps and currentDevOps.ops:
		devOpsList.append(currentDevOps)
	if not devOpsList:
		usage()
		exit(1)
	return devOpsList

def main():
	try:
		devOpsList = parse_args()
		for devOps in devOpsList:
			devOps.runAll()
	except (RazerEx) as e:
		print(e)
		return 1
	return 0

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