#
# 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 sys, os, pickle
import cmdwin, ctrlwin, filewin, connectwin, bookmarkwin
import optionwin, aboutwin
import main, threads, defaults

from intl import _

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

if os.environ.has_key ('HOME'):
	CONFIG_DIR = os.path.join (os.environ['HOME'], ".ftpcube")
elif os.environ.has_key ('HOMEPATH'):
	CONFIG_DIR = os.path.join (os.environ['USERPROFILE'], "FtpCube Config")
else:
	CONFIG_DIR = main.PREFIX

class Application:

	"""Main application object

	The main application object sets up the framework for the main application
	window by drawing both the menu, toolbars, and skeleton boxes. Subsequent
	GUI objects are then created and given the skeleton boxes as their containers.
	The main application object also provides some interface functions that let
	various sub objects access information from other sub objects. There should
	only be one application instance, and subsequent objects expect to find the
	instance as main.app."""

	BINARY = 0
	ASCII = 1

	def __init__ (self, title):
		"""Initializes the application instance, creates the main menu and toolbar,
		and arranges the skeleton of boxes that will contain the various compound
		widgets that make up the main window."""

		# Initialize instance variables
		self.opts = { }
		self.main_thread = None # Need to set this to None explicitly

		self.config_file = self.check_configuration_files ()

		# Create the main window and set the appropriate icon
		self.main_window = gtk.GtkWindow (gtk.WINDOW_TOPLEVEL)
		self.main_window.set_title (title)
		self.main_window.connect ('destroy', self.quit)
		icon_pic, icon_mask = gtk.create_pixmap_from_xpm (self.main_window, None, os.path.join (main.ICONS_PREFIX, "ftpcube.xpm"))
		self.main_window.set_icon (gtk.GtkPixmap (icon_pic, icon_mask))

		# Load the window size preset if we have one, otherwise, default to 75% of the
		# screen size
		width = None
		height = None
		if self.opts.has_key ('win_size'):
			width, height = self['win_size']
		if width and height:
			self.set_size (width, height)
		else:
			width = int (gtk.screen_width () * 0.75)
			height = int (gtk.screen_height () * 0.75)
			self.main_window.set_default_size (width, height)

		packer = gtk.GtkPacker ()
		self.main_window.add (packer)
		packer.show ()

		self.menu_init (packer)
		self.toolbar_init (packer)

		vert_pane = gtk.GtkVPaned ()
		vert_pane.show ()
		packer.add (vert_pane, side=gtk.SIDE_TOP, anchor=gtk.ANCHOR_NW, options=GTK.PACK_EXPAND+GTK.FILL_X+GTK.FILL_Y)

		# Create the command window
		box = gtk.GtkVBox ()
		self.cmdwin = cmdwin.CmdWin (box)
		box.show ()
		vert_pane.add1 (box)

		# Create a paned thing for the fileview
		outer_paned = gtk.GtkHPaned ()
		inner_paned = gtk.GtkHPaned ()
		outer_paned.show ()
		inner_paned.show ()
		vert_pane.add2 (outer_paned)

		# Create the control window
		ctrl_box = gtk.GtkFrame ()
		ctrl_box.set_usize (250, -1)
		self.ctrlwin = ctrlwin.CtrlWin (ctrl_box)
		outer_paned.add1 (ctrl_box)
		ctrl_box.show ()
		outer_paned.add2 (inner_paned)
		# Set some parameters of the control window
		self.ctrlwin.download_tab.set_max_attempts (self.opts['retries'])
		self.ctrlwin.threads_tab.set_max_threads (3)

		# Create the local window
		# Calculate the size given the previous box so our panes look even
		box = gtk.GtkFrame ()
		self.localwin = filewin.LocalWin (box)
		inner_paned.add1 (box)
		box.show ()

		# Create the remote window
		box = gtk.GtkFrame ()
		self.remotewin = filewin.RemoteWin (box)
		inner_paned.add2 (box)
		box.show ()

		# Now set the slider accordingly
		size = (width - ctrl_box.size_request ()[0]) / 2
		inner_paned.set_position (size)

		self.main_window.show ()

	def __getitem__ (self, key):
		return self.opts[key]

	def __setitem__ (self, key, val):

		self.opts[key] = val
		return self.opts[key]

	def update (self, hash):
		self.opts.update (hash)

	def has_key (self, key):
		return self.opts.has_key (key)

	def keys (self):
		return self.opts.keys ()

	def get_opt_hash (self):

		opts = { }
		for key in self.keys ():
			opts[key] = self[key]
		return opts

	def check_configuration_files (self):
		"""Checks to see if application configuration data exists, and
		loads it if it does. If the directory containing the configuration
		information does not exist, it is created. Appropriate defaults
		are loaded if the configuration file doesn't exist."""

		config_file = os.path.join (CONFIG_DIR, "config")

		# Create configuration directory and files if necessary
		if not os.path.exists (CONFIG_DIR):
			try:
				os.mkdir (CONFIG_DIR)
				os.chmod (CONFIG_DIR, 0700)
			except OSError, strerror:
				GtkExtra.message_box (title=_("ftpcube Error"), message=_("ftpcube Error: %s") %strerror, buttons=( [ _("Ok") ]))
		
		if not os.path.exists (config_file):
			self.save_config (config_file, defaults.defaults)

		# Load the configuration file
		options = self.load_config (config_file)
		for o in options.keys ():
			self[o] = options[o]
		return config_file

	def get_config_file (self):
		return self.config_file

	def load_config (self, path):
		"""Returns a hash containing the configuration data found in
		the file at 'path'. The configuration data is a hash that is
		stored in pickle format."""

		file = open (path, 'r')
		pickler = pickle.Unpickler (file)
		config = pickler.load ()
		return config

	def save_config (self, path, opts):
		"""Saves the configuration data in the hash 'opts' into a
		pickled file found at 'path'."""

		file = open (path, 'w')
		pickler = pickle.Pickler (file)
		pickler.dump (opts)
		file.close ()

		# Set the file mode after every save just in case
		try:
			os.chmod (path, 0600)
		except OSError, strerror:
			GtkExtra.message_box (title=_("ftpcube Error"), message=_("ftpcube Error: %s") %strerror, buttons=( [ _("Ok") ]))

	def get_size (self):
		"""Returns a tuple containing the allocated width and height of the main window."""

		x, y, w, h = self.main_window.get_allocation ()
		size = (w, h)
		return size

	def set_size (self, width, height):
		"""Sets the size of the main window."""

		if width and height:
			self.main_window.set_default_size (width, height)

	def menu_init (self, container):
		"""Initializes and draws the main menu as well as sets the menu handlers."""

		# Add the keyboard accelerators to the main window
		ag = gtk.GtkAccelGroup ()
		item = gtk.GtkItemFactory (gtk.GtkMenuBar, "<main>", ag)
		self.main_window.add_accel_group (ag)

		# Assign references to menu handlers
		file = self.menu_process_file
		local = self.menu_process_local
		remote = self.menu_process_remote
		help = self.menu_process_help

		item.create_items ([
			(_('/_File'), None, None, 0, '<Branch>'),
			(_('/_File/_Connect'), None, file, 1, ''),
			(_('/_File/_Quick Connect'), None, file, 2, ''),
			(_('/_File/_Bookmarks'),  None, file, 3, ''),
			(_('/_File/_Disconnect'), None, file, 4, ''),
			(_('/_File/<Separator>'), None, None, 0, '<Separator>'),
			(_('/_File/_Options'), None, file, 5, ''),
			(_('/_File/<Separator>'), None, None, 0, '<Separator>'),
			(_('/_File/E_xit'), '<alt>F4', file, 6,''),

			(_('/_Local/_Upload Files'), None, local, 1, ''),
			(_('/_Local/_Rename Files'), None, local, 2, ''),
			(_('/_Local/_Delete Files'), None, local, 3, ''),
			(_('/_Local/<Separator>'), None, None, 0, '<Separator>'),
			(_('/_Local/_Create Directory'), None, local, 4,   ''),
			(_('/_Local/C_hange Directory'), None, local, 5, ''),

			(_('/_Remote/_Download Files'), None, remote, 1, ''),
			(_('/_Remote/_Rename Selected'), None, remote, 2, ''),
			(_('/_Remote/D_elete Files'), None, remote, 3, ''),
			(_('/_Remote/<Separator>'), None, None, 0, '<Separator>'),
			(_('/_Remote/_Create Directory'), None, remote, 4, ''),
			(_('/_Remote/Remo_ve Directories'), None, remote, 5, ''),
			(_('/_Remote/C_hange Directory'),  None, remote, 6, ''),

			(_('/_Help'), None, None, 0, '<Branch>'),
			(_('/_Help/_About'), None, help, 1, '')
		])
		mainmenu = item.get_widget ('<main>')

		# Now add the menu bar into the canvas
		menubox = gtk.GtkVBox ()
		menubox.pack_start (mainmenu, expand=gtk.FALSE)
		mainmenu.show ()
		menubox.show ()
		container.add (menubox, side=gtk.SIDE_TOP, anchor=gtk.ANCHOR_NW, options=GTK.FILL_X)

	def menu_process_file (self, action, widget):
		"""Invokes the appropriate handler for a menu option under the
		file menu."""

		if action == 0:
			pass
		elif action == 1:
			self.connect_window ()
		elif action == 2:
			self.quick_connect_window ()
		elif action == 3:
			self.bookmark_window ()
		elif action == 4:
			self.disconnect ()
		elif action == 5:
			self.options_window ()
		elif action == 6:
			self.quit (self)

	def menu_process_local (self, action, widget):
		"""Invokes the appropriate handler for a menu option under the
		file menu."""

		if action == 1:
			self.localwin.upload_selected (None, None)
		elif action == 2:
			self.localwin.rename_selected (None, None)
		elif action == 3:
			self.localwin.delete_selected (None, None)
		elif action == 4:
			self.localwin.create_directory (None, None)
		elif action == 5:
			self.localwin.cd_selected (None, None)

	def menu_process_remote (self, action, widget):
		"""Invokes the appropriate handler for a menu option under the
		remote  menu."""

		if self.main_thread:
			if action == 1:
				self.remotewin.download_selected (None, None)
			elif action == 2:
				self.remotewin.rename_selected (None, None)
			elif action == 3:
				self.remotewin.delete_selected (None, None)
			elif action == 4:
				self.remotewin.mkdir_selected (None, None)
			elif action == 5:
				self.remotewin.rmdir_selected (None, None)
			elif action == 6:
				self.remotewin.cd_selected (None, None)

	def menu_process_help (self, action, widget):
		"""Invokes the appropriate hanlder for a menu option under
		the help menu."""

		if action == 1:
			self.about_window ()

	def toolbar_init (self, container):
		"""Initializes and draws the main toolbar, as well as sets the
		button handlers."""

		toolbox = gtk.GtkHBox ()
		toolbar = gtk.GtkToolbar (gtk.ORIENTATION_HORIZONTAL, gtk.TOOLBAR_ICONS)
		toolbar.set_space_size (10)
		toolbar.set_space_style (GTK.TOOLBAR_SPACE_LINE)

		connect_pic, connect_mask = gtk.create_pixmap_from_xpm (self.main_window, None, os.path.join (main.ICONS_PREFIX, "connect.xpm"))
		quick_pic, quick_mask = gtk.create_pixmap_from_xpm (self.main_window, None, os.path.join (main.ICONS_PREFIX, "quick_connect.xpm"))
		bookmark_pic, bookmark_mask = gtk.create_pixmap_from_xpm (self.main_window, None, os.path.join (main.ICONS_PREFIX, "bookmark.xpm"))
		abort_pic, abort_mask = gtk.create_pixmap_from_xpm (self.main_window, None, os.path.join (main.ICONS_PREFIX, "abort.xpm"))
		refresh_pic, refresh_mask = gtk.create_pixmap_from_xpm (self.main_window, None, os.path.join (main.ICONS_PREFIX, "refresh.xpm"))
		disconnect_pic, disconnect_mask = gtk.create_pixmap_from_xpm (self.main_window, None, os.path.join (main.ICONS_PREFIX, "disconnect.xpm"))
		exit_pic, exit_mask = gtk.create_pixmap_from_xpm (self.main_window, None, os.path.join (main.ICONS_PREFIX, "exit.xpm"))

		def connect_button (button, toolbar=toolbar, self=self):
			self.connect_window ()

		def quick_connect_button (button, toolbar=toolbar, self=self):
			self.quick_connect_window ()

		def bookmark_button (button, toolbar=toolbar, self=self):
			self.bookmark_window ()

		def abort_button (button, toolbar=toolbar, self=self):
			self.abort ()

		def refresh_button (button, toolbar=toolbar, self=self):

			if self.main_thread is not None:
				self.remotewin.refresh_selected (None, None)

		def disconnect_button (button, toolbar=toolbar, self=self):
			self.disconnect ()

		def quit_button (button, toolbar=toolbar, self=self):
			self.quit (self.main_window)

		toolbar.append_item (_("Normal Connect"), _("Normal Connect"),
			_("Normal Connect"),
			gtk.GtkPixmap (connect_pic, connect_mask),
			connect_button)
		toolbar.append_item (_("Quick Connect"), _("Quick Connect"),
			_("Quick Connect"),
			gtk.GtkPixmap (quick_pic, quick_mask),
			quick_connect_button)
		toolbar.append_item (_("Disconnect"), _("Disconnect"),
			_("Disconnect"),
			gtk.GtkPixmap (disconnect_pic, disconnect_mask),
			disconnect_button)
		toolbar.append_item (_("Abort"), _("Abort Current Actions"),
			_("Abort Current Actions"),
			gtk.GtkPixmap (abort_pic, abort_mask),
			abort_button)
		toolbar.append_item (_("Refresh"), _("Refresh"),
			_("Refresh"),
			gtk.GtkPixmap (refresh_pic, refresh_mask),
			refresh_button)
		toolbar.append_space ()
		toolbar.append_item (_("Bookmarks"), _("Bookmarks"),
			_("Bookmarks"),
			gtk.GtkPixmap (bookmark_pic, bookmark_mask),
			bookmark_button)
		toolbar.append_space ()
		toolbar.append_item (_("Quit"), _("Quit FtpCube"),
			_("Quit FtpCube"),
			gtk.GtkPixmap (exit_pic, exit_mask),
			quit_button)

		toolbar.show ()
		toolbox.pack_start (toolbar)
		toolbox.show ()

		# Now create the transfer type buttons
		self.ascii_button = gtk.GtkRadioButton (None, _("ASCII"))
		self.ascii_button.show ()
		self.ascii_button.connect ("clicked", self.ascii_clicked)
		toolbox.pack_end (self.ascii_button, expand=GTK.FALSE)
		self.binary_button = gtk.GtkRadioButton (self.ascii_button, _("BINARY"))
		self.binary_button.show ()
		self.binary_button.connect ("clicked", self.binary_clicked)
		toolbox.pack_end (self.binary_button, expand=GTK.FALSE, padding=2)

		# Add the entry box in before the transfer type buttons
		self.dir_entry_list = [ ]
		self.dir_entry_box = gtk.GtkCombo ()
		self.dir_entry_box.entry.connect ("key_press_event", self.entry_key_pressed)
		self.dir_entry_box.list.connect ("button_press_event", self.entry_clicked)
		self.dir_entry_box.disable_activate ()
		self.dir_entry_box.show ()
		toolbox.pack_end (self.dir_entry_box, padding=5)

		if self['transfer_type'] == self.ASCII:
			self.ascii_button.set_active (gtk.TRUE)
		else:
			self.binary_button.set_active (gtk.TRUE)

		container.add (toolbox, side=gtk.SIDE_TOP, anchor=gtk.ANCHOR_NW, options=GTK.FILL_X)

	def ascii_clicked (self, button):
		"""A handler for when the ascii radio button is clicked."""

		self['transfer_type'] = self.ASCII

	def binary_clicked (self, button):
		"""A handler for when the binary radio button is clicked."""

		self['transfer_type'] = self.BINARY

	def connect_window (self):
		"""A handler for bringing up the connection window. If there is currently
		an active connection, a disconnection dialogue is displayed."""

		if self.main_thread is None:
			connectwin.ConnectWin (self.initiate_connection, self)
		else:
			self.display_disconnect_box (self.main_thread.get_host (), 'ConnectWin')

	def quick_connect_window (self):
		"""A handler for bringing up the quick connection window. If there is currently
		an active connection, a disconnection dialogue is displayed."""

		if self.main_thread is None:
			connectwin.QuickConnectWin (self.initiate_connection, self)
		else:
			self.display_disconnect_box (self.main_thread.get_host (), 'QuickConnectWin')

	def bookmark_window (self):
		"""A handler for bringing up the bookmark window"""

		bookmarkwin.BookmarkWin (CONFIG_DIR)

	def display_disconnect_box (self, host, type, opts=None):
		"""Displays a disconnection dialogue. If the user chooses to disconnect, the
		current connection is severed and the appropriate connection window displayed."""

		choice = GtkExtra.message_box (title=_("ftpcube - Disconnect?"),
		                      message=_("You are currently connected to: %s\nDo you wish to break the connection and connect to a new host?") %host,
		                      buttons=([ _("Ok"), _("Cancel") ]))

		if choice == _("Ok"):
			self.disconnect ()
			if opts is None:
				opts = self

			if type == 'ConnectWin':
				connectwin.ConnectWin (self.initiate_connection, opts)
			elif type == 'QuickConnectWin':
				connectwin.QuickConnectWin (self.initiate_connection, opts)

	def disconnect (self):
		"""Disconnects the main thread from the current active connection."""

		if self.main_thread is not None:
			self.main_thread.cancel_thread ()

		# Clear remote status stuff
		self.remotewin.clear_cache ()
		self.dir_entry_list = [ ]
		self.dir_entry_box.entry.set_text ('')

	def options_window (self):
		"""A handler for bringing up the option window."""

		optionwin.OptionWindow ()

	def about_window (self):
		"""A handler for bringing up the about window."""

		aboutwin.AboutWindow ()

	def initiate_connection (self, connectwin):
		"""Extracts the connection information (a hash) from the 'connectwin'
		object and creates a main thread object with the connection information."""

		opts = { }
		# Seed the options with the main ones
		opts.update (self.opts)
		# Update the defaults to reflect the connect window
		opts.update (connectwin.get_options ())

		self.main_thread = threads.MainThread (opts, name=_("MainThread"))
		self.ctrlwin.attach_main_thread (self.main_thread)

	def abort (self):
		"""Attempts to abort the current action of the main thread."""

		if self.main_thread is not None:
			self.main_thread.abort ()

	def display_banner (self):
		"""Prints the initial greeting in the command window."""

		self.cmdwin.display_banner ()

	def ftp_local_tell_user (self, msg):
		self.cmdwin.insert_color_text ('local', msg)

	def ftp_remote_tell_user (self, msg):
		self.cmdwin.insert_color_text ('remote', msg)

	def ftp_error_tell_user (self, msg):
		self.cmdwin.insert_color_text ('error', msg)

	def update_remote_status_dir (self, dir):
		self.remotewin.update_status_dir (dir)

	def entry_key_pressed (self, entry, event):

		# Update with an enter
		if self.main_thread and event.string == '\r':
			self.entry_action ()

	def entry_clicked (self, button, event):

		if self.main_thread:
			self.entry_action ()

	def entry_action (self):

		dir = self.dir_entry_box.entry.get_text ()
		self.main_thread.cwd (dir)
		self.remotewin.update_status_dir (dir)
		self.remotewin.get_listing ()

	def update_dir_entry_status (self, dir):

		if not dir in self.dir_entry_list:
			self.dir_entry_list.append (dir)

		self.dir_entry_box.set_popdown_strings (self.dir_entry_list)
		self.dir_entry_box.entry.set_text (dir)

	def get_ftp_listing (self):
		self.main_thread.list ()

	def update_remote_listing (self, dir):
		self.remotewin.update_listing (dir)

	def update_local_listing (self, dir):
		self.localwin.update_listing (dir)

	def get_local_dir (self):
		return self.localwin.get_dir ()

	def get_remote_dir (self):
		return self.remotewin.get_dir ()

	def update_list_transfer (self, amt):
		"""Updates the status bar in the remote file window to display the
		number of bytes currently transferred."""

		remote_dir = self.get_remote_dir ()
		self.remotewin.update_status (_("Getting listing for %s: %d bytes") %(remote_dir, amt))

	def list_transfer_finished (self):

		self.remotewin.update_status_dir (self.remotewin.get_dir ())

	def transfer (self, remote_path, local_path, files, direction, method):
		"""Adds the list of files into the transfer queue via the control
		window object."""

		main_opts = self.main_thread.get_opts ()

		self.ctrlwin.add_to_queue ({
			'files'       : files,
			'server'      : main_opts['host'],
			'port'        : main_opts['port'],
			'username'    : main_opts['username'],
			'password'    : main_opts['password'],
			'remote_path' : remote_path,
			'local_path'  : local_path,
			'maxattempts' : main_opts['retries'],
			'direction'   : direction,
			'method'      : method
		})

	def get_thread_list (self):
		return self.ctrlwin.get_thread_list ()

	def add_to_download_list (self, file):
		self.ctrlwin.add_to_downloads (file)

	def add_to_failure_list (self, hash):
		self.ctrlwin.add_to_failure (hash)

	def get_cwd (self):
		return self.localwin.get_dir ()

	def get_file_size (self, file):
		return self.remotewin.get_file_size (file)

	def quit (self, win):

		# Save the size before exiting
		self['win_size'] = self.get_size ()
		config_file = os.path.join (CONFIG_DIR, "config")
		self.save_config (config_file, self.get_opt_hash ())

		gtk.mainquit ()

# Conversion function from bytes sizes to pretty strings
def beautify_size (size):
	"""Returns a string describing the number of bytes 'size'
	in Mneumonic form."""

	if size / 1073741824:
		return _("%0.2f Gbytes") %(float (size) / 1073741824.0)
	elif size / 1048576:
		return _("%0.2f Mbytes") %(float (size) / 1048576.0)
	elif size / 1024:
		return _("%0.2f KBytes") %(float (size) / 1024.0)
	else:
		return _("%s Bytes") %(size)
	return None
