// #undef TRACE
using System;
using System.Text;
using System.IO;
using System.IO.Ports;
using System.Threading;

using Microsoft.SPOT;

namespace CPF
{
    // This class is used for communicating with the A3LA through the serial interface using
    // Hayes compatible AT commands
    public class A3LA : IDisposable
    {
        #region Public classes and types

        public enum Result { Success, Failure, TimeOut };

        public enum Response { Match, NoMatch, Timeout };

        public enum BuffersToClear { MobileOriginated, MobileTerminated, BothOriginatedAndTerminated };

        public const int MT_Offset                               = 256;
        public const int MO_TransferredSuccessfully              =   0;
        public const int MO_Transferred_MT_TooBig                =  -1;
        public const int MO_TransferredNoLocationUpdate          =  -2;
        public const int MO_TransferDidNotFinishInAllowedTime    = -10;
        public const int MO_GSS_ReportsMsqQueueFull              = -11;
        public const int MO_MessageHasTooManySegments            = -12;
        public const int MO_GSS_ReportsSessionDidNotComplete     = -13;
        public const int MO_InvalidSegmentSize                   = -14;
        public const int MO_AccessDenied                         = -15;
        public const int MO_ModemLocked                          = -16;
        public const int MO_GatewayNotResponding                 = -17;
        public const int MO_RF_Lost                              = -18;
        public const int MO_LinkFailure                          = -19;
        public const int MO_UnableToInitiateCall                 = -32;
        public const int MO_ModemBusy                            = -35;
        public const int MT_NoMessageReceived                    = -(MT_Offset +  0); 
        public const int MT_MessageReceived                      = -(MT_Offset +  1); 
        public const int MT_MessageReceptionError                = -(MT_Offset +  2);

        #endregion

        static void Trace(string s)
        {
#if (TRACE)
            Debug.Print(s);
#endif
        }

        #region Internal properties and functions

        // true when disposed

        private enum ResponseStates { MatchingPrompt, ExtractingResponse, MatchingTerminator };

        private Uart            _A3LA;
        private byte[]          _request;
        private int             _requestLength;
        private byte[]          _response;
        private int             _responseLength;
        private bool            _disposed;
        private byte[]          _received;
        private ByteString      _requestMsg;
        private ByteString      _responseMsg;
        private ByteString      _timeoutMsg;
        private ByteString      _expectedMsg;
        private ByteString      _unexpectedMsg;
        private ByteString      _logMsg;
        private ByteString      _timeUnit;
        private StringBuilder   _msg;

        private void WriteLog(ByteString logMessage)
        {
            _msg.Clear();
            _msg.Append(logMessage.ToString());
            EngrLogger.write(EngrLogger.ColumnNums.comment, EngrLogger.getPrefix(_msg));
        }

        private void LogRequest(byte[] request)
        {
            _logMsg.Clear();
            _logMsg.Append(_requestMsg);
            _logMsg.Append(request, request.Length);
            WriteLog(_logMsg);
        }

        private void LogRequest(ByteString request)
        {
            _logMsg.Clear();
            _logMsg.Append(_requestMsg);
            WriteLog(_logMsg);
        }

        private void LogExpectedResponse(byte[] response, int length)
        {
            _logMsg.Clear();
            _logMsg.Append(_responseMsg);
            _logMsg.Append(response, length);
            _logMsg.Append(_expectedMsg);
            WriteLog(_logMsg);
        }

        private void LogUnexpectedResponse(byte[] response, int length)
        {
            _logMsg.Clear();
            _logMsg.Append(_requestMsg);
            _logMsg.Append(response, length);
            _logMsg.Append(_unexpectedMsg);
            WriteLog(_logMsg);
        }

        private void LogTimeOut(int timeout)
        {
            _logMsg.Clear();
            _logMsg.Append(_timeoutMsg);
            Conversions.IntegerToByteString(ref _logMsg, _timeoutMsg.Length, timeout);
            _logMsg.Append(_timeUnit);
            WriteLog(_logMsg);
        }

        private void SendCommand(byte[] command)
        {
            LogRequest(command);
            _A3LA.SendLine(command);
        }

        private Response WaitForResponse(byte[] response, out int responseLength, byte[] prompt, byte[] terminator, int timeout)
        {
            int matchPromptPos = 0;
            int matchTerminatorPos = 0;
            int receivedLength;
            ResponseStates state;
            byte ch;

            responseLength = 0;
            receivedLength = 0;
            state = (prompt == null) ? ResponseStates.MatchingTerminator : ResponseStates.MatchingPrompt;
            DateTime timeoutAt = DateTime.Now.AddMilliseconds(timeout);

            // Receive lines until timed out
            while ((DateTime.Now < timeoutAt) | (timeout == -1))
            {
                while (_A3LA.Available() == true)
                {
                    Thread.Sleep(1);
                    ch = _A3LA.GetChar();
                    // Save received character but remove embedded CRLFs
                    if ((ch != IridiumStrings.CR[0]) && (ch != IridiumStrings.LF[0]))
                        _received[receivedLength++] = ch;
                    // Trace("Read " + Utils.StringFromByte(ch));
                    switch (state)
                    {
                        case ResponseStates.MatchingPrompt:
                            //Trace("Matching Prompt State");
                            if (ch == prompt[matchPromptPos])
                            {
                                // Trace("Match in Prompt String");
                                matchPromptPos++;
                                if (matchPromptPos == prompt.Length)
                                {
                                    // Trace("All of prompt string matched");
                                    state = ResponseStates.ExtractingResponse;
                                }
                            }
                            else
                            {
                                //Trace("Prompt string did not Match");
                                matchPromptPos = (ch == prompt[0]) ? 1 : 0;
                            }
                            break;

                        case ResponseStates.ExtractingResponse:
                            //Trace("Extracting Response State");
                            if (response != null)
                            {
                                if (ch == terminator[0])
                                {
                                    _A3LA.PushBackChar(ch);
                                    //Trace("Push back CR to input FIFO");
                                    state = ResponseStates.MatchingTerminator;
                                    //Trace("End of response string");
                                }
                                else
                                {
                                    //Trace("Writing " + Utils.StringFromByte(ch) + " To Response Buffer");
                                    response[responseLength++] = ch;
                                }
                            }
                            break;

                        case ResponseStates.MatchingTerminator:
                            //Trace("Matching Terminator State");
                            if (ch == terminator[matchTerminatorPos])
                            {
                                //Trace("Match in Terminator String");
                                matchTerminatorPos++;
                                if (matchTerminatorPos == terminator.Length)
                                {
                                    //Trace("All of terminator String Matched");
                                    LogExpectedResponse(_received, receivedLength);
                                    return Response.Match;
                                }
                            }
                            else
                            {
                                //Trace("Terminator string did not Match");
                                matchPromptPos = (ch == terminator[0]) ? 1 : 0;
                            }
                            break;

                    }
                }
            }
            _logMsg.Clear();
            _logMsg.Append(_responseMsg);
            if (DateTime.Now > timeoutAt)
                LogTimeOut(timeout);
            else
                LogUnexpectedResponse(_received,  receivedLength);
            return ((DateTime.Now > timeoutAt) ? Response.Timeout : Response.NoMatch);
        }

        #endregion

        #region Constructor/Destructor

        // Create new Iridium A3LA Driver Interface class
        public A3LA(String serialPort, int baudRate = 19200)
        {
            _request        = new byte[512];
            _response       = new byte[512];
            _received       = new byte[512];
            _A3LA           = new Uart(serialPort, baudRate);
            _requestMsg     = new ByteString("Host ===> IRIDIUM: ");
            _responseMsg    = new ByteString("Host <=== IRIDIUM: ");
            _expectedMsg    = new ByteString(" Expected Response");
            _unexpectedMsg  = new ByteString(" Unexpected Response");
            _timeoutMsg     = new ByteString("Host <=== Modem: Timeout after ");
            _timeUnit       = new ByteString(" ms");
            _logMsg         = new ByteString();
            _msg            = new StringBuilder();
            _disposed       = false;
        }

        // Dispose AT Interface
        public void Dispose()
        {
            try
            {
                if (_disposed == false)
                    _A3LA.Dispose();
            }
            finally
            {
                _disposed = true;
            }
        }

        #endregion

        public void PowerOn()
        {
            int unused;
            // Do whatever is needed to turn the iridium modem on here....
            // Wait if necessary....
            // Probe the modem to make sure it is attached and working
            SendCommand(IridiumStrings.AT);
            if (WaitForResponse(null, out  unused, null, IridiumStrings.OK, -1) != A3LA.Response.Match)
                throw new A3LA_Exception(A3LA_Exception.FAILED_INITIALIZE);

            // Turn off command echo (it confuses the response parsing)
            SendCommand(IridiumStrings.AT_E0);
            if (WaitForResponse(null, out unused, null, IridiumStrings.OK, -1) != A3LA.Response.Match)
                throw new A3LA_Exception(A3LA_Exception.FAILED_INITIALIZE);

            // Disable flow control
            SendCommand(IridiumStrings.AT_AMP_K0);
            if (WaitForResponse(null, out unused, null, IridiumStrings.OK, -1) != A3LA.Response.Match)
                throw new A3LA_Exception(A3LA_Exception.FAILED_INITIALIZE);
        }

        public void PowerOff()
        {
        }

        public void Register()
        {
            int unused;
            // Register with Central Services
            SendCommand(IridiumStrings.AT_SBDAREG);
            if (WaitForResponse(null, out unused, null, IridiumStrings.OK, -1) != A3LA.Response.Match)
                throw new A3LA_Exception(A3LA_Exception.COULD_NOT_REGISTER);
        }

        public int SignalQuality()
        {
            int length;
            SendCommand(IridiumStrings.AT_CSQ_RQ);
            if (WaitForResponse(_response, out length, IridiumStrings.CSQ_RSP, IridiumStrings.OK, 120000) != A3LA.Response.Match)
                throw new A3LA_Exception(A3LA_Exception.ERROR);
            if (length != 1)
                throw new A3LA_Exception(A3LA_Exception.DOMAIN_ERROR);
            int result = (int) (_response[0] - 0x30);
            if (result < 0 || result > 5)
                throw new A3LA_Exception(A3LA_Exception.RANGE_ERROR);
            return result;
        }

        //public Result Position(byte[] response, out int responseLength)
        //{
        //    int pos = 0;
        //    SendCommand(IridiumStrings.AT_PUBX);
        //    WaitForResponse(response, out responseLength, IridiumStrings.GGA_RSP, IridiumStrings.OK, -1);
        //    if (!Utils.Match(response, responseLength, IridiumStrings.NMEA_GPGGA, ref pos))
        //        return Result.Failure;
        //    else
        //        return Result.Success;
        //}

        public Result SatelliteTimeAndDate(byte[] response, out int responseLength)
        {
            int pos = 0;
            SendCommand(IridiumStrings.AT_ZDA);
            WaitForResponse(response, out responseLength, IridiumStrings.ZDA_RSP, IridiumStrings.OK, -1);
            if (!Utils.Match(response, _responseLength, IridiumStrings.NMEA_GPZDA, ref pos))
                return Result.Failure;
            else
                return Result.Success;
        }

        public Response SendTextMessage(byte[] message, int length)
        {
            Response rsp;
            int unused;
            _requestLength = 0;
            // Copy the command in the request buffer
            foreach (byte ch in IridiumStrings.AT_SBDWT)
                _request[_requestLength++] = ch;
            // Copy an equals sign "=" into the buffer
            _request[_requestLength++] = IridiumStrings.EQUALS[0];
            // Copy the message into the request buffer[src];
            for (int src = 0; src < length; )
                _request[_requestLength++] = message[src++];
            // Append CR-LF
            _request[_requestLength++] = IridiumStrings.CR[0];
            _request[_requestLength++] = IridiumStrings.LF[0];
            //Debug.Print("Length of send message request:\r\n" + length.ToString());
            //Debug.Print("Data in send message request:\r\n" + Utils.StringFromBytes(message, length));
            //Debug.Print(Utils.StringFromBytes(_request, _requestLength - 2) + " ===> Modem");
            _A3LA.SendRawData(_request, 0, _requestLength);
            rsp = WaitForResponse(null, out  unused, null, IridiumStrings.OK, 5000);
            return rsp;
       }

        public Result ClearMobileOriginatedSequenceNumber()
        {
            Response    rsp;
            int         error;
            bool        eol;
            bool        success;
            int         pos = 0; ;
            SendCommand(IridiumStrings.AT_SBDC);
            rsp = WaitForResponse(_response, out _responseLength, null, IridiumStrings.OK, -1);
            error = NumberParser.BytesToInt(_response, _responseLength, ref pos, out eol, out success);
            if (!success)
                throw new A3LA_Exception(A3LA_Exception.FORMAT_ERROR);
            if (error == 0)
                return Result.Success;
            else
                return Result.Failure;
        }

        public Result ClearBuffers(BuffersToClear action)
        {
            Response    rsp;
            int         error;
            bool        eol;
            bool        success;
            int         pos = 0; ;
            switch (action)
            {
                case BuffersToClear.MobileOriginated :
                    SendCommand(IridiumStrings.AT_SBDD0);
                    break;

                case BuffersToClear.MobileTerminated :
                    SendCommand(IridiumStrings.AT_SBDD1);
                    break;

                case BuffersToClear.BothOriginatedAndTerminated :
                    SendCommand(IridiumStrings.AT_SBDD2);
                    break;

                default:
                    throw new A3LA_Exception(A3LA_Exception.RANGE_ERROR);
            }
            rsp = WaitForResponse(_response, out _responseLength, null, IridiumStrings.OK, -1);
            error = NumberParser.BytesToInt(_response, _responseLength, ref pos, out eol, out success);
            if (!success)
                throw new A3LA_Exception(A3LA_Exception.FORMAT_ERROR);
            if (error == 0)
                return Result.Success;
            else
                return Result.Failure;
        }

        public void ReceiveTextMessage(byte[] message, out int length)
        {
            Response rsp;
            SendCommand(IridiumStrings.AT_SBDRT);
            rsp = WaitForResponse(message, out length, IridiumStrings.SBDRT_RSP, IridiumStrings.I_NEED_DOLLARS, -1);
        }

        public void SendBinaryMessage(byte[] message, int length)
        {
            int checksum = 0;

            // Copy the command in the request buffer
            foreach (byte ch in IridiumStrings.AT_SBDWT)
                _request[_requestLength++] = ch;

            // Insert length of text message into the request buffer
            _request[_requestLength++] = (byte) ((length & 0xFF00) >> 8);
            _request[_requestLength++] = (byte)  (length & 0x00FF);

            // Copy the message into the request buffer[src];
            for (int src = 0; src < length; )
            {
                checksum += message[src];
                _request[_requestLength++] = message[src++];
            }

            // Insert the checksum into request buffer
            _request[_requestLength++] = (byte)((checksum & 0xFF00) >> 8);
            _request[_requestLength++] = (byte) (checksum & 0x00FF);
            _request[_requestLength++] = IridiumStrings.CR[0];
            _request[_requestLength++] = IridiumStrings.LF[0];
            _A3LA.SendRawData(_request, 0, _requestLength);
        }

        public void Status(out int mo_Status, out int mo_NextSeqNum,
                           out int mt_Status, out int mt_NextSeqNum,
                           out int mt_Length, out int mt_Queued)
        {
            int pos = 0;
            bool eol;
            bool success;
            Response rsp;
     
            SendCommand(IridiumStrings.AT_SBDIX);
            rsp = WaitForResponse(_response, out _responseLength, IridiumStrings.SBDIX_RSP, IridiumStrings.OK, -1);
            // Trace("Response from Status Message:\t\t" + Utils.StringFromBytes(_response, _responseLength));
            if (rsp != A3LA.Response.Match)
                throw new A3LA_Exception(A3LA_Exception.ERROR);

            // Parse the MO Status, 0 <= status <= 2
            mo_Status  = NumberParser.BytesToInt(_response, _responseLength, ref pos, out eol, out success);
            if (!success)
                throw new A3LA_Exception(A3LA_Exception.FORMAT_ERROR);

            if ((mo_Status < 0) || (mo_Status > 2))
                throw new A3LA_Exception(A3LA_Exception.RANGE_ERROR);

            // Parse the modem originated sequence #
            mo_NextSeqNum = NumberParser.BytesToInt(_response, _responseLength, ref pos, out eol, out success);
            if (!success)
                throw new A3LA_Exception(A3LA_Exception.FORMAT_ERROR);

            if ((mo_NextSeqNum < 0) || (mo_NextSeqNum > 65535))
                throw new A3LA_Exception(A3LA_Exception.RANGE_ERROR);

            // Parse the MT Status, 0 <= status <= 2
            mt_Status = NumberParser.BytesToInt(_response, _responseLength, ref pos, out eol, out success);
            if (!success)
                throw new A3LA_Exception(A3LA_Exception.FORMAT_ERROR);

            if ((mt_Status < 0) || (mt_Status > 2))
                throw new A3LA_Exception(A3LA_Exception.RANGE_ERROR);

            // Parse the modem terminated sequence #
            mt_NextSeqNum = NumberParser.BytesToInt(_response, _responseLength, ref pos, out eol, out success);
            if (!success)
                throw new A3LA_Exception(A3LA_Exception.FORMAT_ERROR);

            // Parse the message Length #
            mt_Length = NumberParser.BytesToInt(_response, _responseLength, ref pos, out eol, out success);
            if (!success)
                throw new A3LA_Exception(A3LA_Exception.FORMAT_ERROR);

            // Parse the number of queued messages
            mt_Queued = NumberParser.BytesToInt(_response, _responseLength, ref pos, out eol, out success);
            if (!success)
                throw new A3LA_Exception(A3LA_Exception.FORMAT_ERROR);

            if (eol != true)
                throw new A3LA_Exception(A3LA_Exception.FORMAT_ERROR);
        }
    }
}
