using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Windows.Forms;
using System.Threading;
using System.Diagnostics;

// ensure you have read the Readme and license.txt file!

/* License:
I am releasing this app with source code under the GNU GPL (GNU General Public License) with the following terms:
- It is NOT to be used in any form for commercial purposes!
- Code may not be used in other apps for any purpose.
- Changes may be made to the code provided ALL changes are clearly marked with #region tags and \\\ documentation tags. The documentation tags
  must include:
  - the author of the change(s)
  - why the change(s) was/were made
  - the complete source code is emailed to TheDarkWraith@hotmail.com
- Additions may be added to the code provided that ALL additions are clearly marked with #region tags and \\\ documentation tags. The documentation tags
  must include:
  - the author of the addition(s)
  - why the addition(s) was/were made
  - the complete source code is emailed to TheDarkWraith@hotmail.com
- The assembly name will remain as Weber-Marelli 1_6M_ECU_Tools
- The default namespace will remain as TheDarkWraith.WM16MEV
- The icon for the main form (WM16MEV.cs) will remain as MrFX_Racerboy.ico (the current icon)
- Any new forms added will use the same icon (MrFX_Racerboy.ico)
- The target framework will remain .NET framework 2.0
- The title of the main form (WM16MEV.cs) will remain as Weber-Marelli 1.6M ECU Tools by TheDarkWraith vy where y is the current exe version as built
- Individual code files (i.e. SerialComms.cs for one) will not be used outside of the app and may NOT be used in any other app
- The code will remain C# code and WILL NOT be translated to any other computer language
- All new code files and forms will contain a copy of this license in complete in comment tags right after any using statements at start of file
- The status strip (statusStrip1) of the main form (WM16MEV.cs) WILL NOT BE DELETED OR HIDDEN and the right hand side of this status strip WILL contain the tool strip status label
  called tsslLicense WITH the following text: MAY NOT BE USED FOR COMMERCIAL PURPOSES
*/

namespace TheDarkWraith.WM16MEV
{
    /// <summary>
    /// This class will handle all the serial communications with the 1.6M ECU
    /// </summary>
    #region Class SerialComms
    internal class SerialComms
    {
        #region Delegates
        internal delegate void Sending(bool value);
        internal delegate void Receiving(bool value);
        internal delegate void ECUInitialized();
        internal delegate void Message(String message, bool addtostatus, bool addtrace);
        #endregion

        #region Events
        internal event Sending OnSendData;
        internal event Receiving OnReceiveData;
        internal event ECUInitialized ECUIsInitialized;
        internal static event Message ShowMessage;
        #endregion

        #region Constants
        private const int data_bits = 8;
        private const System.IO.Ports.StopBits stop_bits = System.IO.Ports.StopBits.One;
        private const System.IO.Ports.Parity parity = System.IO.Ports.Parity.None;
        private const System.IO.Ports.Handshake handshaking = System.IO.Ports.Handshake.None;
        // the 1.6M ECU only supports a message up to 256 bytes long
        private int max_transmit_bytes = 256;
        private Byte[] ecu_init = new Byte[] {0x0F, 0xAA, 0xCC};
        private const int ctransmit_delay = 95;    // in ms
        private int cbaud_rate_initializing = 1200;
        private int cbaud_rate_initialized = 7812;
        private int cbaud_rate_highspeed = 62500;
        #endregion

        #region Static variables
        private static List<String> available_comm_ports = null;
        #endregion

        #region Variables
        private System.IO.Ports.SerialPort serial_port = null;
        private List<Byte> bytes_received = null;
        private byte[] iso_code = null;
        //private HighPrecisionTimer hpt = null;
        private DialogResult ecu_initialized = DialogResult.None;
        private bool abort = false;
        private byte[] transmit_bytes = new Byte[1];
        #endregion

        #region Properties
        #region SP
        internal System.IO.Ports.SerialPort SP
        {
            get
            {
                return serial_port;
            }
        }
        #endregion

        #region Available_Comm_Ports
        internal static List<String> Available_Comm_Ports
        {
            get
            {
                return available_comm_ports;
            }
        }
        #endregion

        #region CommPortIsOpen
        internal bool CommPortIsOpen
        {
            get
            {
                if (serial_port == null)
                {
                    return false;
                }

                return serial_port.IsOpen;
            }
        }
        #endregion

        #region ECU_Initialized
        internal DialogResult ECU_Initialized
        {
            get
            {
                return ecu_initialized;
            }
        }
        #endregion

        #region ISOCode
        internal byte[] ISOCode
        {
            get
            {
                return iso_code;
            }
        }
        #endregion
        #endregion

        #region Constructors
        /// <summary>
        /// Static constructor that initializes the available_comm_ports list with all the available comm ports on the users computer
        /// </summary>
        static SerialComms()
        {
            available_comm_ports = new List<string>();

            GetAvailableCommPorts(true);
        }

        /// <summary>
        /// Creates an instance of the serial port and initializes to initial values. Only comm ports allowed are COM1-COM9
        /// </summary>
        /// <param name="port_name">The comm port to use, i.e. COM1</param>
        public SerialComms()
        {
            bytes_received = new List<byte>();
            //hpt = new HighPrecisionTimer();
        }
        #endregion

        #region Methods
        #region Abort
        internal void Abort()
        {
            abort = true;
            if (ShowMessage == null)
            {
                Trace.WriteLine("Abort set");
            }
            else
            {
                ShowMessage("Abort set", true, true);
            }
        }
        #endregion

        #region Reset
        internal void Reset()
        {
            abort = false;
            if (ShowMessage == null)
            {
                Trace.WriteLine("Abort reset");
            }
            else
            {
                ShowMessage("Abort reset", true, true);
            }
        }
        #endregion

        #region Get available comm ports (Static)
        internal static void GetAvailableCommPorts(bool silent)
        {
            available_comm_ports.Clear();

            if (!silent)
            {
                if (ShowMessage == null)
                {
                    Trace.WriteLine("Scanning for available COM ports...");
                }
                else
                {
                    ShowMessage("Scanning for available COM ports...", true, true);
                }
            }

            foreach (String s in System.IO.Ports.SerialPort.GetPortNames())
            {
                if (s.EndsWith("i") || s.Length > 4)
                {
                    if (!silent)
                    {
                        if (ShowMessage == null)
                        {
                            Trace.WriteLine(s + " ignored!");
                        }
                        else
                        {
                            ShowMessage(s + " ignored!", false, true);
                        }
                    }
                    continue;
                }
                available_comm_ports.Add(s);

                if (!silent)
                {
                    if (ShowMessage == null)
                    {
                        Trace.WriteLine(s + " found!");
                    }
                    else
                    {
                        ShowMessage(s + " found!", false, true);
                    }
                }
            }

            if (ShowMessage == null)
            {
                Trace.WriteLine("Scan complete");
            }
            else
            {
                ShowMessage("Scan complete", true, true);
            }
        }
        #endregion

        #region Init
        internal bool Init(String port_name)
        {
            Trace.WriteLine("SerialComms.Init");

            if (!OpenCommPort(port_name))
            {
                Trace.WriteLine("SerialComms.Init: failed to open comm port");
                return false;
            }

            Trace.WriteLine("SerialComms.Init: comm port opened");

            if (!InitializeCommPort(cbaud_rate_initializing))
            {
                Trace.WriteLine("SerialComms.Init: failed to initialize comm port");
                return false;
            }

            Trace.WriteLine("SerialComms.Init: comm port initialized");

            serial_port.DtrEnable = true;
            serial_port.RtsEnable = true;

            Application.DoEvents();

            if (ShowMessage == null)
            {
                Trace.WriteLine("Waiting for ECU wakeup...");
            }
            else
            {
                ShowMessage("Waiting for ECU wakeup...", true, true);
            }

            serial_port.ReceivedBytesThreshold = 6;
            serial_port.DataReceived += new System.IO.Ports.SerialDataReceivedEventHandler(ECUWakeUp);

            while (iso_code == null)
            {
                if (abort)
                {
                    Trace.WriteLine("SerialComms.Init: while retrieving ISO code an abort was received");
                    abort = false;
                    return false;
                }
                Application.DoEvents();
            }

            Trace.WriteLine("SerialComms.Init: ISO code received");

            Thread.Sleep(550);

            return true;
        }
        #endregion

        #region InitComms
        internal bool InitComms()
        {
            Trace.WriteLine("SerialComms.InitComms");

            if (!InitializeECU())
            {
                Trace.WriteLine("SerialComms.InitComms: failed to initialize ECU");
                return false;
            }

            if (ShowMessage == null)
            {
                Trace.WriteLine("Setting baud rate to " + cbaud_rate_initialized);
            }
            else
            {
                ShowMessage("Setting baud rate to " + cbaud_rate_initialized, true, true);
            }
            serial_port.BaudRate = cbaud_rate_initialized; // 7680;

            while (ecu_initialized != DialogResult.Yes)
            {
                if (abort)
                {
                    Trace.WriteLine("SerialComms.InitComms: while initializing ECU an abort was received");
                    abort = false;
                    return false;
                }
                Application.DoEvents();
            }

            Trace.WriteLine("SerialComms.InitComms: ECU initialized");

            return true;
        }
        #endregion

        #region Initialize Comm Port
        internal bool InitializeCommPort(int baud_rate)
        {
            String port_name = "";
            if (serial_port == null)
            {
                return false;
            }
            else
            {
                port_name = serial_port.PortName;
            }

            if (ShowMessage == null)
            {
                Trace.WriteLine("Initializing " + port_name + " at " + baud_rate + " baud...");
            }
            else
            {
                ShowMessage("Initializing " + port_name + " at " + baud_rate + " baud...", true, true);
            }
            serial_port.BaudRate = baud_rate;
            serial_port.DataBits = data_bits;
            serial_port.StopBits = stop_bits;
            serial_port.Parity = parity;
            serial_port.Handshake = handshaking;
            serial_port.ReadExisting();
            serial_port.DiscardInBuffer();
            serial_port.DiscardOutBuffer();
            serial_port.ReadTimeout = int.MaxValue;
            //serial_port.WriteTimeout = 0;
            //serial_port.BreakState = true;
            //serial_port.BreakState = false;
            if (ShowMessage == null)
            {
                Trace.WriteLine(port_name + " initialized as " + serial_port.BaudRate + "," + serial_port.DataBits + "," + serial_port.Parity + "," + serial_port.StopBits);
                Trace.WriteLine(port_name + " write buffer size=" + serial_port.WriteBufferSize);
                Trace.WriteLine(port_name + " read buffer size=" + serial_port.ReadBufferSize);
            }
            else
            {
                ShowMessage(port_name + " initialized as " + serial_port.BaudRate + "," + serial_port.DataBits + "," + serial_port.Parity + "," + serial_port.StopBits, false, true);
                ShowMessage(port_name + " write buffer size=" + serial_port.WriteBufferSize, false, true);
                ShowMessage(port_name + " read buffer size=" + serial_port.ReadBufferSize, false, true);
            }

            return true;
        }
        #endregion

        #region Open Comm Port
        internal bool OpenCommPort(String port_name)
        {
            if (!port_name.StartsWith("COM") || port_name.Length > 4 || !char.IsDigit(port_name, port_name.Length - 1) || !available_comm_ports.Contains(port_name))
            {
                if (ShowMessage == null)
                {
                    Trace.WriteLine("Invalid COM port passed: " + port_name);
                }
                else
                {
                    ShowMessage("Invalid COM port passed: " + port_name, true, true);
                }
                serial_port = null;
                return false;
            }

            if (serial_port != null && serial_port.IsOpen)
            {
                CloseCommPort();
            }

            serial_port = new System.IO.Ports.SerialPort(port_name);
            serial_port.WriteBufferSize = max_transmit_bytes;
            serial_port.ReadBufferSize = max_transmit_bytes;

            try
            {
                ecu_initialized = DialogResult.None;
                serial_port.Open();
                serial_port.ReadExisting();
                DiscardInBuffer();
                DiscardOutBuffer();
                if (ShowMessage == null)
                {
                    Trace.WriteLine(serial_port.PortName + " opened");
                }
                else
                {
                    ShowMessage(serial_port.PortName + " opened", true, true);
                }
            }
            catch (Exception e)
            {
                serial_port = null;
                if (ShowMessage == null)
                {
                    Trace.WriteLine("OpenCommPort: Exception is " + e.Message);
                }
                else
                {
                    ShowMessage("OpenCommPort: Exception is " + e.Message, true, true);
                }
                MessageBox.Show(e.Message, "Exception");
                return false;
            }

            return true;
        }
        #endregion

        #region Close Comm Port
        internal bool CloseCommPort()
        {
            if (serial_port == null)
            {
                if (ShowMessage == null)
                {
                    Trace.WriteLine("CloseCommPort: No COM port defined!");
                }
                else
                {
                    ShowMessage("CloseCommPort: No COM port defined!", true, true);
                }
                return false;
            }

            if (!serial_port.IsOpen)
            {
                if (ShowMessage == null)
                {
                    Trace.WriteLine(serial_port.PortName + " already closed!");
                }
                else
                {
                    ShowMessage(serial_port.PortName + " already closed!", true, true);
                }
                return true;
            }

            ecu_initialized = DialogResult.None;
            iso_code = null;
            String port_name = serial_port.PortName;
            serial_port.ReadExisting();
            serial_port.DiscardOutBuffer();
            serial_port.DiscardInBuffer();
            serial_port.Close();
            serial_port.DtrEnable = false;
            serial_port.Dispose();
            serial_port = null;

            if (ShowMessage == null)
            {
                Trace.WriteLine(port_name + " closed");
            }
            else
            {
                ShowMessage(port_name + " closed", true, true);
            }

            return true;
        }
        #endregion

        #region DiscardInBuffer
        internal void DiscardInBuffer()
        {
            if (serial_port != null)
            {
                serial_port.DiscardInBuffer();
                Trace.WriteLine("SerialComms: discarded in buffer");
            }
        }
        #endregion

        #region DiscardOutBuffer
        internal void DiscardOutBuffer()
        {
            if (serial_port != null)
            {
                serial_port.DiscardOutBuffer();
                Trace.WriteLine("SerialComms: discarded out buffer");
            }
        }
        #endregion

        #region Transmit
        internal bool Transmit(Byte byte_to_transmit, bool add_transmit_delay)
        {
            if (serial_port == null || !serial_port.IsOpen)
            {
                return false;
            }

            transmit_bytes[0] = byte_to_transmit;

            try
            {
                //OnSendData(true);

                //serial_port.DiscardOutBuffer();
                serial_port.Write(transmit_bytes, 0, 1);

                if (add_transmit_delay)
                {
                    Thread.Sleep(ctransmit_delay);
                }

                //OnSendData(false);

                return true;
            }
            catch (Exception t)
            {
                OnSendData(false);

                if (ShowMessage == null)
                {
                    Trace.WriteLine("Transmit: " + t.Message);
                }
                else
                {
                    ShowMessage("Transmit: " + t.Message, true, true);
                }
                MessageBox.Show("Transmit: " + t.Message, "Exception");
                return false;
            }
        }

        internal bool Transmit(Byte[] bytes_to_transmit, int time_between_bytes_in_ms)
        {
            if (serial_port == null || !serial_port.IsOpen || bytes_to_transmit == null || bytes_to_transmit.Length > max_transmit_bytes)
            {
                return false;
            }

            try
            {
                OnSendData(true);

                serial_port.DiscardOutBuffer();

                for (int i = 0; i < bytes_to_transmit.Length; i++)
                {
                    serial_port.Write(bytes_to_transmit, i, 1);
                    Thread.Sleep(time_between_bytes_in_ms);
                }

                OnSendData(false);

                return true;
            }
            catch (Exception t)
            {
                OnSendData(false);

                if (ShowMessage == null)
                {
                    Trace.WriteLine("Transmit: " + t.Message);
                }
                else
                {
                    ShowMessage("Transmit: " + t.Message, true, true);
                }
                MessageBox.Show("Transmit: " + t.Message, "Exception");
                return false;
            }
        }
        #endregion

        #region Receive
        internal List<Byte> Receive(int bytes_expected, int timeout)
        {
            if (serial_port == null || !serial_port.IsOpen)
            {
                Trace.WriteLine("SerialComms.Receive: serial port == NULL || !Open");
                return null;
            }

            if (bytes_received == null)
            {
                bytes_received = new List<byte>();
            }
            else
            {
                bytes_received.Clear();
            }

            if (serial_port != null && serial_port.IsOpen)
            {
                /*
                if (serial_port.BytesToRead != 0)
                {
                    OnReceiveData(true);
                }
                */

                if (bytes_expected < 0 && timeout > 0)
                {
                    Thread.Sleep(timeout);
                }

                while (serial_port.BytesToRead != 0)
                {
                    bytes_received.Add((Byte)serial_port.ReadByte());

                    if (bytes_received.Count == bytes_expected)
                    {
                        break;
                    }

                    /*
                    if (bytes_received[bytes_received.Count - 1] == 0xFE)
                    {
                    }
                    */
                }

                //OnReceiveData(false);

                if (bytes_expected > 0 && bytes_received.Count != bytes_expected)
                {
                    //OnReceiveData(true);

                    while (timeout > 0)
                    {
                        Thread.Sleep(1);
                        timeout -= 1;

                        if (serial_port != null && serial_port.IsOpen)
                        {
                            while (serial_port.BytesToRead != 0)
                            {
                                bytes_received.Add((Byte)serial_port.ReadByte());

                                if (bytes_received.Count == bytes_expected)
                                {
                                    break;
                                }
                            }

                            if (bytes_received.Count == bytes_expected)
                            {
                                break;
                            }
                        }
                    }

                    //OnReceiveData(false);

                    if (bytes_received.Count != bytes_expected)
                    {
                        Trace.WriteLine("SerialComms.Receive: bytes received count != number of bytes expected");
                        for (int i = 0; i < bytes_received.Count; i++)
                        {
                            Trace.WriteLine("\t0x" + bytes_received[i].ToString("X2"));
                        }
                        bytes_received.Clear();
                        return null;
                    }
                }
            }

            if (bytes_received != null && bytes_received.Count == 0)
            {
                bytes_received = null;
            }

            return bytes_received;
        }
        #endregion

        #region ECU wakeup
        private void ECUWakeUp(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
        {
            serial_port.DataReceived -= ECUWakeUp;

            if (Receive(6, 300) == null || bytes_received[0] != 0x55)
            {
                Trace.WriteLine("SerialComms.ECUWakeUp: bytes received not as expected. Parsing...");
                for (int i = 0; i < bytes_received.Count; i++)
                {
                    Trace.Write("0x" + bytes_received[i].ToString("X2"));
                }
                Trace.WriteLine("");

                if (bytes_received[0] == 0xFF && bytes_received[1] == 0x55)
                {
                    List<byte> bytes_received_temp = new List<byte>();
                    for (int i = 1; i < bytes_received.Count; i++)
                    {
                        bytes_received_temp.Add(bytes_received[i]);
                    }

                    if (Receive(1, 300) == null)
                    {
                        Trace.WriteLine("Failed to receive correct ECU wakeup! Cycle ignition switch again");
                        MessageBox.Show("Failed to receive correct ECU wakeup! Cycle ignition switch again", "Error", MessageBoxButtons.OK);
                        serial_port.DataReceived += ECUWakeUp;
                        return;
                    }
                    else
                    {
                        bytes_received_temp.Add(bytes_received[0]);
                        bytes_received = bytes_received_temp;
                    }
                }
                else
                {
                    Trace.WriteLine("Failed to receive correct ECU wakeup! Cycle ignition switch again");
                    MessageBox.Show("Failed to receive correct ECU wakeup! Cycle ignition switch again", "Error", MessageBoxButtons.OK);
                    serial_port.DataReceived += ECUWakeUp;
                    return;
                }
            }
            else
            {
                Trace.WriteLine("SerialComms.ECUWakeUP: bytes received:");
                for (int i = 0; i < bytes_received.Count; i++)
                {
                    Trace.Write("0x" + bytes_received[i].ToString("X2"));
                }
                Trace.WriteLine("");
            }

            if (ShowMessage == null)
            {
                Trace.WriteLine("ECU wakeup!");
            }
            else
            {
                ShowMessage("ECU wakeup!", true, true);
            }

            iso_code = new Byte[4] { bytes_received[1], bytes_received[2], bytes_received[3], bytes_received[4] };
            Trace.WriteLine("ISO code:");
            for (int i = 0; i < iso_code.Length; i++)
            {
                Trace.Write("0x" + iso_code[i].ToString("X2"));
            }
            Trace.WriteLine("");
        }
        #endregion

        #region InitializeECU
        private bool InitializeECU()
        {
            if (ShowMessage == null)
            {
                Trace.WriteLine("Initializing ECU...");
            }
            else
            {
                ShowMessage("Initializing ECU...", true, true);
            }

            serial_port.ReceivedBytesThreshold = 3;
            serial_port.DataReceived += InitializeECUResponse;

            //hpt.Timer(HighPrecisionTimer.HPT.Reset);
            //hpt.Timer(HighPrecisionTimer.HPT.Stop);

            int count = 0;
            while (count < ecu_init.Length)
            {
                if (!Transmit(new byte[] {ecu_init[count]}, ctransmit_delay))
                {
                    Trace.WriteLine("SerialComms.InitializeECU: failed to transmit byte to ECU");
                    serial_port.DataReceived -= InitializeECUResponse;
                    return false;
                }
                count++;
                if (count < ecu_init.Length)
                {
                    Thread.Sleep(110);
                }
                else
                {
                    Thread.Sleep(150);
                }
            }

            return true;
        }
        #endregion

        #region InitializeECUResponse
        private void InitializeECUResponse(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
        {
            if (Receive(3, 300) == null || bytes_received.Count != 3 || bytes_received[0] != ecu_init[0] || bytes_received[1] != ecu_init[1] || bytes_received[2] != ecu_init[2])
            {
                ecu_initialized = DialogResult.No;
                if (ShowMessage == null)
                {
                    Trace.WriteLine("Failed to initialize ECU!");
                }
                else
                {
                    ShowMessage("Failed to initialize ECU!", true, true);
                }
                MessageBox.Show("Failed to initialize ECU!", "ECU init", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            serial_port.DataReceived -= InitializeECUResponse;

            //Trace.WriteLine("Sleeping for 1 second to let ECU finish 'setting up'...");
            // sleep for 1 second to ensure ECU finishes 'setting up'
            //Thread.Sleep(1000);

            if (ShowMessage == null)
            {
                Trace.WriteLine("ECU initialized!");
            }
            else
            {
                ShowMessage("ECU initialized!", true, true);
            }

            if (ECUIsInitialized != null)
            {
                Trace.WriteLine("Firing ECU is initialized event");
                ECUIsInitialized();
            }

            ecu_initialized = DialogResult.Yes;
        }
        #endregion

        #region SetLowSpeedComms
        internal bool SetLowSpeedComms()
        {
            if (serial_port == null)
            {
                return false;
            }

            try
            {
                serial_port.BaudRate = cbaud_rate_initialized;

                Trace.WriteLine("SerialComms: baud rate set to " + cbaud_rate_highspeed);

                // sleep for 0.5 second to ensure changes have taken effect
                Thread.Sleep(500);
            }
            catch
            {
                return false;
            }

            return true;
        }
        #endregion

        #region SetHighSpeedComms
        internal bool SetHighSpeedComms()
        {
            if (serial_port == null)
            {
                return false;
            }

            try
            {
                serial_port.BaudRate = cbaud_rate_highspeed;

                Trace.WriteLine("SerialComms: baud rate set to " + cbaud_rate_highspeed);

                // sleep for 0.5 second to ensure changes have taken effect
                Thread.Sleep(500);
            }
            catch
            {
                return false;
            }

            return true;
        }
        #endregion

        #region Destroy
        internal void Destroy()
        {
            if (serial_port != null)
            {
                if (serial_port.IsOpen)
                {
                    CloseCommPort();
                }
            }
        }
        #endregion
        #endregion
    }
    #endregion
}
