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

using System.IO.Ports;
using SerialPortTester;

using Common.Logging;

namespace SerialWrap
{
    public interface ISerialPortIo : IDisposable
    {
        string PortName { get; }
        string ReadLine();
        void WriteLine(string text);
    }

    public class SerialPortConfig
    {
        public string Name { get; private set; }
        public int BaudRate { get; private set; }
        public int DataBits { get; private set; }
        public StopBits StopBits { get; private set; }
        public Parity Parity { get; private set; }
        public bool DtrEnable { get; private set; }
        public bool RtsEnable { get; private set; }

        public SerialPortConfig(
            string name,
            int baudRate,
            int dataBits,
            StopBits stopBits,
            Parity parity,
            bool dtrEnable,
            bool rtsEnable)
        {
            if (String.IsNullOrWhiteSpace(name)) throw new ArgumentNullException("name");

            this.RtsEnable = rtsEnable;
            this.BaudRate = baudRate;
            this.DataBits = dataBits;
            this.StopBits = stopBits;
            this.Parity = parity;
            this.DtrEnable = dtrEnable;
            this.Name = name;
        }

        public override string ToString()
        {
            return String.Format(
                "{0} (Baud: {1}/DataBits: {2}/Parity: {3}/StopBits: {4}/{5})",
                this.Name,
                this.BaudRate,
                this.DataBits,
                this.Parity,
                this.StopBits,
                this.RtsEnable ? "RTS" : "No RTS");
        }
    }

    // Wrapper around SerialPort
    public class SerialPortIo : ISerialPortIo
    {
        protected ILog Log { get; private set; }
        static readonly ILog s_Log = LogManager.GetLogger(typeof(SerialPortIo));

        readonly SerialPort _port;
        readonly Stream _internalSerialStream;

        private int readTimeout;
        private int writeTimeout;

        public int ReadTimeout
        {
            get { return readTimeout; }
            set 
            {
                readTimeout = value;
                _port.ReadTimeout = readTimeout;
            }
        }

        public int WriteTimeout
        {
            get { return writeTimeout; }
            set
            {
                writeTimeout = value;
                _port.WriteTimeout = writeTimeout;
            }
        }

        public void DiscardInBuffer()
        {
            _port.DiscardInBuffer();
        }

        public bool IsOpen
        {
            get { return _port.IsOpen; }
        }

        public SerialPortIo(SerialPortConfig portConfig)
        {
            if (portConfig == null) throw new ArgumentNullException("portConfig");

            this.Log = LogManager.GetLogger(this.GetType());

            // http://zachsaw.blogspot.com/2010/07/net-serialport-woes.html
            SerialPortFixer.Execute(portConfig.Name);

            var port = new SerialPort(
                portConfig.Name,
                portConfig.BaudRate,
                portConfig.Parity,
                portConfig.DataBits,
                portConfig.StopBits)
            {
                RtsEnable = portConfig.RtsEnable,
                DtrEnable = portConfig.DtrEnable,
                ReadTimeout = 5000,
                WriteTimeout = 5000
            };
            port.Open();

            try
            {
                this._internalSerialStream = port.BaseStream;
                this._port = port;
                this._port.DiscardInBuffer();
                this._port.DiscardOutBuffer();
            }
            catch (Exception ex)
            {
                Stream internalStream = this._internalSerialStream;

                if (internalStream == null)
                {
                    FieldInfo field = typeof(SerialPort).GetField(
                        "internalSerialStream",
                        BindingFlags.Instance | BindingFlags.NonPublic);

                    // This will happen if the SerialPort class is changed
                    // in future versions of the .NET Framework
                    if (field == null)
                    {
                        this.Log.WarnFormat(
                            "An exception occured while creating the serial port adaptor, "
                            + "the internal stream reference was not acquired and we were unable "
                            + "to get it using reflection. The serial port may not be accessible "
                            + "any further until the serial port object finalizer has been run: {0}",
                            ex);

                        throw;
                    }

                    internalStream = (Stream)field.GetValue(port);
                }

                this.Log.DebugFormat(
                    "An error occurred while constructing the serial port adaptor: {0}", ex);

                SafeDisconnect(port, internalStream);
                throw;
            }
        }

        public string PortName
        {
            get { return this._port.PortName; }
        }

        public int Read(char[] buffer, int offset, int count)
        {
            return this._port.Read(buffer, offset, count);
        }

        public int Read(byte[] buffer, int offset, int count)
        {
            return this._port.Read(buffer, offset, count);
        }

        public string ReadLine()
        {
            //return this._port.ReadTo(Environment.NewLine);
            return this._port.ReadLine();
        }

        public string ReadExisting()
        {
            return this._port.ReadExisting();
        }

        public void Write(string text)
        {
            this._port.Write(text);
        }

        public void Write(char[] buffer, int offset, int count)
        {
            this._port.Write(buffer, offset, count);
        }
    

        public void WriteLine(string text)
        {
            //this._port.Write(text);
            //this._port.Write("\r");
            this._port.WriteLine(text);
        }

        public void Dispose()
        {
            this.Dispose(true);
        }

        protected void Dispose(bool disposing)
        {
            SafeDisconnect(this._port, this._internalSerialStream);

            if (disposing)
            {
                GC.SuppressFinalize(this);
            }
        }

        /// <summary>
        /// Safely closes a serial port and its internal stream even if
        /// a USB serial interface was physically removed from the system
        /// in a reliable manner.
        /// </summary>
        /// <param name="port"></param>
        /// <param name="internalSerialStream"></param>
        /// <remarks>
        /// The <see cref="SerialPort"/> class has 3 different problems in disposal
        /// in case of a USB serial device that is physically removed:
        /// 
        /// 1. The eventLoopRunner is asked to stop and <see cref="SerialPort.IsOpen"/> 
        /// returns false. Upon disposal this property is checked and closing 
        /// the internal serial stream is skipped, thus keeping the original 
        /// handle open indefinitely (until the finalizer runs which leads to the next problem)
        /// 
        /// The solution for this one is to manually close the internal serial stream.
        /// We can get its reference by <see cref="SerialPort.BaseStream" />
        /// before the exception has happened or by reflection and getting the 
        /// "internalSerialStream" field.
        /// 
        /// 2. Closing the internal serial stream throws an exception and closes 
        /// the internal handle without waiting for its eventLoopRunner thread to finish, 
        /// causing an uncatchable ObjectDisposedException from it later on when the finalizer 
        /// runs (which oddly avoids throwing the exception but still fails to wait for 
        /// the eventLoopRunner).
        /// 
        /// The solution is to manually ask the event loop runner thread to shutdown
        /// (via reflection) and waiting for it before closing the internal serial stream.
        /// 
        /// 3. Since Dispose throws exceptions, the finalizer is not suppressed.
        /// 
        /// The solution is to suppress their finalizers at the beginning.
        /// </remarks>
        static void SafeDisconnect(SerialPort port, Stream internalSerialStream)
        {
            GC.SuppressFinalize(port);
            GC.SuppressFinalize(internalSerialStream);

            ShutdownEventLoopHandler(internalSerialStream);

            try
            {
                s_Log.DebugFormat("Disposing internal serial stream");
                internalSerialStream.Close();
            }
            catch (Exception ex)
            {
                s_Log.DebugFormat(
                    "Exception in serial stream shutdown of port {0}: {1}", port.PortName, ex);
            }

            try
            {
                s_Log.DebugFormat("Disposing serial port");
                port.Close();
            }
            catch (Exception ex)
            {
                s_Log.DebugFormat("Exception in port {0} shutdown: {1}", port.PortName, ex);
            }
        }

        static void ShutdownEventLoopHandler(Stream internalSerialStream)
        {
            try
            {
                s_Log.DebugFormat("Working around .NET SerialPort class Dispose bug");

                FieldInfo eventRunnerField = internalSerialStream.GetType()
                    .GetField("eventRunner", BindingFlags.NonPublic | BindingFlags.Instance);

                if (eventRunnerField == null)
                {
                    s_Log.WarnFormat(
                        "Unable to find EventLoopRunner field. "
                        + "SerialPort workaround failure. Application may crash after "
                        + "disposing SerialPort unless .NET 1.1 unhandled exception "
                        + "policy is enabled from the application's config file.");
                }
                else
                {
                    object eventRunner = eventRunnerField.GetValue(internalSerialStream);
                    Type eventRunnerType = eventRunner.GetType();

                    FieldInfo endEventLoopFieldInfo = eventRunnerType.GetField(
                        "endEventLoop", BindingFlags.Instance | BindingFlags.NonPublic);

                    FieldInfo eventLoopEndedSignalFieldInfo = eventRunnerType.GetField(
                        "eventLoopEndedSignal", BindingFlags.Instance | BindingFlags.NonPublic);

                    FieldInfo waitCommEventWaitHandleFieldInfo = eventRunnerType.GetField(
                        "waitCommEventWaitHandle", BindingFlags.Instance | BindingFlags.NonPublic);

                    if (endEventLoopFieldInfo == null
                        || eventLoopEndedSignalFieldInfo == null
                        || waitCommEventWaitHandleFieldInfo == null)
                    {
                        s_Log.WarnFormat(
                            "Unable to find the EventLoopRunner internal wait handle or loop signal fields. "
                            + "SerialPort workaround failure. Application may crash after "
                            + "disposing SerialPort unless .NET 1.1 unhandled exception "
                            + "policy is enabled from the application's config file.");
                    }
                    else
                    {
                        s_Log.DebugFormat(
                            "Waiting for the SerialPort internal EventLoopRunner thread to finish...");

                        var eventLoopEndedWaitHandle =
                            (WaitHandle)eventLoopEndedSignalFieldInfo.GetValue(eventRunner);
                        var waitCommEventWaitHandle =
                            (ManualResetEvent)waitCommEventWaitHandleFieldInfo.GetValue(eventRunner);

                        endEventLoopFieldInfo.SetValue(eventRunner, true);

                        // Sometimes the event loop handler resets the wait handle
                        // before exiting the loop and hangs (in case of USB disconnect)
                        // In case it takes too long, brute-force it out of its wait by 
                        // setting the handle again.
                        do
                        {
                            waitCommEventWaitHandle.Set();
                        } while (!eventLoopEndedWaitHandle.WaitOne(2000));

                        s_Log.DebugFormat("Wait completed. Now it is safe to continue disposal.");
                    }
                }
            }
            catch (Exception ex)
            {
                s_Log.ErrorFormat(
                    "SerialPort workaround failure. Application may crash after "
                    + "disposing SerialPort unless .NET 1.1 unhandled exception "
                    + "policy is enabled from the application's config file: {0}",
                    ex);
            }
        }
    }
}