// FTPConnection.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.Net;
using System.IO;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Collections.Generic;
using System.Collections;
using bareFTP.Protocol;
using bareFTP.Preferences;

namespace bareFTP.Protocol.Ftp
{
	public class FTPConnection : IProtocol
	{
		
		protected FTPClient ftpClient;
		private FTPMode ftpMode;
		//private IDialogHost host;
		private ConnectionProperties conn_props;
		private string cwd;
		Config conf;
		private bool xferinprogress = false;
		string system = string.Empty;
		
		public FTPConnection(ConnectionProperties conn_props)
		{
			this.conn_props = conn_props;
		}
		
		public ProtocolType ProtocolType
		{
			get 
			{ 
				if(conn_props.FTPS)
					return ProtocolType.FTPS;
				else
					return ProtocolType.FTP;
			}
		}
		
		public virtual void Init(IDialogHost host, Config config)
		{
			//this.host = host;
			this.conf = config;
			if(conn_props.Bookmark != null)
				ftpMode = conn_props.Passive ? FTPMode.Passive : FTPMode.Active;
			else
				ftpMode = conf.FTP_PassiveMode ? FTPMode.Passive : FTPMode.Active;
			
		}
		
		public virtual void Open()
		{
			if(conn_props.Port == 0)
				conn_props.Port = conf.FTPDefaultPort;
			Open(conn_props.Hostname, conn_props.Port, conn_props.User, conn_props.Password);
		}
		
		public virtual void Open (string remoteHost, string user, string password)
		{
			Open(remoteHost, conf.FTPDefaultPort, user, password);
		}

		public virtual void Open (string remoteHost, int remotePort, string user, string password)
		{
			// Creating a new FTPClient
			
			if(string.IsNullOrEmpty(user) && conf.FTP_EmptyUserAnonymous)
				user = "anonymous";
			if(string.IsNullOrEmpty(password) && user == "anonymous" && conf.FTP_EmailAsAnonymousPass)
				password = conf.FTP_EmailAddress;
			
			int port = remotePort;
			if(port < 0)
				port = conf.FTPDefaultPort;
			
			ftpClient = new FTPClient(ftpMode, conn_props.FTPS, conf);
			
			ftpClient.LogTextEmitted += new EventHandler(OnClientFeedback);
			ftpClient.RemoteCharset = conn_props.RemoteCharset;
			
			if(conn_props.Bookmark != null)
				ftpClient.ProtLevel = conn_props.Bookmark.EncryptData;
			
			ftpClient.Connect(remoteHost, port, user, password);

			
			List<FTPReply> replies = ftpClient.CheckReply(ftpClient.SendCommand("SYST"), true, 215);
			foreach(FTPReply r in replies)
				if(r.ReplyCode == 215)
					system = r.Message;

			ftpClient.RemoteSystem = system;
			
		}
		
		// TODO: Fix these names...
		protected void OnClientFeedback(object sender, EventArgs e)
		{
			OnLogTextEmitted(e as LogTextEmittedArgs);
		}

		public virtual void Close ()
		{
			if(ftpClient != null)
				ftpClient.Close();
		}
		
		public virtual bool Connected
		{
			get {
				//if(ftpClient != null)
					return ftpClient.Connected;
				//else
				//	return false;
			}
		}
		
		public virtual bool TransferInProgress
		{
			get {
				return this.xferinprogress;
			}
			set {
				this.xferinprogress = value;
			}
		}
		
		public virtual ConnectionProperties ConnProperties
		{
			get { return conn_props; }
			set { conn_props = value; }
		}
		
		public void SendCommand(string command)
		{
			ftpClient.SendCustomCommand(command);
		}
		
		public virtual bool IsDir(string cwd, string dir)
		{
			try
			{
				return ftpClient.IsDir(cwd, dir);
			}
			catch(TimeoutException)
			{
				ReConnect();
				return ftpClient.IsDir(cwd, dir);
			}
		}
		
		public virtual List<string> Dir ()
		{
			try
			{
				return ftpClient.Dir();
			}
			catch(TimeoutException)
			{
				ReConnect();
				return ftpClient.Dir();
			}
		}
		
		public virtual List<RemoteFile> XDir ()
		{
			List<string> listing = ftpClient.XDir();
			
			if(listing.Count == 0)
				return new List<RemoteFile>();
			
			if(system.IndexOf("VMS") >= 0)
				return ListParserVMS.ParseList(listing);
			else
			{
				List<RemoteFile> files = ListParser.ParseList(listing);
				
				// Find linked file size if we need and have possibility
				System.Text.RegularExpressions.Match match;
				
				if(ftpClient.Features.Contains("MLSD") || ftpClient.Features.Contains("SIZE"))
				{
					foreach(RemoteFile f in files)
					{
						if(f.IsLink)
						{
							
						}
						if(!f.IsDir && f.IsLink)
						{
							long size = f.Size;
							bool found = false;
							// MLST is the prefered way...
							if(ftpClient.Features.Contains("MLSD"))
							{
								List<FTPReply> replies = ftpClient.CheckReply(ftpClient.SendCommand("MLST " + f.Filename), true, 216);
								foreach(FTPReply r in replies)
								{
									if(r.ReplyCode == 250)
									{
										// First check if this is is a directory
										match = System.Text.RegularExpressions.Regex.Match(r.Message, @"\btype=\b(\w+)");
										if(match.Success)
										{
											try
											{
												if(match.Groups[1].Captures[0].ToString().Trim() == "dir")
												f.IsDir = true;
												found = true;
												break;
											} catch {}
										}
										
										// If not a directory, try to get size..
										match = System.Text.RegularExpressions.Regex.Match(r.Message, @"\bsize=\b(\d+)");
										if(match.Success)
										{
											try
											{
												size = Convert.ToInt64(match.Groups[1].Captures[0].ToString());
												found = true;
											} catch {}
											break;
										}
									}
								}	
							}
							if(!found && ftpClient.Features.Contains("SIZE"))
							{
								List<FTPReply> replies = ftpClient.CheckReply(ftpClient.SendCommand("SIZE " + f.Linkdest), true, 216);
								foreach(FTPReply r in replies)
								{
									if(r.ReplyCode == 213)
									{
										try
										{
											size = Int64.Parse(r.Message.Trim());
										} catch{}
										break;
									}
								}
							}
							
							f.Size = size;
						}
					}
				}
				
				return files;
			}
		}

		public virtual void SendFile (XferFile file)
		{
			if(file.Status == DownloadStatus.Skipped || file.Status == DownloadStatus.Aborted)
				return;
			
			if(file.IsDir)
			{
				try
				{
					MakeDir(file.Path.RelativePathRemote);
				}
				catch(Exception ex)
				{
					OnLogTextEmitted(new LogTextEmittedArgs(MessageType.Error, ex.Message));
					file.Status = DownloadStatus.Failed;
				}
			}
			else
			{
				FileStream fs = null;
				try
				{
					
					fs = new FileStream(file.Path.FileNameLocalAbs, FileMode.Open, FileAccess.Read, FileShare.Read);
					ftpClient.StoreFile(file, file.Action, fs);
					
				}
				catch(TimeoutException)
				{
					ReConnect();
					ftpClient.StoreFile(file, file.Action, fs);
				}
				catch(TransferAbortedException)
				{
					Open();
					SetCurrentDirectory(cwd);
				}
				catch(Exception ex)
				{
					Open();
					SetCurrentDirectory(cwd);
					OnLogTextEmitted(new LogTextEmittedArgs(MessageType.Error, ex.Message));
					file.Status = DownloadStatus.Failed;
				}
				finally
				{
					if(fs != null)
						fs.Close();
				}
				
			}
			
			if(!string.IsNullOrEmpty(file.Permissions) && conf.General_PreserveFilePermissions && file.Status == DownloadStatus.Finished)
			{
				if(file.Status == DownloadStatus.Finished)
				{
					int mode;
					if(Int32.TryParse(file.Permissions, out mode))
						ftpClient.Chmod(mode.ToString(), file.Path.FileNameRemoteAbs, true);
				}
			}
		}
		
		public virtual void GetFile (XferFile file)
		{
			
			if(file.Status == DownloadStatus.Skipped || file.Status == DownloadStatus.Aborted)
				return;
			
			string path = string.Empty;
			
			
			if(file.IsDir)
			{
				try
				{
					path = System.IO.Path.Combine(file.Path.LocalPath, file.Path.RelativePathLocal);
					if(!Directory.Exists(path))
					{
						Directory.CreateDirectory(path);
					}
					
				}
				catch(Exception ex)
				{
					OnLogTextEmitted(new LogTextEmittedArgs(MessageType.Error, ex.Message));
					file.Status = DownloadStatus.Failed;
				}
			}
			else
			{
				try
				{
					
					FileMode mode;
					
					path = System.IO.Path.Combine(file.Path.LocalPath, file.Path.RelativePathLocal);
					path = System.IO.Path.Combine(path, file.Path.FileName);
					
					if(file.Action == FileAction.Resume || file.Action == FileAction.Append)
						mode = FileMode.Append;
					else
					{
					
						if(System.IO.File.Exists(path))
							mode = FileMode.Truncate;
						else
							mode = FileMode.CreateNew;
					}

					using(FileStream fs = new FileStream(file.Path.FileNameLocalAbs, mode))
					{
						try
						{
							ftpClient.RetrieveFile(file, file.Action, fs);
						}
						catch(TimeoutException)
						{
							ReConnect();
							ftpClient.RetrieveFile(file, file.Action, fs);
						}
						finally
						{
							fs.Flush();
							fs.Close();
						}
					}
					
				}
				catch(TransferAbortedException)
				{
					Open();
					SetCurrentDirectory(cwd);
				}
				catch(Exception ex)
				{
					OnLogTextEmitted(new LogTextEmittedArgs(MessageType.Error, ex.Message));
					file.Status = DownloadStatus.Failed;
				}
			}

			if(!string.IsNullOrEmpty(file.Permissions) && conf.General_PreserveFilePermissions)
			{
				if(file.Status == DownloadStatus.Failed)
					return;
				
				bareFTP.Common.Utils.LocalChmod.Chmod(path, file.Permissions);
			}
		}

		public virtual void DeleteFile (string remoteFileName)
		{
			try
			{
				ftpClient.Delete(remoteFileName);
			}
			catch(TimeoutException)
			{
				ReConnect();
				ftpClient.Delete(remoteFileName);
			}
		}

		public virtual void MoveFile (string remoteFileName, string toRemotePath)
		{
			throw new NotImplementedException();
		}

		public virtual void RenameFile (string fromRemoteFileName, string toRemoteFileName)
		{
			try
			{
				ftpClient.RenameFile(fromRemoteFileName, toRemoteFileName);
			}
			catch(TimeoutException)
			{
				ReConnect();
				ftpClient.RenameFile(fromRemoteFileName, toRemoteFileName);
			}
			
			
		}

		public virtual void SetCurrentDirectory (string remotePath)
		{
			try
			{
				ftpClient.ChangeDir(remotePath);
			}
			catch(TimeoutException)
			{
				ReConnect();
				SetCurrentDirectory(remotePath);
			}
		}

		public virtual string GetCurrentDirectory ()
		{
			string retVal = ftpClient.GetCurrentDirectory();
			
			if(retVal.IndexOf("\"") >= 0)
			{
				retVal = retVal.Remove(0, retVal.IndexOf("\"")+1);
				retVal = retVal.Remove(retVal.IndexOf("\""));
			}
			cwd = retVal;
			
			return retVal;
			 
		}

		public virtual void MakeDir (string directoryName)
		{
			ftpClient.MakeDir(directoryName);
		}
		
		public virtual void RemoveDir (string directoryName)
		{
			ftpClient.RemoveDir(directoryName);
		}
		
		public virtual void Chmod(string mode, string filename)
		{
			ftpClient.Chmod(mode, filename);
		}
		
		public virtual void Abort()
		{
			ftpClient.Abort();
			LostConnection(cwd, null);
		}
		
		public virtual string CurrentDirectory
		{
			get { return cwd; }
		}
		
		private void ReConnect()
		{
			Open();
			SetCurrentDirectory(cwd);	
		}
		
		public event EventHandler LostConnection;
		
		public event EventHandler LogTextEmitted;
		public virtual void OnLogTextEmitted(LogTextEmittedArgs e)
		{
			try
			{
				LogTextEmitted(this, e);
			} catch{}
		}
	}
	
}
