#!/usr/bin/env python
from __future__ import unicode_literals, print_function


import argparse
parser = argparse.ArgumentParser(description='BlueZ/obex helper script')
parser.add_argument('--debug', action='store_true', help='verbose operation mode')
cmds = parser.add_subparsers(title='supported operations')
cmd = cmds.add_parser('list', help='list local and linked bluetooth hardware')
cmd.set_defaults(call='bluez', action='list')
cmd = cmds.add_parser('disconnect', help='disconnect linked hardware')
cmd.set_defaults(call='bluez', action='disconnect')

cmd = cmds.add_parser('input_connect', help='force-connect input devices')
cmd.add_argument('-d', '--device-path',
	help='dbus object path of a device to connect to (see "list")')
cmd.set_defaults(call='bluez', action='input_connect')

cmd = cmds.add_parser('remove', help='remove input device(s)')
cmd.add_argument('path', nargs='+',
	help='dbus object paths of devices to remove (use "list")')
cmd.set_defaults(call='bluez', action='remove')

cmd_get = cmds.add_parser('get', help='fetch fs content from remote device(s) using obex')
cmd_put = cmds.add_parser('put', help='upload content  to remote device(s) using obex')
for cmd in [cmd_get, cmd_put]:
	cmd.add_argument('-B', '--channel', type=int, default=11,
		help='number of OBEX File Transfer (0x1106) channel on device (default: autodetect)')
	cmd.add_argument('-b', '--bt-mac', type=str,
		default='00:1D:6E:07:B1:F7', help='mac of a remote device (default: %(default)s)')

cmd_get.add_argument('path', nargs='*', default='E:/Images/',
	help='remote paths to operate on (%(default)s, if none specified)')
cmd_get.add_argument('-g', '--grab', action='store_true',
	help='fetch all files in a given path(s) (w/o recursing into any subpaths)')
cmd_get.add_argument('-r', '--recursive', action='store_true',
	help='recursively list or fetch everything in a given path(s)')
cmd_get.add_argument('-d', '--dest', default='.',
	help='local path to put fetch stuff into (default: %(default)s)')
cmd_get.set_defaults(call='obex', action='get')

cmd_put.add_argument('path', nargs='+', help='local path(s) to transfer')
cmd_put.add_argument('-d', '--dest', default='E:/Trash/',
	help='remote path to put all the stuff into (default: %(default)s)')
cmd_put.add_argument('-r', '--recursive', action='store_true',
	help='recursively transfer everything in a given path(s)')
cmd_put.set_defaults(call='obex', action='put')

argz = parser.parse_args()


import logging
logging.basicConfig(level=logging.DEBUG if argz.debug else logging.INFO)
log = logging.getLogger()

import itertools as it, operator as op, functools as ft


def obex(argz):
	if argz.recursive: raise NotImplementedError

	from fgc.enc import enc_default
	import obexftp, types, os, sys
	from lxml import etree
	from time import sleep # reconnection fails w/o timeout

	action = getattr(argz, 'action', None)

	from contextlib import contextmanager
	from weakref import ref
	_null = open('/dev/null', 'wb')
	stderr_bak = os.dup(2)
	null_release = ref(_null, ft.partial(os.close, stderr_bak))
	@contextmanager
	def no_stderr():
		os.dup2(_null.fileno(), 2)
		yield
		os.dup2(stderr_bak, 2)

	argz.path = list(path.replace('\\', '/') for path in ( argz.path
		if not isinstance(argz.path, types.StringTypes) else [argz.path] ))

	if action == 'get':

		with no_stderr():
			obex = obexftp.client(obexftp.BLUETOOTH)
			obex.connect(argz.bt_mac, argz.channel)
		flist = list()
		for path in argz.path:
			if not path.endswith('/'):
				log.debug('Treating {} as filename'.format(path))
				flist.append(path)
			else:
				with no_stderr(): xml = obex.list(path.encode(enc_default))
				flist.extend( os.path.join(path, el.attrib['name'])
					for el in etree.fromstring(xml).xpath('/folder-listing/file') )
		with no_stderr(): obex.disconnect()

		for src in flist:
			log.info(src)
			if argz.grab:
				with no_stderr():
					sleep(0.1)
					obex = obexftp.client(obexftp.BLUETOOTH)
					obex.connect(argz.bt_mac, argz.channel)
				dst = os.path.join(argz.dest, os.path.basename(src))
				if os.path.exists(dst):
					log.error('Local path already exists: {}, skipping'.format(dst))
					continue
				log.debug('Downloading...')
				with no_stderr():
					obex.get_file(src.encode(enc_default), dst.encode(enc_default))
					obex.disconnect()

	elif action == 'put':

		for src in argz.path:
			log.info(src)
			with no_stderr():
				sleep(0.1)
				obex = obexftp.client(obexftp.BLUETOOTH)
				obex.connect(argz.bt_mac, argz.channel)
			dst = os.path.join(argz.dest, os.path.basename(src))
			log.debug('Uploading...')
			with no_stderr():
				obex.put_file(src.encode(enc_default), dst.encode(enc_default))
				obex.disconnect()


def bluez(argz):
	action = getattr(argz, 'action', None)
	info_logger = log.debug if action != 'list' else log.info

	import dbus
	bus = dbus.SystemBus()

	manager = dbus.Interface(bus.get_object('org.bluez', '/'), 'org.bluez.Manager')
	# print(dbus.Interface(manager, 'org.freedesktop.DBus.Introspectable').Introspect())

	for iface_id in manager.ListAdapters():
		info_logger('Interface: {}'.format(iface_id))

		iface = dbus.Interface(bus.get_object('org.bluez', iface_id), 'org.bluez.Adapter')

		for dev_id in iface.GetProperties()['Devices']:
			info_logger(' --- Device: {}'.format(dev_id))

			if action == 'remove':
				if dev_id in argz.path:
					info_logger('Removing device')
					iface.RemoveDevice(dev_id)
				continue

			dev = dbus.Interface(bus.get_object('org.bluez', dev_id), 'org.bluez.Device')
			dev_props = dev.GetProperties()
			info_logger('Name: {}, Type: {}'.format(dev_props['Name'], dev_props.get('Icon', 'unknown')))

			if dev_props.get('Connected', False):
				if action == 'disconnect':
					info_logger('Disconnecting device...')
					dev.Disconnect()
				elif action == 'input_connect': info_logger('Already connected')
				else: info_logger('Connected')
			else:
				if action == 'input_connect' and (not argz.device_path or argz.device_path == dev_id):
					info_logger('Connecting to device...')
					try: dbus.Interface(bus.get_object('org.bluez', dev_id), 'org.bluez.Input').Connect()
					except dbus.exceptions.DBusException as err: info_logger('Failure: {}'.format(err))
				else: info_logger('Not connected')


locals()[argz.call](argz)
