import os, shutil
import gtk, gobject

import help_box
from dialog import Dialog, alert
from zeroinstall.injector.iface_cache import iface_cache
from zeroinstall.injector import basedir, namespaces, model
from zeroinstall.zerostore import BadDigest, manifest
from zeroinstall import support
from treetips import TreeTips

ROX_IFACE = 'http://rox.sourceforge.net/2005/interfaces/ROX-Filer'

# Model columns
ITEM = 0
SELF_SIZE = 1
PRETTY_SIZE = 2
TOOLTIP = 3
ITEM_OBJECT = 4

def popup_menu(bev, obj):
	menu = gtk.Menu()
	for i in obj.menu_items:
		if i is None:
			item = gtk.SeparatorMenuItem()
		else:
			name, cb = i
			item = gtk.MenuItem(name)
			item.connect('activate', lambda item, cb=cb: cb(obj))
		item.show()
		menu.append(item)
	menu.popup(None, None, None, bev.button, bev.time)

def size_if_exists(path):
	"Get the size for a file, or 0 if it doesn't exist."
	if path and os.path.isfile(path):
		return os.path.getsize(path)
	return 0

def get_size(path):
	"Get the size for a directory tree. Get the size from the .manifest if possible."
	man = os.path.join(path, '.manifest')
	if os.path.exists(man):
		size = os.path.getsize(man)
		for line in file(man):
			if line[:1] in "XF":
				size += long(line.split(' ', 4)[3])
	else:
		size = 0
		for root, dirs, files in os.walk(path):
			for name in files:
				size += os.path.getsize(os.path.join(root, name))
	return size

def summary(iface):
	if iface.summary:
		return iface.get_name() + ' - ' + iface.summary
	return iface.get_name()

def get_selected_paths(tree_view):
	"GTK 2.0 doesn't have this built-in"
	selection = tree_view.get_selection()
	paths = []
	def add(model, path, iter):
		paths.append(path)
	selection.selected_foreach(add)
	return paths

tips = TreeTips()

# Responses
DELETE = 0

class CachedInterface(object):
	def __init__(self, uri, size):
		self.uri = uri
		self.size = size

	def delete(self):
		if not self.uri.startswith('/'):
			cached_iface = basedir.load_first_cache(namespaces.config_site,
					'interfaces', model.escape(self.uri))
			if cached_iface:
				#print "Delete", cached_iface
				os.unlink(cached_iface)
		user_overrides = basedir.load_first_config(namespaces.config_site,
					namespaces.config_prog,
					'user_overrides', model.escape(self.uri))
		if user_overrides:
			#print "Delete", user_overrides
			os.unlink(user_overrides)
	
	def __cmp__(self, other):
		return self.uri.__cmp__(other.uri)

class ValidInterface(CachedInterface):
	def __init__(self, iface, size):
		CachedInterface.__init__(self, iface.uri, size)
		self.iface = iface
		self.in_cache = []

	def append_to(self, model, iter):
		iter2 = model.append(iter,
				  [self.uri, self.size, None, summary(self.iface), self])
		for cached_impl in self.in_cache:
			cached_impl.append_to(model, iter2)
	
	def get_may_delete(self):
		for c in self.in_cache:
			if not isinstance(c, LocalImplementation):
				return False	# Still some impls cached
		return True

	may_delete = property(get_may_delete)
	
class InvalidInterface(CachedInterface):
	may_delete = True

	def __init__(self, uri, ex, size):
		CachedInterface.__init__(self, uri, size)
		self.ex = ex

	def append_to(self, model, iter):
		model.append(iter, [self.uri, self.size, None, self.ex, self])
	
class LocalImplementation:
	may_delete = False

	def __init__(self, impl):
		self.impl = impl

	def append_to(self, model, iter):
		model.append(iter, [self.impl.id, 0, None, 'This is a local version, not held in the cache.', self])

class CachedImplementation:
	may_delete = True

	def __init__(self, cache_dir, name):
		self.impl_path = os.path.join(cache_dir, name)
		self.size = get_size(self.impl_path)
		self.name = name

	def delete(self):
		#print "Delete", self.impl_path
		shutil.rmtree(self.impl_path)
	
	def open_rox(self):
		os.spawnlp(os.P_WAIT, '0launch', '0launch', ROX_IFACE, '-d', self.impl_path)
	
	def verify(self):
		try:
			manifest.verify(self.impl_path)
		except BadDigest, ex:
			box = gtk.MessageDialog(None, 0,
						gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, str(ex))
			if ex.detail:
				swin = gtk.ScrolledWindow()
				buffer = gtk.TextBuffer()
				mono = buffer.create_tag('mono', family = 'Monospace')
				buffer.insert_with_tags(buffer.get_start_iter(), ex.detail, mono)
				text = gtk.TextView(buffer)
				text.set_editable(False)
				text.set_cursor_visible(False)
				swin.add(text)
				swin.set_shadow_type(gtk.SHADOW_IN)
				swin.set_border_width(4)
				box.vbox.pack_start(swin)
				swin.show_all()
				box.set_resizable(True)
		else:
			box = gtk.MessageDialog(None, 0,
						gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
						'Contents match digest; nothing has been changed.')
		box.run()
		box.destroy()

	menu_items = [('Open in ROX-Filer', open_rox),
		      ('Verify integrity', verify)]

class UnusedImplementation(CachedImplementation):
	def append_to(self, model, iter):
		model.append(iter, [self.name, self.size, None, self.impl_path, self])

class KnownImplementation(CachedImplementation):
	def __init__(self, cached_iface, cache_dir, impl, impl_size):
		CachedImplementation.__init__(self, cache_dir, impl.id)
		self.cached_iface = cached_iface
		self.impl = impl
		self.size = impl_size
	
	def delete(self):
		CachedImplementation.delete(self)
		self.cached_iface.in_cache.remove(self)

	def append_to(self, model, iter):
		model.append(iter,
			['Version %s : %s' % (self.impl.get_version(), self.impl.id),
			 self.size, None,
			 None,
			 self])
	
	def __cmp__(self, other):
		if hasattr(other, 'impl'):
			return self.impl.__cmp__(other.impl)
		return -1

class CacheExplorer(Dialog):
	def __init__(self):
		Dialog.__init__(self)
		self.set_title('Zero Install Cache')
		self.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2)

		# Model
		self.model = gtk.TreeStore(str, int, str, str, object)
		self.tree_view = gtk.TreeView(self.model)

		# Tree view
		swin = gtk.ScrolledWindow()
		swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
		swin.set_shadow_type(gtk.SHADOW_IN)
		swin.add(self.tree_view)
		self.vbox.pack_start(swin, True, True, 0)
		self.tree_view.set_rules_hint(True)
		swin.show_all()

		column = gtk.TreeViewColumn('Item', gtk.CellRendererText(), text = ITEM)
		column.set_resizable(True)
		self.tree_view.append_column(column)

		cell = gtk.CellRendererText()
		cell.set_property('xalign', 1.0)
		column = gtk.TreeViewColumn('Size', cell, text = PRETTY_SIZE)
		self.tree_view.append_column(column)

		def button_press(tree_view, bev):
			if bev.button != 3:
				return False
			pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
			if not pos:
				return False
			path, col, x, y = pos
			obj = self.model[path][ITEM_OBJECT]
			if obj and hasattr(obj, 'menu_items'):
				popup_menu(bev, obj)
		self.tree_view.connect('button-press-event', button_press)

		# Tree tooltips
		def motion(tree_view, ev):
			if ev.window is not tree_view.get_bin_window():
				return False
			pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y))
			if pos:
				path = pos[0]
				row = self.model[path]
				tip = row[TOOLTIP]
				if tip:
					if tip != tips.item:
						tips.prime(tree_view, tip)
				else:
					tips.hide()
			else:
				tips.hide()

		self.tree_view.connect('motion-notify-event', motion)
		self.tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide())

		# Responses

		self.add_button(gtk.STOCK_HELP, gtk.RESPONSE_HELP)
		self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_OK)
		self.add_button(gtk.STOCK_DELETE, DELETE)
		self.set_default_response(gtk.RESPONSE_OK)

		selection = self.tree_view.get_selection()
		def selection_changed(selection):
			any_selected = False
			for x in get_selected_paths(self.tree_view):
				obj = self.model[x][ITEM_OBJECT]
				if obj is None or not obj.may_delete:
					self.set_response_sensitive(DELETE, False)
					return
				any_selected = True
			self.set_response_sensitive(DELETE, any_selected)
		selection.set_mode(gtk.SELECTION_MULTIPLE)
		selection.connect('changed', selection_changed)
		selection_changed(selection)

		def response(dialog, resp):
			if resp == gtk.RESPONSE_OK:
				self.destroy()
			elif resp == gtk.RESPONSE_HELP:
				cache_help.display()
			elif resp == DELETE:
				self.delete()
		self.connect('response', response)
	
	def delete(self):
		errors = []

		model = self.model
		paths = get_selected_paths(self.tree_view)
		paths.reverse()
		for path in paths:
			item = model[path][ITEM_OBJECT]
			assert item.delete
			try:
				item.delete()
			except OSError, ex:
				errors.append(str(ex))
			else:
				model.remove(model.get_iter(path))
		self.update_sizes()

		if errors:
			alert(self, "Failed to delete:\n%s" % '\n'.join(errors))

	def populate_model(self):
		# Find cached implementations

		unowned = {}	# Impl ID -> Store
		duplicates = [] # TODO

		for s in iface_cache.stores.stores:
			if os.path.isdir(s.dir):
				for id in os.listdir(s.dir):
					if id in unowned:
						duplicates.append(id)
					unowned[id] = s

		ok_interfaces = []
		error_interfaces = []

		# Look through cached interfaces for implementation owners
		all = iface_cache.list_all_interfaces()
		all.sort()
		for uri in all:
			iface_size = 0
			try:
				if uri.startswith('/'):
					cached_iface = uri
				else:
					cached_iface = basedir.load_first_cache(namespaces.config_site,
							'interfaces', model.escape(uri))
				user_overrides = basedir.load_first_config(namespaces.config_site,
							namespaces.config_prog,
							'user_overrides', model.escape(uri))

				iface_size = size_if_exists(cached_iface) + size_if_exists(user_overrides)
				iface = iface_cache.get_interface(uri)
			except Exception, ex:
				error_interfaces.append((uri, str(ex), iface_size))
			else:
				cached_iface = ValidInterface(iface, iface_size)
				for impl in iface.implementations.values():
					if impl.id.startswith('/') or impl.id.startswith('.'):
						cached_iface.in_cache.append(LocalImplementation(impl))
					if impl.id in unowned:
						cached_dir = unowned[impl.id].dir
						impl_path = os.path.join(cached_dir, impl.id)
						impl_size = get_size(impl_path)
						cached_iface.in_cache.append(KnownImplementation(cached_iface, cached_dir, impl, impl_size))
						del unowned[impl.id]
				cached_iface.in_cache.sort()
				ok_interfaces.append(cached_iface)

		if error_interfaces:
			iter = self.model.append(None, [_("Invalid interfaces (unreadable)"),
						 0, None,
						 _("These interfaces exist in the cache but cannot be "
						   "read. You should probably delete them."),
						   None])
			for uri, ex, size in error_interfaces:
				item = InvalidInterface(uri, ex, size)
				item.append_to(self.model, iter)

		unowned_sizes = []
		local_dir = os.path.join(basedir.xdg_cache_home, '0install.net', 'implementations')
		for id in unowned:
			if unowned[id].dir == local_dir:
				impl = UnusedImplementation(local_dir, id)
				unowned_sizes.append((impl.size, impl))
		if unowned_sizes:
			iter = self.model.append(None, [_("Unowned implementations and temporary files"),
						0, None,
						_("These probably aren't needed any longer. You can "
						  "delete them."), None])
			unowned_sizes.sort()
			unowned_sizes.reverse()
			for size, item in unowned_sizes:
				item.append_to(self.model, iter)

		if ok_interfaces:
			iter = self.model.append(None,
				[_("Interfaces"),
				 0, None,
				 _("Interfaces in the cache"),
				   None])
			for item in ok_interfaces:
				item.append_to(self.model, iter)
		self.update_sizes()
	
	def update_sizes(self):
		"""Set PRETTY_SIZE to the total size, including all children."""
		m = self.model
		def update(itr):
			total = m[itr][SELF_SIZE]
			child = m.iter_children(itr)
			while child:
				total += update(child)
				child = m.iter_next(child)
			m[itr][PRETTY_SIZE] = support.pretty_size(total)
			return total
		itr = m.get_iter_root()
		while itr:
			update(itr)
			itr = m.iter_next(itr)

cache_help = help_box.HelpBox("Cache Explorer Help",
('Overview', """
When you run a program using Zero Install, it downloads the program's 'interface' file, \
which gives information about which versions of the program are available. This interface \
file is stored in the cache to save downloading it next time you run the program.

When you have chosen which version (implementation) of the program you want to \
run, Zero Install downloads that version and stores it in the cache too. Zero Install lets \
you have many different versions of each program on your computer at once. This is useful, \
since it lets you use an old version if needed, and different programs may need to use \
different versions of libraries in some cases.

The cache viewer shows you all the interfaces and implementations in your cache. \
This is useful to find versions you don't need anymore, so that you can delete them and \
free up some disk space."""),

('Invalid interfaces', """
The cache viewer gets a list of all interfaces in your cache. However, some may not \
be valid; they are shown in the 'Invalid interfaces' section. It should be fine to \
delete these. An invalid interface may be caused by a local interface that no longer \
exists, by a failed attempt to download an interface (the name ends in '.new'), or \
by the interface file format changing since the interface was downloaded."""),

('Unowned implementations and temporary files', """
The cache viewer searches through all the interfaces to find out which implementations \
they use. If no interface uses an implementation, it is shown in the 'Unowned implementations' \
section.

Unowned implementations can result from old versions of a program no longer being listed \
in the interface file. Temporary files are created when unpacking an implementation after \
downloading it. If the archive is corrupted, the unpacked files may be left there. Unless \
you are currently unpacking new programs, it should be fine to delete everything in this \
section."""),

('Interfaces', """
All remaining interfaces are listed in this section. You may wish to delete old versions of \
certain programs. Deleting a program which you may later want to run will require it to be downloaded \
again. Deleting a version of a program which is currently running may cause it to crash, so be careful!
"""))


syntax highlighted by Code2HTML, v. 0.9.1