# Copyright 2013 - 2020 Patrick Ulbrich <zulu99@gmx.net>
# Copyright 2020 Dan Christensen <jdc@uwo.ca>
# Copyright 2020 Denis Anuschewski
#
# 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.
#

import gi

gi.require_version('Notify', '0.7')
gi.require_version('GLib', '2.0')
gi.require_version('Gtk', '3.0')

import os
import dbus
import threading
from gi.repository import Notify, Gio, Gtk
from Mailnag.common.plugins import Plugin, HookTypes
from Mailnag.common.i18n import _
from Mailnag.common.subproc import start_subprocess
from Mailnag.common.exceptions import InvalidOperationException

NOTIFICATION_MODE_COUNT			= '0'
NOTIFICATION_MODE_SHORT_SUMMARY	= '3'
NOTIFICATION_MODE_SUMMARY		= '1'
NOTIFICATION_MODE_SINGLE		= '2'

plugin_defaults = { 
	'notification_mode' : NOTIFICATION_MODE_SHORT_SUMMARY,
	'max_visible_mails' : '10'
}


class LibNotifyPlugin(Plugin):
	def __init__(self):
		# dict that tracks all notifications that need to be closed
		self._notifications = {}
		self._initialized = False
		self._lock = threading.Lock()
		self._notification_server_wait_event = threading.Event()
		self._notification_server_ready = False
		self._is_gnome = False
		self._mails_added_hook = None
		
	
	def enable(self):
		self._max_mails = int(self.get_config()['max_visible_mails'])
		self._notification_server_wait_event.clear()
		self._notification_server_ready = False
		self._notifications = {}
		
		# initialize Notification
		if not self._initialized:
			Notify.init("Mailnag")
			self._is_gnome = self._is_gnome_environment(('XDG_CURRENT_DESKTOP', 'GDMSESSION'))
			self._initialized = True
		
		def mails_added_hook(new_mails, all_mails):
			self._notify_async(new_mails, all_mails)
		
		self._mails_added_hook = mails_added_hook
		
		def mails_removed_hook(remaining_mails):
			self._notify_async([], remaining_mails)
		
		self._mails_removed_hook = mails_removed_hook
		
		controller = self.get_mailnag_controller()
		hooks = controller.get_hooks()
		
		hooks.register_hook_func(HookTypes.MAILS_ADDED, 
			self._mails_added_hook)
		
		hooks.register_hook_func(HookTypes.MAILS_REMOVED,
			self._mails_removed_hook)
	
	def disable(self):
		controller = self.get_mailnag_controller()
		hooks = controller.get_hooks()
		
		if self._mails_added_hook != None:
			hooks.unregister_hook_func(HookTypes.MAILS_ADDED,
				self._mails_added_hook)
			self._mails_added_hook = None
		
		if self._mails_removed_hook != None:
			hooks.unregister_hook_func(HookTypes.MAILS_REMOVED,
				self._mails_removed_hook)
			self._mails_removed_hook = None
		
		# Abort possible notification server wait
		self._notification_server_wait_event.set()
		# Close all open notifications 
		# (must be called after _notification_server_wait_event.set() 
		# to prevent a possible deadlock)
		self._close_notifications()

	
	def get_manifest(self):
		return (_("LibNotify Notifications"),
				_("Shows a popup when new mails arrive."),
				"2.1",
				"Patrick Ulbrich <zulu99@gmx.net>")


	def get_default_config(self):
		return plugin_defaults
	
	
	def has_config_ui(self):
		return True
	
	
	def get_config_ui(self):
		radio_mapping = [
			(NOTIFICATION_MODE_COUNT,				Gtk.RadioButton(label = _('Count of new mails'))),
			(NOTIFICATION_MODE_SHORT_SUMMARY,		Gtk.RadioButton(label = _('Short summary of new mails'))),
			(NOTIFICATION_MODE_SUMMARY,				Gtk.RadioButton(label = _('Detailed summary of new mails'))),
			(NOTIFICATION_MODE_SINGLE,				Gtk.RadioButton(label = _('One notification per new mail')))
		]

		box = Gtk.Box()
		box.set_spacing(12)
		box.set_orientation(Gtk.Orientation.VERTICAL)
		
		label = Gtk.Label()
		label.set_markup('<b>%s</b>' % _('Notification mode:'))
		label.set_alignment(0.0, 0.0)
		box.pack_start(label, False, False, 0)
		
		inner_box = Gtk.Box()
		inner_box.set_spacing(6)
		inner_box.set_orientation(Gtk.Orientation.VERTICAL)
		
		last_radio = None
		for m, r in radio_mapping:
			if last_radio != None:
				r.join_group(last_radio)
			inner_box.pack_start(r, False, False, 0)
			last_radio = r
		
		alignment = Gtk.Alignment()
		alignment.set_padding(0, 6, 18, 0)
		alignment.add(inner_box)
		box.pack_start(alignment, False, False, 0)
		
		box._radio_mapping = radio_mapping
		
		return box
	
	
	def load_ui_from_config(self, config_ui):
		config = self.get_config()		
		radio = [ r for m, r in config_ui._radio_mapping if m == config['notification_mode'] ][0]
		radio.set_active(True)
	
	
	def save_ui_to_config(self, config_ui):
		config = self.get_config()
		mode = [ m for m, r in config_ui._radio_mapping if r.get_active() ] [0]
		config['notification_mode'] = mode


	def _notify_async(self, new_mails, all_mails):
		def thread():
			with self._lock:
				# The desktop session may have started Mailnag 
				# before the libnotify dbus daemon.
				if not self._notification_server_ready:
					if not self._wait_for_notification_server():
						return
					self._notification_server_ready = True
			
				config = self.get_config()
				if config['notification_mode'] == NOTIFICATION_MODE_SINGLE:
					self._notify_single(new_mails, all_mails)
				else:
					if len(all_mails) == 0:
						if '0' in self._notifications:
							# The user may have closed the notification:
							try_close(self._notifications['0'])
							del self._notifications['0']
					elif len(new_mails) > 0:
						if config['notification_mode'] == NOTIFICATION_MODE_COUNT:
							self._notify_count(len(all_mails))
						elif config['notification_mode'] == NOTIFICATION_MODE_SHORT_SUMMARY:
							self._notify_short_summary(new_mails, all_mails)
						elif config['notification_mode'] == NOTIFICATION_MODE_SUMMARY:
							self._notify_summary(new_mails, all_mails)
					
		t = threading.Thread(target = thread)
		t.start()
	
	
	def _notify_short_summary(self, new_mails, all_mails):
		summary = ""
		body = ""
		lst = []
		mails = self._prepend_new_mails(new_mails, all_mails)
		mail_count = len(mails)
		
		if len(self._notifications) == 0:
			self._notifications['0'] = self._get_notification(" ", None, None) # empty string will emit a gtk warning
		
		i = 0
		n = 0
		while (n < 3) and (i < mail_count):
			s = self._get_sender(mails[i])
			if s not in lst:
				lst.append(s)
				n += 1
			i += 1
		
		if self._is_gnome:
			senders = "<i>%s</i>" % ", ".join(lst)
		else:
			senders = ", ".join(lst)
		
		if mail_count > 1:
			summary = _("{0} new mails").format(str(mail_count))
			if (mail_count - i) > 1:
				body = _("from {0} and others.").format(senders)
			else:
				body = _("from {0}.").format(senders)
		else:
			summary = _("New mail")
			body = _("from {0}.").format(senders)
		
		self._notifications['0'].update(summary, body, "mail-unread")
		self._notifications['0'].show()
		
	
	def _notify_summary(self, new_mails, all_mails):
		summary = ""		
		body = ""
		mails = self._prepend_new_mails(new_mails, all_mails)
		
		if len(self._notifications) == 0:
			self._notifications['0'] = self._get_notification(" ", None, None) # empty string will emit a gtk warning

		ubound = len(mails) if len(mails) <= self._max_mails else self._max_mails

		for i in range(ubound):
			if self._is_gnome:
				body += "%s:\n<i>%s</i>\n\n" % (self._get_sender(mails[i]), mails[i].subject)
			else:
				body += "%s  -  %s\n" % (ellipsize(self._get_sender(mails[i]), 20), ellipsize(mails[i].subject, 20))

		if len(mails) > self._max_mails:
			if self._is_gnome:
				body += "<i>%s</i>" % _("(and {0} more)").format(str(len(mails) - self._max_mails))
			else:
				body += _("(and {0} more)").format(str(len(mails) - self._max_mails))

		if len(mails) > 1: # multiple new emails
			summary = _("{0} new mails").format(str(len(mails)))
		else:
			summary = _("New mail")

		self._notifications['0'].update(summary, body, "mail-unread")
		self._notifications['0'].show()
	
	
	def _notify_single(self, new_mails, all_mails):
		# Remove notifications for messages not in all_mails:
		for k, n in list(self._notifications.items()):
			if hasattr(n, 'mail') and not (n.mail in all_mails):
				# The user may have closed the notification:
				try_close(n)
				del self._notifications[k]

		# In single notification mode new mails are
		# added to the *bottom* of the notification list.
		new_mails.sort(key = lambda m: m.datetime, reverse = False)
		
		for mail in new_mails:
			n = self._get_notification(self._get_sender(mail), mail.subject, "mail-unread")
			# Remember the associated message, so we know when to remove the notification:
			n.mail = mail
			notification_id = str(id(n))
			if self._is_gnome:
				n.add_action("mark-as-read", _("Mark as read"), 
					self._notification_action_handler, (mail, notification_id))			
			n.show()
			self._notifications[notification_id] = n


	def _notify_count(self, count):
		if len(self._notifications) == 0:
			self._notifications['0'] = self._get_notification(" ", None, None) # empty string will emit a gtk warning
		
		if count > 1: # multiple new emails
			summary = _("{0} new mails").format(str(count))
		else:
			summary = _("New mail")
		
		self._notifications['0'].update(summary, None, "mail-unread")
		self._notifications['0'].show()
	
	
	def _close_notifications(self):
		with self._lock:
			for n in self._notifications.values():
				try_close(n)
			self._notifications = {}
	
	
	def _get_notification(self, summary, body, icon):
		n = Notify.Notification.new(summary, body, icon)		
		n.set_category("email")
		n.set_hint_string("desktop-entry", "mailnag")
		
		if self._is_gnome:
			n.add_action("default", "default", self._notification_action_handler, None)

		return n
	
	
	def _wait_for_notification_server(self):
		bus = dbus.SessionBus()
		while not bus.name_has_owner('org.freedesktop.Notifications'):
			self._notification_server_wait_event.wait(5)
			if self._notification_server_wait_event.is_set():
				return False
		return True

	
	def _notification_action_handler(self, n, action, user_data):
		with self._lock:
			if action == "default":
				mailclient = get_default_mail_reader()
				if mailclient != None:
					start_subprocess(mailclient)

				# clicking the notification bubble has closed all notifications
				# so clear the reference array as well. 
				self._notifications = {}
			elif action == "mark-as-read":
				controller = self.get_mailnag_controller()
				try:
					controller.mark_mail_as_read(user_data[0].id)
				except InvalidOperationException:
					pass
				
				# clicking the action has closed the notification
				# so remove its reference.
				del self._notifications[user_data[1]]
	

	def _get_sender(self, mail):
		name, addr = mail.sender
		if len(name) > 0: return name
		else: return addr
	
	
	def _prepend_new_mails(self, new_mails, all_mails):
		# The mail list (all_mails) is sorted by date (mails with most recent 
		# date on top). New mails with no date or older mails that come in 
		# delayed won't be listed on top. So if a mail with no or an older date 
		# arrives, it gives the impression that the top most mail (i.e. the mail 
		# with the most recent date) is re-notified.
		# To fix that, simply put new mails on top explicitly.  
		return new_mails + [m for m in all_mails if m not in new_mails]


	def _is_gnome_environment(self, env_vars):
		for var in env_vars:
			if 'gnome' in os.environ.get(var, '').lower().split(':'):
				return True
		return False


def get_default_mail_reader():
	mail_reader = None
	app_info = Gio.AppInfo.get_default_for_type ("x-scheme-handler/mailto", False)

	if app_info != None:
		executable = Gio.AppInfo.get_executable(app_info)

		if (executable != None) and (len(executable) > 0):
			mail_reader = executable

	return mail_reader


def ellipsize(str, max_len):
	if max_len < 3: max_len = 3
	if len(str) <= max_len:
		return str
	else:
		return str[0:max_len - 3] + '...'


# If the user has closed the notification, an exception is raised.
def try_close(notification):
        try:
                notification.close()
        except:
                pass
