using System;
using System.Collections.Generic;
using System.IO;
#if !CORE
using System.Linq;
using System.Net.Security;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Text;
#endif

namespace FluentFTP {

#if !CORE
	/// <summary>
	/// .NET SslStream doesn't close TLS connection properly.
	/// It does not send the close_notify alert before closing the connection.
	/// FtpSslStream uses unsafe code to do that.
	/// This is required when we want to downgrade the connection to plaintext using CCC command.
	/// Thanks to Neco @ https://stackoverflow.com/questions/237807/net-sslstream-doesnt-close-tls-connection-properly/22626756#22626756
	/// </summary>
	internal class FtpSslStream : SslStream {

		private bool sentCloseNotify = false;

		public FtpSslStream(Stream innerStream)
			: base(innerStream) {
		}
		public FtpSslStream(Stream innerStream, bool leaveInnerStreamOpen)
			: base(innerStream, leaveInnerStreamOpen) {
		}
		public FtpSslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback)
			: base(innerStream, leaveInnerStreamOpen, userCertificateValidationCallback) {
		}
		public FtpSslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback, LocalCertificateSelectionCallback userCertificateSelectionCallback)
			: base(innerStream, leaveInnerStreamOpen, userCertificateValidationCallback, userCertificateSelectionCallback) {
		}
#if !NET20 && !NET35
		public FtpSslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback, LocalCertificateSelectionCallback userCertificateSelectionCallback, EncryptionPolicy encryptionPolicy)
			: base(innerStream, leaveInnerStreamOpen, userCertificateValidationCallback, userCertificateSelectionCallback, encryptionPolicy) {
		}
#endif
		public override void Close() {
			try {
				if (!sentCloseNotify) {
					SslDirectCall.CloseNotify(this);
					sentCloseNotify = true;
				}
			} finally {
				base.Close();
			}
		}
	}

	internal unsafe static class SslDirectCall {
		/// <summary>
		/// Send an SSL close_notify alert.
		/// </summary>
		/// <param name="sslStream"></param>
		public static void CloseNotify(SslStream sslStream) {
			if (sslStream.IsAuthenticated && sslStream.CanWrite) {
				bool isServer = sslStream.IsServer;

				byte[] result;
				int resultSz;
				var asmbSystem = typeof(System.Net.Authorization).Assembly;

				int SCHANNEL_SHUTDOWN = 1;
				var workArray = BitConverter.GetBytes(SCHANNEL_SHUTDOWN);

				var sslstate = FtpReflection.GetField(sslStream, "_SslState");
				var context = FtpReflection.GetProperty(sslstate, "Context");

				var securityContext = FtpReflection.GetField(context, "m_SecurityContext");
				var securityContextHandleOriginal = FtpReflection.GetField(securityContext, "_handle");
				SslNativeApi.SSPIHandle securityContextHandle = default(SslNativeApi.SSPIHandle);
				securityContextHandle.HandleHi = (IntPtr)FtpReflection.GetField(securityContextHandleOriginal, "HandleHi");
				securityContextHandle.HandleLo = (IntPtr)FtpReflection.GetField(securityContextHandleOriginal, "HandleLo");

				var credentialsHandle = FtpReflection.GetField(context, "m_CredentialsHandle");
				var credentialsHandleHandleOriginal = FtpReflection.GetField(credentialsHandle, "_handle");
				SslNativeApi.SSPIHandle credentialsHandleHandle = default(SslNativeApi.SSPIHandle);
				credentialsHandleHandle.HandleHi = (IntPtr)FtpReflection.GetField(credentialsHandleHandleOriginal, "HandleHi");
				credentialsHandleHandle.HandleLo = (IntPtr)FtpReflection.GetField(credentialsHandleHandleOriginal, "HandleLo");

				int bufferSize = 1;
				SslNativeApi.SecurityBufferDescriptor securityBufferDescriptor = new SslNativeApi.SecurityBufferDescriptor(bufferSize);
				SslNativeApi.SecurityBufferStruct[] unmanagedBuffer = new SslNativeApi.SecurityBufferStruct[bufferSize];

				fixed (SslNativeApi.SecurityBufferStruct* ptr = unmanagedBuffer)
				fixed (void* workArrayPtr = workArray) {
					securityBufferDescriptor.UnmanagedPointer = (void*)ptr;

					unmanagedBuffer[0].token = (IntPtr)workArrayPtr;
					unmanagedBuffer[0].count = workArray.Length;
					unmanagedBuffer[0].type = SslNativeApi.BufferType.Token;

					SslNativeApi.SecurityStatus status;
					status = (SslNativeApi.SecurityStatus)SslNativeApi.ApplyControlToken(ref securityContextHandle, securityBufferDescriptor);
					if (status == SslNativeApi.SecurityStatus.OK) {
						unmanagedBuffer[0].token = IntPtr.Zero;
						unmanagedBuffer[0].count = 0;
						unmanagedBuffer[0].type = SslNativeApi.BufferType.Token;

						SslNativeApi.SSPIHandle contextHandleOut = default(SslNativeApi.SSPIHandle);
						SslNativeApi.ContextFlags outflags = SslNativeApi.ContextFlags.Zero;
						long ts = 0;

						var inflags = SslNativeApi.ContextFlags.SequenceDetect |
									SslNativeApi.ContextFlags.ReplayDetect |
									SslNativeApi.ContextFlags.Confidentiality |
									SslNativeApi.ContextFlags.AcceptExtendedError |
									SslNativeApi.ContextFlags.AllocateMemory |
									SslNativeApi.ContextFlags.InitStream;

						if (isServer) {
							status = (SslNativeApi.SecurityStatus)SslNativeApi.AcceptSecurityContext(ref credentialsHandleHandle, ref securityContextHandle, null,
								inflags, SslNativeApi.Endianness.Native, ref contextHandleOut, securityBufferDescriptor, ref outflags, out ts);
						} else {
							status = (SslNativeApi.SecurityStatus)SslNativeApi.InitializeSecurityContextW(ref credentialsHandleHandle, ref securityContextHandle, null,
								inflags, 0, SslNativeApi.Endianness.Native, null, 0, ref contextHandleOut, securityBufferDescriptor, ref outflags, out ts);
						}
						if (status == SslNativeApi.SecurityStatus.OK) {
							byte[] resultArr = new byte[unmanagedBuffer[0].count];
							Marshal.Copy(unmanagedBuffer[0].token, resultArr, 0, resultArr.Length);
							Marshal.FreeCoTaskMem(unmanagedBuffer[0].token);
							result = resultArr;
							resultSz = resultArr.Length;
						} else {
							throw new InvalidOperationException(string.Format("AcceptSecurityContext/InitializeSecurityContextW returned [{0}] during CloseNotify.", status));
						}
					} else {
						throw new InvalidOperationException(string.Format("ApplyControlToken returned [{0}] during CloseNotify.", status));
					}
				}

				var innerStream = (Stream)FtpReflection.GetProperty(sslstate, "InnerStream");
				innerStream.Write(result, 0, resultSz);
			}
		}
	}

	internal unsafe static class SslNativeApi {
		internal enum BufferType {
			Empty,
			Data,
			Token,
			Parameters,
			Missing,
			Extra,
			Trailer,
			Header,
			Padding = 9,
			Stream,
			ChannelBindings = 14,
			TargetHost = 16,
			ReadOnlyFlag = -2147483648,
			ReadOnlyWithChecksum = 268435456
		}

		[StructLayout(LayoutKind.Sequential, Pack = 1)]
		internal struct SSPIHandle {
			public IntPtr HandleHi;
			public IntPtr HandleLo;
			public bool IsZero {
				get {
					return this.HandleHi == IntPtr.Zero && this.HandleLo == IntPtr.Zero;
				}
			}
			[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
			internal void SetToInvalid() {
				this.HandleHi = IntPtr.Zero;
				this.HandleLo = IntPtr.Zero;
			}
			public override string ToString() {
				return this.HandleHi.ToString("x") + ":" + this.HandleLo.ToString("x");
			}
		}
		[StructLayout(LayoutKind.Sequential)]
		internal class SecurityBufferDescriptor {
			public readonly int Version;
			public readonly int Count;
			public unsafe void* UnmanagedPointer;
			public SecurityBufferDescriptor(int count) {
				this.Version = 0;
				this.Count = count;
				this.UnmanagedPointer = null;
			}
		}

		[StructLayout(LayoutKind.Sequential)]
		internal struct SecurityBufferStruct {
			public int count;
			public BufferType type;
			public IntPtr token;
			public static readonly int Size = sizeof(SecurityBufferStruct);
		}

		internal enum SecurityStatus {
			OK,
			ContinueNeeded = 590610,
			CompleteNeeded,
			CompAndContinue,
			ContextExpired = 590615,
			CredentialsNeeded = 590624,
			Renegotiate,
			OutOfMemory = -2146893056,
			InvalidHandle,
			Unsupported,
			TargetUnknown,
			InternalError,
			PackageNotFound,
			NotOwner,
			CannotInstall,
			InvalidToken,
			CannotPack,
			QopNotSupported,
			NoImpersonation,
			LogonDenied,
			UnknownCredentials,
			NoCredentials,
			MessageAltered,
			OutOfSequence,
			NoAuthenticatingAuthority,
			IncompleteMessage = -2146893032,
			IncompleteCredentials = -2146893024,
			BufferNotEnough,
			WrongPrincipal,
			TimeSkew = -2146893020,
			UntrustedRoot,
			IllegalMessage,
			CertUnknown,
			CertExpired,
			AlgorithmMismatch = -2146893007,
			SecurityQosFailed,
			SmartcardLogonRequired = -2146892994,
			UnsupportedPreauth = -2146892989,
			BadBinding = -2146892986
		}
		[Flags]
		internal enum ContextFlags {
			Zero = 0,
			Delegate = 1,
			MutualAuth = 2,
			ReplayDetect = 4,
			SequenceDetect = 8,
			Confidentiality = 16,
			UseSessionKey = 32,
			AllocateMemory = 256,
			Connection = 2048,
			InitExtendedError = 16384,
			AcceptExtendedError = 32768,
			InitStream = 32768,
			AcceptStream = 65536,
			InitIntegrity = 65536,
			AcceptIntegrity = 131072,
			InitManualCredValidation = 524288,
			InitUseSuppliedCreds = 128,
			InitIdentify = 131072,
			AcceptIdentify = 524288,
			ProxyBindings = 67108864,
			AllowMissingBindings = 268435456,
			UnverifiedTargetName = 536870912
		}
		internal enum Endianness {
			Network,
			Native = 16
		}

		[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
		[DllImport("secur32.dll", ExactSpelling = true, SetLastError = true)]
		internal static extern int ApplyControlToken(ref SSPIHandle contextHandle, [In] [Out] SecurityBufferDescriptor outputBuffer);

		[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
		[DllImport("secur32.dll", ExactSpelling = true, SetLastError = true)]
		internal unsafe static extern int AcceptSecurityContext(ref SSPIHandle credentialHandle, ref SSPIHandle contextHandle, [In] SecurityBufferDescriptor inputBuffer, [In] ContextFlags inFlags, [In] Endianness endianness, ref SSPIHandle outContextPtr, [In] [Out] SecurityBufferDescriptor outputBuffer, [In] [Out] ref ContextFlags attributes, out long timeStamp);

		[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
		[DllImport("secur32.dll", ExactSpelling = true, SetLastError = true)]
		internal unsafe static extern int InitializeSecurityContextW(ref SSPIHandle credentialHandle, ref SSPIHandle contextHandle, [In] byte* targetName, [In] ContextFlags inFlags, [In] int reservedI, [In] Endianness endianness, [In] SecurityBufferDescriptor inputBuffer, [In] int reservedII, ref SSPIHandle outContextPtr, [In] [Out] SecurityBufferDescriptor outputBuffer, [In] [Out] ref ContextFlags attributes, out long timeStamp);
	}

#endif
}