# -*- coding: utf-8 -*-
"""
UTc! module: keyconfig

Purpose
=======
 A script to help users automatically configure UTc!'s gameplay interface to
 their liking.
 
 All data is read from and written to ~/.UTc!/keybindings.cfg
 
Legal
=====
 All code, unless otherwise indicated, is original, and subject to the
 terms of the GPLv3, which is provided in COPYING.
 
 (C) Neil Tallim, 2009
"""
import os
import pygame
import time

import src.path_utils as path_utils

import src.input.capture as capture

_JOYSTICK_MAPPING = {} #: A map that associates joystick IDs with symbolic names.

def _determineBinding(event_reader, favour_axis):
	"""
	Waits for the user to activate an input event, then returns its symbolic
	representation.
	
	If multiple axes are activated, the strongest one will be selected.
	
	@type event_reader: L{src.input.capture.EventReader}
	@param event_reader: The EventReader that will be used to capture user input.
	@type favour_axis: bool
	@param favour_axis: If True, axis data, if both axis data and button data are
	    provided, will be used; otherwise, button data will take priority.
	
	@rtype: str|None
	@return: The symbolic mapping of the user's chosen input binding, or None if
	    the user chose to unbind the function by pressing escape.
	"""
	def _processAxes(axes):
		#Look for axes.
		for (id, axes) in axes.iteritems():
			if axes:
				strongest_axis = (None, 0.0)
				for (axis, value) in axes:
					if abs(value) > abs(strongest_axis[1]):
						strongest_axis = (axis, value)
						
				direction = "positive"
				if strongest_axis[1] < 0.0:
					direction = "negative"
					
				return "axis %i/%s, %s" % (axis, direction, _JOYSTICK_MAPPING[id])
				
	def _processButtons(buttons):
		#Look for buttons.
		for (id, buttons) in buttons.iteritems():
			if buttons:
				return "button %i, %s" % (buttons[0][0], _JOYSTICK_MAPPING[id])
				
	pygame.event.clear() #Clear the stack.
	print "Activate the desired key or joypad axis"
	pygame.event.set_grab(True)
	try:
		while True:
			(axes, buttons, keys, new_keys) = event_reader.getEvents()
			if new_keys: #Prioritize keys.
				key = new_keys[0][0]
				if key == pygame.constants.K_ESCAPE:
					return None
				return "keycode %i (%s)" % (key, pygame.key.name(key))
				
			if favour_axis:
				result = _processAxes(axes) or _processButtons(buttons)
				if result:
					return result
			else:
				result = _processButtons(buttons) or _processAxes(axes)
				if result:
					return result
			
			time.sleep(0.05)
	finally:
		event_reader.clearPositions()
		pygame.event.set_grab(False)
		
def _presentBindingMenu(options, section, favour_axis, bindings, event_reader):
	"""
	A generic function to present the user with a list of functions that may be
	bound.
	
	@type options: sequence
	@param options: A collection of (function_name, option_name) values that
	    connect the function to be bound to the option where its binding will be
	    stored.
	@type section: basestring
	@param section: The section of the bindings configuration data where these
	    options reside.
	@type favour_axis: bool
	@param favour_axis: If True, axis data, if both axis data and button data are
	    provided, will be used; otherwise, button data will take priority.
	@type bindings: L{src.input.capture.KeyConfig}
	@param bindings: The bindings configuration data being manipulated.
	@type event_reader: L{src.input.capture.EventReader}
	@param event_reader: The EventReader that will be used to capture user input.
	
	@rtype: bool
	@return: True if the user opted to exit this menu.
	"""
	for (i, (text, option)) in enumerate(options):
		print "%i) %s: %s" % (i + 1, text, bindings.get(section, option))
	print "%i) Exit submenu" % (len(options) + 1)
	print
	print "Choose an option:",
	try:
		choice = int(raw_input())
		if 0 < choice < len(options) + 1:
			(option) = options[choice - 1][1]
			bindings.set(section, option, _determineBinding(event_reader, favour_axis))
		elif choice == len(options) + 1:
			return True
		else:
			print "Invalid option specified."
	except ValueError:
		print "Selections must take the form of an integer"
	return False
	
def _presentMenu(bindings, event_reader):
	"""
	A main menu function to present the user with a list of function categories
	that contain gameplay functions that may be bound.
	
	@type bindings: L{src.input.capture.KeyConfig}
	@param bindings: The bindings configuration data being manipulated.
	@type event_reader: L{src.input.capture.EventReader}
	@param event_reader: The EventReader that will be used to capture user input.
	
	@rtype: bool
	@return: True if the user opted to save data; False if data should be discarded.
	"""
	while True: 
		print "Keybinding configuration utility"
		print "1) Bind movement functons"
		print "2) Bind weapons functions"
		print "3) Bind reactor functions"
		print "4) Bind targeting functions"
		print "5) Save settings and exit"
		print "6) Discard changes and exit"
		print
		print "Choose an option:",
		try:
			choice = int(raw_input())
			if 0 < choice < 7:
				if choice == 1:
					_presentMovementMenu(bindings, event_reader)
				elif choice == 2:
					_presentWeaponsMenu(bindings, event_reader)
				elif choice == 3:
					_presentReactorMenu(bindings, event_reader)
				elif choice == 4:
					_presentTargetingMenu(bindings, event_reader)
				elif choice == 5:
					return True
				elif choice == 6:
					return False
			else:
				print "Invalid option specified."
		except ValueError:
			print "Selections must take the form of an integer"
		print
		
def _presentMovementMenu(bindings, event_reader):
	"""
	Presents the user with a list of movement functions, waiting until the user
	opts to exist the menu before returning.
	
	@type bindings: L{src.input.capture.KeyConfig}
	@param bindings: The bindings configuration data being manipulated.
	@type event_reader: L{src.input.capture.EventReader}
	@param event_reader: The EventReader that will be used to capture user input.
	"""
	while True:
		print
		if _presentBindingMenu((
		 ("Nose up", "pitch-up"),
		 ("Nose down", "pitch-down"),
		 ("Twist left", "yaw-left"),
		 ("Twist right", "yaw-right"),
		 ("Roll left", "roll-left"),
		 ("Roll right", "roll-right"),
		 ("Speed down", "speed-down"),
		 ("Speed up", "speed-up")
		), "movement", True, bindings, event_reader):
			return
			
def _presentWeaponsMenu(bindings, event_reader):
	"""
	Presents the user with a list of weapons functions, waiting until the user
	opts to exist the menu before returning.
	
	@type bindings: L{src.input.capture.KeyConfig}
	@param bindings: The bindings configuration data being manipulated.
	@type event_reader: L{src.input.capture.EventReader}
	@param event_reader: The EventReader that will be used to capture user input.
	"""
	while True:
		print
		if _presentBindingMenu((
		 ("Fire primary weapon", "fire-bolt"),
		 ("Next primary weapon", "next-bolt"),
		 ("Previous primary weapon", "prev-bolt"),
		 ("Fire secondary weapon", "fire-missile"),
		 ("Next secondary weapon", "next-missile"),
		 ("Previous seconary weapon", "prev-missile"),
		 ("Launch countermeasure", "fire-ecm"),
		), "weapons", False, bindings, event_reader):
			return
			
def _presentReactorMenu(bindings, event_reader):
	"""
	Presents the user with a list of reactor functions, waiting until the user
	opts to exist the menu before returning.
	
	@type bindings: L{src.input.capture.KeyConfig}
	@param bindings: The bindings configuration data being manipulated.
	@type event_reader: L{src.input.capture.EventReader}
	@param event_reader: The EventReader that will be used to capture user input.
	"""
	while True:
		print
		if _presentBindingMenu((
		 ("Boost engines allocation", "up-engines"),
		 ("Lower engines allocation", "down-engines"),
		 ("Boost shields allocation", "up-shields"),
		 ("Lower shields allocation", "down-shields"),
		 ("Boost weapons allocation", "up-weapons"),
		 ("Lower weapons allocation", "down-weapons"),
		), "reactor", False, bindings, event_reader):
			return
			
def _presentTargetingMenu(bindings, event_reader):
	"""
	Presents the user with a list of targeting functions, waiting until the user
	opts to exist the menu before returning.
	
	@type bindings: L{src.input.capture.KeyConfig}
	@param bindings: The bindings configuration data being manipulated.
	@type event_reader: L{src.input.capture.EventReader}
	@param event_reader: The EventReader that will be used to capture user input.
	"""
	while True:
		print
		if _presentBindingMenu((
		 ("Target closest hostile", "closest"),
		 ("Target random hostile", "random"),
		 ("Target closest ship under reticle", "reticle"),
		), "targeting", False, bindings, event_reader):
			return
			
if __name__ == "__main__":
	user_path = path_utils.getUserPath()
	keybindings_path = user_path + "keybindings.cfg"
	
	pygame.init()
	pygame.font.init()
	
	#Initialise all joysticks.
	for joystick in [pygame.joystick.Joystick(i) for i in range(pygame.joystick.get_count())]:
		joystick.init()
		_JOYSTICK_MAPPING[joystick.get_id()] = joystick.get_name()
		
	#Set up the event-sink.
	screen = pygame.display.set_mode((380, 50))
	pygame.display.set_caption('Key-capture target')
	howto = pygame.font.Font(None, 28).render("ignore this window; use the terminal", True, (255, 255, 255))
	screen.blit(howto, (190 - howto.get_width() / 2, 25 - howto.get_height() / 2))
	pygame.display.flip()
	
	#Read/set up the keybindings config data.
	bindings = capture.KeyConfig()
	if not bindings.read(keybindings_path):
		bindings.add_section("movement")
		bindings.add_section("weapons")
		bindings.add_section("reactor")
		bindings.add_section("targeting")
		
	#Prompt the user for input until the user indicates that they are done.
	event_reader = capture.EventReader()
	if _presentMenu(bindings, event_reader):
		bindings.write(open(keybindings_path, "w"))
		