﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace OKHOSTING.Files.Net4.FTP
{
	public class FileSystem : Files.FTP.FileSystem
	{
		protected System.Net.FtpClient.FtpClient Connection;

		public System.Net.FtpClient.FtpEncryptionMode EncryptionMode { get; set; } = System.Net.FtpClient.FtpEncryptionMode.None;

		public override bool IsReadOnly
		{
			get
			{
				return false;
			}
		}

		public override bool SupportsOpenWrite
		{
			get
			{
				return true;
			}
		}

		protected void Connect()
		{
			if (Connection == null)
			{
				Connection = new System.Net.FtpClient.FtpClient();
				Connection.Host = Host;
				Connection.Port = Port;
				Connection.Credentials = new System.Net.NetworkCredential(Credentials?.UserName, Credentials?.Password);

				//Connection.DataConnectionType = System.Net.FtpClient.FtpDataConnectionType.AutoPassive;
				Connection.EncryptionMode = EncryptionMode;
				Connection.DataConnectionEncryption = EncryptionMode != System.Net.FtpClient.FtpEncryptionMode.None;
				Connection.ValidateCertificate += Connection_ValidateCertificate;
			}

			if (!Connection.IsConnected)
			{
				Connection.Connect();
			}
		}

		protected void Connection_ValidateCertificate(System.Net.FtpClient.FtpClient control, System.Net.FtpClient.FtpSslValidationEventArgs e)
		{
			e.Accept = true;
		}

		protected Element Parse(System.Net.FtpClient.FtpListItem item)
		{
			Element element;

			if (item.Type == System.Net.FtpClient.FtpFileSystemObjectType.Directory)
			{
				element = new Directory();
			}
			else if (item.Type == System.Net.FtpClient.FtpFileSystemObjectType.File)
			{
				element = new File();
				element.Size = (ulong) item.Size;
			}
			else
			{
				//links not supported
				throw new NotSupportedException();
			}

			element.FullName = item.FullName;
			element.Created = item.Created;
			element.LastRead = item.Modified;
			element.LastWrite = item.Modified;
			element.FileSystem = this;

			return element;
		}

		/// <summary>
		/// Gets and elemento from a key, for private use only
		/// </summary>
		protected Element GetElement(string key)
		{
			System.Net.FtpClient.FtpListItem ftpItem;
			Element element;

			try
			{
				ftpItem = Connection.GetObjectInfo(key);

				return Parse(ftpItem);
			}
			catch
			{
				if (Connection.FileExists(key))
				{
					element = new File(key, this);
					element.Size = (ulong) Connection.GetFileSize(key);
					element.LastWrite = Connection.GetModifiedTime(key);
				}
				else if (Connection.DirectoryExists(key))
				{
					element = new Directory(key, this);
					element.LastWrite = Connection.GetModifiedTime(key);
				}
				else
				{
					throw new ArgumentOutOfRangeException("Element " + key + " was not found");
				}

				return element;
			}
		}

		public override void Add(KeyValuePair<string, Element> item)
		{
			Connect();

			if (item.Value is Directory)
			{
				Connection.CreateDirectory(item.Key, true);
			}
			else
			{
				using (var reader = ((File) item.Value).Read())
				using (var writer = Connection.OpenWrite(item.Key))
				{
					reader.CopyTo(writer);
				}

				//for some reason we need this
				//Connection.Disconnect();
				//Connection.Connect();
			}
		}

		public override bool ContainsKey(string key)
		{
			Connect();
			return Connection.FileExists(key) || Connection.DirectoryExists(key);
		}

		public override ICollection<string> Keys
		{
			get
			{
				Connect();
			
				List<string> keys = new List<string>();

				foreach (KeyValuePair<string, Element> e in this)
				{
					keys.Add(e.Value.FullName);
				}

				return keys;
			}
		}

		public override Element this[string key]
		{
			get
			{
				Connect();

				return GetElement(key);
			}
			set
			{
				Connect();

				if (!Contains(value))
				{
					Add(value);
				}
				else if (value is File)
				{
					Remove(value);
					Add(value);
				}
			}
		}

		public override Element Move(string fullPath, string newFullPath)
		{
			Connect();
			Connection.Rename(fullPath, newFullPath);

			return (File) this[newFullPath];
		}

		public override bool Remove(string key)
		{
			Connect();
			return Remove(this[key]);
		}

		public override bool Remove(KeyValuePair<string, Element> item)
		{
			Connect();

			if (Connection.DirectoryExists(item.Key))
			{
				Connection.DeleteDirectory(item.Key);
				return true;
			}
			else if (Connection.FileExists(item.Key))
			{
				Connection.DeleteFile(item.Key);
				return true;
			}

			return false;
		}

		public override IEnumerable<Element> Search (Directory startingDirectory, bool recursive, string pattern, ElementType? elementType)
		{
			if (startingDirectory == null)
			{
				return Search((string)null, recursive, pattern, elementType);
			}
			else
			{
				return Search(startingDirectory.FullName, recursive, pattern, elementType);
			}
		}

		public override IEnumerable<Element> Search(string startingDirectoryPath, bool recursive, string pattern, ElementType? elementType)
		{
			Connect();

			if (string.IsNullOrWhiteSpace(pattern))
			{
				pattern = IncludeAllPattern;
			}

			//search in a specific subdirectory
			var fullSearch = GetListing(startingDirectoryPath);
			var filteredSearch = fullSearch;

			if (pattern != IncludeAllPattern)
			{
				filteredSearch = filteredSearch.Where(f => f.Name.Contains(pattern)).ToArray();
			}

			if (elementType.HasValue && elementType.Value == ElementType.File)
			{
				filteredSearch = filteredSearch.Where(x => x.Type == System.Net.FtpClient.FtpFileSystemObjectType.File).ToArray();
			}

			if (elementType.HasValue && elementType.Value == ElementType.Directory)
			{
				filteredSearch = filteredSearch.Where(x => x.Type == System.Net.FtpClient.FtpFileSystemObjectType.Directory).ToArray();
			}

			//return all files
			foreach (var fileInfo in filteredSearch.Where(f => f.Type == System.Net.FtpClient.FtpFileSystemObjectType.File))
			{
				Element file = Parse(fileInfo);

				yield return file;
			}

			//return all directories
			foreach (var subDirectoryInfo in filteredSearch.Where(f => f.Type == System.Net.FtpClient.FtpFileSystemObjectType.Directory))
			{
				Element subDirectory = Parse(subDirectoryInfo);

				yield return subDirectory;
			}

			//execute recursion
			if (recursive)
			{
				foreach (var subDirectoryInfo in fullSearch.Where(f => f.Type == System.Net.FtpClient.FtpFileSystemObjectType.Directory))
				{
					foreach (Element e in Search(subDirectoryInfo.FullName, recursive, pattern, elementType))
					{
						yield return e;
					}
				}
			}
		}

		protected System.Net.FtpClient.FtpListItem[] GetListing(string path)
		{
			Exception ex = null;

			for (int i = 0; i < 5; i++)
			{
				try
				{
					if (string.IsNullOrEmpty(path))
					{
						return Connection.GetListing("/");
					}
					else
					{
						return Connection.GetListing(path);
					}
				}
				catch(Exception e)
				{
					Connection.Disconnect();
					Connection.Connect();

					ex = e;
					System.Threading.Thread.Sleep(100);
				}
			}

			throw ex;
		}

		public override Stream OpenRead(string fullPath)
		{
			Connect();
			return Connection.OpenRead(fullPath);
		}

		public override Stream OpenWrite(string fullPath)
		{
			Connect();
			return Connection.OpenWrite(fullPath);
		}

		public override File WriteFile(string fullPath, Stream content)
		{
			Connect();

			using (var writer = Connection.OpenWrite(fullPath))
			{
				content.CopyTo(writer);
			}

			return (File) this[fullPath];
		}

		public override void Dispose()
		{
			try
			{
				Connection?.Disconnect();
				Connection?.Dispose();
				Connection = null;
			}
			catch { }
		}

		/// <summary>
		/// Creates a copy of the current instance, usefull for parallel file processing
		/// </summary>
		/// <returns>A functional copy of the current filesystem instance</returns>
		public override FileSystemBase Clone()
		{
			return new FileSystem()
			{
				Host = this.Host,
				Credentials = this.Credentials,
				EncryptionMode = this.EncryptionMode
			};
		}

		public override string ToString()
		{
			return Host?.ToString();
		}
	}
}