#!/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
import stat
import sys
import uuid
import time
import string

from winswitch.consts import REQUIRED_XPRA_MIN_VERSION, REQUIRED_WINSWITCH_MIN_VERSION
from winswitch.consts import DSL_256K_SPEED, DEFAULT_INTERNET_SPEED, DEFAULT_LAN_SPEED, MINIMUM_LAN_SPEED, MAX_SPEED
from winswitch.util.simple_logger import Logger
from winswitch.globals import OSX, WIN32

logger=Logger("common", log_colour=Logger.CYAN)

DISABLE_GIO_ARG = "--disable-gio"
CAN_USE_GIO = False
if OSX:
	#OSX: (gio import would break), darwin could have a server but it looks like gtk-osx also breaks/is missing gio
	#see:
	#https://bugzilla.gnome.org/show_bug.cgi?id=543148
	#http://pida.co.uk/ticket/368
	pass
elif DISABLE_GIO_ARG not in sys.argv:
	try:
		import gio
		CAN_USE_GIO = gio is not None
	except ImportError, e:
		logger.serror("failed to import gio: %s" % e)
		CAN_USE_GIO = False



def is_valid_path(filename):
	"""
	Tests if the filename is valid and exists (any exceptions will be ignored and will return False)
	"""
	try:
		return	filename and os.path.exists(filename)
	except Exception, e:
		logger.serr(None, e, filename)
		return	False

def is_valid_exe(command):
	if not command:
		return	False
	exe = command
	if os.name=="posix":
		exe = command.split(" ")[0]
	if not is_valid_file(exe):
		return False
	if os.name=="posix":
		return	os.access(exe, os.X_OK)
	return True

def is_suid(command):
	if not os.name=="posix":
		return	False
	if not command:
		return	False
	exe = command.split(" ")[0]
	if not is_valid_file(exe):
		return False
	s = os.stat(exe)
	return (s.st_mode & stat.S_ISUID) != 0

def is_valid_file(filename):
	"""
	Tests if the filename is valid, exists and is a file. (any exceptions will be ignored and will return False)
	"""
	try:
		return	filename and os.path.exists(filename) and os.path.isfile(filename)
	except Exception, e:
		logger.serr(None, e, filename)
		return	False

def is_valid_dir(d):
	"""
	Tests if the filename is valid, exists and is a directory. (any exceptions will be ignored and will return False)
	"""
	try:
		return	d and os.path.exists(d) and os.path.isdir(d)
	except Exception, e:
		logger.serr(None, e, d)
		return	False

def get_bool(s):
	"""
	Parse s into a boolean value.
	"""
	return	(s is not None) and (s is not False) and (s in [1, True, "True", "true", "1", "on", "yes"])

def get_int(s, default=None):
	"""
	Parse s into an int.
	"""
	try:
		return	int(s)
	except:
		return	default

def is_laptop():
	if WIN32:
		return	False
	try:
		from winswitch.util.process_util import get_output
		code, out, _ = get_output(["laptop-detect"])
	except Exception, e:
		logger.slog("error running laptop-detect: %s, assuming workstation" % e)
		return	False
	logger.slog("out=%s, code=%d" % (out, code))
	return	code==0

def get_os_version():
	import platform
	if hasattr(platform, "linux_distribution"):
		s = " ".join(platform.linux_distribution())+" "+platform.machine()
	else:
		uname = platform.uname()
		if uname[0]=='Darwin':
			s = "%s %s %s" % (uname[0], uname[2], uname[4])
		else:
			s = "%s %s %s" % (uname[0], uname[2], uname[3])
	return	s.strip()

def has_unix_group(group_name):
	try:
		from winswitch.util.process_util import get_output
		code, out, _ = get_output(["groups"])
		if code!=0 or not out:
			return	False
		groups = no_newlines(out).split()
		logger.sdebug("found groups: %s" % groups, group_name)
		return group_name in groups
	except Exception, e:
		logger.slog("failed: %s, assuming False" % e, group_name)
		return	False

def is_our_display(display):
	our_display = os.environ.get("DISPLAY", None)
	if not our_display or len(display)<2:
		return	False
	return display.startswith(":") and our_display.startswith(display)		#so :0.0 matches :0



#
# load/save binary data to file
#
def load_binary_file(filename):
	if not is_valid_file(filename):
		return	None
	f = None
	try:
		f = open(filename, mode='rb')
		data = f.read()
	finally:
		if f:
			f.close()
	return	data

def save_binary_file(filename, data):
	assert filename
	if not data or len(data)==0:
		delete_if_exists(filename)
		data = ""
	try:
		f = open(filename, 'wb')
		f.write(data)
	finally:
		f.close()

def delete_if_exists(filename):
	try:
		if filename and os.path.exists(filename):
			os.unlink(filename)
	except Exception, e:
		logger.serr("error", e, filename)

def get_platform_detail():
	platform = sys.platform
	if not platform.startswith("sun"):
		return	platform
	#couldn't find a better way to distinguish opensolaris from solaris...
	RELEASE = "/etc/release"
	data = load_binary_file(RELEASE)
	if data and str(data).lower().find("opensolaris"):
		return	"opensolaris"
	return	"solaris"




def get_latency(line_speed, local=False):
	"""
	Returns the latency to use as buffer for streaming audio.
	(in milliseconds)
	"""
	if local:
		return	20				#local servers shouldn't need *any* latency
	if line_speed>=MAX_SPEED:
		return	50				#tiny token value
	if line_speed>=DEFAULT_LAN_SPEED:
		return	150				#50ms should be fine on an average lan
	if line_speed>=MINIMUM_LAN_SPEED:
		return	250				#100ms should be fine for ~10Mbit/s
	if line_speed>=DEFAULT_INTERNET_SPEED:
		return	400
	if line_speed>=DSL_256K_SPEED:
		return	600
	return	1000;


def get_default_ssh_keyfile():
	if WIN32:
		return "id_rsa"		#FIXME
	DEFAULT_LOCATIONS = [".ssh/id_rsa", ".ssh/id_dsa"]
	for path in DEFAULT_LOCATIONS:
		real_path = os.path.expanduser("~/%s" % path)
		if is_valid_file(real_path):
			return	real_path
	return ""

def get_default_ssh_pub_keyfile():
	if WIN32:
		return "id_rsa.pub"		#FIXME
	DEFAULT_LOCATIONS = [".ssh/id_rsa.pub", ".ssh/id_dsa.pub"]
	for path in DEFAULT_LOCATIONS:
		real_path = os.path.expanduser("~/%s" % path)
		if is_valid_file(real_path):
			return	real_path
	return ""





def get_machine_id():
	return uuid.getnode()

def generate_UUID():
	return ("%s" % uuid.uuid4()).replace("-", "")

DEFAULT_TIME_UNITS = ["minute", "hour", "day", "year"]
TIME_UNIT_NAMES = {"second":60, "minute":60, "hour":24, "day":365, "year":99999}

def get_elapsed_time_string(start_time, show_seconds=True, suffix=" ago"):
	assert start_time>=0
	if start_time==0:
		return	"unknown"
	now = time.time()
	diff = int(now - start_time)
	if diff<0:
		return	"n/a"
	if diff==0:
		return	"now"
	time_units = DEFAULT_TIME_UNITS[:]
	if show_seconds:
		time_units.insert(0, "second")
	else:
		diff = int(diff / 60)

	r = ""
	for name in time_units:
		unit = TIME_UNIT_NAMES[name]
		if diff<=0:
			break
		mod = diff % unit
		if mod!=1:
			name = name + "s"
		out = "%d %s" % (mod, name)
		if len(r)>0:
			r = "%s, %s" % (out, r)
		else:
			r = out
		diff = int(diff / unit)
	return	r+suffix



def csv_list(items, quote=None, sep=', ', before="[", after="]"):
	if not items:
		return	before+after
	if quote:
		sep = "%s%s%s" % (quote, sep, quote)
	return before+("%s" % (sep.join([str(t) for t in items])))+after

def parse_csv(csv):
	if not csv or csv=="[]":
		return	[]
	s = str(csv)
	if not s.startswith("[") or not s.endswith("]"):
		return	[]
	stripped = s[1:len(s)-1]
	return	stripped.split(", ")

def csv_summary(items, quote=None):
	if not items:
		return	"[]"
	sep = ', '
	if quote:
		sep = "%s%s%s" % (quote, sep, quote)
	return "[%s]" % (sep.join([("%s chars" % len(str(t))) for t in items]))

def visible_command(command, max_len=100, no_nl=True):
	assert max_len>3
	if not command:
		return ""
	if no_nl:
		command = no_newlines(command)
	if len(command) < max_len:
		return command
	return command[:max_len-3] + "..."

def unquote(value):
	if not value:
		return value
	l = len(value)
	if l >= 2 and ((value[0]=='"' and value[l-1]=='"') or (value[0]=="'" and value[l-1]=="'")):
		value = value[1:l-1]
	return value

def alphanumspace(x):
	s = str(x)
	return	''.join(ch for ch in s if (ch.isalnum() or ch.isspace()))

def alphanum(x, extras=None):
	s = str(x)
	return	''.join(ch for ch in s if (ch.isalnum() or (extras and extras.find(ch)>=0)))

def alphanumfile(x):
	return alphanum(x, ".-_")

def hash_text(password):
	if not password or len(password)==0:
		return	""
	h = ""
	for _ in range(1, len(password)):
		h += "#"
	return	h


TEXT_ALLOWED = string.printable
for char in ["'", '"', '\n', '\r', '\x0b', '\x0c']:
	TEXT_ALLOWED = TEXT_ALLOWED.replace(char, '')
def plaintext(x):
	s = str(x)
	return	''.join(ch for ch in s if TEXT_ALLOWED.find(ch)>=0)

def no_newlines(x):
	if x is None:
		return	None
	return	str(x).replace("\n", "").replace("\r", "")

def escape_newlines(x):
	if x is None:
		return	None
	return	str(x).replace("\n", "\\n").replace("\r", "\\r")

def unescape_newlines(x):
	if x is None:
		return	None
	return	str(x).replace("\\n", "\n").replace("\\r", "\r")

def make_app_name(window_name):
	return window_name.replace("(via xpra)", "")

def screensizes_to_strings(size_list):
	sizes = []
	for w,h in size_list:
		sizes.append("%sx%s" % (w,h))
	return	sizes

def parse_screensize(screen_size):
	if not screen_size:
		return None
	depth = -1
	if type(screen_size)==tuple:
		#already parsed / already a tuple
		parts = screen_size
	else:
		parts = screen_size.split("x")
	if len(parts)<2 or len(parts)>3:
		return	None
	if len(parts)==3 and len(parts[2])>0:
		depth = int(parts[2])
	width = int(parts[0])
	height = int(parts[1])
	return	(width, height, depth)

def format_screensize(w,h,depth):
	if depth is not None and depth>0:
		return	"%sx%sx%s" % (w,h,depth)
	else:
		return	"%sx%sx" % (w,h)

def parse_version_string(ver):
	nums = []
	for part in ver.split("."):
		try:
			ti_part = int(part)
		except Exception,e:
			logger.serror("failed to parse part '%s': %s" % (part, e), ver)
			break
		nums.append(ti_part)
	return	nums


def get_as_root_list():
	as_root_list = [False]
	if not WIN32:
		as_root_list.insert(0, True)
	return as_root_list


def xpra_path_workaround():
	if os.name=="posix":
		#debian does some really weird and infuriating things:
		debian_xpra = os.path.join("/usr", "lib", "xpra")
		if is_valid_dir(debian_xpra) and debian_xpra not in sys.path:
			sys.path.insert(0, os.path.join("/usr", "lib"))

def get_xpra_version(xpra_command):
	#can't parse xpra command output as we use a gui wrapper on win32 and osx
	if not WIN32 and not OSX:
		if xpra_command:
			try:
				from winswitch.util.process_util import get_output
				code, out, _ = get_output([xpra_command, "--version"])
				if code==0 and out:
					return	no_newlines(out.replace("xpra v", ""))
			except Exception, e:
				logger.serr(None, e, xpra_command)

	#if all else fails, try direct import
	#and hope the gui wrapper will use the same version...
	try:
		xpra_path_workaround()
		from xpra import __version__
		return __version__
	except Exception, e:
		logger.serr("Failed to load xpra! cannot detect version!", e)
	return None

def is_xpra_version_ok(ver_str, min_version=REQUIRED_XPRA_MIN_VERSION):
	if not ver_str:
		return	False
	actual = parse_version_string(ver_str)
	return actual>=min_version

def check_xpra_version(xpra_command):
	version = get_xpra_version(xpra_command)
	if not do_check_xpra_version_string(version):
		return	None
	return	version

def do_check_xpra_version_string(xpra_version):
	if not xpra_version:
		logger.serror("could not detect xpra version, is it installed?", xpra_version)
		return	False
	if is_xpra_version_ok(xpra_version):
		logger.slog("OK: xpra version matches the minimum required (%s)" % str(REQUIRED_XPRA_MIN_VERSION), xpra_version)
		return	True
	else:
		logger.serror("xpra version is too old! The minimum version required is %s" % str(REQUIRED_XPRA_MIN_VERSION), xpra_version)
		return	False

def check_remote_version(ver):
	if ver>=REQUIRED_WINSWITCH_MIN_VERSION:
		logger.slog("OK: remote application version matches the minimum required (%s)" % str(REQUIRED_WINSWITCH_MIN_VERSION), ver)
		return	True
	else:
		logger.serror("remote application version is too old! The minimum version required is %s" % str(REQUIRED_WINSWITCH_MIN_VERSION), ver)
		return	False


def remove_local_env(env):
	"""
	Ensures that the environment does not contain any variables normally associated
	with the local session.
	"""
	for ignore in ["QTDIR", "QTLIB", "QTINC", "CVS_RSH", "COLORTERM", "LS_COLORS", "QT_IM_MODULE", "OLDPWD", "HISTSIZE", "HISTCONTROL", "SHLVL", "MAIL",
					"GDM_LANG", "GDM_KEYBOARD_LAYOUT", "GDMSESSION", "GNOME_DESKTOP_SESSION_ID", "G_BROKEN_FILENAMES", "GNOME_KEYRING_PID", "DESKTOP_SESSION",
					"IMSETTINGS_INTEGRATE_DESKTOP", "GPG_AGENT_INFO", "IMSETTINGS_MODULE", "XMODIFIERS", "GNOME_KEYRING_CONTROL",
					"SSH_ASKPASS", "SSH_AUTH_SOCK", "WINDOWID", "WINDOWPATH", "AVAHI_COMPAT_NOWARN", "LESSOPEN", "SESSION_MANAGER"]:
		if ignore in env:
			del env[ignore]
	return	env
