#!/usr/bin/env python

# This file is part of Window-Switch.
# Copyright (c) 2009-2013 Antoine Martin <antoine@nagafix.co.uk>
# Window-Switch is released under the terms of the GNU GPL v3

import gtk
import os

from winswitch.ui import icons
from winswitch.util.simple_logger import Logger
from winswitch.objects.global_settings import get_settings
from winswitch.util.common import is_valid_file
from winswitch.globals import WIN32, OSX
from winswitch.ui.ui_util import get_ui_util
from winswitch.ui.dialog_util import get_dialog_util
from winswitch.util.gstreamer_util import get_default_gst_sound_sink_options_options, get_default_gst_sound_source_options_options,\
	get_default_gst_sound_sink_module_options, get_default_gst_sound_source_module_options, \
	get_default_gst_sound_clone_module_options
from winswitch.sound.sound_util import has_pa

logger=Logger("config_common", log_colour=Logger.CRIMSON)


MIN_WIDTH = 520
MIN_HEIGHT = 420

PREFER_FILE_CHOOSER = WIN32 or OSX
if PREFER_FILE_CHOOSER:
	MIN_HEIGHT += 80



def check_number(name, s, default=0, min_val=None, max_val=None):
	try:
		i = int(s)
		if min_val and i<min_val:
			text = "%s must be greater than %d" % (name, min_val)
		elif max_val and i>max_val:
			text = "%s must be smaller than %d" % (name, max_val)
		else:
			return (True, i)
	except:
		text = "%s must be a number!" % name
	alert = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, text)
	alert.run()
	alert.destroy()
	return	(False, default)


def new_config_window(name):
	window = gtk.Window()
	window.set_title(name)
	window.set_destroy_with_parent(True)
	window.set_resizable(True)
	window.set_decorated(True)
	window.set_icon(icons.get(get_ui_util().get_best_colour_scheme_icon_name()))
	window.set_position(gtk.WIN_POS_CENTER)
	return	window;

def make_button(name, stock, callback):
	button = gtk.Button(name, stock)
	button.connect('clicked', callback)
	return button

def make_button_hbox(*buttons_spec):
	buttons = []
	for name,stock,callback in buttons_spec:
		buttons.append(make_button(name,stock,callback))
	return	button_hbox(*buttons)

def button_hbox(*buttons):
	button_box = gtk.HBox(True, 0)
	for button in buttons:
		button_box.pack_start(button)
	return	button_box

def make_user_menuitem(user):
	ui_util = get_ui_util()
	label = ui_util.get_user_visible_name(user)
	return	get_ui_util().menuitem(label, get_ui_util().get_user_image(user), "%s on %s" % (user.name, user.host), None)

def make_session_menuitem(session, diff_main=True):
	label = session.session_type
	if session.read_only:
		label = "read-only %s" % session.session_type
	if diff_main:
		if session.shadowed_display:
			label += " (shadow)"
		else:
			label += " (main)"
	return	get_ui_util().menuitem(label, session.get_window_icon(), "on %s" % session.display, None)

def table_init(config_window_obj, columns=2):
	config_window_obj.row = 0
	table = gtk.Table(1, columns=columns)
	table.set_col_spacings(3)
	table.set_row_spacings(3)
	return table


def add_tableline(config_window_obj, table, label=None, entry=None, tooltip=None, x_offset=0, x_factor=1, row_inc=True, homogeneous=-1, label_x_factor=None, entry_x_factor=None):
	row = config_window_obj.row
	label_obj = None
	assert entry or label
	l_al = None
	if label_x_factor is None:
		label_x_factor = x_factor
	if entry_x_factor is None:
		entry_x_factor = x_factor

	if label is not None:
		if type(label)==str:
			label_obj = get_ui_util().make_label(label)
		else:
			label_obj = label
		if entry is None:
			label_end = x_offset+label_x_factor+entry_x_factor
			l_al = gtk.Alignment(xalign=1.0, yalign=0.5, xscale=0.0, yscale=0.0)
		else:
			label_end = x_offset+label_x_factor
			l_al = gtk.Alignment(xalign=1.0, yalign=0.5, xscale=0.0, yscale=0.0)
		if tooltip:
			label_obj.set_tooltip_text(tooltip)
		l_al.add(label_obj)
		label_start = x_offset
		table.attach(l_al, label_start, label_end, row, row + 1, xpadding=10)
	e_al = None
	if entry is not None:
		get_ui_util().fixfont(entry)
		if label is None:
			entry_start = x_offset
			entry_end = x_offset+label_x_factor+entry_x_factor
			e_al = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=0.0)
		else:
			entry_start = x_offset+label_x_factor
			entry_end = x_offset+label_x_factor+entry_x_factor
			e_al = gtk.Alignment(xalign=0.0, yalign=0.5, xscale=0.0, yscale=0.0)
		if tooltip:
			entry.set_tooltip_text(tooltip)
		e_al.add(entry)
		table.attach(e_al, entry_start, entry_end, row, row + 1, xpadding=10)
	if row_inc:
		config_window_obj.row += 1
	if homogeneous>=0 and l_al and e_al:
		l_al.set_size_request(homogeneous, -1)
		e_al.set_size_request(homogeneous, -1)
	return	(label_obj, entry)

def add_table_sep(config_window_obj, table, cols=2, preinc=0, postinc=0):
	if preinc>0:
		row = config_window_obj.row
		for i in range(0, preinc):
			table.attach(gtk.Label(" "), 0, cols, row+i, row+i+1, xpadding=10)
	alignment = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=1.0, yscale=0.0)
	alignment.add(gtk.HSeparator())
	row = config_window_obj.row + preinc
	table.attach(alignment, 0, cols, row, row + 1, xpadding=10)
	if postinc>0:
		row = config_window_obj.row + preinc + 1
		for i in range(0, postinc):
			table.attach(gtk.Label(" "), 0, cols, row+i, row+i+1, xpadding=10)
	config_window_obj.row += preinc + postinc + 1

def keyfile_entry(public=False):
	entry = gtk.FileChooserButton("Select a Key File")
	entry.set_width_chars(10)
	file_filter = gtk.FileFilter()
	if public:
		file_filter.set_name("SSH public keys")
	else:
		file_filter.set_name("SSH private keys")
	if WIN32:
		if public:
			file_filter.add_pattern("*.*")
		else:
			file_filter.add_pattern("*.ppk")
	for part in ["_rsa", "_rsa-*", "_dsa", "_dsa-*"]:
		p = "*%s" % part
		if public:
			p = "%s.pub" % p
		file_filter.add_pattern(p)
	entry.add_filter(file_filter)
	file_filter = gtk.FileFilter()
	file_filter.set_name("Any file")
	file_filter.add_pattern("*")
	file_filter.add_pattern("*.*")
	entry.add_filter(file_filter)
	return	entry

def file_select(name, patterns, any_file=False):
	if not PREFER_FILE_CHOOSER:
		return gtk.Entry(max=255)

	entry = gtk.FileChooserButton("Select the %s executable" % name)
	entry.set_width_chars(20)
	file_filter = gtk.FileFilter()
	file_filter.set_name(name)
	for pattern in patterns:
		if not WIN32:
			pattern = pattern.replace(".exe", "")
		file_filter.add_pattern(pattern)
	entry.add_filter(file_filter)
	entry.set_filter(file_filter)
	if any_file:
		file_filter = gtk.FileFilter()
		if WIN32:
			file_filter.set_name("Any Executable")
			file_filter.add_pattern("*.exe")
			file_filter.add_pattern("*.bat")
		else:
			file_filter.set_name("Any")
			file_filter.add_pattern("*")
		entry.add_filter(file_filter)
	entry.set_width_chars(20)
	return	entry

def set_file_select_change_cb(widget, change_callback):
	if not widget or not change_callback:
		return	None
	if isinstance(widget, gtk.FileChooserButton):
		pass		#tried all sorts - does not work
		#widget.chooser_dialog.connect("close", filechooser_changed_callback)
	elif isinstance(widget, gtk.Entry):
		widget.connect("changed", change_callback)
	else:
		raise Exception("unknown file input type: %s" % type(widget))

def set_filename(entry, filename, mkdir=False):
	#logger.sdebug(None, entry, filename, mkdir)
	if not entry:
		return
	if not filename:
		filename = ""
	if filename.startswith("."):
		filename = os.path.expanduser("~/"+filename)
	if mkdir and filename:
		d = os.path.dirname(filename)
		if d and not os.path.exists(d):
			os.mkdir(d)
	if type(entry) == gtk.FileChooserButton:
		entry.set_filename(filename)
	elif type(entry) == gtk.Entry:
		#This will fire the normal change-event callback automatically (if set)
		entry.set_text(filename)
	else:
		raise Exception("unknown file input type: %s" % type(entry))

def get_filename(entry):
	if not entry:
		return	None
	#logger.sdebug("widget type=%s" % type(entry), entry)
	filename = None
	if isinstance(entry, gtk.FileChooserButton):
		filename = entry.get_filename()
	elif isinstance(entry, gtk.Entry):
		filename = entry.get_text()
	else:
		raise Exception("unknown file input type: %s" % type(entry))
	if not is_valid_file(filename):
		return	""
	return	filename

class TabbedConfigureWindow:
	"""
	Similar in purpose to gtk.Notebook
	(I would have used that if it had been named gtk.TabSomething... didn't find it and made my own)
	"""

	def __init__(self, title):
		Logger(self, log_colour=Logger.CRIMSON)
		self.close_callback = None
		self.settings = get_settings()
		self.title = title
		self.window = None
		self.tabs = {}
		self.width = MIN_WIDTH
		self.height = MIN_HEIGHT
		self.default_section = None
		self.populate_lock = False						#set this flag to true when populating forms (and prevent touch() from firing)
		self.can_apply = False							#the form values have been changed?
		self.can_save = False							#the form values have not been saved since changed?
		self.saved = False
		self.ui_util = get_ui_util()
		self.dialog_util = get_dialog_util()

	def touch(self, *args):
		"""
		Call this function whenever a form element is modified.
		"""
		if self.populate_lock:
			return
		if not self.can_apply or not self.can_save:
			self.can_apply = True
			self.can_save = True
			self.set_buttons_state()

	def untouch(self, *args):
		"""
		Call this method whenever the form is reset.
		"""
		if self.can_apply or self.can_save:
			self.can_apply = False
			self.can_save = False
			self.set_buttons_state()

	def register_changes(self, *args):
		"""
		Pass in the list of gtk widgets placed on the form so we can detect form changes
		and call touch()
		"""
		for x in args:
			if not x:
				continue
			if isinstance(x, gtk.Editable) or isinstance(x, gtk.OptionMenu) or isinstance(x, gtk.ComboBox):
				x.connect("changed", self.touch)
			elif isinstance(x, gtk.ToggleButton):
				x.connect("toggled", self.touch)
			elif isinstance(x, gtk.FileChooserButton):
				x.connect("file-set", self.touch)
			else:
				self.serror("unhandled widget: %s" % type(x), *args)


	def create(self):
		self.tabs = self.create_tabs()
		self.window = self.create_window()
		self.populate_form()
		self.calculate_tabs_size()

	def calculate_tabs_size(self):
		maxw = MIN_WIDTH
		maxh = MIN_HEIGHT
		if self.ui_util.small_screen:
			maxw = int(maxw*3/4)
			maxh = int(maxh*3/4)
		self.width = maxw
		self.height = maxh
		#ensure we give them the same size:
		for _,box in self.tabs:
			w,h = box.size_request()
			#self.sdebug("size_request(%s)=%s" % (btn.get_label(), (w,h)))
			if w>maxw:
				maxw = w
			if h>maxh:
				maxh = h
		for _,box in self.tabs:
			box.set_size_request(maxw, maxh)
		self.width = maxw
		self.height = maxh
		self.sdebug("=%sx%s" % (self.width, self.height))

	def populate_form(self):
		try:
			self.populate_lock = True
			self.do_populate_form()
		finally:
			self.populate_lock = False

	def create_window(self):
		window = new_config_window(self.title)

		#Contents
		self.box = self.create_form_box()
		self.show_section(self.default_section)

		window.set_border_width(0)
		window.add(self.box)
		window.set_geometry_hints(self.box)
		window.connect('delete_event', self.close_window)
		self.ui_util.add_close_accel(window, self.close_window)
		return	window

	def create_tabs(self):
		raise Exception("Method must be overriden")

	def add_main_area(self, box, widget, fill=False):
		spacer = gtk.HBox(False, 0)
		spacer.set_border_width(15)
		spacer.add(widget)
		self.ui_util.add_top_widget(box, spacer, fill)
		self.ui_util.add_top_widget(box, gtk.VBox(), True)		#fill remaining space
		box.show_all()

	def create_form_box(self):
		#main container
		box = gtk.VBox(False, 0)

		#Toolbar
		hbox = gtk.HBox(False, 0)
		for button,_ in self.tabs:
			hbox.pack_start(button)
		box.pack_start(hbox)
		box.pack_start(gtk.HSeparator())

		#Where we place the selected box
		self.container = gtk.VBox()
		align = gtk.Alignment(0.5, 0.0, 1.0, 1.0)
		align.add(self.container)
		align.show()
		box.pack_start(align)

		#actions
		self.cancel_button = make_button("Cancel", gtk.STOCK_CANCEL, self.close_window)
		self.apply_button = make_button("Apply", gtk.STOCK_APPLY, self.apply_changes)
		self.save_button = make_button("Save", gtk.STOCK_SAVE, self.commit_changes)
		self.button_box = button_hbox(self.cancel_button, self.apply_button, self.save_button)
		box.pack_start(self.button_box)
		self.set_buttons_state()

		#hook toolbar callbacks:
		for button,_ in self.tabs:
			button.connect("clicked", self.show_section)
		box.show_all()
		return	box

	def set_buttons_state(self):
		self.apply_button.set_sensitive(self.can_apply)
		self.save_button.set_sensitive(self.can_save)

	def show_section(self, button):
		newbox = None
		for btn,box in self.tabs:
			if btn==button or (button==None and newbox==None):
				btn.set_relief(gtk.RELIEF_NORMAL)
				btn.grab_focus()
				newbox = box
			else:
				btn.set_relief(gtk.RELIEF_NONE)
		if newbox:
			#remove existing box
			for child in self.container.get_children():
				self.container.remove(child)
			#add the right one (aligned to the top)
			self.container.pack_start(newbox)
			if self.window:
				self.window.resize(100, 100)

	def add_tunnel_printfs_options(self, table):
		if not WIN32:
			(self.tunnel_printer_label, self.tunnel_printer_button) = add_tableline(self, table, "Share Local Printers", gtk.CheckButton(), "Allow this server to connect to your local printers")
		else:
			self.tunnel_printer_label = None
			self.tunnel_printer_button = None
		(self.tunnel_fs_label, self.tunnel_fs_button) = add_tableline(self, table, "Allow access to local filesystem", gtk.CheckButton(), "Allow the server to access local files")

	def add_tunnel_sound_options(self, table, show_sinksource_options=False):
		def add_sound_options(label, tooltip, dev_label, dev_tooltip, shown, get_dev_options, selected_device):
			box = gtk.HBox(False)
			button = gtk.CheckButton()
			box.pack_start(button)
			#add_tableline(self, table, "Receive Sound", receive_sound_box, "Receive sound coming from remote applications")
			(button_label, _) = add_tableline(self, table, label, box, tooltip)
			""" combo: used to select the sound module (pulseaudio[src|sink], osxaudio, ossaudio, ...) """
			combo = None
			""" label for the device selection combo """
			device_label = None
			""" device selection combo: only used with some modules at the moment.. (just pulseaudio) """
			device_combo = None
			if shown:
				combo = gtk.combo_box_new_text()
				box.pack_start(combo)
				(device_label, device_combo) = add_tableline(self, table, dev_label, gtk.combo_box_new_text(), dev_tooltip)
				def sound_button_changed(*args):
					enabled = button.get_active()
					combo.set_sensitive(enabled)
					dev_options = get_dev_options()
					dev_enabled = enabled and dev_options is not None and len(dev_options)>0
					for x in [device_label, device_combo]:
						if x:
							x.set_sensitive(dev_enabled)
				button.connect("toggled", sound_button_changed)

				def sound_module_changed(*args):
					""" Re-populate the device options for the new module """
					device_options = get_dev_options()
					device_combo.get_model().clear()
					if device_options:
						index = 0
						for name,description in device_options.items():
							device_combo.append_text(description)
							if name==selected_device or len(device_options)==1:
								device_combo.set_active(index)
							index += 1
						sound_button_changed()				#ensure it is (un)greyed out if required (as this is called on populate)
					else:
						device_combo.append_text("Auto")
						device_combo.set_active(0)
						for x in device_label, device_combo:
							x.set_sensitive(False)
				combo.connect('changed', sound_module_changed)
			else:
				if not self.settings.supports_sound:
					button.set_sensitive(False)
					button.set_tooltip_text("Sorry, sound is not supported on your system")
			return	(button_label, button, combo, device_label, device_combo)


		(self.tunnel_sink_label, self.tunnel_sink_button, self.sound_sink_combo,
		self.sound_sink_device_label, self.sound_sink_device_combo) = add_sound_options("Receive Sound", "Receive sound coming from remote applications",
						"Sound Playback Device", "The device to use to play sound from remote applications",
						show_sinksource_options and self.settings.supports_sound,
						self.get_sound_sink_device_options, self.settings.gst_sound_sink_options.get("device"))
		(self.tunnel_source_label, self.tunnel_source_button, self.sound_source_combo,
		self.sound_source_device_label, self.sound_source_device_combo) = add_sound_options("Send Sound", "Send sound coming from this computer (Microphone, etc) to remote applications",
						"Sound Capture Device", "The device to use to send sound to remote applications",
						show_sinksource_options and self.settings.supports_sound,
						self.get_sound_source_device_options, self.settings.gst_sound_source_options.get("device"))
		(self.tunnel_clone_label, self.tunnel_clone_button, self.clone_combo,
		self.clone_device_label, self.clone_device_combo) = add_sound_options("Clone Sound", "Allow sound on this computer to be cloned somewhere else",
						"Sound Monitor Device", "The device to use to send sound to remote applications",
						show_sinksource_options and self.settings.supports_sound and has_pa,
						self.get_clone_device_options, self.settings.gst_sound_clone_options.get("device"))

	def get_sound_device_options(self, module_options, module_combo, get_options_options):
		""" For a given list of module options (name,module) list,
			The module_combo shown to the user
			And a method to get the list of options for a given module, returns the list of device options
			for the user selected module.
		"""
		if not module_options or not module_combo:
			return None
		module = None
		for _name,_module in module_options:
			if _name==module_combo.get_active_text():
				module = _module
				break
		options = {}
		if module:
			options = get_options_options(module)
		device_options = options.get("device")
		self.sdebug("module=%s, options=%s, device_options=%s" % (module, options, device_options))
		return device_options

	#SINK
	def get_sound_sink_device_options(self):
		sinks = get_default_gst_sound_sink_module_options()
		return	self.get_sound_device_options(sinks, self.sound_sink_combo, get_default_gst_sound_sink_options_options)
	#SOURCE
	def get_sound_source_device_options(self):
		sources = get_default_gst_sound_source_module_options()
		return	self.get_sound_device_options(sources, self.sound_source_combo, get_default_gst_sound_source_options_options)
	#CLONE
	def get_clone_device_options(self):
		clones = get_default_gst_sound_clone_module_options()
		return	self.get_sound_device_options(clones, self.clone_combo, get_default_gst_sound_source_options_options)




	def add_session_type_options(self, table, same_line=False):
		x_offset = 0
		x_factor = 1
		row_inc = not same_line
		(self.preferred_session_type_combo, self.preferred_session_type_warning) = self.add_session_type_option(table,
											"Seamless Default",
											"The default protocol used for starting individual applications in seamless mode",
											x_offset=x_offset, x_factor=x_factor, row_inc=row_inc)
		if same_line:
			row_inc = True
			x_offset = 2
		(self.preferred_desktop_type_combo, self.preferred_desktop_type_warning) = self.add_session_type_option(table,
											"Desktop Default",
											"The default protocol used for starting full remote desktops",
											x_offset=x_offset, x_factor=x_factor, row_inc=row_inc)

	def add_session_type_option(self, table, label_text, tooltip=None, x_offset=0, x_factor=1, row_inc=True):
		hbox = gtk.HBox(False, 0)
		option = gtk.OptionMenu()
		self.ui_util.setfont(option)
		option.set_size_request(90, 16)
		option_label = self.ui_util.make_label("", font="sans 8", line_wrap=True)
		color_obj = gtk.gdk.color_parse("red")
		if color_obj:
			option_label.modify_fg(gtk.STATE_NORMAL, color_obj)
		hbox.pack_start(option)
		hbox.pack_start(self.ui_util.session_type_info_help_button())
		align = gtk.Alignment(xalign=0.2, yalign=0.5, xscale=1.0, yscale=1.0)
		align.add(option_label)
		hbox.pack_start(align)
		hbox.show_all()
		add_tableline(self, table, label_text, hbox, tooltip, x_offset=x_offset, x_factor=x_factor, row_inc=row_inc)
		option.hbox_container = hbox			#just so we can find a reference to it easily (used by populate)
		option_label.hbox_container = hbox		#	"	"
		return (option, option_label)

	def populate_option_line(self, optionmenu, options, current, option_label=None, empty_warning=None):
		self.ui_util.populate_type_menu(optionmenu, options, current)
		optionmenu.hbox_container.show()
		if (option_label and empty_warning):
			if len(options)==0:
				option_label.set_text(empty_warning)
				option_label.show()
				optionmenu.hide()
			else:
				option_label.set_text("")
				option_label.hide()
				optionmenu.show_all()

	def get_selected_option(self, optionmenu, options):
		active = optionmenu.get_history()
		l = len(options)
		if l==0:
			return	None	#empty list of options... cant select one!
		if active<0 or active>=len(options):
			active = 0
		return options[active]

	def show(self):
		self.window.show()

	def refresh(self):
		self.sdebug()
		self.show()
		self.unhide()

	def apply_changes(self, *args):
		self.sdebug()
		try:
			if self.can_apply:
				self.do_apply_changes()
				self.can_apply = False
			self.set_buttons_state()
		except Exception, e:
			self.exc(e)

	def commit_changes(self, *args):
		try:
			if self.can_apply:
				self.do_apply_changes()
			self.do_commit_changes()
			self.untouch()
		except Exception, e:
			self.exc(e)

	def close_window(self, *args):
		self.destroy_window()
		if self.close_callback:
			self.close_callback(False)
		return True

	def destroy_window(self, *args):
		if self.window:
			self.window.destroy()
			self.window = None

	def unhide(self):
		self.show()
		self.window.present()
