# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013  Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program 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 General Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
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 solaar.i18n import _
from logitech_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
_RECEIVER_SEPARATOR = ('~', None, None, None)
#
#
#
def _create_menu(quit_handler):
	menu = Gtk.Menu()
	# per-device menu entries will be generated as-needed
	no_receiver = Gtk.MenuItem.new_with_label(_("No Logitech 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"), quit_handler, stock_id=Gtk.STOCK_QUIT).create_menu_item())
	del about, make
	menu.show_all()
	return menu
_last_scroll = 0
def _scroll(tray_icon, event, direction=None):
	if direction is None:
		direction = event.direction
		now = event.time / 1000.0
	else:
		now = None
	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 = now or _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 peripherals
			if info[0:2] == _picked_device[0:2]:
				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()
try:
	# raise ImportError
	from gi.repository import AppIndicator3
	if _log.isEnabledFor(_DEBUG):
		_log.debug("using AppIndicator3")
	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:
			_ignore, _ignore, 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:
	if _log.isEnabledFor(_DEBUG):
		_log.debug("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('scroll-event', _scroll)
		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:
			_ignore, _ignore, 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: ' % NAME + _("no receiver")
		return
	yield '%s' % NAME
	yield ''
	for _ignore, number, name, status in _devices_info:
		if number is None:  # receiver
			continue
		p = status.to_string()
		if p:  # does it have any properties to print?
			yield '%s' % name
			if status:
				yield '\t%s' % p
			else:
				yield '\t%s (' % p + _("offline") + ')'
		else:
			if status:
				yield '%s (' % name + _("no status") + ')'
			else:
				yield '%s (' % name + _("offline") + ')'
		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)
		# print ("checking %s -> %s", info, level)
		if 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, _ignore, _ignore, _ignore) 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, _ignore, _ignore = _devices_info[index]
		if path == _RECEIVER_SEPARATOR[0]:
			break
		assert path == receiver_path
		assert number != device.number
		if number > device.number:
			break
		index = index + 1
	new_device_info = (receiver_path, device.number, device.name, device.status)
	assert len(new_device_info) == len(_RECEIVER_SEPARATOR)
	_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.number)
	_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[0:2] == removed_device[0:2]:
		# the current pick was unpaired
		_picked_device = None
def _add_receiver(receiver):
	index = len(_devices_info)
	new_receiver_info = (receiver.path, None, receiver.name, None)
	assert len(new_receiver_info) == len(_RECEIVER_SEPARATOR)
	_devices_info.append(new_receiver_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(_RECEIVER_SEPARATOR)
	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, _ignore, _ignore, _ignore = _devices_info[index]
		if path == receiver.path:
			found = True
			_remove_device(index)
		elif found and path == _RECEIVER_SEPARATOR[0]:
			# the separator after this receiver
			_remove_device(index)
			break
		else:
			index += 1
def _update_menu_item(index, device):
	assert device
	assert device.status is not None
	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.online))
	_update_menu_icon(image_widget, icon_name)
#
#
#
# for which device to show the battery info in systray, if more than one
# it's actually an entry in _devices_info
_picked_device = None
# cached list of devices and some of their properties
# contains tuples of (receiver path, device number, name, status)
_devices_info = []
_menu = None
_icon = None
def init(_quit_handler):
	global _menu, _icon
	assert _menu is None
	_menu = _create_menu(_quit_handler)
	assert _icon is None
	_icon = _create(_menu)
def destroy():
	global _icon, _menu, _devices_info
	assert _icon is not None
	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, _ignore, _ignore, _ignore) 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_paired = bool(device)
			receiver_path = device.receiver.path
			index = None
			for idx, (path, number, _ignore, _ignore) in enumerate(_devices_info):
				if path == receiver_path and number == device.number:
					index = idx
			if is_paired:
				if index is None:
					index = _add_device(device)
				_update_menu_item(index, device)
			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 or _last_scroll == 0) and device is not None and device.kind is not None:
		# if it's just a receiver update, it's unlikely the picked device would change
		_picked_device = _pick_device_with_lowest_battery()
	_update_tray_icon()