// Copyright 2010-2014 Zach Saw
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Microsoft.Win32.SafeHandles;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

namespace MatterHackers.SerialPortCommunication.FrostedSerial
{

	/// <summary>
	/// SerialPort helper intended to fix IOExceptions in stock Windows implementation, as described in http://zachsaw.blogspot.com/2010/07/net-serialport-woes.html
	/// </summary>
	public class SerialPortFixer : IDisposable
	{
		public static void Execute(string portName)
		{
			using (new SerialPortFixer(portName))
			{
			}
		}

		#region IDisposable Members

		public void Dispose()
		{
			if (m_Handle != null)
			{
				m_Handle.Close();
				m_Handle = null;
			}
		}

		#endregion IDisposable Members

		#region Implementation

		private const int DcbFlagAbortOnError = 14;
		private const int CommStateRetries = 10;
		private SafeFileHandle m_Handle;

		private SerialPortFixer(string portName)
		{
			const int dwFlagsAndAttributes = 0x40000000;
			const int dwAccess = unchecked((int)0xC0000000);

			if ((portName == null) || !portName.StartsWith("COM", StringComparison.OrdinalIgnoreCase))
			{
				throw new ArgumentException("Invalid Serial Port", "portName");
			}
			SafeFileHandle hFile = CreateFile(@"\\.\" + portName, dwAccess, 0, IntPtr.Zero, 3, dwFlagsAndAttributes, IntPtr.Zero);

			if (hFile.IsInvalid)
			{
				WinIoError();
			}
			try
			{
				int fileType = GetFileType(hFile);
				if ((fileType != 2) && (fileType != 0))
				{
					throw new ArgumentException("Invalid Serial Port", "portName");
				}
				m_Handle = hFile;
				InitializeDcb();
			}
			catch
			{
				hFile.Close();
				m_Handle = null;
				throw;
			}
		}

		[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
		private static extern int FormatMessage(int dwFlags, HandleRef lpSource, int dwMessageId, int dwLanguageId,
												StringBuilder lpBuffer, int nSize, IntPtr arguments);

		[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
		private static extern bool GetCommState(SafeFileHandle hFile, ref Dcb lpDcb);

		[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
		private static extern bool SetCommState(SafeFileHandle hFile, ref Dcb lpDcb);

		[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
		private static extern bool ClearCommError(SafeFileHandle hFile, ref int lpErrors, ref Comstat lpStat);

		[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
		private static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, int dwShareMode,
														IntPtr securityAttrs, int dwCreationDisposition,
														int dwFlagsAndAttributes, IntPtr hTemplateFile);

		[DllImport("kernel32.dll", SetLastError = true)]
		private static extern int GetFileType(SafeFileHandle hFile);

		private void InitializeDcb()
		{
			Dcb dcb = new Dcb();
			GetCommStateNative(ref dcb);
			dcb.Flags &= ~(1u << DcbFlagAbortOnError);
			SetCommStateNative(ref dcb);
		}

		private static string GetMessage(int errorCode)
		{
			StringBuilder lpBuffer = new StringBuilder(0x200);
			if (
				FormatMessage(0x3200, new HandleRef(null, IntPtr.Zero), errorCode, 0, lpBuffer, lpBuffer.Capacity,
							  IntPtr.Zero) != 0)
			{
				return lpBuffer.ToString();
			}
			return "Unknown Error";
		}

		private static int MakeHrFromErrorCode(int errorCode)
		{
			return (int)(0x80070000 | (uint)errorCode);
		}

		private static void WinIoError()
		{
			int errorCode = Marshal.GetLastWin32Error();
			throw new IOException(GetMessage(errorCode), MakeHrFromErrorCode(errorCode));
		}

		private void GetCommStateNative(ref Dcb lpDcb)
		{
			int commErrors = 0;
			Comstat comStat = new Comstat();

			for (int i = 0; i < CommStateRetries; i++)
			{
				if (!ClearCommError(m_Handle, ref commErrors, ref comStat))
				{
					WinIoError();
				}
				if (GetCommState(m_Handle, ref lpDcb))
				{
					break;
				}
				if (i == CommStateRetries - 1)
				{
					WinIoError();
				}
			}
		}

		private void SetCommStateNative(ref Dcb lpDcb)
		{
			int commErrors = 0;
			Comstat comStat = new Comstat();

			for (int i = 0; i < CommStateRetries; i++)
			{
				if (!ClearCommError(m_Handle, ref commErrors, ref comStat))
				{
					WinIoError();
				}
				if (SetCommState(m_Handle, ref lpDcb))
				{
					break;
				}
				if (i == CommStateRetries - 1)
				{
					WinIoError();
				}
			}
		}

		#region Nested type: COMSTAT

		[StructLayout(LayoutKind.Sequential)]
		private struct Comstat
		{
			public readonly uint Flags;
			public readonly uint cbInQue;
			public readonly uint cbOutQue;
		}

		#endregion Nested type: COMSTAT

		#region Nested type: DCB

		[StructLayout(LayoutKind.Sequential)]
		private struct Dcb
		{
			public readonly uint DCBlength;
			public readonly uint BaudRate;
			public uint Flags;
			public readonly ushort wReserved;
			public readonly ushort XonLim;
			public readonly ushort XoffLim;
			public readonly byte ByteSize;
			public readonly byte Parity;
			public readonly byte StopBits;
			public readonly byte XonChar;
			public readonly byte XoffChar;
			public readonly byte ErrorChar;
			public readonly byte EofChar;
			public readonly byte EvtChar;
			public readonly ushort wReserved1;
		}

		#endregion Nested type: DCB

		#endregion Implementation
	}
}