#!/usr/bin/env python
#
#   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
from ConfigParser import *
from pyrazer import *


razer = None

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

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

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

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

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

class OpGetFreq(Operation):
	def run(self, idstr):
		freqs = getRazer().getSupportedFreqs(idstr)
		profiles = getRazer().getProfiles(idstr)
		for profile in profiles:
			sys.stdout.write("Profile %2u:   " % (profile + 1))
			curFreq = getRazer().getCurrentFreq(idstr, profile)
			output = []
			for freq in freqs:
				if freq == curFreq:
					output.append("*%u Hz" % freq)
				else:
					output.append(" %u Hz" % freq)
			print ", ".join(output)

class OpGetRes(Operation):
	def run(self, idstr):
		mappings = getRazer().getSupportedDpiMappings(idstr)
		profiles = getRazer().getProfiles(idstr)
		for profile in profiles:
			sys.stdout.write("Profile %2u:   " % (profile + 1))
			curMapping = getRazer().getDpiMapping(idstr, profile)
			output = []
			for mapping in mappings:
				if mapping[0] == curMapping:
					output.append("*%u (%u DPI)" % (mapping[0] + 1, mapping[1]))
				else:
					output.append(" %u (%u DPI)" % (mapping[0] + 1, mapping[1]))
			print ", ".join(output)

class OpPrintLeds(Operation):
	def run(self, idstr):
		leds = getRazer().getLeds(idstr)
		output = []
		for led in leds:
			if led[1] == 0:
				state = "off"
			elif led[1] == 1:
				state = "on"
			else:
				state = "unknown"
			output.append("%s => %s" % (led[0], state))
		print ",  ".join(output)

class OpSetLed(Operation):
	def __init__(self, cfgStr):
		self.cfgStr = cfgStr

	def run(self, idstr):
		cfgStr = self.cfgStr.split(":")
		try:
			newState = cfgStr[-1].strip().lower()
			ledName = ":".join(cfgStr[:-1])
			if not ledName:
				raise ValueError()
			if newState in ["0", "off", "false"]:
				newState = False
			elif newState in ["1", "on", "true"]:
				newState = True
			else:
				raise ValueError()
		except (IndexError, ValueError):
			raise RazerEx("Invalid parameter to --setled option")
		error = getRazer().setLed(idstr, ledName, newState)
		if error:
			raise RazerEx("Failed to set LED state (%s)" % Razer.strerror(error))

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

	def run(self, idstr):
		try:
			(profile, mapping) = self.parseProfileValuePair(self.param, idstr)
		except ValueError:
			raise RazerEx("Invalid parameter to --res option")
		if mapping >= 100:
			# Mapping is in DPI. Get the mapping ID.
			mappings = getRazer().getSupportedDpiMappings(idstr)
			try:
				(mapping, res, mutable) = filter(lambda x: x[1] == mapping, mappings)[0]
			except IndexError:
				raise RazerEx("Invalid resolution %d" % mapping)
		else:
			mapping = mapping - 1
		error = getRazer().setDpiMapping(idstr, profile - 1, mapping)
		if error:
			raise RazerEx("Failed to set resolution to %u (%s)" %\
					(mapping, Razer.strerror(error)))

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

	def run(self, idstr):
		try:
			(profile, freq) = self.parseProfileValuePair(self.param, idstr)
		except ValueError:
			raise RazerEx("Invalid parameter to --freq option")
		error = getRazer().setFrequency(idstr, profile - 1, 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 "-r|--res [profile:]DPI   Changes the scan resolution. profile is optional."
	print "-R|--getres              Prints the resolutions"
	print "-f|--freq [profile:]FREQ Changes the scan frequency. profile is optional."
	print "-F|--getfreq             Prints the frequencies"
	print "-L|--leds                Print the identifiers of the LEDs on the device"
	print "-l|--setled LED:off      Toggle the LED with the identifier \"LED\" ON or OFF"
	print "-X|--flashfw FILE        Flash a firmware image to the 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:Vc:S:X:",
			[ "help", "version", "background",
			  "scan", "reconfigure", "device=", "res=",
			  "getres", "freq=", "getfreq", "leds", "setled=",
			  "fwver", "config=", "sleep=", "flashfw=", ])
	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"):
			scanDevices()
			exit(0)
		if o in ("-K", "--reconfigure"):
			reconfigureDevices()
			exit(0)
		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 ("-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(OpSetLed(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), e:
		print e
		return 1
	return 0

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