#
#  opensearch.py : An OpenSearch module for the deskbar applet.
#
#  Copyright (C) 2008 by Arthur Perton
#
#  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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# 
#  Authors: Arthur Perton <arthurperton at no_spam_me gmail.com>
#  1.0 : Initial release.

import deskbar.interfaces.Module
import deskbar.interfaces.Match
from deskbar.handlers.actions.ShowUrlAction import ShowUrlAction
from deskbar.core.Utils import load_icon, url_show
from gettext import gettext as _
import gtk
import htmllib
import os
import pickle
import re
import urllib
from xml.dom import minidom
import xml.parsers.expat

HANDLERS = ["OpenSearchHandler"]

SETTINGS_FILE = os.path.join(os.path.expanduser("~"), deskbar.USER_DESKBAR_DIR, "opensearch.pkl")
LOG_FILE = os.path.join(os.path.expanduser("~"), deskbar.USER_DESKBAR_DIR, "opensearch.log")
#URL_TEMPLATE = "http://blogsearch.google.com/blogsearch_feeds?q={searchTerms}&ui=blg&output=rss"

#WEBSITE_NAME = 0
#WEBSITE_DESCRIPTION_DOCUMENT_URL = 1
#WEBSITE_COMMENT = 2
#WEBSITE_ENABLED = 3

def log(message):
	pass
	#file = open(LOG_FILE, 'a')
	#file.write(message + '\n')
	#file.close();

class OpenSearchAction(ShowUrlAction):
	def __init__(self, name, url):
		ShowUrlAction.__init__(self, name, url)

	def get_verb(self):
		return _("%(name)s")

class OpenSearchMatch(deskbar.interfaces.Match):
	def __init__(self, name=None, url=None, snippet=None, **args):
		deskbar.interfaces.Match.__init__(self, name=name, category="websearch", **args)
		#self.set_snippet("Test")
		self.add_action(OpenSearchAction(name, url))

	def get_category(self):
		return "websearch"

	def get_verb(self):
		return _("Website: <b>%(name)s</b>")
                
class OpenSearchHandler(deskbar.interfaces.Module):
	INFOS = {
			"icon": deskbar.core.Utils.load_icon("gtk-find"),
			"name": _("OpenSearch"),
			"description": _("Searches any OpenSearch compliant website"),
			"version": "1.0",
			}
	
	def __init__(self):
		#self.load_websites()
		log('-------------------')
		log('initializing module')
		deskbar.interfaces.Module.__init__(self)
			
	def query(self, query):
		self.load_websites()
		for website in self.websites:
			log('WEBSITE: ' + website[0])
			if not website[3]:
				continue
			info = self.get_description_document_info(website[1])
			url_template = info[0]
			image_url = info[1]
			if url_template != None:
				pixbuf = None
				#if image_url != None:
				#	try:
				#		log(image_url)
				#		pixbuf_loader = gtk.gdk.PixbufLoader()
				#		image_file = urllib.urlopen(image_url)
				#		for image_data in image_file:
				#			pixbuf_loader.write(image_data)
				#		pixbuf_loader.close()
				#		pixbuf = pixbuf_loader.get_pixbuf()
				#	except:
				#		pass
				try:	
					log('url_template: ' + url_template);
					url = url_template.replace("{searchTerms}", urllib.quote_plus(query))
					url = re.sub('{[^}]*}', '0', url)
					log('url: ' + url);
					xml_document = minidom.parse(urllib.urlopen(url))
					root_tag = xml_document.documentElement.tagName
					# atom:
					if root_tag == "feed":
						item_nodes = xml_document.getElementsByTagName("entry")
						for item_node in item_nodes:
							title = item_node.getElementsByTagName("title")[0].firstChild.data
							link = item_node.getElementsByTagName("link")[0].getAttribute("href")
							title = xml_unescape(title)
							self._emit_query_ready(query, [OpenSearchMatch(name=_(website[0] + ": Visit '" + title + "'"), url=link, pixbuf=pixbuf)])
					# rss:
					elif root_tag == "rss":
						item_nodes = xml_document.getElementsByTagName("item")
						for item_node in item_nodes:
							title = item_node.getElementsByTagName("title")[0].firstChild.data
							link = item_node.getElementsByTagName("link")[0].firstChild.data
							title = xml_unescape(title)
							self._emit_query_ready(query, [OpenSearchMatch(name=_(website[0] + ": Visit '" + title + "'"), url=link, pixbuf=pixbuf)])
				except:
					self._emit_query_ready(query, [OpenSearchMatch(name=_(website[0] + ": Search for '" + query + "'"), url=url, pixbuf=pixbuf)])

	def has_config(self):
		return True
    
	def show_config(self, parent):
		dialog = OpenSearchSettingsDialog(parent)
		dialog.run()
		dialog.destroy()
		
	def load_websites(self):
		file = open(SETTINGS_FILE, 'rb')
		self.websites = pickle.load(file)
		file.close()
	
	def get_description_document_info(self, url):
		info = [None, None] # 0 = url_template, 1 = icon 
		try:
			xml_document = minidom.parse(urllib.urlopen(url))
			url_elements = xml_document.getElementsByTagName("Url")
			url_element = None
			# try to find the Url for getting rss or atom results:
			for element in url_elements:
				type = element.getAttribute("type").strip()
				if type == 'application/rss+xml' or type == 'application/atom+xml':
					url_element = element
					break
			# if not found, pick the first Url as a default:
			if url_element == None:
				url_element = url_elements[0]
			url_template = url_element.getAttribute("template").strip()
			# cover for a common error in description documents:
			if url_template == '':
				url_template = url_element.firstChild.data.strip()
			if url_template != '':
				info[0] = url_template
					
			image_elements = xml_document.getElementsByTagName("Image")
			if len(image_elements) > 0:
				image_url = image_elements[0].firstChild.data.strip()
				if image_url != '':
					info[1] = image_url
		except:
			pass
		finally:
			return info

class OpenSearchSettingsDialog(gtk.Dialog):
	def __init__(self, parent):
		gtk.Dialog.__init__(
				self, _("OpenSearch Settings"), parent,
				gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_NO_SEPARATOR,
				(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE))
		
		self.set_default_size(480, 400)
		self.connect("destroy", self.on_destroy)
		
		#self.vbox.set_spacing(10)
		
		label_title = gtk.Label()
		label_title.set_markup('<b>OpenSearch Settings</b>')
		label_title.set_alignment(0, 0.5)
		self.vbox.pack_start(label_title, False, True)
		
		label_instruction = gtk.Label("You can manage the list of websites used by the OpenSearch module here.\n")
		label_instruction.set_alignment(0, 0.5)
		label_instruction.set_line_wrap(True)
		self.vbox.pack_start(label_instruction, False, True)
		
		# add a hbox:		
		hbox = gtk.HBox(spacing=12)
		self.vbox.pack_start(hbox)
		
		# create a liststore for the treeview data:
		self.liststore = gtk.ListStore(str, str, str, 'gboolean')
		self.load_settings(self.liststore)
		#self.liststore.append(['Example', 'http://www.example.com/doc.xml', 'Example Website', True])
		#self.liststore.append(['Other Example', 'http://www.otherexample.com/doc.xml', 'Another Example Website', False])


		# construct the treeview:
		self.treeview = gtk.TreeView(self.liststore)
		self.treeview.connect('row_activated', self.on_row_activated)
		self.treeview.get_selection().connect("changed", self.on_treeview_changed)

		#self.treeview.set_border_width(50)

		self.treeview.set_rules_hint(True)
		#hbox.pack_start(self.treeview)

		self.cell0 = gtk.CellRendererToggle()
		self.cell0.set_property("activatable", True)
		self.cell0.connect('toggled', self.on_toggled, self.liststore)

		self.cell1 = gtk.CellRendererText()

		self.treeviewcolumn0 = gtk.TreeViewColumn('Enabled', self.cell0, active=3)
		self.treeviewcolumn1 = gtk.TreeViewColumn('Website', self.cell1)
		self.treeviewcolumn1.set_cell_data_func(self.cell1, self.set_website_text)

		self.treeview.append_column(self.treeviewcolumn0)
		self.treeview.append_column(self.treeviewcolumn1)

		self.treeview.set_tooltip_column(1)

		# add a scrollwindow for the treeview:
		scrollwindow = gtk.ScrolledWindow()
		scrollwindow.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
		scrollwindow.set_shadow_type(gtk.SHADOW_IN)
		scrollwindow.add(self.treeview)
		hbox.pack_start(scrollwindow)

		# add a vbuttonbox containing the buttons on the right:
		vbuttonbox = gtk.VButtonBox()
		vbuttonbox.set_layout(gtk.BUTTONBOX_START)
		hbox.pack_start(vbuttonbox, False, True)
	
		# add the buttons:
		self.add_button = gtk.Button(stock=gtk.STOCK_ADD)
		self.add_button.connect('clicked', self.on_add_button_clicked)
		vbuttonbox.pack_start(self.add_button)

		self.remove_button = gtk.Button(stock=gtk.STOCK_REMOVE)
		self.remove_button.connect('clicked', self.on_remove_button_clicked)
		self.remove_button.set_sensitive(False)
		vbuttonbox.pack_start(self.remove_button)

		self.edit_button = gtk.Button(stock=gtk.STOCK_EDIT)
		self.edit_button.connect('clicked', self.on_edit_button_clicked)
		self.edit_button.set_sensitive(False)
		vbuttonbox.pack_start(self.edit_button)
        
        # show all:
		self.vbox.show_all()

	def set_website_text(self, column, cell, model, iter):
		cell.set_property('markup', _("<b>" + model.get_value(iter, 0) + "</b>\n" + model.get_value(iter, 2)))
		return

	def on_toggled(self, cell, path, model):
		iter = model.get_iter(path)
		model.set_value(iter, 3, not cell.get_active())
		return
		
	def save_settings(self, model):
		settings = []
		for row in model:
			setting = []
			for value in row:
				setting.append(value)
			settings.append(setting)
		file = open(SETTINGS_FILE, 'wb')
		pickle.dump(settings, file)
		file.close()
		
	def load_settings(self, model=None):
		if not os.path.isfile(SETTINGS_FILE):
			return False
		try:
			file = open(SETTINGS_FILE, 'rb')
			settings = pickle.load(file)
		except:
			file.close()
			return False
		file.close()
		if model == None:
			model = gtk.ListStore(str, str, str, 'gboolean')
		for setting in settings:
			model.append(setting)
		return True

	
	def add_website(self, name, url, description):
		self.liststore.append([name, url, description, True])

	def edit_website(self, name, url, description, iter = None):
		if iter == None:
			iter = self.get_selected()
		if iter != None:
			self.liststore.set(iter, 0, name, 1, url, 2, description)

	def remove_website(self, iter):
		self.liststore.remove(iter)	

	def on_add_button_clicked(self, button):
		dialog = OpenSearchAddWebsiteDialog(self)
		dialog.run()
		dialog.destroy()
	
	def on_edit_button_clicked(self, button):
		iter = self.get_selected()
		if iter != None: 
			dialog = OpenSearchEditWebsiteDialog(self)
			dialog.run()
			dialog.destroy()
		
	def on_remove_button_clicked(self, button):
		iter = self.get_selected()
		if iter != None: 
			self.remove_website(iter)
			
	def on_row_activated(self, treeview, path, view_column):
		self.on_edit_button_clicked(None)	
	
	def on_treeview_changed(self, widget):
		sensitive = (self.get_selected() != None)
		self.remove_button.set_sensitive(sensitive)
		self.edit_button.set_sensitive(sensitive)
		return True
	
	def on_destroy(self, widget):
		self.save_settings(self.liststore)
		
	def get_selected(self):
		return self.treeview.get_selection().get_selected()[1]
			
class OpenSearchEditWebsiteDialog(gtk.Dialog):
	def __init__(self, parent):
		gtk.Dialog.__init__(
				self, self.get_title(), parent,
				gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_NO_SEPARATOR,
				(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, 
				 gtk.STOCK_OK, gtk.RESPONSE_OK))
				 
		self.settings_dialog = parent
		self.connect("response", self.on_response)

		self.set_default_size(500, -1)

		label_title = gtk.Label()
		label_title.set_markup('<b>' + self.get_title() + '</b>')
		label_title.set_alignment(0, 0.5)
		self.vbox.pack_start(label_title, False, True)
		
		label_instruction = gtk.Label("Enter the location (URL) of the OpenSearch description document for this website. The name and description can be loaded automatically from this document or you can just enter your own values manually.")
		label_instruction.set_alignment(0, 0.5)
		label_instruction.set_line_wrap(True)
		self.vbox.pack_start(label_instruction, False, True)
		
		hbox = gtk.HBox()
		
		self.vbox.pack_start(child=hbox, padding=10)

		table = gtk.Table(rows=3, columns=3, homogeneous=False)
		table.set_col_spacings(7);
		table.set_row_spacings(7);
		hbox.pack_start(child=table, padding=10)
		
		self.entry_location = gtk.Entry()
		
		label_location = gtk.Label("_Location (URL):")
  		label_location.set_use_underline(True)
  		label_location.set_mnemonic_widget(self.entry_location)
		label_location.set_alignment(0, 0.5)
		
		refresh_button = gtk.Button(stock=gtk.STOCK_REFRESH)
		refresh_button.connect('clicked', self.on_refresh_button_clicked)

		table.attach(label_location, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
		table.attach(self.entry_location, 1, 2, 0, 1, gtk.FILL | gtk.EXPAND, gtk.FILL)
		table.attach(refresh_button, 2, 3, 0, 1, gtk.FILL, gtk.FILL)
		
		self.entry_name = gtk.Entry()
		
		label_name = gtk.Label("_Name:")
  		label_name.set_use_underline(True)
  		label_name.set_mnemonic_widget(self.entry_name)
  		label_name.set_alignment(0, 0.5)

		table.attach(label_name, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
		table.attach(self.entry_name, 1, 3, 1, 2, gtk.FILL | gtk.EXPAND, gtk.FILL)
		
		self.entry_description = gtk.Entry()
		
		label_description = gtk.Label("_Description:")
  		label_description.set_use_underline(True)
  		label_description.set_mnemonic_widget(self.entry_description)
		label_description.set_alignment(0, 0.5)
		
		table.attach(label_description, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
		table.attach(self.entry_description, 1, 3, 2, 3, gtk.FILL | gtk.EXPAND, gtk.FILL)
		
		self.set_entry_texts()
		
		 # show all:
		self.vbox.show_all()
			
	def get_title(self):
		return _("Edit OpenSearch Website")

	def set_entry_texts(self):
		iter = self.settings_dialog.get_selected()
		if iter != None:
			self.entry_name.set_text(self.settings_dialog.liststore.get_value(iter, 0))
			self.entry_location.set_text(self.settings_dialog.liststore.get_value(iter, 1))
			self.entry_description.set_text(self.settings_dialog.liststore.get_value(iter, 2))

	def validate(self):
		if len(self.entry_location.get_text().strip()) < 1:
			show_message(self, "Location missing", "Please enter a valid location (URL) for this website")
			return False
		if len(self.entry_name.get_text().strip()) < 1:
			show_message(self, "Name missing", "Please enter a name for this website")
			return False
		if len(self.entry_description.get_text().strip()) < 1:
			show_message(self, "Description missing", "Please enter a description for this website")
			return False
		return True

	def on_response(self, widget, response_id):
		if response_id == gtk.RESPONSE_OK:
			if not self.validate():
				self.emit_stop_by_name("response")
			else:
				self.settings_dialog.edit_website(self.entry_name.get_text(), self.entry_location.get_text(), self.entry_description.get_text())
	
	def on_refresh_button_clicked(self, button):
		try:
			url = self.entry_location.get_text()
			xml_document = minidom.parse(urllib.urlopen(url))
		except:
			show_message(
					self,
					"Unable to load description document",
					"Make sure the URL points to a valid OpenSearch description document.")
			return
		try:
			short_name = xml_document.getElementsByTagName("ShortName")[0].firstChild.data
			self.entry_name.set_text(short_name)
			description = xml_document.getElementsByTagName("Description")[0].firstChild.data
			self.entry_description.set_text(description)
		except:
			return
			

class OpenSearchAddWebsiteDialog(OpenSearchEditWebsiteDialog):			
	def get_title(self):
		return _("Add OpenSearch Website")
		
	def set_entry_texts(self):
		self.entry_name.set_text("")
		self.entry_location.set_text("http://")
		self.entry_description.set_text("")
		
	def on_response(self, widget, response_id):
		if response_id == gtk.RESPONSE_OK:
			if not self.validate():
				self.emit_stop_by_name("response")
			else:
				self.settings_dialog.add_website(self.entry_name.get_text(), self.entry_location.get_text(), self.entry_description.get_text())

def show_message(parent, message, secondary_text = None):
	message_dialog = gtk.MessageDialog(
			parent, 
			gtk.DIALOG_MODAL |gtk.DIALOG_DESTROY_WITH_PARENT,
			gtk.MESSAGE_ERROR, 
			gtk.BUTTONS_OK,
			message)
	if secondary_text != None:
		message_dialog.format_secondary_text(secondary_text)
	message_dialog.run()
	message_dialog.destroy()
	
def xml_unescape(s):
	want_unicode = False
	if isinstance(s, unicode):
		s = s.encode("utf-8")
		want_unicode = True

	# the rest of this assumes that "s" is UTF-8
	list = []

	# create and initialize a parser object
	p = xml.parsers.expat.ParserCreate("utf-8")
	p.buffer_text = True
	p.returns_unicode = want_unicode
	p.CharacterDataHandler = list.append

	# parse the data wrapped in a dummy element
	# (needed so the "document" is well-formed)
	p.Parse("<e>", 0)
	p.Parse(s, 0)
	p.Parse("</e>", 1)

	# join the extracted strings and return
	es = ""
	if want_unicode:
		es = u""
	return es.join(list)
