//-------------------------------------------------------------------------------------
// SID Monitor - Utility for Sudden Ionospheric Disturbances Monitoring Stations
// Copyright (C) 2006-2011 - Lionel Loudet
//
// 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//-------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;
using System.IO.Ports;
using System.Threading;

// added ------------------------------------------------------------------------------
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace SID_monitor
{


    /// SerialPort IOException Workaround in C#
    /// Zach Saw's Blog
    /// http://zachsaw.blogspot.com/2010_07_01_archive.html
    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

        #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

        #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

        #endregion
    
    }


    /// <summary>
    /// Serial Port class overloading to fix poor handling of USB disconnection by .NET leading to an
    /// UnauthorizedAccess Exception if the connection is lost before the port is closed.
    /// Source: http://social.msdn.microsoft.com/Forums/en-US/netfxbcl/thread/8a1825d2-c84b-4620-91e7-3934a4d47330/
    /// </summary>
    public partial class SerialPortFixed : SerialPort
    {
        public SerialPortFixed(string portName) : base(portName) { }

        public new void Open()
        {
            try
            {
                base.Open();

                /*
                ** because of the issue with the FTDI USB serial device,
                ** the call to the stream's finalize is suppressed
                **
                ** it will be un-suppressed in Dispose if the stream
                ** is still good
                */
                GC.SuppressFinalize(BaseStream);
            }
            catch
            {
            }
        }

        public new void Dispose()
        {
            Dispose(true);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing && (base.Container != null))
            {
                base.Container.Dispose();
            }

            try
            {
                /*
                ** because of the issue with the FTDI USB serial device,
                ** the call to the stream's finalize is suppressed
                **
                ** an attempt to un-suppress the stream's finalize is made
                ** here, but if it fails, the exception is caught and
                ** ignored
                */
                if (BaseStream.CanRead)
                {
                    GC.ReRegisterForFinalize(BaseStream);
                }
            }
            catch
            {
            }

            base.Dispose(disposing);
        }
    }

    /// <summary>
    /// 
    /// Serial Port ADC converter
    /// 
    /// Adapted from MAXIM Application note 827
    /// http://www.maxim-ic.com/appnotes.cfm/appnote_number/827
    /// "12 BIT A/D ON PC SERIAL PORT AND DRAWS MICROPOWER"
    /// Wettroth  12/5/94,	Maxim Integrated Products- Sunnyvale
    /// 
    /// </summary>
    public class SerialADC
    {
        TimeSpan ClockHoldTime = TimeSpan.FromTicks((long)(0.01 * TimeSpan.TicksPerMillisecond));    // 10µs clock high or low hold time -- min 0.125µs
        TimeSpan TimeoutMAX187 = TimeSpan.FromTicks((long)(0.05 * TimeSpan.TicksPerMillisecond)); // 50µs MAX187 timeout -- min 8.5µs
        TimeSpan CSPulseWidth = TimeSpan.FromTicks((long)(0.01 * TimeSpan.TicksPerMillisecond)); // 10µs MAX187 /CS pulse width -- min 0.5µs

        private int ThreadTimeout = 8 * SID_monitor.Properties.Settings.Default.AcquisitionUpdate / 10;  //timeout for completion of ADC thread. Set by default to 80% of the acquisition period

        private SerialPortFixed port = null;
        private UInt32 ad_value;
        private String errorMsg = String.Empty;
        private String port_name = String.Empty;


        public SerialADC(string port_name)
        {
            this.port_name = String.Copy(port_name);
            this.init();
        }


        private void init()
        {
            for (int i = 0; i < SerialPortFixed.GetPortNames().Length; i++) // we check if the port_name is in the list of valid ports
            {
                if (port_name == SerialPortFixed.GetPortNames()[i])
                {

                    try
                    {
                        SerialPortFixer.Execute(port_name);
                        port = new SerialPortFixed(port_name);
                        break;
                    }
                    catch (Exception ex)
                    {
                        Program.MainForm.outputTextBoxDockablePanel.AddOutputTextErrorMessage("*** Cannot access serial port \"" + port_name + "\"\n*** " + ex.ToString().Split('\n')[0] + "\n");
                    }

                }
            }
            if (port == null)
            {
                Program.MainForm.outputTextBoxDockablePanel.AddOutputTextErrorMessage("*** Cannot access serial port \"" + port_name + "\"\n");
            }
        }


        public UInt32 GetADC()
        {
            if (port != null)
            {
                try
                {
                    Thread currentGetADCThread = new Thread(new ThreadStart(GetADCThread));
                    currentGetADCThread.Priority = ThreadPriority.AboveNormal;
                    currentGetADCThread.Start();
                    Thread.Sleep(0);

                    if (currentGetADCThread.Join(ThreadTimeout) == false)
                    {
                        Program.MainForm.outputTextBoxDockablePanel.AddOutputTextWarningMessage("Timeout exceeded for ADC response. Increase Acquition Update Period.\n");
                        // if the thread timed out, it may not had time to close the serial port
                        if ((port != null) && (port.IsOpen))
                        {
                            port.Close();
                        }
                        return 0;
                    }

                    if (!String.IsNullOrEmpty(errorMsg))
                    {
                        Program.MainForm.outputTextBoxDockablePanel.AddOutputTextWarningMessage(errorMsg);
                        return 0;
                    }
                    return ad_value;
                }
                catch (Exception ex)
                {
                    Program.MainForm.outputTextBoxDockablePanel.AddOutputTextRRDToolErrorMessage(ex.ToString().Split('\n')[0] + "\n");
                    return 0;
                }

            }
            else
            {
                Program.MainForm.outputTextBoxDockablePanel.AddOutputTextErrorMessage("*** Serial port not defined. Trying to reopen.\n");
                this.init();
                return 0;
            }
        }

        public void GetADCThread()
        {
            ad_value = 0;
            errorMsg = String.Empty;

            try
            {
                port.Open();

                // unsets RTS (RTS=-5V; /CS = high) to deselect MAX187
                port.RtsEnable = false;
                Thread.Sleep(CSPulseWidth);

                // sets DTR (DTR=+5V; SCLK = low)
                port.DtrEnable = true;

                // sets RTS (RTS=+5V; /CS = low) to select MAX187
                port.RtsEnable = true;
                Thread.Sleep(TimeoutMAX187);   // wait convert complete

                if (port.DsrHolding == true)  // conversion not complete -- DOUT should be high (DSR -5V=false)
                {
                    errorMsg += "ADC converter not responding.\n";
                }

                // clock in 12 bits to ad_value
                for (int i = 1; i <= 12; i++)
                {
                    Thread.Sleep(ClockHoldTime);
                    // unsets DTR (DTR=-5V; SCLK = high)
                    port.DtrEnable = false;       /* clock high */
                    Thread.Sleep(ClockHoldTime);

                    // sets DTR (DTR=+5V; SCLK = low)
                    port.DtrEnable = true;        /* clock low */
                    Thread.Sleep(ClockHoldTime);

                    // get a bit and put in place
                    if (port.DsrHolding == false) // DOUT is high (DSR -5V=false)
                    {
                        ad_value += 1;
                    }
                    // shift left one
                    ad_value = ad_value << 1;
                }

                // a 13th clock
                // unsets DTR (DTR=-5V; SCLK = high)
                port.DtrEnable = false;       /* clock high */
                Thread.Sleep(ClockHoldTime);
                // sets DTR (DTR=+5V; SCLK = low)
                port.DtrEnable = true;        /* clock low */
                Thread.Sleep(ClockHoldTime);


                // unsets RTS (RTS=-5V; /CS = high) to deselect MAX187
                port.RtsEnable = false;
                Thread.Sleep(CSPulseWidth);

                // unsets DTR (DTR=-5V; SCLK = high)
                port.DtrEnable = false;

                ad_value = ad_value >> 1;   // fixup

            }
            // The UnauthorizedAccess Exception should not be raised when using the workaround implemented in the SerialPortFixed class 
            catch (UnauthorizedAccessException ex)
            {
                errorMsg += "!!! Cannot acquire data. " + ex.ToString().Split('\n')[0] + " Try disconnecting/reconnecting the serial/USB interface.\n";
            }
            catch (Exception ex)
            {
                errorMsg += "*** Cannot acquire data. " + ex.ToString().Split('\n')[0] + "\n";
            }
            finally
            {
                if ((port != null) && (port.IsOpen))
                {
                    port.Close();
                }
            }
        }
        

        #region Properties
        public bool IsOpened
        {
            get
            {
                return port.IsOpen;
            }
        }

        public string PortName
        {
            get
            {
                return port.PortName;
            }
        }
        #endregion

    
    }


}
