#! /usr/bin/env python
#
# pysdbus
#
# Copyright (C) 2019 Mario Kicherer (dev@kicherer.org)
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
# 
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
# 
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
#

from __future__ import print_function
import sys
import pysdbus

def show_iface(obj, iface, signatures=None):
	if signatures is None:
		signatures = obj.getSignatures()
	
	if iface in signatures:
		for item in signatures[iface]:
			entry = signatures[iface][item]
			if entry["type"] in ["method", "signal"]:
				out_sign = entry.get("out", "")
				in_sign = entry.get("in", "")
				
				name = item
				if entry["type"] == "method":
					name += "("+in_sign+")"
				
				print("\t", name, end="")
				if entry["type"] == "signal":
					print("*", end="")
				if out_sign:
					print(" -> "+out_sign+"", end="")
				print()
			else:
				print("\t", item+" \""+entry["signature"]+"\" "+entry["access"])
	else:
		print("error, no such interface \"%s\"" % iface, file=sys.stderr)

def is_signal(obj, iface, item):
	signatures = obj.getSignatures()
	
	if iface in signatures and item in signatures[iface]:
		entry = signatures[iface][item]
		
		return entry["type"] == "signal"
	else:
		return False

def print_value(value):
	if args.json:
		import json
		
		def replace_item(item):
			if (
				isinstance(item, pysdbus.UInt16)
				or isinstance(item, pysdbus.UInt32)
				or isinstance(item, pysdbus.Byte)
				):
				return int(item)
			elif isinstance(item, dict):
				return replace_dict_values(item)
			elif isinstance(item, list):
				for idx in range(len(item)):
					item[idx] = replace_item(item[idx])
			else:
				return item
		
		def replace_dict_values(dic):
			from ctypes import c_uint32
			for key in list(dic.keys()).copy():
				if (
					isinstance(key, pysdbus.UInt16)
					or isinstance(key, pysdbus.UInt32)
					or isinstance(key, pysdbus.Byte)
					):
					dic[str(key)] = dic[key]
					del dic[key]
					key = str(key)
				
				if isinstance(dic[key], dict):
					replace_dict_values(dic[key])
				else:
					dic[key] = replace_item(dic[key])
					#print(type(key), type(dic[key]), key, dic[key])
		
		replace_item(value)
		print(json.dumps(value))
	else:
		import pprint
		pprint.pprint(value)

if __name__ == '__main__':
	from argparse import ArgumentParser, REMAINDER
	
	ap = ArgumentParser()
	ap.add_argument("--remote")
	ap.add_argument("--system", action="store_true")
	ap.add_argument("--monitor", action="store_true")
	ap.add_argument("--json", action="store_true")
	ap.add_argument("-d", "--debug", action="store_true")
	
	ap.add_argument('argv', nargs=REMAINDER, help="[service] [path] [interface] [item] [values]")
	args = ap.parse_args()
	
	if args.debug:
		pysdbus.llapi.debug = True
	
	if args.remote:
		bus = pysdbus.RemoteSystemBus(args.remote)
	elif args.system:
		bus = pysdbus.SystemBus()
	else:
		try:
			bus = pysdbus.UserBus()
		except Exception as e:
			if e.errno == -2:
				bus = pysdbus.SystemBus()
			else:
				raise
	
	argv = [sys.argv[0]] + args.argv
	
	if len(argv) == 1:
		#
		# show a list of available services
		#
		
		obj = bus.getObject("org.freedesktop.DBus", "/org/freedesktop/DBus")
		
		iface = obj.getInterface("org.freedesktop.DBus")
		
		reply = iface.ListNames()
		
		dbus_obj = bus.getObject("org.freedesktop.DBus", "/org/freedesktop/DBus")
		dbus_iface = dbus_obj.getInterface("org.freedesktop.DBus")
		
		dic = {}
		for name in reply:
			pid = int(dbus_iface.GetConnectionUnixProcessID(name))
			if pid not in dic:
				dic[pid] = []
			dic[pid].append(name)
		
		print("ProcessID | Services")
		for pid in sorted(dic.keys()):
			services = sorted(dic[pid])
			print("%5d |" % pid, " ".join(services))
		
		sys.exit(0)
	
	if len(argv) >= 2:
		#
		# the following code works with a service whose name we get here
		#
		
		service = argv[1]
	
	if len(argv) >= 3:
		#
		# the following code works with a object whose path and proxy object we get here
		#
		
		path = argv[2]
		
		obj = bus.getObject(service, path)
	else:
		#
		# show all objects of the service
		#
		
		def print_obj_tree(obj_path):
			obj = bus.getObject(service, obj_path)
			
			for node in obj.introspection_proxy.getXMLRoot().findall("node"):
				name = obj_path.rstrip("/")+"/"+node.attrib["name"]
				
				print(name)
				
				print_obj_tree(name)
		
		print_obj_tree("/")
		
		sys.exit(0)
	
	if len(argv) >= 4:
		#
		# the following code works with the requested interface of the object
		#
		
		iface = argv[3]
		
		obj_iface = obj.getInterface(iface)
	else:
		#
		# show the interfaces of the object and the contents of the interface
		#
		
		if not args.monitor:
			signatures = obj.getSignatures()
			
			for iface in sorted(signatures.keys()):
				print(iface)
				
				show_iface(obj, iface, signatures)
			
			sys.exit(0)
		else:
			def on_prop_changed_cb(mp):
				mp.dump()
			
			obj.addPropertiesChangedCallback(on_prop_changed_cb, msg_proxy_callback=True)
			
			eloop = pysdbus.EventLoop(bus)
			try:
				eloop.loop()
			except KeyboardInterrupt:
				eloop.stop()
	
	if len(argv) >= 5:
		#
		# the following code works with a property, method or signal
		#
		
		item = argv[4]
	else:
		#
		# show only this interface
		#
		
		show_iface(obj, iface)
		sys.exit(0)
	
	if len(argv) >= 6:
		if obj_iface.is_property(item):
			#
			# set a property to the given value
			#
			
			obj.setProperty(iface, item, "s", argv[5], signature="ssv")
		
		# TODO if item is a method, parse the function parameters
	else:
		if obj_iface.is_signal(item):
			#
			# subscribe to a signal
			#
			
			def signal_cb(mp):
				mp.dump()
			
			obj_iface.add_match(item, signal_cb, msg_proxy_callback=True)
			
			eloop = pysdbus.EventLoop(bus)
			try:
				eloop.loop()
			except KeyboardInterrupt:
				eloop.stop()
		elif obj_iface.is_property(item):
			#
			# First, check if the given name is a property. If yes, show the
			# property value. If not, assume this is a method and call it.
			#
			
			if not args.monitor:
				try:
					value = obj.getProperty(iface, item)
					found = True
				except pysdbus.MethodNotFound:
					found = False
				except Exception as e:
					raise
				
				if found:
					print("%s %s" % (iface, item), end=" ")
					print_value(value)
					sys.exit(0)
			else:
				def on_prop_changed_cb(new_value):
					if new_value is None:
						print(f"{item} is gone")
					else:
						print(f"new value for {item}: ", end="")
						print_value(new_value)
				
				obj_iface.addPropertyChangedCallback(item, on_prop_changed_cb)
				
				eloop = pysdbus.EventLoop(bus)
				try:
					eloop.loop()
				except KeyboardInterrupt:
					eloop.stop()
		elif obj_iface.is_method(item):
			mp = pysdbus.MethodProxy(obj_iface, item)
			
			try:
				result = mp.call()
			except pysdbus.MethodNotFound:
				print("error, no such method found", file=sys.stderr)
				sys.exit(1)
			
			print_value(result)
		else:
			print("error, unexpected element:", item, file=sys.stderr)
			sys.exit(1)
