#!/usr/bin/env python3
#
#  ftlaunch
#  Copyright (C) 2022 FabulaTech
#

import signal
import os
import subprocess
import threading
import shlex
import re
import configparser
import json
import gi
import sys

gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk, GdkPixbuf

FT_TITLE = "FabulaTech Remote Desktop Client"

FT_RESOURCE_PATH = [os.path.dirname(os.path.abspath(__file__)),
		"/opt/ftplugins/share/ftlaunch"]
FT_GUI_FILE = "ftlaunch.glade"
FT_ICON_FILE = "ftlaunch-gui-icon.svg"
FT_LOGO_FILE = "ftlaunch-gui-logo.png"

FT_HISTORY_FILE = os.path.join(os.path.expanduser("~"), ".ftlaunch_history")
FT_HISTORY_MAX_ITEMS = 10

FT_RDPBR_NAME = "ftrdpbr"

class SessionThread:
	def __init__(self):
		self.proc = None
		self.cb = None
	
	def __get_rdp_version(self):
		version = 0.0
		try:
			prefix = "This is FreeRDP version "
			proc = subprocess.Popen(shlex.split("xfreerdp --version"),
					stdout=subprocess.PIPE,
					stderr=subprocess.STDOUT,
					universal_newlines=True)
			stdoutdata, stderrdata = proc.communicate()
			if re.match(prefix + "[\d]+\.[\d]+\.[\d]+", stdoutdata):
				stdoutdata = stdoutdata[len(prefix):]
				version = float(re.match("[\d]+\.[\d]+", stdoutdata).group())
		except:
			pass
		finally:
			return version

	def __session(self):
		log_str = ""
		err_str = ""
		rdp_version = self.__get_rdp_version()
		#sys.stdout.write(str(rdp_version) + "\n")
		try:
			# Make process args
			cmd_line  = "xfreerdp"
			if rdp_version > 1.0 or rdp_version == 0.0:
				cmd_line += " /u:" + self.user
				cmd_line += " /p:" + self.pwd
				cmd_line += " /dvc:" + FT_RDPBR_NAME
				# XXX - temporary solution. We should ask users whether to continue or not
				proc = subprocess.Popen(shlex.split("xfreerdp --help"),
						stdout=subprocess.PIPE,
						stderr=subprocess.STDOUT,
						universal_newlines=True)
				stdoutdata, stderrdata = proc.communicate(None)
				if stdoutdata.find("cert-ignore") != -1:
					cmd_line += " /cert-ignore"
			else:
				cmd_line += " -u " + self.user
				cmd_line += " -p " + self.pwd
				cmd_line += " --plugin " + FT_RDPBR_NAME
				# XXX - temporary solution. We should ask users whether to continue or not
				proc = subprocess.Popen(shlex.split("xfreerdp --help"),
						stdout=subprocess.PIPE,
						stderr=subprocess.STDOUT,
						universal_newlines=True)
				stdoutdata, stderrdata = proc.communicate(None)
				if stdoutdata.find("ignore-certificate") != -1:
					cmd_line += " --ignore-certificate"

			if len(self.params) != 0:
				cmd_line += " " + self.params
				
			if rdp_version > 1.0 or rdp_version == 0.0:
				cmd_line += " /v:" + self.srv
			else:
				cmd_line += " " + self.srv

			# Run subprocess
			self.proc = subprocess.Popen(shlex.split(cmd_line),
					stdout=subprocess.PIPE,
					stderr=subprocess.STDOUT,
					universal_newlines=True)

			# Check output
			stdoutdata, stderrdata = self.proc.communicate(None)
			if stdoutdata and len(stdoutdata):
				log_str = stdoutdata
		except (ValueError, OSError) as err:
			err_str = str(cmd_line) + "\n" + str(err)
		finally:
			GLib.idle_add(self.cb, log_str, err_str)

	def start(self, srv, user, pwd, params, cb):
		self.srv = srv
		self.user = user
		self.pwd = pwd
		self.params = params
		self.cb = cb
		t = threading.Thread(target = self.__session)
		t.daemon = True
		t.start()
	
	def stop(self): 
		if self.proc:
			self.proc.terminate()

class Handler:
	def __init__(self, builder, res_path):
		self.session = None
		self.logo_height = 0
		self.logo_width = 0
		self.res_path = res_path

		# GUI main window
		self.gui_window = builder.get_object("wndApp")
		self.gui_logo = builder.get_object("imgLogo")
		self.gui_server_entry = builder.get_object("entryServer")
		self.gui_login_entry = builder.get_object("entryLogin")
		self.gui_passwd_entry = builder.get_object("entryPassword")
		self.gui_parameters_entry = builder.get_object("entryParameters")
		self.gui_server_lst = builder.get_object("liststoreServer")
		self.gui_login_lst = builder.get_object("liststoreLogin")
		self.gui_parameters_lst = builder.get_object("liststoreParameters")
		self.gui_window.set_title(FT_TITLE)
		self.gui_window.set_icon_from_file(os.path.join(res_path, FT_ICON_FILE))
		self.gui_logo.set_from_file(os.path.join(res_path, FT_LOGO_FILE))
		self.__restore_history()
		self.gui_window.show_all()

		# GUI dialogs
		self.gui_dlg_err = builder.get_object("dialogError")
		self.gui_dlg_info = builder.get_object("dialogInfo")
		self.gui_dlg_log = builder.get_object("dialogLog")
		self.gui_txt_log = builder.get_object("textviewLog")

		# GUI tray
		self.gui_tray_menu = builder.get_object("menuTray")
		self.gui_tray_item = builder.get_object("menuitemServer")
		self.ind3 = None
		try:
			gi.require_version("AppIndicator3", "0.1")
			from gi.repository import AppIndicator3
			self.ind3 = AppIndicator3
		except:
			pass
		if  self.ind3:
			self.gui_ind = AppIndicator3.Indicator.new(FT_TITLE, 
					os.path.join(res_path, FT_ICON_FILE),
					AppIndicator3.IndicatorCategory.APPLICATION_STATUS)
			self.gui_ind.set_status(AppIndicator3.IndicatorStatus.PASSIVE)
			self.gui_ind.set_menu(self.gui_tray_menu)
		else:
			self.gui_tray = builder.get_object("statusTray")
			self.gui_tray.set_property("visible", False)
			self.gui_tray.set_property("title", FT_TITLE)
			self.gui_tray.set_property("tooltip-text", FT_TITLE)
			self.gui_tray.set_from_file(os.path.join(res_path, FT_ICON_FILE))
			self.gui_tray.connect("popup-menu", self.onTrayClick)

	def __show_tray(self):
		self.gui_tray_item.set_label("Disconnect from " + 
				self.gui_server_entry.get_text())
		if self.ind3:
			self.gui_ind.set_status(self.ind3.IndicatorStatus.ACTIVE)
		else:
			self.gui_tray.set_property("visible", True)
	
	def __hide_tray(self):
		if self.ind3:
			self.gui_ind.set_status(self.ind3.IndicatorStatus.PASSIVE)
		else:
			self.gui_tray.set_property("visible", False)
	
	def onTrayClick(self, icon, button, time):
		self.gui_tray_menu.popup(None, None, 
				Gtk.StatusIcon.position_menu,
				icon, button, time)

	def __show_error(self, msg):
		self.gui_dlg_err.format_secondary_text(msg)
		self.gui_dlg_err.run()
		self.gui_dlg_err.hide()

	def __locate_freerdp(self):
		for path in os.environ["PATH"].split(":"):
			if os.path.exists(os.path.join(path, "xfreerdp")):
				return os.path.join(path, "xfreerdp")
		return None

	def __session_cb(self, log, err):
		self.__hide_tray()
		self.gui_window.show()
		if len(err):
			self.__show_error(err)
		elif len(log):
			self.gui_dlg_info.format_secondary_text("Remote desktop session is finished\nShow log?")
			resp = self.gui_dlg_info.run()
			self.gui_dlg_info.hide()
			if resp == Gtk.ResponseType.YES:
				self.gui_txt_log.get_buffer().set_text(log)
				self.gui_dlg_log.run()
				self.gui_dlg_log.hide()

	def on_btnConnect_clicked(self, *args):
		srv = self.gui_server_entry.get_text()
		user = self.gui_login_entry.get_text()
		pwd = self.gui_passwd_entry.get_text()
		params = self.gui_parameters_entry.get_text()
		# Check if xfreerdp is installed
		if not self.__locate_freerdp():
			self.__show_error("Unable to find 'xfreerdp'")
			return
		# Check for entries filling
		if len(srv) == 0:
			self.__show_error( "You must enter server address")
			return
		elif len(user) == 0:
			self.__show_error("You must enter login")
			return
		elif len(pwd) == 0:
			self.__show_error("You must enter password")
			return
		# Save text entries
		self.__list_add_head(self.gui_server_lst, srv)
		self.__list_add_head(self.gui_login_lst, user)
		self.__list_add_head(self.gui_parameters_lst, params)
		# Start remote session
		self.__save_history()
		self.session = SessionThread()
		self.session.start(srv, user, pwd, params, self.__session_cb)
		self.gui_window.hide()
		self.__show_tray()

	def on_menuitemServer_activate(self, *args):
		if self.session:
			self.session.stop()

	def __list_add_head(self, lst, value):
		if not value or FT_HISTORY_MAX_ITEMS <= 0:
			return
		for row in lst:
			if row[0] == value:
				lst.move_after(row.iter, None)
				return
		while len(lst) >= FT_HISTORY_MAX_ITEMS:
			del lst[FT_HISTORY_MAX_ITEMS - 1]
		lst.set(lst.prepend(), 0, value)

	def __list_add_tail(self, lst, value):
		if not value or value in [ row[0] for row in lst ]:
			return
		if len(lst) < FT_HISTORY_MAX_ITEMS:
			lst.set(lst.append(), 0, value)

	def __save_history(self):
		try:
			history = configparser.RawConfigParser()
			history.add_section("Entries")
			value = [ row[0] for row in self.gui_server_lst ]
			history.set("Entries", "address", json.dumps(value))
			value = [ row[0] for row in self.gui_login_lst ]
			history.set("Entries", "login", json.dumps(value))
			value = [ row[0] for row in self.gui_parameters_lst ]
			history.set("Entries", "parameters", json.dumps(value))
			with open(FT_HISTORY_FILE, "w") as historyfile:
				history.write(historyfile)
		except:
			pass

	def __restore_history(self):
		try:
			history = configparser.RawConfigParser()
			history.read(FT_HISTORY_FILE)
			for value in json.loads(history.get("Entries", "address")):
				self.__list_add_tail(self.gui_server_lst, value)
			for value in json.loads(history.get("Entries", "login")):
				self.__list_add_tail(self.gui_login_lst, value)
			for value in json.loads(history.get("Entries", "parameters")):
				self.__list_add_tail(self.gui_parameters_lst, value)
		except:
			pass

	def on_imgLogo_draw(self, widget, event):
		allocation = widget.get_allocation()
		if (self.logo_height != allocation.height 
				or self.logo_width != allocation.width):
			self.logo_height = allocation.height
			self.logo_width = allocation.width
			dest_pixbuf = GdkPixbuf.Pixbuf.new(
					colorspace = GdkPixbuf.Colorspace.RGB,
					has_alpha = False,
					bits_per_sample = 8,
					width = allocation.width,
					height = allocation.height)
			dest_pixbuf.fill(0xffffffff)
			src_pixbuf = GdkPixbuf.Pixbuf.new_from_file(
					os.path.join(self.res_path, FT_LOGO_FILE))
			src_pixbuf.composite(dest_pixbuf,
					0, 0,
					dest_pixbuf.get_width(),
					dest_pixbuf.get_height(),
					0, 0,
					1.0, 1.0,
					2,
					255)
			widget.set_from_pixbuf(dest_pixbuf)

	def __quit(self):
		Gtk.main_quit()

	def on_btnQuit_clicked(self, *args):
		self.__quit()

	def on_app_destroy(self, *args):
		self.__quit()

def locate_path(paths, filename):
	for path in paths:
		if os.path.isfile(os.path.join(path, filename)):
			return path
	return None

def app():
	path = locate_path(FT_RESOURCE_PATH, FT_GUI_FILE)
	if not path:
		sys.stderr.write("Unable to find '" + FT_GUI_FILE + "'\n")
		sys.exit()
	builder = Gtk.Builder()
	builder.add_from_file(os.path.join(path, FT_GUI_FILE))
	builder.connect_signals(Handler(builder, path))
	builder.get_object("wndApp").show_all()

signal.signal(signal.SIGINT, signal.SIG_DFL)
app()
Gtk.main()

