#
#
#
from __future__ import absolute_import, division, print_function, unicode_literals
from logging import getLogger, DEBUG as _DEBUG
_log = getLogger(__name__)
del getLogger
from time import time as _timestamp
from gi.repository import Gtk, GLib
from gi.repository.Gdk import ScrollDirection
from solaar import NAME
from logitech.unifying_receiver.status import KEYS as _K
from . import icons as _icons
from .window import popup as _window_popup, toggle as _window_toggle
#
# constants
#
_TRAY_ICON_SIZE = 32 #  pixels
_MENU_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
#
#
#
def _create_menu():
	menu = Gtk.Menu()
	# per-device menu entries will be generated as-needed
	no_receiver = Gtk.MenuItem.new_with_label('No receiver found')
	no_receiver.set_sensitive(False)
	menu.append(no_receiver)
	menu.append(Gtk.SeparatorMenuItem.new())
	from .action import about, make
	menu.append(about.create_menu_item())
	menu.append(make('application-exit', 'Quit', Gtk.main_quit).create_menu_item())
	del about, make
	menu.show_all()
	return menu
try:
	# raise ImportError
	from gi.repository import AppIndicator3
	_log.info("using AppIndicator3")
	_last_scroll = 0
	def _scroll(ind, _, direction):
		if direction != ScrollDirection.UP and direction != ScrollDirection.DOWN:
			# ignore all other directions
			return
		if len(_devices_info) < 4:
			# don't bother with scrolling when there's only one receiver
			# with only one device (3 = [receiver, device, separator])
			return
		# scroll events come way too fast (at least 5-6 at once)
		# so take a little break between them
		global _last_scroll
		now = _timestamp()
		if now - _last_scroll < 0.33:  # seconds
			return
		_last_scroll = now
		# if _log.isEnabledFor(_DEBUG):
		# 	_log.debug("scroll direction %s", direction)
		global _picked_device
		candidate = None
		if _picked_device is None:
			for info in _devices_info:
				# pick first peripheral found
				if info[1] is not None:
					candidate = info
					break
		else:
			found = False
			for info in _devices_info:
				if not info[1]:
					# only conside peripherals
					continue
				# compare peripheral serials
				if info[1] == _picked_device[1]:
					if direction == ScrollDirection.UP and candidate:
						# select previous device
						break
					found = True
				else:
					if found:
						candidate = info
						if direction == ScrollDirection.DOWN:
							break
						# if direction is up, but no candidate found before _picked,
						# let it run through all candidates, will get stuck with the last one
					else:
						if direction == ScrollDirection.DOWN:
							# only use the first one, in case no candidates are after _picked
							if candidate is None:
								candidate = info
						else:
							candidate = info
			# if the last _picked_device is gone, clear it
			# the candidate will be either the first or last one remaining,
			# depending on the scroll direction
			if not found:
				_picked_device = None
		_picked_device = candidate or _picked_device
		if _log.isEnabledFor(_DEBUG):
			_log.debug("scroll: picked %s", _picked_device)
		_update_tray_icon()
	def _create(menu):
		theme_paths = Gtk.IconTheme.get_default().get_search_path()
		ind = AppIndicator3.Indicator.new_with_path(
						'indicator-solaar',
						_icons.TRAY_INIT,
						AppIndicator3.IndicatorCategory.HARDWARE,
						':'.join(theme_paths))
		ind.set_title(NAME)
		ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
		ind.set_attention_icon_full(_icons.TRAY_ATTENTION, '')
		# ind.set_label(NAME, NAME)
		ind.set_menu(menu)
		ind.connect('scroll-event', _scroll)
		return ind
	def _destroy(indicator):
		indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE)
	def _update_tray_icon():
		if _picked_device:
			_, _, name, _, device_status = _picked_device
			battery_level = device_status.get(_K.BATTERY_LEVEL)
			battery_charging = device_status.get(_K.BATTERY_CHARGING)
			tray_icon_name = _icons.battery(battery_level, battery_charging)
			description =  '%s: %s' % (name, device_status)
		else:
			# there may be a receiver, but no peripherals
			tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_INIT
			tooltip_lines = _generate_tooltip_lines()
			description = '\n'.join(tooltip_lines).rstrip('\n')
		# icon_file = _icons.icon_file(icon_name, _TRAY_ICON_SIZE)
		_icon.set_icon_full(tray_icon_name, description)
	def _update_menu_icon(image_widget, icon_name):
		image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE)
		# icon_file = _icons.icon_file(icon_name, _MENU_ICON_SIZE)
		# image_widget.set_from_file(icon_file)
		# image_widget.set_pixel_size(_TRAY_ICON_SIZE)
	def attention(reason=None):
		if _icon.get_status != AppIndicator3.IndicatorStatus.ATTENTION:
			_icon.set_attention_icon_full(_icons.TRAY_ATTENTION, reason or '')
			_icon.set_status(AppIndicator3.IndicatorStatus.ATTENTION)
			GLib.timeout_add(10 * 1000, _icon.set_status, AppIndicator3.IndicatorStatus.ACTIVE)
except ImportError:
	_log.info("using StatusIcon")
	def _create(menu):
		icon = Gtk.StatusIcon.new_from_icon_name(_icons.TRAY_INIT)
		icon.set_name(NAME)
		icon.set_title(NAME)
		icon.set_tooltip_text(NAME)
		icon.connect('activate', _window_toggle)
		icon.connect('popup_menu',
						lambda icon, button, time:
							menu.popup(None, None, icon.position_menu, icon, button, time))
		return icon
	def _destroy(icon):
		icon.set_visible(False)
	def _update_tray_icon():
		tooltip_lines = _generate_tooltip_lines()
		tooltip = '\n'.join(tooltip_lines).rstrip('\n')
		_icon.set_tooltip_markup(tooltip)
		if _picked_device:
			_, _, name, _, device_status = _picked_device
			battery_level = device_status.get(_K.BATTERY_LEVEL)
			battery_charging = device_status.get(_K.BATTERY_CHARGING)
			tray_icon_name = _icons.battery(battery_level, battery_charging)
		else:
			# there may be a receiver, but no peripherals
			tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_ATTENTION
		_icon.set_from_icon_name(tray_icon_name)
	def _update_menu_icon(image_widget, icon_name):
		image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE)
	_icon_before_attention = None
	def _blink(count):
		global _icon_before_attention
		if count % 2:
			_icon.set_from_icon_name(_icons.TRAY_ATTENTION)
		else:
			_icon.set_from_icon_name(_icon_before_attention)
		if count > 0:
			GLib.timeout_add(1000, _blink, count - 1)
	def attention(reason=None):
		global _icon_before_attention
		if _icon_before_attention is None:
			_icon_before_attention = _icon.get_icon_name()
			GLib.idle_add(_blink, 9)
#
#
#
def _generate_tooltip_lines():
	if not _devices_info:
		yield '%s: no receivers' % NAME
		return
	yield '%s' % NAME
	yield ''
	for _, serial, name, _, status in _devices_info:
		if serial is None:  # receiver
			continue
		p = str(status)
		if p:  # does it have any properties to print?
			yield '%s' % name
			if status:
				yield '\t%s' % p
			else:
				yield '\t%s (offline)' % p
		else:
			if status:
				yield '%s no status' % name
			else:
				yield '%s (offline)' % name
		yield ''
def _pick_device_with_lowest_battery():
	if not _devices_info:
		return None
	picked = None
	picked_level = 1000
	for info in _devices_info:
		if info[1] is None: # is receiver/separator
			continue
		level = info[-1].get(_K.BATTERY_LEVEL)
		if not picked or (level is not None and picked_level > level):
			picked = info
			picked_level = level or 0
	if _log.isEnabledFor(_DEBUG):
		_log.debug("picked device with lowest battery: %s", picked)
	return picked
#
#
#
def _add_device(device):
	assert device
	assert device.receiver
	receiver_path = device.receiver.path
	assert receiver_path
	index = None
	for idx, (path, _, _, _, _) in enumerate(_devices_info):
		if path == receiver_path:
			# the first entry matching the receiver serial should be for the receiver itself
			index = idx + 1
			break
	assert index is not None
	# proper ordering (according to device.number) for a receiver's devices
	while True:
		path, _, _, number, _ = _devices_info[index]
		if path == '-':
			break
		assert path == receiver_path
		assert number != device.number
		if number > device.number:
			break
		index = index + 1
	new_device_info = (receiver_path, device.serial, device.name, device.number, device.status)
	_devices_info.insert(index, new_device_info)
	# label_prefix = b'\xE2\x94\x84 '.decode('utf-8')
	label_prefix = '   '
	new_menu_item = Gtk.ImageMenuItem.new_with_label(label_prefix + device.name)
	new_menu_item.set_image(Gtk.Image())
	new_menu_item.show_all()
	new_menu_item.connect('activate', _window_popup, receiver_path, device.serial)
	_menu.insert(new_menu_item, index)
	return index
def _remove_device(index):
	assert index is not None
	menu_items = _menu.get_children()
	_menu.remove(menu_items[index])
	removed_device = _devices_info.pop(index)
	global _picked_device
	if _picked_device and _picked_device[1] == removed_device[1]:
		# the current pick was unpaired
		_picked_device = None
def _add_receiver(receiver):
	index = len(_devices_info)
	device_info = (receiver.path, None, receiver.name, None, None)
	_devices_info.append(device_info)
	new_menu_item = Gtk.ImageMenuItem.new_with_label(receiver.name)
	_menu.insert(new_menu_item, index)
	icon_set = _icons.device_icon_set(receiver.name)
	new_menu_item.set_image(Gtk.Image().new_from_icon_set(icon_set, _MENU_ICON_SIZE))
	new_menu_item.show_all()
	new_menu_item.connect('activate', _window_popup, receiver.path)
	_devices_info.append(('-', None, None, None, None))
	separator = Gtk.SeparatorMenuItem.new()
	separator.set_visible(True)
	_menu.insert(separator, index + 1)
	return 0
def _remove_receiver(receiver):
	index = 0
	found = False
	# remove all entries in devices_info that match this receiver
	while index < len(_devices_info):
		path, _, _, _, _ = _devices_info[index]
		if path == receiver.path:
			found = True
			_remove_device(index)
		elif found and path == '-':
			# the separator after this receiver
			_remove_device(index)
			break
		else:
			index += 1
def _update_menu_item(index, device_status):
	menu_items = _menu.get_children()
	menu_item = menu_items[index]
	level = device_status.get(_K.BATTERY_LEVEL)
	charging = device_status.get(_K.BATTERY_CHARGING)
	icon_name = _icons.battery(level, charging)
	image_widget = menu_item.get_image()
	image_widget.set_sensitive(bool(device_status))
	_update_menu_icon(image_widget, icon_name)
#
#
#
# for which device to show the battery info in systray, if more than one
_picked_device = None
# cached list of devices and some of their properties
_devices_info = []
_menu = None
_icon = None
def init():
	global _menu, _icon
	_menu = _create_menu()
	_icon = _create(_menu)
def destroy():
	global _icon, _menu, _devices_info
	i, _icon = _icon, None
	_destroy(i)
	i = None
	_icon = None
	_menu = None
	_devices_info = None
def update(device=None):
	if _icon is None:
		return
	if device is not None:
		if device.kind is None:
			# receiver
			is_alive = bool(device)
			receiver_path = device.path
			if is_alive:
				index = None
				for idx, (path, _, _, _, _) in enumerate(_devices_info):
					if path == receiver_path:
						index = idx
						break
				if index is None:
					_add_receiver(device)
			else:
				_remove_receiver(device)
		else:
			# peripheral
			is_alive = device.status is not None
			receiver_path = device.receiver.path
			index = None
			for idx, (path, serial, name, _, _) in enumerate(_devices_info):
				if path == receiver_path and serial == device.serial:
					index = idx
			if is_alive:
				if index is None:
					index = _add_device(device)
				_update_menu_item(index, device.status)
			else:
				# was just unpaired
				if index:
					_remove_device(index)
		menu_items = _menu.get_children()
		no_receivers_index = len(_devices_info)
		menu_items[no_receivers_index].set_visible(not _devices_info)
		menu_items[no_receivers_index + 1].set_visible(not _devices_info)
	global _picked_device
	if not _picked_device:
		_picked_device = _pick_device_with_lowest_battery()
	_update_tray_icon()