#!/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 os.path
import hashlib
import re
import ConfigParser

from winswitch.util.which import which

from winswitch.util.simple_logger import Logger, msig
logger = Logger("load_desktop_menus", log_colour=Logger.HIGHLIGHTED_BLUE)
debug_import = logger.get_debug_import()


debug_import("common")
from winswitch.util.common import csv_list, visible_command, load_binary_file, is_valid_file, delete_if_exists
debug_import("file_io")
from winswitch.util.file_io import get_menu_dir, get_xsessions_dir, get_actions_dir, save_properties
debug_import("paths")
from winswitch.util.paths import XSESSION_DIRS, WINSWITCH_COMMAND_WRAPPER, PREFIX_SEARCH_ORDER
debug_import("consts")
from winswitch.consts import DEFAULT_DISABLED_CATEGORIES, DEFAULT_IGNORED_CATEGORIES, DEFAULT_IGNORED_DIRECTORIES, DEFAULT_IGNORED_ONLYSHOWNIN, \
	DEFAULT_SOUNDOUT_CATEGORIES, DEFAULT_SOUNDIN_CATEGORIES, DEFAULT_VIDEO_CATEGORIES, \
	DEFAULT_SOUNDOUT_ENABLED_COMMANDS, DEFAULT_SOUNDIN_ENABLED_COMMANDS, DEFAULT_VIDEO_ENABLED_COMMANDS, \
	COMMANDS_WORKAROUNDS, DEFAULT_IGNORED_COMMANDS, DEFAULT_RUN_ONCE_COMMANDS
debug_import("server_command")
from winswitch.objects.server_command import ServerCommand
debug_import("which")
from winswitch.util.which import win32_which
debug_import("globals")
from winswitch.globals import WIN32, OSX
debug_import("all done!")

ICON_DEBUG = False
LOG_LOADED = False
LOG_SKIPPED = False								#shows list of ignored desktop files / directories
LOG_COUNT = False								#shows how many desktop files were loaded per directory
LOG_MENU = False								#shows what menus and commands were loaded (new on win32 so debug it)

def load_start_menu(ignored_commands, whitelist_commands, ignored_categories=DEFAULT_IGNORED_CATEGORIES, ignored_directories=DEFAULT_IGNORED_DIRECTORIES):
	(server_commands, menu_directories) = do_load_start_menu(ignored_commands, whitelist_commands, ignored_categories, ignored_directories)
	if len(server_commands)==0 and (OSX or WIN32):
		create_default_start_menu()
		#reload what we just created
		(server_commands, menu_directories) = do_load_start_menu(ignored_commands, whitelist_commands, ignored_categories, ignored_directories)
	return	(server_commands, menu_directories)

def create_default_start_menu():
	"""
	Creates a few default .desktop entries for applications normally shipped with the operating system.
	"""
	logger.serror("no commands or directories found.. creating default entries")
	menu_dir = get_menu_dir()

	if WIN32:
		APPS = [ ("Notepad", "notepad", "notepad", "Applications"),
				("Wordpad", "write", "wordpad", "Applications"),
				("Internet Explorer", "iexplore", "iexplore", "Applications"),
				("Explorer", "explorer", "explorer", "Applications"),
				("Task Manager", "taskmgr", "task", "System"),
				("Registry Editor", "regedit", "regedit", "System"),
				("Character Map", "charmap", "charmap", "Utilities"),
				("On-Screen Keyboard", "osk", "keyboard", "Utilities"),
				("Disk Defragmenter", "dfrg.msc", "disk", "System"),
				("Free Cell", "freecell", "freecell", "Games"),
				("Minesweeper", "winmine", "mine", "Games"),
				("MS Paint", "mspaint", "mspaint", "Applications"),
				("Command Prompt", "cmd", "msdos", "System") ]
	else:
		#those ship with the X server and are present on OSX:
		APPS = [ ("Terminal", "xterm", "term", None),
				("Calculator", "xcalc", "calc", None),
				("Clock", "xclock", "clock", None),
				("X Logo", "xlogo", "xlogo", None),
				("Load", "xload", "load", None),
				("Eyes", "xeyes", "eyes", None) ]
	for name, cmd, icon, categories in APPS:
		if WIN32:
			#which for win32:
			real_cmd = win32_which(cmd)
			if real_cmd:
				#try to extract the icon data
				base_filename = ("%s" % name).replace(" ", "_")
				full_filename = os.path.join(menu_dir, base_filename)
				try:
					delete_if_exists(full_filename)
					filename = win32_export_command_icon(real_cmd, full_filename)
					if is_valid_file(filename):
						icon = filename
				except Exception, e:
					logger.serr("failed to extract icon data from %s" % real_cmd, e)
			else:
				real_cmd = cmd				#RDP seems happy to find commands by name for us otherwise
			logger.slog("real command(%s)=%s" % (name, real_cmd))
		else:
			real_cmd = "/usr/X11/bin/%s" % cmd
			if not is_valid_file(real_cmd):
				real_cmd = which(cmd)
			logger.sdebug("which(%s)=%s" % (cmd, real_cmd))
			if real_cmd is None or not is_valid_file(real_cmd):
				continue
		props = {u"Encoding" : u"UTF-8",
			u"Name" : name,
			u"Exec" : real_cmd,
			u"Icon" : icon,
			u"Terminal" : "false",
			u"Type" : "Application",
			u"NoDisplay" : "false",
			}
		if categories:
			props[u"Categories"] = categories
		desktop_filename = ("%s.desktop" % name).replace(" ", "_")
		full_filename = os.path.join(menu_dir, desktop_filename)
		save_properties(full_filename, props, header="[Desktop Entry]", quote="")

def win32_export_command_icon(exe_path, base_filename):
	import win32ui, win32gui, win32con, win32api		#@UnresolvedImport

	ico_x = win32api.GetSystemMetrics(win32con.SM_CXICON)
	ico_y = win32api.GetSystemMetrics(win32con.SM_CYICON)

	large, small = win32gui.ExtractIconEx(exe_path, 0)
	capture = large
	destroy = small
	if not large:
		capture = small
		destroy = large
	if not capture:
		return None
	if destroy:
		win32gui.DestroyIcon(destroy[0])

	hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0))
	hbmp = win32ui.CreateBitmap()
	hbmp.CreateCompatibleBitmap(hdc, ico_x, ico_y)
	hdc = hdc.CreateCompatibleDC()

	hdc.SelectObject(hbmp)
	hdc.FillSolidRect( (0,0, ico_x, ico_y), 0xFFFFFF)
	hdc.DrawIcon( (0,0), capture[0] )
	bmp_filename = base_filename+".bmp"
	hbmp.SaveBitmapFile(hdc, bmp_filename)
	hdc.DeleteDC()
	#re-save it as a png:
	try:
		import gtk.gdk
		pixbuf = gtk.gdk.pixbuf_new_from_file(bmp_filename)
		#if not pixbuf.get_has_alpha():
		pixbuf = pixbuf.add_alpha(True, chr(0xFF), chr(0xFF), chr(0xFF))
		png_filename = base_filename+".png"
		pixbuf.save(png_filename, "png")
		return	png_filename
	except Exception, e:
		logger.serr("failed to convert using gdk pixbuf", e)
		return	bmp_filename


def do_load_start_menu(ignored_commands, whitelist_commands, ignored_categories=DEFAULT_IGNORED_CATEGORIES, ignored_directories=DEFAULT_IGNORED_DIRECTORIES):
	"""
	Returns ([ServerCommand], [Directories])
	"""
	sig = msig(ignored_commands, whitelist_commands, ignored_categories, ignored_directories)
	menu_dir = get_menu_dir()
	logger.debug(sig+" loading from %s" % menu_dir)
	server_commands = load_menu_files(menu_dir, ".desktop", True, ignored_commands, whitelist_commands, ignored_directories=ignored_directories)

	if WIN32:
		#there are no desktop directories on win32, so create virtual ones for each category we found..
		filtered_directories = {}
		for command in server_commands:
			if not command.categories or len(command.categories)==0:		#we always set at least one but the user may have tweaked it!
				continue
			category = command.categories[0]
			command.menu_category = category								#also assign the menu_category
			if command in filtered_directories:
				continue
			#def __init__(self, id, name, command, comment, icon_filename, names={}, comments={}, categories=[]):
			category_command = ServerCommand(category, category, None, category, category, None, None, None)
			category_command.type = ServerCommand.CATEGORY
			filtered_directories[category] = category_command
		menu_directories = filtered_directories.values()
		return	(server_commands, menu_directories)

	if len(server_commands)==0 and not WIN32:
		xdg_data_home = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
		data_dirs = os.environ.get("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/").split(":")
		menu_home_dirs = data_dirs + [xdg_data_home]
		menu_dirs = [os.path.join(x, "applications") for x in menu_home_dirs]
		logger.log(sig+" no files found in %s, using system locations: %s" % (menu_dir, menu_dirs))
		server_commands = load_menu_files_from_dirs(menu_dirs, ".desktop", True, ignored_commands, whitelist_commands,
												ignored_directories=ignored_directories,
												file_type=ServerCommand.COMMAND)

	desktop_dirs = [("%s/share/desktop-directories" % x) for x in PREFIX_SEARCH_ORDER]
	all_menu_directories = load_menu_files_from_dirs(desktop_dirs, ".directory", False, file_type=ServerCommand.CATEGORY)
	filtered_directories = {}
	#filter directories: only the ones we have commands for:
	for server_command in server_commands:
		menu_category = None
		for category in server_command.categories:
			if category in ignored_categories:
				continue		#ignore those
			#try filename match first
			for directory in all_menu_directories:
				if directory.filename == category:
					filtered_directories[category] = directory
					menu_category = directory.name
					break
			if not menu_category:
				for directory in all_menu_directories:
					if directory.name == category:
						filtered_directories[category] = directory
						menu_category = directory.name
						break
			if menu_category:
				break
		server_command.menu_category = menu_category
		if LOG_MENU:
			logger.log(sig+" category(%s / %s)=%s" % (server_command, csv_list(server_command.categories), menu_category))
	if LOG_MENU:
		logger.debug(sig+" all_menu_directories=%s" % csv_list(all_menu_directories))
		logger.log(sig+" filtered_directories=%s" % csv_list(filtered_directories.keys()))
		logger.log(sig+" commands=%s" % visible_command(csv_list(server_commands)))
	else:
		logger.log(sig+" loaded %d commands and %d directories" % (len(server_commands), len(filtered_directories.keys())))
	menu_directories = filtered_directories.values()
	return	(server_commands, menu_directories)

def load_menu_files_from_dirs(dirs, extension, req_cmd,
							ignored_commands=None, whitelist_commands=["*"],
							ignored_in=DEFAULT_IGNORED_ONLYSHOWNIN,
							ignored_directories=[],
							file_type=ServerCommand.COMMAND):
	menu_files = []
	for menu_dir in dirs:
		menu_files += load_menu_files(menu_dir, extension, req_cmd,
							ignored_commands, whitelist_commands,
							ignored_in,
							True,
							ignored_directories,
							file_type)
	return	menu_files

def load_menu_files(menu_dir, extension, req_cmd, ignored_commands=None, whitelist_commands=["*"],
					ignored_in=DEFAULT_IGNORED_ONLYSHOWNIN,
					recurse=True,
					ignored_directories=[],
					file_type=ServerCommand.COMMAND):
	sig = msig(menu_dir, extension, req_cmd, ignored_commands, whitelist_commands, ignored_in, recurse, ignored_directories, file_type)
	logger.debug(sig)
	menu_files = []
	if not os.path.exists(menu_dir) or not os.path.isdir(menu_dir):
		return	menu_files
	count = 0
	skipped = []
	for menu_file in sorted(os.listdir(menu_dir)):
		full_path = os.path.join(menu_dir, menu_file)
		if os.path.isdir(full_path):
			if not recurse or menu_file in ignored_directories:
				logger.debug(sig+" sub-directory %s ignored" % menu_file)
				continue
			sub_files = load_menu_files(full_path, extension, req_cmd, ignored_commands, whitelist_commands, ignored_in, recurse, ignored_directories, file_type)
			menu_files += sub_files
		if not menu_file.endswith(extension):
			continue
		#noext = menu_file[:len(menu_file)-len(extension)]
		sc = None
		try:
			sc = load_menu_file(menu_dir, menu_file, req_cmd, ignored_commands, whitelist_commands, ignored_in, skipped, file_type=file_type)
		except Exception, e:
			logger.error(sig, e)
		if sc:
			menu_files.append(sc)
			count += 1
	if len(skipped)>0 and LOG_SKIPPED:
		logger.debug(sig+" these commands were ignored (as per configuration): %s" % csv_list(skipped))
	if LOG_COUNT:
		logger.debug(sig+" found %d items" % len(menu_files))
	return	menu_files


def get_default_browse_command():
	options = {"thunar": "thunar ~/Desktop",		#preferred: nice and simple
			"pcmanfm": "pcmanfm --no-desktop ~/Desktop",
			#"nautilus": "nautilus --no-desktop",	cannot be used as it runs as a daemon and the process exits
			"xfe": "xfe ~/Desktop",
			"rox-filer": "rox-filer ~/Desktop",
			"gentoo" : "gentoo -1 ~/Desktop -2 ~/Desktop",
			"mc": "xterm mc"
			}
	order = ["thunar", "pcmanfm", "xfe", "rox-filer", "gentoo", "mc"]
	def bin_exists(command):
		c = which(command)
		return c and os.path.exists(c)
	for cmd in order:
		command = options.get(cmd)
		assert command
		if not bin_exists(cmd):
			logger.sdebug("%s not found" % cmd)
			continue
		rc = command.split()[0]			#ie: "xterm mc" -> "xterm"
		if rc!=cmd and not bin_exists(rc):
			logger.sdebug("%s not found for %s" % (rc, command))
			continue
		return	command
	return	None

def load_actions(create=True):
	"""
	Returns the list of actions defined in ACTIONS_DIR.
	If there aren't any, one will be created ("default browse command")
	"""
	actions_dir = get_actions_dir()
	action_commands = load_menu_files(actions_dir, ".desktop", True, file_type=ServerCommand.ACTION)
	if len(action_commands)==0 and create and not WIN32 and not OSX:
		cmd = get_default_browse_command()
		logger.slog("no files found in %s, adding a default browse command: %s" % (actions_dir, cmd))
		desktop_filename = os.path.join(actions_dir, "browse.desktop")
		props = {u"Encoding" : u"UTF-8",
				u"Name" : "Browse",
				u"Comment" : "Browse files on this computer",
				u"Exec" : cmd,
				u"Icon" : "browse.png",
				u"Terminal" : "false",
				u"Type" : "Application",
				u"NoDisplay" : "false",
				}
		save_properties(desktop_filename, props, header="[Desktop Entry]", quote="")
		return	load_actions(False)
	logger.sdebug("=%s" % csv_list(action_commands))
	return	action_commands

def load_desktop_commands(ignored_xsessions, whitelist_xsessions):
	if WIN32:
		return []
	return	load_xsessions(ignored_xsessions, whitelist_xsessions)

def load_xsessions(ignored_xsessions, whitelist_xsessions):
	"""
	Returns the list of xsessions
	"""
	xsessions_dir = get_xsessions_dir()
	logger.sdebug("loading from %s" % xsessions_dir, ignored_xsessions, whitelist_xsessions)
	xsession_commands = load_menu_files(xsessions_dir, ".desktop", True, file_type=ServerCommand.DESKTOP)
	if len(xsession_commands)==0 and not WIN32:
		logger.slog("no files found in %s, using system locations: %s" % (xsessions_dir, XSESSION_DIRS), ignored_xsessions, whitelist_xsessions)
		xsession_commands = load_menu_files_from_dirs(XSESSION_DIRS, ".desktop", True, ignored_commands=ignored_xsessions, whitelist_commands=whitelist_xsessions, file_type=ServerCommand.DESKTOP)
	for xsession in xsession_commands:
		xsession.menu_category = "Virtual Desktop"
	logger.sdebug("=%s" % str(xsession_commands), ignored_xsessions, whitelist_xsessions)
	return	xsession_commands


def load_menu_file(menu_dir, filename, req_cmd, ignored_commands=None, whitelist_commands=["*"],
				ignored_in=DEFAULT_IGNORED_ONLYSHOWNIN,
				skipped=[],
				disabled_categories=DEFAULT_DISABLED_CATEGORIES,
				file_type=ServerCommand.COMMAND):
	sig = msig(menu_dir, filename, req_cmd, ignored_commands, whitelist_commands, ignored_in, skipped, disabled_categories, file_type)
	short_sig = msig(menu_dir, filename, req_cmd, "[..]", "[..]")
	full_path = os.path.join(menu_dir, filename)
	desktop_info = ConfigParser.ConfigParser()
	if not desktop_info.read(full_path):
		logger.debug(short_sig+" failed to load '%s'" % full_path)
		return	None

	enabled = True
	try:
		no_display = desktop_info.get('Desktop Entry', 'NoDisplay')
		if no_display and (no_display == "true" or no_display == "1"):
			enabled = False
	except ConfigParser.NoOptionError:
		pass

	try:
		only_show_in = desktop_info.get('Desktop Entry', 'OnlyShowIn')
		if only_show_in and len(only_show_in)>0:
			shown_in = only_show_in.split(";")
			ignored = True
			for shown in shown_in:
				if len(shown)>0 and shown not in ignored_in:
					ignored = False
					break
			if ignored:
				enabled = False
	except ConfigParser.NoOptionError:
		pass

	try:
		name = desktop_info.get('Desktop Entry', 'Name')
	except ConfigParser.NoOptionError:
		name = None

	if not name or len(name)==0:
		logger.debug(sig+" name is missing!")
		return	None

	try:
		cmd = desktop_info.get('Desktop Entry', 'Exec')
	except ConfigParser.NoOptionError:
		cmd = None

	if req_cmd and (not cmd or len(cmd)==0):
		return	None

	#if we have re-written the Exec line to use the wrapper, ignore it for our purpose:
	if cmd and cmd.startswith(WINSWITCH_COMMAND_WRAPPER):
		cmd = cmd[(len(WINSWITCH_COMMAND_WRAPPER)+1):]

	may_ignore = True
	if cmd:
		for old_cmd,new_cmd in COMMANDS_WORKAROUNDS.items():
			if cmd.startswith(old_cmd):
				logger.debug(short_sig+" command %s replaced with %s" % (old_cmd, new_cmd))
				cmd = cmd.replace(old_cmd, new_cmd)
				enabled = True
				may_ignore = False			#dealt with
				break

	#remove params placeholders from command
	noparams = cmd
	if cmd:
		pos = cmd.find(" ")
		if pos>0:
			noparams = cmd[:pos]

	if may_ignore and ignored_commands:
		ignored = False
		ic = matches_any(noparams, ignored_commands)
		if ic:
			ignored = True

		if not ignored:
			wl = matches_any(noparams, whitelist_commands)
			if wl is None:
				ignored = True

		if ignored:
			skipped.append(name)
			enabled = False

	try:
		comment = desktop_info.get('Desktop Entry', 'Comment')
	except ConfigParser.NoOptionError:
		comment = None

	try:
		icon_filename = desktop_info.get('Desktop Entry', 'Icon')
	except ConfigParser.NoOptionError:
		icon_filename = None

	try:
		categories_list = desktop_info.get('Desktop Entry', 'Categories')
	except ConfigParser.NoOptionError:
		categories_list = None

	categories = []
	uses_sound_out = matches_any(noparams, DEFAULT_SOUNDOUT_ENABLED_COMMANDS) is not None
	uses_sound_in = matches_any(noparams, DEFAULT_SOUNDIN_ENABLED_COMMANDS) is not None
	uses_video = matches_any(noparams, DEFAULT_VIDEO_ENABLED_COMMANDS) is not None
	run_once_only = matches_any(noparams, DEFAULT_RUN_ONCE_COMMANDS) is not None

	if categories_list:
		categories = sorted(categories_list.split(";"))
		for cat in categories:
			if not cat:
				continue
			if cat in disabled_categories:
				skipped.append(name)
				if may_ignore:
					enabled = False
			if cat.startswith("X-") or cat=="":
				categories.remove(cat)
			cat = cat.lower()
			if matches_any(cat, DEFAULT_SOUNDOUT_CATEGORIES):
				uses_sound_out = True
			if matches_any(cat, DEFAULT_SOUNDIN_CATEGORIES):
				uses_sound_in = True
			if matches_any(cat, DEFAULT_VIDEO_CATEGORIES):
				uses_video = True

	names = {}
	comments = {}
	for key,value in desktop_info.items('Desktop Entry'):
		_NAMEKEY='Name['
		_COMMENTKEY="Comment["
		_ENDKEY="]"
		if not key.endswith(_ENDKEY):
			continue
		l = len(key)
		if key.startswith(_NAMEKEY):
			locale = key[len(_NAMEKEY):l-len(_ENDKEY)]
			if locale:
				names[locale] = value
		if key.startswith(_COMMENTKEY):
			locale = key[len(_COMMENTKEY):l-len(_ENDKEY)]
			if locale:
				comments[locale] = value

	if LOG_LOADED:
		logger.debug(short_sig+" name=%s, cmd=%s, categories=%s" % (name, cmd, str(categories)))

	_debug = ICON_DEBUG #or name=="gedit"
	if _debug:
		logger.debug(sig+" %s: (%s,%s), icon=%s" % (filename, name, cmd, icon_filename))
	raw_data = load_binary_file(full_path)
	md5 = hashlib.md5()
	md5.update(raw_data)
	md5.update(file_type)
	cmd_uuid = md5.hexdigest()
	sc = ServerCommand(cmd_uuid, name, cmd, comment, icon_filename, names, comments, categories)
	sc.type = file_type
	sc.filename = filename
	sc.enabled = enabled
	sc.uses_sound_out = uses_sound_out
	sc.uses_sound_in = uses_sound_in
	sc.uses_video = uses_video
	sc.run_once_only = run_once_only
	pos = filename.rfind(".")
	if pos>0:
		sc.filename = filename[:pos]
	sc.icon_names = []
	if icon_filename:
		sc.icon_names.append(icon_filename)
	if not sc.icon_data:
		if cmd:
			sc.icon_names.append(os.path.basename(cmd))
		if name:
			sc.icon_names.append(name)
		pos = filename.rfind(".")
		if pos>0:
			sc.icon_names.append(filename[:pos])
		if icon_filename:
			pos = icon_filename.rfind("-")
			if pos>0:
				sc.icon_names.append(icon_filename[:pos])
	return	sc



def matches_any(x, list_of_regexps, _debug=False):
	if not list_of_regexps or not x:
		if _debug:
			logger.sdebug("no regexps or no value!", x, list_of_regexps)
		return None
	for v in list_of_regexps:
		#straight match
		if x==v:
			if _debug:
				logger.sdebug("direct match!", x, list_of_regexps)
			return	v
		# substring match, convert to a RE
		rstr = r"^%s$" % v.replace("*", ".*")
		regexp = re.compile(rstr)
		m = regexp.match(x)
		if _debug:
			logger.sdebug("%s.match(%s)=%s" % (rstr, x, m), x, list_of_regexps)
		if m:
			return	v
	return	None


def icon_info(data):
	if not data:
		return None
	return	"%d bytes" % len(data)



def main():
	global ICON_DEBUG, LOG_SKIPPED, LOG_COUNT, LOG_MENU, LOG_LOADED
	ICON_DEBUG = False
	LOG_LOADED = True
	LOG_SKIPPED = True
	LOG_COUNT = True
	LOG_MENU = True
	logger.slog()
	logger.slog("default_browse_command=%s" % get_default_browse_command())
	logger.slog("xsessions=%s" % load_xsessions([], []))
	logger.slog("actions=%s" % load_actions())
	(commands, categories) = load_start_menu(DEFAULT_IGNORED_COMMANDS, [])
	logger.slog("start_menu commands=%s" % csv_list(commands))
	logger.slog("start_menu categories=%s" % csv_list(categories))


if __name__ == "__main__":
	main()
