#!/usr/bin/python

import BitTorrent.download, BitTorrent.bencode
import os.path, threading, sys, time, re
import gobject, gtk, gtk.glade

app_name         = 'gnome-btdownload'
app_version      = '0.0.11'
max_torrent_size = 0x200000 # 2 MB

rfc_2396_uri_regexp = r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?"

open_path   = None
locate_file = None
get_url     = None

if not open_path or not locate_file or not get_url:
	try:
		import gnome, gnome.vfs
		
		gnome_program = gnome.program_init(app_name, app_version)
		
		def gnome_open_path(path):
			uri = 'file://' + path
			
			application = gnome.vfs.mime_get_default_application(gnome.vfs.get_mime_type(uri))
		
		# Disabled until mime_get_default_application is exported by gnome-python...
		#if not open_path:
		#	open_path = gnome_open_path
		
		def gnome_locate_file(filename, sub):
			print gnome_program.locate_file(gnome.FILE_DOMAIN_APP_DATADIR, filename, True)

		# Disabled until gnome_program.locate_file is exported by gnome-python...
		#if not locate_file:
		#	locate_file = gnome_locate_file

		def gnome_get_url(uri, read_bytes):
			handle = gnome.vfs.open(uri, gnome.vfs.OPEN_READ)

			return handle.read(read_bytes)
		
		if not get_url:
			get_url = gnome_get_url
	except:
		print 'Couldn\'t load gnome...'
	
if not locate_file or not get_url:
	try:
		def fallback_locate_attempt_prefixes(path):
			prefixes = ['', 'usr/', 'usr/local/']

			# Try them locally
			for prefix in prefixes:
				if os.path.exists(prefix + path):
					return prefix + path;

			# Try them from root
			for prefix in prefixes:
				if os.path.exists('/' + prefix + path):
					return '/' + prefix + path;

			return None
			
		def fallback_locate_attempt(prefix, path, sub, filename):
			if sub:
				prefix_path_sub_file = fallback_locate_attempt_prefixes(prefix + '/' + path + '/' + sub + '/' + filename)
				if prefix_path_sub_file:
					return prefix_path_sub_file
			
			prefix_path_file = fallback_locate_attempt_prefixes(prefix + '/' + path + '/' + filename)
			if prefix_path_file:
				return prefix_path_file
			
			return None

		def fallback_locate_file(type, filename, sub=None):
			if type == 'data':
				# Sources:   Common   Common         FreeBSD        FreeBSD
				prefixes = ['share', 'local/share', 'share/gnome', 'X11R6/share/gnome']

				for prefix in prefixes:
					result = fallback_locate_attempt(prefix, app_name, sub, filename)
					if result:
						return result
			
			print >> sys.stderr, 'Couldn\'t locate file, will probably explode...'
			return None
		
		if not locate_file:
			locate_file = fallback_locate_file
		
		def fallback_get_url(uri, read_bytes):
			f = file(uri, 'rb')

			return f.read(read_bytes)
		
		if not get_url:
			get_url = fallback_get_url
	except:
		print 'Couldn\'t load gnome-fallback...'

def fmt_time(all):
	minutes = int(all / 60)
	seconds = all - (minutes * 60)
	
	return '%0.2u.%0.2u' % (minutes, seconds)

def guess_path_type(path, extension):
	try:
		mo = re.search(rfc_2396_uri_regexp, path);

		if mo.group(1) and mo.group(5):
			return 'url'
	except:
		pass
	
	if path[-len(extension):] == extension:
		return 'other'
	
	return None

class GtkHigErrorDialog:
	glade_xml = None
	dialog    = None
	
	def on_dialog_destroy(self, widget, data=None):
		self.dialog = None
	
	def on_okbutton_clicked(self, widget, data=None):
		self.dialog.destroy()
	
	def __init__(self, text, subtext='', modal=False):
		self.glade_xml = gtk.glade.XML(locate_file('data', 'errdiag.glade', 'glade'))
		
		self.dialog = self.glade_xml.get_widget('dialog_hig_error')
		
		self.glade_xml.get_widget('label_text').set_markup(text)
		self.glade_xml.get_widget('label_subtext').set_markup(subtext)
		
		self.glade_xml.signal_autoconnect({
			'on_okbutton_clicked':
				self.on_okbutton_clicked
		})
		
		self.dialog.set_modal(modal)

class GtkFileSaveDialog:
	dialog = None
	result = None
	
	def on_dialog_destroy(self, widget, data=None):
		self.dialog = None
	
	def on_okbutton_clicked(self, widget, data=None):
		self.result = self.dialog.get_filename()
		self.dialog.destroy()
	
	def on_cancelbutton_clicked(self, widget, data=None):
		self.result = None
		self.dialog.destroy()
	
	def __init__(self, title, default=None, modal=False, multiple=False):
		self.dialog = gtk.FileSelection(title)
		
		self.dialog.set_select_multiple(multiple)
		self.dialog.connect('destroy', self.on_dialog_destroy)
		
		self.dialog.ok_button.connect('clicked', self.on_okbutton_clicked)
		self.dialog.cancel_button.connect('clicked', self.on_cancelbutton_clicked)
		
		if default:
			self.dialog.set_filename(default)
		
		self.dialog.set_modal(modal)
		self.dialog.show()

class BtState:
	# Torrent information
	path_origin     = 'Unknown'
	size_total      = 0
	args            = []
	# Local information
	path_output     = ''
	# Transfer information
	done            = False
	event           = None
	thread          = None
	activity        = None
	time_begin      = 0.0
	time_remaining  = 0.0
	dl_rate         = 0.0
	dl_amount       = 0
	dl_pre_amount   = 0
	ul_rate         = 0.0
	ul_amount       = None
	ul_pre_amount   = 0
	max_uploads     = 0
	max_upload_rate = 0.0
	# Implementation information
	params          = {}
	params_pounce   = True

	def get_dl_amount(self):
		if self.activity == 'checking existing file':
			return self.dl_amount
		else:
			return self.dl_amount + self.dl_pre_amount
	
	def get_ul_amount(self):
		if self.ul_amount:
			return self.ul_amount + self.ul_pre_amount
		elif self.ul_pre_amount > 0:
			return self.ul_pre_amount
		else:
			return None
	
	def file(self, default, size, saveas, dir):
		self.done       = False
		self.size_total = size
		
		if saveas:
			self.path_output = os.path.abspath(saveas)
		else:
			self.path_output = os.path.abspath(default)
		
		return self.path_output
	
	def status(self, dict):
		if not self.done:
			if dict.has_key('downRate'):
				self.dl_rate = float(dict['downRate'])
			
			dl_amount = None
			if dict.has_key('downTotal'):
				dl_amount = long(dict['downTotal'] * (1 << 20))
			elif dict.has_key('fractionDone'):
				dl_amount = long(float(dict['fractionDone']) * self.size_total)
			if dl_amount:
				if dl_amount == 0:
					self.dl_pre_amount = self.dl_amount
				self.dl_amount = dl_amount
			
			if dict.has_key('timeEst'):
				self.time_remaining = float(dict['timeEst'])
		
		if dict.has_key('upRate'):
			self.ul_rate = float(dict['upRate'])
		
		if dict.has_key('upTotal'):
			self.ul_amount = long(dict['upTotal'] * (1 << 20))

		if dict.has_key('activity'):		
			self.activity = dict['activity']

			# Incorporate the previous phase(s) in our download amount
			self.dl_pre_amount += self.dl_amount
			self.dl_amount = 0
	
	def finished(self):
		self.done = True
		self.dl_amount = self.size_total - self.dl_pre_amount
	
	def path(self, path):
		self.path_output = path
	
	def param(self, params):
		if params:
			self.params = params
			
			if self.params_pounce:
				self.cap_uploads(self.max_uploads)
				self.cap_upload_rate(self.max_upload_rate)
				
				self.params_pounce = False
		else:
			self.params = {}
	
	# This is used to keep BitTorrent from blocking the rest of the program
	def download_thread(self, file, status, finished, error, cols, path, param):
		try:
			# BitTorrent 3.3-style
			BitTorrent.download.download(self.args, file, status, finished, error, self.event, cols, path, param)
		except:
			# BitTorrent 3.2-style
			BitTorrent.download.download(self.args, file, status, finished, error, self.event, cols, path)
	
	def download(self, file, status, finished, error, cols, path, param, resuming=False):
		self.done          = False
		self.time_begin    = time.time()
		self.event         = threading.Event()
		self.thread        = threading.Thread(None, self.download_thread, 'bt_dl_thread', (file, status, finished, error, cols, path, param))
		self.dl_rate       = 0.0
		self.dl_amount     = 0
		self.dl_pre_amount = 0
		self.ul_rate       = 0.0
		self.params        = None
		self.params_pounce = True

		if resuming:
			if self.ul_amount:
				self.ul_pre_amount += self.ul_amount
		else:
			self.ul_pre_amount = 0
		self.ul_amount = None
		
		self.thread.start()
	
	def join(self):
		if self.event:
			self.event.set()
			self.event = None
		if self.thread:
			self.thread.join()
			self.thread = None
	
	def cap_uploads(self, uploads):
		self.max_uploads = int(uploads)
		
		if self.params and self.params.has_key('max_uploads'):
			self.params['max_uploads'](self.max_uploads)
			return self.max_uploads
		else:
			return None
	
	def cap_upload_rate(self, upload_rate):
		self.max_upload_rate = upload_rate
		
		if self.params and self.params.has_key('max_upload_rate'):
			self.params['max_upload_rate'](int(self.max_upload_rate * (1 << 10)))
			return int(self.max_upload_rate * (1 << 10))
		else:
			return None
	
	def __init__(self, args):
		for i in range(0,len(args)):
			if args[i] == '--saveas':
				self.path_output = os.path.abspath(args[i+1])
				i += 1
				continue
			else:
				path_type = guess_path_type(args[i], '.torrent')
				
				if path_type != None:
					if path_type == 'url':
						mo = re.search(rfc_2396_uri_regexp, args[i])
						
						if mo.group(2) == 'file':
							self.path_origin = os.path.abspath(mo.group(5))
						else:
							self.path_origin = mo.group(1) + mo.group(3) + mo.group(5)
					else:
						self.path_origin = os.path.abspath(args[i])
					
					# Get the output name from the torrent file
					try:
						torrent_file = get_url(self.path_origin, max_torrent_size)
						torrent_info = BitTorrent.bencode.bdecode(torrent_file)
					
						self.path_output = torrent_info['info']['name']
					except:
						# Uh, just guess
						self.path_output = os.path.basename(self.path_origin)

						if self.path_output[-len('.torrent'):] == '.torrent':
							self.path_output = self.path_output[:-len('.torrent')]
					
					if path_type == 'other':
						self.args.append('--responsefile')
					elif path_type == 'url':
						self.args.append('--url')
			
			self.args.append(args[i])

class GtkClient:
	glade_xml       = None
	bt_state        = None
	bt_events       = []
	
	def log_event(self, type, text):
		t = fmt_time(time.time() - self.bt_state.time_begin)
		
		if type == 'Error' and self.glade_xml.get_widget('checkbutton_events_display_error_dialogs').get_active():
			# Try to specially adapt the error message
			try:
				mo = re.search(r"([A-Za-z \'\,]+) - [\<]?([^\>]+)[\>]?", str(text))
				
				GtkHigErrorDialog('<b>' + mo.group(1) + '</b>', mo.group(2))
			except:
				GtkHigErrorDialog(str(text))
		
		if self.bt_events:
			self.bt_events.append((t, type, text))
		else:
			print >> sys.stderr, '%s, %s: %s' % (t, type, text)
	
	# Bt Callbacks
	def on_bt_file(self, default, size, saveas, dir):
		path = self.bt_state.file(default, size, saveas, dir)
		
		gtk.threads_enter()
		
		label_download_file_output = self.glade_xml.get_widget('label_download_file_output')
		label_download_file_output.set_text(path)
		
		gtk.threads_leave()
		
		return path
	
	def on_bt_status(self, dict = {}, fractionDone = None, timeEst = None, downRate = None, upRate = None, activity = None):
		# To support BitTorrent 3.2, pack anything supplied seperately from dict into dict
		if fractionDone:
			dict['fractionDone'] = fractionDone
		if timeEst:
			dict['timeEst'] = timeEst
		if downRate:
			dict['downRate'] = downRate
		if upRate:
			dict['upRate'] = upRate
		if activity:
			dict['activity'] = activity
		
		self.bt_state.status(dict)
		
		gtk.threads_enter()
		
		progressbar_download_status = self.glade_xml.get_widget('progressbar_download_status')
		label_download_elapsed_output = self.glade_xml.get_widget('label_download_time_elapsed_output')

		label_download_elapsed_output.set_text(fmt_time(time.time() - self.bt_state.time_begin))
		
		if dict.has_key('spew'):
			print >> sys.stderr, 'Spew: %s' % (dict['spew'])
		
		if dict.has_key('fractionDone'):
			window_main = self.glade_xml.get_widget('window_main')
			
			progressbar_download_status.set_percentage(dict['fractionDone'])
			window_main.set_title(str(int(dict['fractionDone'] * 100)) + "% of " + self.bt_state.path_origin)
		
		if dict.has_key('downTotal') or dict.has_key('fractionDone'):
			label_download_status_output = self.glade_xml.get_widget('label_download_status_output')
			
			label_download_status_output.set_text('%.1f of %.1f MB at %.2f KB/s' %
				(float(self.bt_state.get_dl_amount()) / (1 << 20),
				 float(self.bt_state.size_total)      / (1 << 20),
				 float(self.bt_state.dl_rate)         / (1 << 10)))
			
		if dict.has_key('timeEst'):
			label_download_time_remaining_output = self.glade_xml.get_widget('label_download_time_remaining_output')
			
			label_download_time_remaining_output.set_text(fmt_time(dict['timeEst']))
		
		if dict.has_key('upRate') or dict.has_key('upTotal'):
			label_upload_status_output = self.glade_xml.get_widget('label_upload_status_output')
			
			if self.bt_state.get_ul_amount():
				label_upload_status_output.set_text('%.1f MB at %.2f KB/s' %
					(float(self.bt_state.get_ul_amount()) / (1 << 20),
					 float(self.bt_state.ul_rate)         / (1 << 10)))
			else:
				label_upload_status_output.set_text('%.2f KB/s' %
					(float(self.bt_state.ul_rate) / (1 << 10)))
		
		if dict.has_key('activity'):
			if not self.bt_state.done:
				progressbar_download_status.set_text(dict['activity'])
			
			self.log_event('Activity', dict['activity'])
		
		gtk.threads_leave()
	
	def on_bt_finished(self):
		self.bt_state.finished()
		self.on_bt_status({'fractionDone': float(1.0), 'timeEst': 0, 'activity': 'finished'})
		
		gtk.threads_enter()
		
		progressbar_download_status          = self.glade_xml.get_widget('progressbar_download_status')
		label_download_time_remaining_output = self.glade_xml.get_widget('label_download_time_remaining_output')
		button_open                          = self.glade_xml.get_widget('button_open')
		
		progressbar_download_status.set_percentage(1.0)
		progressbar_download_status.set_text('download complete')
		label_download_time_remaining_output.set_text(fmt_time(0))
		if open_path:
			button_open.set_sensitive(True)
		
		gtk.threads_leave()
	
	def on_bt_error(self, msg):
		gtk.threads_enter()
		
		self.log_event('Error', msg)
		
		gtk.threads_leave()
	
	def on_bt_path(self, path):
		self.bt_state.path(path)
		
		gtk.threads_enter()
		
		label_download_file_output = self.glade_xml.get_widget('label_download_file_output')
		label_download_file_output.set_text(self.bt_state.path_output)
		
		gtk.threads_leave()
	
	def on_bt_param(self, params):
		self.bt_state.param(params)
		
		gtk.threads_enter()
		
		checkbutton_cap_uploads = self.glade_xml.get_widget('checkbutton_cap_uploads')
		spinbutton_cap_uploads  = self.glade_xml.get_widget('spinbutton_cap_uploads')
		checkbutton_cap_upload_rate = self.glade_xml.get_widget('checkbutton_cap_upload_rate')
		spinbutton_cap_upload_rate  = self.glade_xml.get_widget('spinbutton_cap_upload_rate')
		
		if params.has_key('max_uploads'):
			checkbutton_cap_uploads.set_sensitive(True)
			spinbutton_cap_uploads.set_sensitive(True)
		else:
			checkbutton_cap_uploads.set_sensitive(False)
			spinbutton_cap_uploads.set_sensitive(False)
		
		if params.has_key('max_upload_rate'):
			checkbutton_cap_upload_rate.set_sensitive(True)
			spinbutton_cap_upload_rate.set_sensitive(True)
		else:
			checkbutton_cap_upload_rate.set_sensitive(False)
			spinbutton_cap_upload_rate.set_sensitive(False)
		
		gtk.threads_leave()
	
	# Gtk+ Callbacks
	def on_window_main_destroy(self, widget, data=None):
		self.join()
		gtk.main_quit()
	
	def on_button_open_clicked(self, widget, data=None):
		if open_path:
			open_path(self.bt_path)
	
	def on_button_resume_clicked(self, widget, data=None):
		button_resume               = self.glade_xml.get_widget('button_resume')
		button_stop                 = self.glade_xml.get_widget('button_stop')
		button_close                = self.glade_xml.get_widget('button_close')
		
		button_resume.set_sensitive(False)
		button_stop.show()
		button_close.hide()
		
		self.run_bt(resuming=True)
	
	def on_button_stop_clicked(self, widget, data=None):
		self.join()
		
		progressbar_download_status = self.glade_xml.get_widget('progressbar_download_status')
		button_resume               = self.glade_xml.get_widget('button_resume')
		button_stop                 = self.glade_xml.get_widget('button_stop')
		button_close                = self.glade_xml.get_widget('button_close')

		if not self.bt_state.done:
			progressbar_download_status.set_text('transfer stopped')
		button_resume.set_sensitive(True)
		button_stop.hide()
		button_close.show()
	
	def on_button_close_clicked(self, widget, data=None):
		window_main = self.glade_xml.get_widget('window_main')
		window_main.destroy()
	
	def on_checkbutton_cap_uploads_toggled(self, widget, data=None):
		spinbutton_cap_uploads = self.glade_xml.get_widget('spinbutton_cap_uploads')
		
		if widget.get_active():
			self.bt_state.cap_uploads(int(spinbutton_cap_uploads.get_value()))
		else:
			self.bt_state.cap_uploads(0)
	
	def on_spinbutton_cap_uploads_value_changed(self, widget, data=None):
		checkbutton_cap_uploads = self.glade_xml.get_widget('checkbutton_cap_uploads')
		
		if checkbutton_cap_uploads.get_active():
			self.bt_state.cap_uploads(int(widget.get_value()))
	
	def on_checkbutton_cap_upload_rate_toggled(self, widget, data=None):
		spinbutton_cap_upload_rate = self.glade_xml.get_widget('spinbutton_cap_upload_rate')
		
		if widget.get_active():
			self.bt_state.cap_upload_rate(spinbutton_cap_upload_rate.get_value())
		else:
			self.bt_state.cap_upload_rate(0)
	
	def on_spinbutton_cap_upload_rate_value_changed(self, widget, data=None):
		checkbutton_cap_upload_rate = self.glade_xml.get_widget('checkbutton_cap_upload_rate')
		
		if checkbutton_cap_upload_rate.get_active():
			self.bt_state.cap_upload_rate(widget.get_value())
	
	def on_button_events_clear_clicked(self, widget, data=None):
		if self.bt_events:
			self.bt_events.clear()
	
	def setup_treeview_events(self):
		treeview_events = self.glade_xml.get_widget('treeview_events')
		
		list_store = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
		
		treeview_events.set_model(list_store)
		
		treeview_events.append_column(gtk.TreeViewColumn('Time', gtk.CellRendererText(), text=0))
		treeview_events.append_column(gtk.TreeViewColumn('Type', gtk.CellRendererText(), text=1))
		treeview_events.append_column(gtk.TreeViewColumn('Text', gtk.CellRendererText(), text=2))
		
		self.bt_events = list_store
	
	def run_fsd(self):
		fsd = GtkFileSaveDialog('Save location for BitTorrent session', default=self.bt_state.path_output, modal=True)
			
		# Run Gtk+ for a bit to wait on the user to address the file selector
		while fsd.dialog:
			gtk.main_iteration()
		
		return fsd.result
	
	def run_bt(self, resuming=False):
		self.bt_state.download(self.on_bt_file, self.on_bt_status, self.on_bt_finished, self.on_bt_error, 100, self.on_bt_path, self.on_bt_param, resuming=resuming)
	
	def join(self):
		if self.bt_state:
			self.bt_state.join()
	
	def __init__(self, args):
		# Gtk+ Setup
		gtk.threads_init()
		
		# Bt Setup
		self.bt_state = BtState(args)
		
		# Run Gtk+ file selector
		self.bt_state.path_output = self.run_fsd()
		
		if self.bt_state.path_output: # They hit OK
			self.bt_state.args[:0] = ['--saveas', self.bt_state.path_output]
		else: # They hit Cancel
			sys.exit(0)
		
		# Run Gtk+ main window
		self.glade_xml = gtk.glade.XML(locate_file('data', 'dlsession.glade', 'glade'))
	
		self.setup_treeview_events()
		
		self.glade_xml.signal_autoconnect({
			'on_window_main_destroy':
				self.on_window_main_destroy,
			'on_button_open_clicked':
				self.on_button_open_clicked,
			'on_button_resume_clicked':
				self.on_button_resume_clicked,
			'on_button_stop_clicked':
				self.on_button_stop_clicked,
			'on_button_close_clicked':
				self.on_button_close_clicked,
			'on_checkbutton_cap_uploads_toggled':
				self.on_checkbutton_cap_uploads_toggled,
			'on_spinbutton_cap_uploads_value_changed':
				self.on_spinbutton_cap_uploads_value_changed,
			'on_checkbutton_cap_upload_rate_toggled':
				self.on_checkbutton_cap_upload_rate_toggled,
			'on_spinbutton_cap_upload_rate_value_changed':
				self.on_spinbutton_cap_upload_rate_value_changed,
			'on_button_events_clear_clicked':
				self.on_button_events_clear_clicked
		})
		
		self.glade_xml.get_widget('label_download_address_output').set_text(self.bt_state.path_origin)
		self.glade_xml.get_widget('window_main').set_title(self.bt_state.path_origin)
		
		# Run Bt
		self.run_bt()
		
		# Run Gtk+
		gtk.main()

def run(args):
	client = GtkClient(args)

if __name__ == '__main__':
	print 'argv:', sys.argv
	
	run(sys.argv[1:])
