// FileTreeRemote.cs
//
//  Copyright (C) 2008-2009 Christian Eide
//
// 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
//

using System;
using System.Collections.Generic;
using System.Threading;
using Gtk;
using bareFTP.Protocol;
using bareFTP.Connection;
using bareFTP.Common.Utils;
using Mono.Unix;

namespace bareFTP.Gui.FileManager
{
	public delegate void ExternalUploadRequestHandler(List<string> localFiles, FileTreeNode rnode);
	
	public class FileManagerRemote : Gtk.VBox
	{
		FileList filelist;
		Gtk.Entry pathEntry;
		bool isConnected;
		Connection.Connection conn;
		
		private FileListCache cache;
		private bareFTP.Protocol.BarePath cwd;
		private List<RemoteFile> currentDirFileList;
		
		public event System.EventHandler UploadRequest;
		public event System.EventHandler DownloadRequest;
		public event System.EventHandler UserAction;
		public event System.EventHandler StatusBarMessage;
		public event System.EventHandler DirChanged;
		
		public event ExternalUploadRequestHandler ExternalUploadRequest;
		
		private System.Threading.EventWaitHandle ewh;
		private Gui.ActivityProgressBar progress;
		private Button btnup;
		private Button btnnewfolder;
		private string showhiddenstr;
		private bool disabled = false;
		private bareFTP.Preferences.Config config;
		
		public new bool HasFocus
		{
			get { return filelist.HasFocus; }	
		}
		
		public FileManagerRemote(bareFTP.Preferences.Config config, Gui.ActivityProgressBar progressBar) : base(false, 1)
		{
			this.config = config;
			
			ewh = new System.Threading.EventWaitHandle(false, System.Threading.EventResetMode.AutoReset);
			progress = progressBar;
			cwd = new BarePath();
			isConnected = false;
			conn = null;
			Gtk.HBox hbox = new Gtk.HBox(false, 1);
			
			pathEntry = new Gtk.Entry();
			pathEntry.Activated += new EventHandler(OnPathEntryActivated);
			hbox.PackStart(pathEntry);
			
			btnup = new Gtk.Button();
			btnup.CanFocus = true;
			Gtk.Image w44 = new Gtk.Image();
            w44.Pixbuf = Gtk.IconTheme.Default.LoadIcon(Gtk.Stock.GoUp, 16, 0);
			w44.TooltipText = Catalog.GetString("Parent Directory");
            btnup.Add(w44);
            btnup.Label = null;
			btnup.Relief = ReliefStyle.None;
            btnup.Clicked += new EventHandler(DirUpClicked); 
            hbox.PackStart(btnup, false, false, 0);
            
            btnnewfolder = new Gtk.Button();
			btnnewfolder.CanFocus = true;
			Gtk.Image w45 = new Gtk.Image();
            if(Gtk.IconTheme.Default.HasIcon("stock_new-dir"))
				w45.Pixbuf = Gtk.IconTheme.Default.LoadIcon("stock_new-dir", 16, 0);
			else
				w45.Pixbuf = Gtk.IconTheme.Default.LoadIcon(Gtk.Stock.Directory, 16, 0);
			w45.TooltipText = Catalog.GetString("New Directory");
            btnnewfolder.Add(w45);
            btnnewfolder.Label = null;
			btnnewfolder.Relief = ReliefStyle.None;
            btnnewfolder.Clicked += new System.EventHandler(OnNewDirClicked);
			hbox.PackStart(btnnewfolder, false, false, 0);
			
			this.PackStart(hbox, false, false, 0);
			
			Gtk.ScrolledWindow scrollw = new Gtk.ScrolledWindow();
			filelist = new FileList(new Gtk.ListStore (typeof (FileTreeNode)), bareFTP.Protocol.ProtocolType.UNDEFINED, config);
			
			Gtk.TargetEntry[] te1 = new TargetEntry[] { 
				new TargetEntry("treeViewLocal", Gtk.TargetFlags.App, 0),
				new TargetEntry("text/uri-list", Gtk.TargetFlags.OtherApp, 1)};
			
			Gtk.TargetEntry[] te2 = new TargetEntry[] { 
				new TargetEntry("text/uri-list", Gtk.TargetFlags.OtherApp, 1),
				new TargetEntry("treeViewRemote", Gtk.TargetFlags.App, 0)};
			
			//filelist.EnableModelDragSource(Gdk.ModifierType.Button1Mask, te2, Gdk.DragAction.Move | Gdk.DragAction.Move);
			Gtk.Drag.SourceSet (filelist, Gdk.ModifierType.Button1Mask, te2, Gdk.DragAction.Copy);
			Gtk.Drag.DestSet(filelist, DestDefaults.All, te1, Gdk.DragAction.Copy);
			//filelist.EnableModelDragDest(te1, Gdk.DragAction.Copy);
			
			filelist.DragDataGet += OnDragDataGet;
			filelist.DragDataReceived += OnDragDataReceived;
			
			filelist.ButtonPressEvent += OnButtonPress;
			filelist.ButtonReleaseEvent += OnButtonRelease;
			
			filelist.DirectoryChanged += new DirectoryChangedHandler(OnDirChanged);
			filelist.NewDirectoryNamed += DirectoryNamed;
			filelist.FileRenameRequest += new FileRenameRequestHandler(OnFileRenameRequest);
			filelist.DeleteRequest += new DeleteRequestHandler(OnDeleteRequest);
			filelist.TransferRequest += new TransferRequestHandler(OnTransferRequested);
			filelist.ChmodRequested += new ChmodRequestHandler(OnChmodRequested);
			filelist.LinkActivated += new LinkActivatedHandler(OnLinkActivated);
			filelist.SendCommandRequested += HandleFilelistSendCommandRequested;
			filelist.NewDirRequested += OnNewDirClicked;
			filelist.CdUp += CdUp;;
			filelist.ColumnWidths = config.GUI_RightColWidths;
			scrollw.Add(filelist);
			scrollw.ShadowType = Gtk.ShadowType.EtchedIn;
			
			pathEntry.Sensitive = false;
			
			this.PackStart(scrollw);
		}

		void HandleFilelistSendCommandRequested (object sender, EventArgs e)
		{
			conn.SendCommand(sender.ToString());
		}
		
		public string ColumnWidths
		{
			get { return filelist.ColumnWidths; }
			set { filelist.ColumnWidths = value; }
		}
		
		protected override void OnFocusGrabbed ()
		{
			this.filelist.GrabFocus();
		}
		
		public void CdUp()
		{
			ChangeDir(cwd.RemoveElementRemote(), true, false, true);
		}
		
		protected void OnFileRenameRequest(FileRenamedArgs args)
		{
			UserAction(null,null);
			try
			{
				this.conn.RenameFile(args.OldName, args.NewName);
			}
			catch(FtpException) {}
    		ChangeDir(cwd.RemotePath, false);
		}
		
		protected void OnChmodRequested(FileListActionArgs args)
		{
			UserAction(null,null);
			
			bareFTP.Gui.Dialog.ChmodDialog dlg = new bareFTP.Gui.Dialog.ChmodDialog();
			
			if(filelist.SelectedFiles.Count == 1)
			{
				FileTreeNode n = filelist.SelectedFileNodes[0];
				dlg.Permissions = n.Rights;
			}
				
			dlg.Modal = true;
			if(dlg.Run() == (int)ResponseType.Ok)
			{
				try
				{
					foreach(string filename in filelist.SelectedFiles)
						conn.Chmod(dlg.Permissions, filename);
				}
				catch(FtpException) {}
				ChangeDir(cwd.RemotePath, false);
			}
			
			dlg.Destroy();
		}
		
		protected void OnDeleteRequest(FileListActionArgs args)
		{
			if(!bareFTP.Gui.Dialog.Dialogs.QuestionDialog(Catalog.GetString("Are you sure you want to delete the selected files?")))
				return;

			System.Threading.Thread t = new System.Threading.Thread(DoDelete);
			t.Start();
					
		}
		
		protected void OnTransferRequested(FileListActionArgs args)
		{
			UserAction(null,null);
			// From context menu...
			DownloadRequest(null,null);
		}
		
		protected void DirectoryNamed(Gtk.EditedArgs args)
    	{
			UserAction(null,null);
			
			try
			{
				this.conn.MakeDir(args.NewText);
			}
			catch(FtpException) {}
    		ChangeDir(cwd.RemotePath, false);
    	}
    	
    	public void ChangeDir(string nPath, bool usecache)
		{
			ChangeDir(nPath, usecache, false, false);
		}
		
		public bool ChangeDirSynchronized(string nPath)
		{
			if(filelist.DirectoryExisist(nPath) || string.IsNullOrEmpty(nPath))
			{
				ChangeDir(nPath, true, true, false);
				return true;
			}
			else
				return false;
		}
		
		public void ChangeDir(string nPath, bool usecache, bool internalCall, bool previousdir)
		{
			ChangeDirParams cdp = new ChangeDirParams();
			cdp.internalCall = internalCall;
			cdp.nPath = nPath;
			if(config.General_EnableDirectoryCache)
				cdp.usecache = usecache;
			else
				cdp.usecache = false;
			
			cdp.previousdir = previousdir;
			
			Thread t = new Thread(new ParameterizedThreadStart(DoChangeDir));
			t.Start(cdp);

		}
		
		public void SetConnection(Connection.Connection conn, string startDir)
		{
			pathEntry.Sensitive = true;
			this.conn = conn;
			filelist.ProtocolType = conn.Protocol;
			IsConnected = true;
			
			cache = new FileListCache(180);
			ChangeDir(startDir, false);
		}
		
		public void RemoveConnection()
		{
			pathEntry.Text = string.Empty;
			pathEntry.Sensitive = false;
			IsConnected = false;
			ChangeDir(null, false, true, false);
			
		}
		
		protected void OnDirChanged(DirectoryChangedArgs args)
		{
		   ChangeDir(args.NewPath, !string.IsNullOrEmpty(args.NewPath), false, args.NewPath.EndsWith(".."));
			if(DirChanged != null)
				DirChanged.Invoke(args.NewPath, null);
		}
		
		protected void OnLinkActivated(DirectoryChangedArgs args)
		{
			if(conn.IsDir(args.NewPath))
				ChangeDir(args.NewPath, false);
			else
				DownloadRequest(null, null);
		}

		protected void OnPathEntryActivated(object o, EventArgs args)
		{
			ChangeDir(pathEntry.Text, false);
		}
		
		protected void OnNewDirClicked(object o, EventArgs args)
		{
			UserAction(null,null);
			
			if(conn == null || !conn.Connected)
				return;
			
			this.filelist.MakeDir(Catalog.GetString("New Directory"), true);
		}
		
		protected void DirUpClicked(object o, EventArgs args)
		{
			
			if(conn == null || !conn.Connected)
				return;

			ChangeDir(cwd.RemoveElementRemote(), true, false, true);
			if(DirChanged != null)
				DirChanged.Invoke("..", null);

		}
		
		public bool IsConnected
		{
			get { return this.isConnected; }
			set { this.isConnected = value; }
		}
		
		public List<RemoteFile> CurrentDirRemoteFiles
		{
			get { return this.currentDirFileList; }
		}
		
		private Gtk.ListStore MakeNodeStore(List<RemoteFile> filelist)
		{
			
			//Gtk.ListStore store = new Gtk.ListStore (typeof (FileTreeNode));
			Gtk.ListStore store = new Gtk.ListStore(typeof(FileTreeNode),
			                                        typeof(Gdk.Pixbuf),
			                                        typeof(string),
				                                    typeof(string),
				                                    typeof(string),
				                                    typeof(string),
				                                    typeof(string),
				                                    typeof(string));
			
			bool showHidden;
			if(!string.IsNullOrEmpty(showhiddenstr))
				showHidden = bool.Parse(showhiddenstr);
			else
				showHidden = this.filelist.ShowHiddenFiles;
			
			foreach(RemoteFile rfile in filelist)
			{
				if(rfile.Filename != ".")
				{
					if(showHidden || rfile.Filename == ".." || (!showHidden && !rfile.Filename.StartsWith(".")))
					{
						FileTreeNode rnode = new FileTreeNode(rfile);
						
						string str_size = string.Empty;
						string str_date = string.Empty;
						
						if(!rnode.IsDir)
							str_size = Gnome.Vfs.Format.FileSizeForDisplay(rnode.Size);
						if(rnode.Date < DateTime.MaxValue)
							str_date = rnode.Date.ToString("r");
						
						store.AppendValues (rnode, rnode.Type, rnode.FileName, str_size, str_date, rnode.User, rnode.Group, rnode.Rights);
					}
				}
			}
			
			currentDirFileList = filelist;
            return store;
		}
		
		public List<RemoteFile> SelectedFileTree
		{
			get 
			{
				bool handledisabling = true;
				if(!disabled)
					Disable();
				else
					handledisabling = false;
				
				List<RemoteFile> paths = new List<RemoteFile>();
         		
				Thread t = new Thread(new ParameterizedThreadStart(DoTraverse));
				t.Start(paths);
				ewh.WaitOne();
				conn.SetCurrentDirectory(cwd.RemotePath);
				if(handledisabling)
					Enable();
				return paths;
			}
		}

		private void Traverser(bareFTP.Protocol.BarePath path, List<RemoteFile> _paths)
		{
							
			conn.SetCurrentDirectory(path.RemotePathAbs);
			
			foreach(RemoteFile file in conn.XDir())
			{
				if(file.Filename != "." && file.Filename != "..")
				{
					file.Path.SetBaseRemote(cwd.ToString(true));
					if(!file.IsDir)
						file.Path.SetFileName(path.RelativePathRemote, file.Filename);
					else
					{
						file.Path.SetFileName(path.RelativePathRemote, string.Empty);
						file.Path.AddRelativeElement(file.Filename);
					}
					_paths.Add(file);
					if(file.IsDir)
					{
						bareFTP.Protocol.BarePath tpath = new bareFTP.Protocol.BarePath();
						tpath.SetBaseRemote(cwd.ToString(true));
						tpath.SetFileName(path.RelativePathRemote, string.Empty);
						tpath.AddRelativeElement(file.Filename);
						Traverser(tpath,  _paths);
					}
					
				}
			}
			
		}
		
		[GLib.ConnectBefore]
		private void OnButtonPress (object sender, Gtk.ButtonPressEventArgs args)
		{
			bool withModifider = (args.Event.State & Gdk.ModifierType.ShiftMask) != 0 || (args.Event.State & Gdk.ModifierType.ControlMask) != 0;
			if (filelist.IsClickedNodeSelected ((int)args.Event.X, (int)args.Event.Y) && filelist.MultipleNodesSelected () && !withModifider) {
				args.RetVal = true;
			}
			
		}

		private void OnButtonRelease (object sender, Gtk.ButtonReleaseEventArgs args)
		{
			if (args.Event.Button == 3) {
				filelist.ShowPopup ();
			}
			
			bool withModifider = (args.Event.State & Gdk.ModifierType.ShiftMask) != 0 || (args.Event.State & Gdk.ModifierType.ControlMask) != 0;
			if (args.Event.Button == 1 && filelist.MultipleNodesSelected () && !withModifider) {
				filelist.Selection.UnselectAll ();
				Gtk.TreePath path;
				if (filelist.GetPathAtPos ((int)args.Event.X, (int)args.Event.Y, out path))
					filelist.Selection.SelectPath (path);
			}
		}
		
		private void OnDragDataGet(object o, DragDataGetArgs args)
		{
			string str = string.Empty;
			filelist.SetSelection(filelist.DragItems);
			if(filelist != null && filelist.DragItems.Count > 0)
			{
				Gnome.Vfs.Vfs.Initialize();
				
				foreach(FileTreeNode item in filelist.SelectedFileNodes)
				{
					Gnome.Vfs.Uri uri = new Gnome.Vfs.Uri(conn.Protocol.ToString().ToLower() + "://" + System.Web.HttpUtility.UrlEncode(cwd.ToString(true)) + "/" + System.Web.HttpUtility.UrlEncode(item.FileName));
					uri.HostName = conn.ConnProperties.Hostname;
					uri.UserName = conn.ConnProperties.User;
					str += uri.ToString() +  "\n";
				}
			}
			Gnome.Vfs.Vfs.Shutdown();
			args.SelectionData.Set(args.SelectionData.Target, 8,
			                    System.Text.Encoding.UTF8.GetBytes(str));
		}
		
		private void OnDragDataReceived(object o, DragDataReceivedArgs args)
		{
			FileTreeNode node = null;
			Gtk.TreePath path;
			Gtk.TreeIter iter;
			Gtk.TreeViewDropPosition pos;
			filelist.GetDestRowAtPos(args.X, args.Y, out path, out pos);
			
			if(path != null)
			{
				filelist.Model.GetIter(out iter, path);
				node = (FileTreeNode)filelist.Model.GetValue(iter,0);
			}
			
			string files = System.Text.Encoding.Default.GetString(args.SelectionData.Data);
			if(args.Info == 0 && this.IsConnected && !string.IsNullOrEmpty(files.Trim()))
			{
				UploadRequest(node,null);
			}
			
			if(args.Info == 1)
			{
				List<string> lfiles = new List<string>();
				foreach(string s in files.Split(Environment.NewLine.ToCharArray()))
				{
					if(!string.IsNullOrEmpty(s.Trim()) && s.StartsWith("file://"))
						lfiles.Add(System.Web.HttpUtility.UrlDecode(s.Substring(7).Trim()));
				}
				
				ExternalUploadRequest(lfiles, node);
			}
		}
		
		public BarePath CurrentDir
		{
			get { return this.cwd; }
		}
		
		public string ShowHidden
		{
			set { showhiddenstr = value;}	
		}
		
		///////////THREADED REMOTE ACTIONS//////////////
		
		private void DoTraverse(object obj)
		{
			try
			{
				List<RemoteFile> paths = obj as List<RemoteFile>;
				foreach(FileTreeNode fnode in filelist.SelectedFileNodes)
				{
					if(fnode.FileName == "..")
						continue;
						
					RemoteFile rfile = new RemoteFile();
					rfile.Path.SetBaseRemote(cwd.ToString(true));
					
					rfile.Size = Convert.ToInt64(fnode.Size);
					rfile.IsDir =  fnode.IsDir;
					rfile.IsLink = fnode.IsLink;
					rfile.Linkdest = fnode.LinkName;
					if(!string.IsNullOrEmpty(fnode.Rights))
						rfile.Permissions = PermissionParser.RWXToMode(fnode.Rights);
					
					if(!rfile.IsDir)
						rfile.Path.SetFileName(string.Empty, fnode.FileName);
					else
						rfile.Path.SetFileName(fnode.FileName, string.Empty);
					
					paths.Add(rfile);
					if(rfile.IsDir)
						Traverser(rfile.Path, paths);
				}
			}
			catch{}
			
			ewh.Set();
		}
		
		
		private void DoChangeDir(object obj)
		{
			bool handledisabling = true;
			
			if(!disabled)
				Disable();
			else
				handledisabling = false;
			
			
			ChangeDirParams cdp = obj as ChangeDirParams;
			bool internalCall = cdp.internalCall;
			string nPath = cdp.nPath;
			bool usecache = cdp.usecache;
			
			if(!config.General_EnableDirectoryCache)
				usecache = false;
			
			bool foundcachedstore = false;
			
			if(!internalCall)
				UserAction(null,null);
			
			try
			{
				Gtk.ListStore store;
				
				if(nPath == null)
				{
					store = MakeNodeStore(new List<RemoteFile>());
					Gtk.Application.Invoke( delegate {
						filelist.Model = store;
					});
					return;
				}

				if(string.IsNullOrEmpty(nPath))
				{
					store = MakeNodeStore(conn.XDir());
				}
				else
	    		{
					if(!isConnected)
					{
						bareFTP.Gui.Dialog.Dialogs.ErrorDialog(Catalog.GetString("Can't change directory when not connected"));
						pathEntry.Text = String.Empty;
						return;
					}
					
					try
					{
	    				conn.SetCurrentDirectory(nPath);
					}
					catch
					{
						// Probably a non-existing dir. Revert.
						nPath = cwd.RemotePath;	
					}
	    			
					// Some dirty hacks here..
					string assumedpath = cwd.ToString(true);
					if(nPath == "..")
					{
						assumedpath = assumedpath.Substring(0, assumedpath.LastIndexOf("/"));
						if(string.IsNullOrEmpty(assumedpath))
							assumedpath = "/";
					}
					else
						assumedpath = System.IO.Path.Combine(assumedpath, nPath);
					
	    			if(!usecache)
					{
	    				store = null;
						cache.RemoveEntry(assumedpath);
					}
	    			else
					{
	    				store = cache.GetCachedStore(assumedpath);
						if(store != null)
						{
							foundcachedstore = true;
							cwd.SetBaseRemote(assumedpath);
						}
					}
	    			
					if(store == null)
					{
						store = MakeNodeStore(conn.XDir());
					}
	    		}
				
				if(!foundcachedstore)
					cwd.SetBaseRemote(conn.GetCurrentDirectory());
				
				// Add a directory listing entry in cache
				string newpath = cwd.ToString(true);
				if(config.General_EnableDirectoryCache)
					cache.AddEntry(newpath, store);
				
				// Add a treepath entry in cache (for remembering where you came from)
				if(!cdp.previousdir && filelist.Selection.GetSelectedRows().Length == 1)
					cache.AddCachedTreePath(pathEntry.Text, filelist.Selection.GetSelectedRows()[0]);
				
				Gtk.Application.Invoke( delegate {
					filelist.SetModel(store);
					
					if(cdp.previousdir && cache.GetCachedTreePath(newpath) != null)
					{
						filelist.SetCursorOnCell(cache.GetCachedTreePath(newpath), filelist.GetColumn(0), filelist.Columns[0].Cells[0], false);
						filelist.Selection.SelectPath(cache.GetCachedTreePath(newpath));
						cache.RemoveCachedTreePath(newpath);
					}
					else
						filelist.ScrollToPoint(0,0);
					
					pathEntry.Text = newpath;
					
					
				});
			}
			catch(Exception)
			{
				//ChangeDir("..", false);
			}
			finally
			{
				if(handledisabling)
					Enable();
			}
		}
		
		private void DoDelete()
		{
			UserAction(null,null);
			bool handledisabling = true;
			
			if(!disabled)
				Disable();
			else
				handledisabling = false;

			try
			{
				if(StatusBarMessage != null)
					StatusBarMessage(null, new StatusBarMessageArgs(Mono.Unix.Catalog.GetString("Retrieving directory contents.."), false));
				List<RemoteFile> rfiles = SelectedFileTree;
				
				if(rfiles != null)
				{
					// Reversing list to start deleting in the "deepest" end of the tree..
					rfiles.Reverse();

					if(StatusBarMessage != null)
						StatusBarMessage(null, new StatusBarMessageArgs(Mono.Unix.Catalog.GetString("Deleting.."), false));
					
					foreach(RemoteFile file in rfiles)
					{
						if(file.IsDir)
						{
							try
							{
								conn.RemoveDir(file.Path.FileNameRemoteAbs);
							} 
							catch(FtpException) {}
						}
						
						else
						{
							try
							{
								conn.DeleteFile(file.Path.FileNameRemoteAbs);
							}
							catch(FtpException) {}
						}
					}

					if(StatusBarMessage != null)
						StatusBarMessage(null, new StatusBarMessageArgs("", false));
					
				}
			}
			catch(Exception ex)
			{
				if(StatusBarMessage != null)
					StatusBarMessage(null, new StatusBarMessageArgs(ex.Message, true));
			}
			finally
			{
				if(handledisabling)
					Enable();
				ChangeDir(cwd.ToString(true), false, true, false);
			}
		}

		public void Disable()
		{
			Gtk.Application.Invoke(delegate {
					disabled = true;
					progress.Start();
					filelist.EventsDisabled = true;
					pathEntry.Sensitive = false;
					btnnewfolder.Sensitive = false;
					btnup.Sensitive = false;
				});
		}

		public void Enable()
		{
			Gtk.Application.Invoke(delegate {
					disabled = false;
					progress.Stop();
					filelist.EventsDisabled = false;
					pathEntry.Sensitive = true;
					btnnewfolder.Sensitive = true;
					btnup.Sensitive = true;
				});
		}
		
		internal class ChangeDirParams
		{
			public string nPath;
			public bool usecache;
			public bool internalCall;
			public bool previousdir;
		}
	}
	
}
