#
# FtpCube
# Copyright (C) 2001 Michael Gilfix
#
# This file is part of FtpCube.
#
# You should have received a file COPYING containing license terms
# along with this program; if not, write to Michael Gilfix
# (mgilfix@eecs.tufts.edu) for a copy.
#
# This version of FtpCube is open source; you can redistribute it and/or
# modify it under the terms listed in the file COPYING.
#
# 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.
#

import gtk, GtkExtra
import string # For backwards compatibility with 1.5.x
import time, sys, os
import main, app, ftp, logging

from intl import _

# For compatibility with win32 pygtk
if sys.platform == "win32":
	import GTKconst
	GTK = GTKconst
else:
	import GTK

class FtpThread:

	def __init__ (self, connect_opts, name=None, silence=0):

		# Instance variables
		self.progress = (0, 0)
		self.start_time = 0.0
		self.idle = 0
		self.done = 0
		self.idle_disabled = 0
		self.name = name
		self.silence = silence
		self.opts = connect_opts
		self.logging = 0
		self.logs = {
			'download_log' : None,
			'upload_log'   : None,
		}

		# Create an event box to catch all button presses on the lower widgets
		self.entry_box = gtk.GtkEventBox ()
		self.entry_box.connect ("button_press_event", self.popup_menu)
		self.entry_box.show ()

		# Load the pixmaps for the action pictures
		self.load_pixmaps ()

		vbox = gtk.GtkVBox ()
		vbox.show ()
		self.entry_box.add (vbox)

		self.progress_packer = gtk.GtkPacker ()
		self.progressbar = gtk.GtkProgressBar ()
		self.progressbar.set_usize (200, 10)
		self.progress_packer.add (self.progressbar, options=GTK.FILL, pad_y=4)
		vbox.pack_start (self.progress_packer)

		table = gtk.GtkTable (2, 4)
		table.show ()
		vbox.pack_start (table, expand=gtk.FALSE, fill=gtk.FALSE)

		# Button and label
		self.pixmap = gtk.GtkPixmap (self.connect_pixmap, self.connect_mask)
		self.pixmap.set_usize (20, 20)
		self.pixmap.show ()
		table.attach (self.pixmap, 0, 1, 1, 2, xoptions=GTK.EXPAND, yoptions=GTK.EXPAND, xpadding=2)

		self.label = gtk.GtkLabel ()
		self.label.set_line_wrap (gtk.TRUE)
		self.label.set_usize (200, -1)
		self.label.show ()
		table.attach (self.label, 1, 2, 0, 3, xoptions=GTK.EXPAND, yoptions=GTK.EXPAND, xpadding=2)

		separator = gtk.GtkHSeparator ()
		separator.show ()
		table.attach (separator, 0, 2, 3, 4)

		self.initiate_connect ()
		self.update_local_listing ()

	def load_pixmaps (self):

		self.idle_pixmap, self.idle_mask = gtk.create_pixmap_from_xpm (self.entry_box, None, os.path.join (main.ICONS_PREFIX, "thread_idle.xpm"))
		self.connect_pixmap, self.connect_mask = gtk.create_pixmap_from_xpm (self.entry_box, None, os.path.join (main.ICONS_PREFIX, "thread_connect.xpm"))
		self.listing_pixmap, self.listing_mask = gtk.create_pixmap_from_xpm (self.entry_box, None, os.path.join (main.ICONS_PREFIX, "thread_listing.xpm"))
		self.abort_pixmap, self.abort_mask = gtk.create_pixmap_from_xpm (self.entry_box, None, os.path.join (main.ICONS_PREFIX, "thread_abort.xpm"))
		self.action_pixmap, self.action_mask = gtk.create_pixmap_from_xpm (self.entry_box, None, os.path.join (main.ICONS_PREFIX, "thread_action.xpm"))
		self.download_pixmap, self.download_mask = gtk.create_pixmap_from_xpm (self.entry_box, None, os.path.join (main.ICONS_PREFIX, "thread_download.xpm"))

	def get_thread_name (self):

		if self.name is None:
			return ''
		else:
			return "[%s] " %self.name

	def popup_menu (self, widget, event):

		if event.button == 3:
			opts = [
				(_('/Cancel Download'), None, self.cancel_download),
				(_('/Cancel Thread'), None, self.cancel_thread),
				('/<separator>', None, None),
			]

			if not self.idle_disabled:
				opts.append ((_('/Disable Idle Timeout'), None, self.disable_idle_timeout))
			else:
				opts.append ((_('/Enable Idle Timeout'), None, self.enable_idle_timeout))

			menu = GtkExtra.MenuFactory (GtkExtra.MENU_FACTORY_MENU)
			menu.add_entries (opts)
			menu.popup (None, None, None, event.button, event.time)
			menu.show ()

	def initiate_connect (self):

		self.pixmap.set (self.connect_pixmap, self.connect_mask)
		self.label.set_text (self.get_thread_name () + _("Host: %s\nConnecting...") %(self.opts['host']))

		self.connect = ftp.FTP (
			host=self.opts['host'],
			port=self.opts['port'],
			silence=self.silence
		)

		def handler (msg, self=self):

			gtk.threads_enter ()
			main.app.ftp_error_tell_user (msg)
			gtk.threads_leave ()
			self.done = 1

		self.set_exception_handler (handler)
		self.connect.initiate_connect ()

		# Set ourselves in accordance with the main.app default
		self.set_log_active (main.app['logging'])

		self.connect.get_welcome_msg ()
		self.connect.perform_login (self.opts['username'], self.opts['password'])

	def update_local_listing (self):

		if self.opts['localdir']:
			main.app.update_local_listing (self.opts['localdir'])

	def get_opts (self):

		# Return a copy
		opts = { }
		opts.update (self.opts)
		return opts

	def get_host (self):
		return self.opts['host']

	def get_port (self):
		return self.opts['port']

	def get_retries (self):
		return self.opts['retries']

	def set_log_active (self, bool):

		if bool:
			for key in self.logs.keys ():
				if self.logs[key] is None:
					self.logs[key] = logging.Log (os.path.join (app.CONFIG_DIR, main.app[key]), mark=0)
		else:
			for key in self.logs.keys ():
				if self.logs[key] is not None:
					old_ref = self.logs[key]
					self.logs[key] = None
					del old_ref
		self.logging = bool

	def log (self, direction, user, host, src, dest, size):

		key = direction + '_log'
		if self.logs [key] is not None:
			self.logs[key].write (_("%s@%s: %s -> %s [%d bytes]") %(user, host, src, dest, size))

	def save_transfer_state (self, remote_dir, local_dir, file, size, direction, attempt, type, method):

		self.remote_path = remote_dir
		self.local_path = local_dir
		self.filename = file
		self.size = size
		self.direction = direction
		self.attempt = attempt
		self.type = type
		self.method = method

	def initiate_transfer (self, remote_dir, local_dir, file, size, direction, attempt, method):

		self.save_transfer_state (remote_dir, local_dir, file, size, direction, attempt, 'f', method)
		self.connect.cwd (remote_dir)
		self.progress = (0, size)
		self.progressbar.update (0.0)
		self.start_time = time.time ()
		self.update_transfer_estimate ()

		# For whether or not we want to resume
		rest = None

		if self.direction == ftp.DOWNLOAD:
			local_file = os.path.join (local_dir, file)

			# Check if the file already exists and whether or not we should resume
			if os.path.exists (local_file) and os.path.isfile (local_file):
				try:
					file_size = os.path.getsize (local_file)
				except OSError, (errno, strerror):
					GtkExtra.message_box (title=_("ftpcube Error"), message=_("ftpcube Error: %s") %strerror, buttons=( [ _("Ok") ]))

				if file_size < size:
					rest = file_size
					self.progress = (0, size - file_size)

			if method == main.app.BINARY:
				self.connect.retrieve_binary (file, local_dir, self, rest=rest)
			else:
				self.connect.retrieve_ascii (file, local_dir, self)
			self.log (_('download'), self.opts['username'], self.opts['host'], string.join ([ remote_dir, file ], '/'), os.path.join (local_dir, file), size)
		elif self.direction == ftp.UPLOAD:
			local_file = os.path.join (local_dir, file)

			# Get the size of the uploading file. If we were given a size that's
			# smaller than it, then we have the size of the remote fragment and
			# we want to perform a resume
			try:
				file_size = os.path.getsize (local_file)
			except OSError, (errno, strerror):
				GtkExtra.message_box (title=_("ftpcube Error"), message=_("ftpcube Error: %s") %strerror, buttons=( [ _("Ok") ]))

			if size < file_size:
				rest = size
				self.progress = (0, file_size - size)

			if method == main.app.BINARY:
				self.connect.upload_binary (file, local_dir, self, rest=rest)
			else:
				self.connect.upload_ascii (file, local_dir, self)
			self.log (_('upload'), self.opts['username'], self.opts['host'], os.path.join (local_dir, file), string.join ([ remote_dir, file ], '/'), size)

	def recurse_directory (self, remote_dir, local_dir, file, size, direction, attempt, method):

		self.save_transfer_state (remote_dir, local_dir, file, size, direction, attempt, 'd', method)
		self.pixmap.set (self.listing_pixmap, self.listing_mask)

		if remote_dir == '/':
			remote_path = remote_dir + file
		else:
			remote_path = "%s/%s" %(remote_dir, file)

		if local_dir == '/':
			local_path = local_dir + file
		else:
			local_path = "%s/%s" %(local_dir, file)

		if direction == ftp.DOWNLOAD:
			if not os.path.exists (local_path):
				try:
					os.makedirs (local_path)
				except OSError, (errno, strerror):
					GtkExtra.message_box (title=_("ftpcube Error"), message=_("ftpcube Error: %s") %strerror, buttons=( [ _("Ok") ]))
					return

			self.connect.cwd (remote_path)

			def callback (listing, remote_path=remote_path, local_path=local_path, direction=direction, method=method, self=self):

				files = [ ]
				for file in listing:
					gtk.threads_enter ()
					entry = main.app.remotewin.parse_list_entry (file)
					gtk.threads_leave ()

					if entry is not None:
						entry = list (entry)
						# Fix up the flags
						entry[3] = entry[3][0]
						# Copy over the size into the truesize entry since
						# we don't care about mangling the size in the thread
						entry.append (entry[1])
						files.append (tuple (entry))

				gtk.threads_enter ()
				main.app.transfer (remote_path, local_path, files, direction, method)
				gtk.threads_leave ()

			# Retreive the new listing and execute the callback
			self.connect.list (callback=callback)
		else:
			self.connect.mkdir (remote_path)
			files = main.app.localwin.read_dir (local_path)
			# Purge the '..' entry
			for f in files:
				if f[0] == '..':
					files.remove (f)
			main.app.transfer (remote_path, local_path, files, direction, method)

	def recurse_link (self, remote_dir, local_dir, file, size, direction, attempt, method):

		def callback (errmsg, remote_dir=remote_dir, local_dir=local_dir, file=file, size=size, direction=direction, \
		              attempt=attempt, method=method, self=self):

			self.recurse_directory (remote_dir, local_dir, file, size, direction, attempt, method)

		# Set the callback handler in case this really wasn't really a directory
		self.connect.set_exception_handler (callback)
		self.initiate_transfer (remote_dir, local_dir, file, size, direction, attempt, method)

	def update_transfer (self, amt):

		if not self.progressbar['visible']:
			self.progress_packer.show ()
			self.progressbar.show ()
			self.pixmap.set (self.download_pixmap, self.download_mask)
		self.idle = 0

		current, total = self.progress
		self.progress = (current + amt, total)
		percentage = float (self.progress[0]) / float (self.progress[1])
		# If the percentage is great than 1.0, such as in the case of a link
		# where we don't know the real size, set it to 1.0
		if percentage > 1.0:
			percentage = 1.0
		self.progressbar.update (percentage)

	def update_transfer_estimate (self):

		# Make the necessary calculations to create the status label
		rate = self.transfer_speed ()
		if rate:
			remaining = float (self.progress[1] - self.progress[0]) / (rate * 1024.0)
		else:
			remaining = 0
		remaining = int (remaining)
		hrs = remaining / 3600
		min = (remaining % 3600) / 60
		sec = remaining % 60

		if self.direction == ftp.DOWNLOAD:
			self.label.set_text (self.get_thread_name () + _("Host: %s\nDownloading %s with %.02f kb/s, %d of %d bytes, %d:%2d:%2d sec left")
			                     %(self.opts['host'], self.filename, rate, self.progress[0],
			                       self.progress[1], hrs, min, sec))
		elif self.direction == ftp.UPLOAD:
			self.label.set_text (self.get_thread_name () + _("Host: %s\nUploading %s with %.02f kb/s, %d of %d bytes, %d:%2d:%2d sec left")
			                     %(self.opts['host'], self.filename, rate, self.progress[0],
			                       self.progress[1], hrs, min, sec))

	def isTransfering (self):

		current, total = self.progress
		if current == 0 or current == total:
			return 0
		else:
			return 1

	def transfer_speed (self):

		elapsed = time.time () - self.start_time
		if elapsed:
			return (float (self.progress[0]) / float (elapsed)) / 1024 # In KB
		else:
			return 0

	def cancel_download (self, button):

		self.label.set_text (self.get_thread_name () + _("Host: %s\nCancelling download...\n") %(self.opts['host']))
		self.pixmap.set (self.abort_pixmap, self.abort_mask)
		self.connect.abort ()

	def cancel_thread (self, button=None):

		if self.busy ():
			self.cancel_download (button)
		self.done = 1

	def abort (self):

		self.pixmap.set (self.abort_pixmap, self.abort_mask)
		self.label.set_text (self.get_thread_name () + _("Host: %s\nAborting...\n") %(self.opts['host']))
		self.idle = 0
		self.connect.abort ()

	def disable_idle_timeout (self, button):
		self.idle_disabled = 1

	def enable_idle_timeout (self, button):

		self.idle = 0
		self.idle_disabled = 0

	def reset (self):
		self.connect.reset ()

	def set_exception_handler (self, callback):
		self.connect.set_exception_handler (callback)

	def clear_exception_handler (self):
		self.connect.clear_exception_handler ()

	def finished (self):

		if not self.done:
			return self.done
		else:
			if self.connect is not None:
				self.connect.destroy ()
			return self.done

	def list (self):

		self.pixmap.set (self.listing_pixmap, self.listing_mask)
		self.label.set_text (self.get_thread_name () + _("Host: %s\nGetting Dir Listing for %s\n")
		                     %(self.opts['host'], main.app.get_remote_dir ()))
		self.idle = 0
		self.connect.list ()

	def delete (self, file):

		self.pixmap.set (self.action_pixmap, self.action_mask)
		self.label.set_text (self.get_thread_name () + _("Host: %s\nDeleting File...\n") %(self.opts['host']))
		self.idle = 0
		self.connect.delete (file)

	def rename (self, name, new_name):

		self.pixmap.set (self.action_pixmap, self.action_mask)
		self.label.set_text (self.get_thread_name () + _("Host: %s\nRenaming %s to %s...\n") %(self.opts['host'], name, new_name))
		self.idle = 0
		self.connect.rename (name, new_name)

	def mkdir (self, dir):

		self.pixmap.set (self.action_pixmap, self.action_mask)
		self.label.set_text (self.get_thread_name () + _("Host: %s\nCreating Directory...\n") %(self.opts['host']))
		self.idle = 0
		self.connect.mkdir (dir)

	def rmdir (self, file):

		self.pixmap.set (self.action_pixmap, self.action_mask)
		self.label.set_text (self.get_thread_name () + _("Host: %s\nRemoving Directory...\n") %(self.opts['host']))
		self.idle = 0
		self.connect.rmdir (file)

	def cwd (self, dir):

		self.pixmap.set (self.action_pixmap, self.action_mask)
		self.label.set_text (self.get_thread_name () + _("Host: %s\nChanging Directory...\n") %(self.opts['host']))
		self.idle = 0
		self.connect.cwd (dir)

	def update_idle (self):

		self.idle = self.idle + 1
		self.label.set_text (self.get_thread_name () + _("Host: %s\nIdle for %d secs") %(self.opts['host'], self.idle))
		self.pixmap.set (self.idle_pixmap, self.idle_mask)

	def busy (self):

		# If self.connect is none, then there was an error at some point
		# so we consider ourselves busy
		if self.connect is None:
			return 1
		else:
			return self.connect.ftp_busy ()

	def get_entry (self):
		return self.entry_box

class MainThread (FtpThread):

	def __init__ (self, connect_opts, name=None):

		FtpThread.__init__ (self, connect_opts, name=name)

		self.abort_conn = 0

	def initiate_connect (self):

		self.abort_conn = 0
		self.pixmap.set (self.connect_pixmap, self.connect_mask)
		self.label.set_text (self.get_thread_name () + _("Host: %s\nConnecting...") %(self.opts['host']))

		self.attempt = 0

		self.connect = ftp.FTP (
			host=self.opts['host'],
			port=self.opts['port'],
			silence=self.silence,
			logging=main.app['logging'],
		)

		self.connect.set_exception_handler (self.connect_exception_handler)
		self.connect.initiate_connect ()
		self.connect.set_exception_handler (self.connect_exception_handler)
		self.connect.get_welcome_msg ()
		self.connect.set_exception_handler (self.connect_exception_handler)
		self.connect.perform_login (self.opts['username'], self.opts['password'])

		# This will get wiped out of the queue if the handler is called
		self.complete_connection ()

	def complete_connection (self):

		if not self.opts['remotedir']:
			self.opts['remotedir'] = '/'
		self.connect.cwd (self.opts['remotedir'])
		main.app.update_remote_status_dir (self.opts['remotedir'])
		self.connect.list ()

	def retry_connect (self, attempt):

		self.attempt = attempt

		self.connect = ftp.FTP (
			host = self.opts['host'],
			port = self.opts['port'],
			silence=self.silence,
			logging=main.app['logging'],
		)

		self.connect.set_exception_handler (self.connect_exception_handler)
		self.connect.initiate_connect ()
		self.connect.get_welcome_msg ()
		self.connect.perform_login (self.opts['username'], self.opts['password'])

		# This will get wiped out of the queue if the handler is called
		self.complete_connection ()

	def abort (self):

		if not self.abort_conn:
			self.abort_conn = 1
		else:
			FtpThread.abort (self)

	def connect_exception_handler (self, msg):

		if self.attempt == self.opts['retries']:
			gtk.threads_enter ()
			main.app.ftp_error_tell_user (_("Maximum connection attempt hit. Aborting connection..."))
			gtk.threads_leave ()
			self.done = 1
		else:
			# Wait a bit
			delay = self.opts['delay']
			gtk.threads_enter ()
			main.app.ftp_error_tell_user (_("Waiting %d secs before re-attempting connection...") %delay)
			gtk.threads_leave ()

			slept = 0
			while slept < delay:
				time.sleep (1)
				slept = slept + 1

				# If we should abort, cancel the connection thread
				if self.abort_conn:
					self.done = 1
					break

			if self.connect is not None:
				gtk.threads_enter ()
				self.connect.destroy ()
				gtk.threads_leave ()
				self.connect = None

			if not self.abort_conn:
				self.retry_connect (self.attempt + 1)

	def initiate_transfer (self, remote_dir, local_dir, file, size, direction, attempt, method):

		old_dir = main.app.get_remote_dir ()
		FtpThread.initiate_transfer (self, remote_dir, local_dir, file, size, direction, attempt, method)
		self.connect.cwd (old_dir)

	def recurse_directory (self, remote_dir, local_dir, file, size, direction, attempt, method):

		old_dir = main.app.get_remote_dir ()
		FtpThread.recurse_directory (self, remote_dir, local_dir, file, size, direction, attempt, method)
		self.connect.cwd (old_dir)

	def recurse_link (self, remote_dir, local_dir, file, size, direction, attempt, method):

		old_dir = main.app.get_remote_dir ()
		FtpThread.recurse_link (self, remote_dir, local_dir, file, size, direction, attempt, method)
		self.connect.cwd (old_dir)

	def update_transfer (self, amt):

		# Explicitly set the pixmap every time since we may have received
		# some asynch action that might have changed it.
		self.pixmap.set (self.download_pixmap, self.download_mask)
		FtpThread.update_transfer (self, amt)

	def update_idle (self):

		FtpThread.update_idle (self)

		if self.progressbar['visible']:
			self.progressbar.hide ()
			self.progress_packer.hide ()

		# Check if we need to send a noop
		if (self.idle % self.opts['timeout']) == 0:
			self.connect.noop ()

		# Check if we should disconnect
		if self.idle == self.opts['main_idle']:
			self.cancel_thread ()

	def set_log_active (self, bool):

		self.connect.set_log_active (bool)
		FtpThread.set_log_active (self, bool)

class TransferThread (FtpThread):

	def __init__ (self, connect_opts, name=None):

		FtpThread.__init__ (self, connect_opts, name=name, silence=1)

		self.connect.set_exception_handler (self.transfer_exception_handler)

	def transfer_exception_handler (self, msg):

		gtk.threads_enter ()
		main.app.add_to_failure_list ({
			'server'      : self.opts['host'],
			'file'        : self.filename,
			'size'        : str (self.size),
			'error'       : str (msg),
			'direction'   : str (self.direction),
			'port'        : str (self.opts['port']),
			'username'    : self.opts['username'],
			'password'    : self.opts['password'],
			'remote_path' : self.remote_path,
			'local_path'  : self.local_path,
			'attempt'     : self.attempt,
			'type'        : self.type,
			'method'      : str (self.method),
		})
		gtk.threads_leave ()

	def update_idle (self):

		if not self.idle_disabled:
			FtpThread.update_idle (self)
		if self.idle >= self.opts['thread_idle']:
			self.done = 1

		if self.progressbar['visible']:
			self.progressbar.hide ()
			self.progress_packer.hide ()
