using System;
using System.IO.Ports;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;

namespace HWModules
{
    public class I2CSerialPort : IUart 
    {

        // Private Members
        private I2CUartBridge _bridge;
        private MotherBoard _motherBoard;
        private byte[] _inBuff;
        private byte[] _outBuff;
        private const int BUFFSIZE = 2048;
        private object _rwLock;
        private string _portName;

        public int BaudRate { get; private set; }
        public int DataBits { get; private set; }
        public Parity Parity { get; private set; }
        public StopBits StopBits { get; private set; }
        public bool IsOpen { get; protected set; }

        public int TimeOut
        {
            set
            {
                _bridge.TimeOut = value;
            }
        }

        /// <summary>
        /// Port name, must be windows style, COM7 ... COM12 at present.
        /// </summary>
        public string PortName
        {
            get { return _portName; }
            private set
            {
                I2CDevice.Configuration cfg = PortNameToConfig(value);
                int slotnum = PortNameToSlotNumber(value);

                if (cfg == null || slotnum < 0) 
                    return;

                try
                {MbInterrupt = PortNameToIrqLine(value); }
                catch (ArgumentOutOfRangeException e)
                {return;}

                _portName = value;
                I2CConfig = cfg;
                SlotNumber = slotnum;

            }
        }

        public MbInterrupt MbInterrupt { get; private set; }
        public I2CDevice.Configuration I2CConfig { get; private set; }
        public int SlotNumber { get; private set; }


        /// <summary>
        /// Event to handled new data on the port (as received by IRQ.
        /// </summary>
        public event UartDataReceivedEventHandler DataReceived;

        /// <summary>
        /// Event to handle errors on this port. 
        /// </summary>
        public event UartDataReceivedEventHandler ErrorReceived;

        /// <summary>
        /// Main constructor, supplied commName also sets I2c address and mother
        /// board slot number
        /// </summary>
        /// <param name="commName">Comm port name, in windows style. Appropriate
        /// values are: COM7, COM8, ... , COM12 at present.</param>
        /// <param name="baud">Value of the baudrate</param>
        /// <param name="dataBits">Databits (7,8,9)</param>
        /// <param name="parity">Parity of the port (None, Odd, Even, Mark, Space)</param>
        /// <param name="stopBits">Stopbits of the uart (1, 1.5, 2)</param>
        public I2CSerialPort(String commName, 
            int baud=9600, int dataBits=8, 
            Parity parity= Parity.None, StopBits stopBits=StopBits.One )
        {
            PortName = commName;
            BaudRate = baud;
            DataBits = dataBits;
            Parity = parity;
            StopBits = stopBits;
            _motherBoard = MotherBoard.Instance;
            _inBuff = new  byte[BUFFSIZE];
            _outBuff = new byte[BUFFSIZE];
            _rwLock = new object();
        }

        /// <summary>
        /// Converts the supplied port name to 7-bit i2c address
        /// </summary>
        /// <param name="portName">Comm port name, in windows style. Appropriate
        /// values are: COM7, COM8, ... , COM12 at present.</param>
        /// <returns></returns>
        private static I2CDevice.Configuration PortNameToConfig(string portName)
        {
            ushort address = 0;

            if (String.Equals(portName, "COM7"))
                address = 0x60 >> 1;
            if (String.Equals(portName, "COM8"))
                address = 0x62 >> 1;
            if (String.Equals(portName, "COM9"))
                address = 0x68 >> 1;
            if (String.Equals(portName, "COM10"))
                address = 0x6A >> 1;
            if (String.Equals(portName, "COM11"))
                address = 0x6C >> 1;
            if (String.Equals(portName, "COM12"))
                address = 0x6E >> 1;

            return address == 0 ? null : new I2CDevice.Configuration(address, 400);//TODO P0 NEED TO SPEED THIS BACK UP
        }

        /// <summary>
        /// Converts the input comm port name to the appropriate slot
        /// number that it maps to.
        /// </summary>
        /// <param name="value">Comm port name, in windows style. Appropriate
        /// values are: COM7, COM8, ... , COM12 at present.</param>
        /// <returns></returns>
        private static int PortNameToSlotNumber(string value)
        {
            if (String.Equals(value, "COM7") ||
                String.Equals(value, "COM8") || 
                String.Equals(value, "COM9"))
            {
                return 3;
            }

            if (String.Equals(value, "COM10") ||
                String.Equals(value, "COM11") ||
                String.Equals(value, "COM12"))
            {
                return 4;
            }

            else
            {
                return -1;
            }
        }

        /// <summary>
        /// Convert from the input comm port name to the appropriate IRQ
        /// interruptport on the motherboard. 
        /// </summary>
        /// <param name="portName"></param>
        /// <returns>Comm port name, in windows style. Appropriate
        /// values are: COM7, COM8, ... , COM12 at present.</returns>
        private static MbInterrupt PortNameToIrqLine(string portName)
        {
            if (String.Equals(portName, "COM7"))
                return MbInterrupt.CH07IRQ;
            else if (String.Equals(portName, "COM8"))
                return MbInterrupt.CH08IRQ;
            else if (String.Equals(portName, "COM9"))
                return MbInterrupt.CH09IRQ;
            else if (String.Equals(portName, "COM10"))
                return MbInterrupt.CH10IRQ;
            else if (String.Equals(portName, "COM11"))
                return MbInterrupt.CH11IRQ;
            else if (String.Equals(portName, "COM12"))
                return MbInterrupt.CH12IRQ;
            else 
                throw new ArgumentOutOfRangeException();
        }

        /// <summary>
        /// Opens the serial port by virtue of initializing the connection 
        /// to the I2C and configuring it. 
        /// </summary>
        /// <returns>True on success, false on failure.</returns>
        public virtual bool Open()
        {
            if (!InitI2CBridge())
            {
                Debug.Print("Failed to open the i2c bridge");
                return false;
            }

            // Attempt to attached the IRQ
            _motherBoard.RegisterInterruptHandler(MbInterrupt, ProcessHardwareIrq);
            _motherBoard.ClearInterrupt(MbInterrupt);
            IsOpen = true;
            return true;

        }

        private bool InitI2CBridge()
        {
            try
            {
                if (_bridge != null)
                {
                    _bridge.Dispose();
                    _bridge = null;
                }
                _bridge = new I2CUartBridge(I2CConfig, SlotNumber, BaudRate, Parity, DataBits, StopBits);
                return _bridge != null;
            }
            catch
            {
                _bridge.Dispose();
                _bridge = null;
                return false;
            }
        }

        /// <summary>
        /// Dispose of the I2C object and effectively close the port
        /// </summary>
        public void Close()
        {
            if (!IsOpen) return;
            _bridge.Dispose();
            _bridge = null;
        }

        public int BytesToRead { get; private set; }

        /// <summary>
        /// Read in bytes from the local buffer.
        /// </summary>
        /// <param name="buffer">target byte array to store data</param>
        /// <param name="offset">target byte array start location</param>
        /// <param name="count">number of bytes to copy</param>
        /// <returns>Number of bytes copied.</returns>
        public int Read(byte[] buffer, int offset, int count)
        {
            if (count <= 0 || offset > BytesToRead) return 0;
            if (count> BUFFSIZE) count = BUFFSIZE-offset;

            lock (_rwLock)
            {
                Array.Copy(_inBuff, 0, buffer, offset, count);
                BytesToRead -= count;

                //now shift data.
                if (BytesToRead != 0)
                {
                    Array.Copy(_inBuff, count, _inBuff, 0, BytesToRead );
                }

            }
            return count;
        }

        /// <summary>
        /// Read a single byte from the fifo buffer.
        /// </summary>
        /// <returns></returns>
        public byte ReadByte()
        {
            byte outByte;
            lock (_rwLock)
            {
                outByte = _inBuff[0];
                BytesToRead -= 1;
                Array.Copy(_inBuff,1,_inBuff,0,BytesToRead);
            }

            return outByte;
        }

        /// <summary>
        /// Number of Bytes ready to write out to the serial port.
        /// </summary>
        public int BytesToWrite { get; private set; }

        /// <summary>
        /// Write out bytes directly to the i2c chip for immediate write.
        /// </summary>
        public void Write(byte[] buffer, int offset, int count)
        {
            lock (_rwLock)
            {
                _bridge.i2cprint(buffer, count, offset);
            }
        }

        /// <summary>
        /// Write a single byte out to the serial port.
        /// </summary>
        public void WriteByte(byte value)
        {
                _outBuff[0] = value;
                Write(_outBuff,0,1);
        }

        public void Flush()
        {
            throw new NotImplementedException();
        }

        private byte[] _fifoBytes = new byte[1024];
        /// <summary>
        /// Delegate to process i2c interactions when an interrupt is generated.
        /// </summary>
        public void ProcessHardwareIrq(uint port, uint state, DateTime time)
        {
            lock (_rwLock)
            {
                try
                {
                    Array.Clear(_fifoBytes, 0, _fifoBytes.Length);
                    int bytesRead = 0;

                    do
                    {
                        bytesRead = _bridge.FifoRead(_fifoBytes);
                        if (bytesRead <= 0) continue;
                        if (bytesRead + BytesToRead > _inBuff.Length)
                        {
                            Debug.Print("Buffer Overrun on I2CSerialPort");
                            bytesRead = _inBuff.Length - BytesToRead;
                        }
                        //Debug.Print("INRX: " + bytesRead);
                        Array.Copy(_fifoBytes, 0, _inBuff, BytesToRead, bytesRead);
                        BytesToRead += bytesRead;
                    } while (bytesRead > 0);

                }
                finally
                {
                    _motherBoard.ClearInterrupt(MbInterrupt);
                }
            }

            //Debug.Print("I2CSerialPort: Got IRQ, bytes read = " + BytesToRead.ToString());
            if (DataReceived != null) DataReceived(this, new EventArgs());

        }
    }
}