using System;
using Microsoft.SPOT;

namespace CPF
{
    public class NMEA_Parser
    {
        public delegate void EndOfMessageDelegate(ByteString s);
        private EndOfMessageDelegate _endOf_GGA_Message;
        private EndOfMessageDelegate _endOf_RMC_Message;   

        private const uint      _MAX_FIELD_SIZE     = 15;
        private const byte      _PERIOD             = (byte) '.';
        private const byte      _COMMA              = (byte) ',';
        private const byte      _CR                 = (byte) '\r';
        private const byte      _LF                 = (byte) '\n';
        private const byte      _ASTERISK           = (byte) '*';
        private const byte      _DOLLAR             = (byte) '$';
        private const byte      _UPPER_CASE_A       = (byte) 'A';
        private const byte      _UPPER_CASE_F       = (byte) 'F';
        private const byte      _LOWER_CASE_A       = (byte) 'a';
        private const byte      _LOWER_CASE_F       = (byte) 'f';
        private const byte      _ZERO               = (byte) '0';
        private const byte      _NINE               = (byte) '9';
        private const byte      _MINUS              = (byte) '-';
        private const byte      _NORTH              = (byte) 'N';
        private const byte      _SOUTH              = (byte) 'S';
        private const byte      _EAST               = (byte) 'E';
        private const byte      _WEST               = (byte) 'W';

        // Value is arbitrary, just larger than max # of fields in NEMA message
        private const uint      _END_OF_LINE = 100;

        private ByteString      _GPRMC;
        private ByteString      _GPGGA;

        // parsing state variables
        private delegate void   _nmeaArgument(uint argumentNumber);
        private _nmeaArgument   _processArgument;
        private uint            _parity;
        private bool            _isChecksumTerm;
        private ByteString      _term;
        private ByteString      _msg;
        private uint            _curTermNumber;
        private bool            _sentenceHasFix;

        // statistics
        private uint            _encodedCharCount;
        private uint            _sentencesWithFixCount;
        private uint            _failedChecksumCount;
        private uint            _passedChecksumCount;

        public NMEA_Parser(EndOfMessageDelegate process_GGA, EndOfMessageDelegate process_RMC)
        {
            _term                   = new ByteString();
            _parity                 = 0;
            _isChecksumTerm         = false;
            _curTermNumber          = 0;
            _sentenceHasFix         = false;
            _encodedCharCount       = 0;
            _sentencesWithFixCount  = 0;
            _failedChecksumCount    = 0;
            _passedChecksumCount    = 0;
            _GPGGA                  = new ByteString("GPGGA");
            _GPRMC                  = new ByteString("GPRMC");
            _msg                    = new ByteString();
            _endOf_GGA_Message      = process_GGA;
            _endOf_RMC_Message      = process_RMC;  
            GPS.Initialize();
        }

        private void Process_GGA(uint argumentNumber)
        {
            switch (argumentNumber)
            {
                case 1 : // Time
                    GPS.Time.Set(_term);
                    break;

                case 2 : // Latitude
                    GPS.Location.SetLatitude(_term);
                    break;

                case 3 : // North / South
                    GPS.Location._rawNewLatData._negative = (_term[0] == _SOUTH);
                    break;

                case 4 :
                    GPS.Location.SetLongitude(_term);
                    break;

                case 5 :
                    GPS.Location._rawNewLngData._negative = (_term[0] == _WEST);
                    break;

                case 6 : // Fix data
                    _sentenceHasFix = (_term[0] > _ZERO);
                    break;

                case  7 : // Satellites used)
                    GPS.Satellites.Set(_term);
                    break;

                case 8 : // HDOP
                    GPS.Hdop.Set(_term);
                    break;

                case 9 : // Altitude
                    GPS.Altitude.Set(_term);
                    break;

                case _END_OF_LINE :
                    GPS.Time.Commit();
                    if (_sentenceHasFix)
                    {
                        GPS.Location.Commit();
                        GPS.Altitude.Commit();
                    }
                    GPS.Satellites.Commit();
                    GPS.Hdop.Commit();
                    _endOf_GGA_Message(_msg);
                    break;

                default : // WTF?
                    // TO DO: Add Eror handling
                    break;
            }
        }

        private void Process_RMC(uint argumentNumber)
        {
            switch (argumentNumber)
            {

                case 1 : // Time
                    GPS.Time.Set(_term);
                    break;

                case 2 : // GPRMC validity
                    _sentenceHasFix = (_term[0] == _UPPER_CASE_A);
                    break;

                case 3 : // Latitude
                    GPS.Location.SetLatitude(_term);
                    break;

                case 4 : // North / South
                    GPS.Location._rawNewLatData._negative = (_term[0] == _SOUTH);
                    break;

                case 5 : // Longitude
                    GPS.Location.SetLongitude(_term);
                    break;

                case 6 : // East / West
                    GPS.Location._rawNewLngData._negative = (_term[0] == _WEST);
                    break;

                case 7 : // Speed
                    GPS.Speed.Set(_term);
                    break;

                case 8 : // Course
                    GPS.Course.Set(_term);
                    break;

                case 9 : // Date
                    GPS.Date.Set(_term);
                    break;

                case _END_OF_LINE:
                    GPS.Date.Commit();
                    GPS.Time.Commit();
                    if (_sentenceHasFix)
                    {
                        GPS.Location.Commit();
                        GPS.Speed.Commit();
                        GPS.Course.Commit();
                    }
                    _endOf_RMC_Message(_msg);
                    break;

                default : // WTF?
                    // TO DO: Add Eror handling
                    break;
            }
        }

        private void Unknown_NMEA_Message(uint argumentNumber)
        {
        }

        // process one character received from GPS
        public bool Parse(byte ch)
        {
            bool isValidSentence = false;
            ++_encodedCharCount;
            switch (ch)
            {
                case _COMMA:
                case _CR:
                case _LF:
                case _ASTERISK:
                    if ((ch != _CR) && (ch != _LF))  // Squash CR-LF
                        _msg.Append(ch);

                    if (ch == _COMMA)
                        _parity ^= ch;

                    if (_term.Available > 0)
                        isValidSentence = EndOfMessage();

                    ++_curTermNumber;
                    _term.Clear();
                    _isChecksumTerm = (ch == _ASTERISK) ? true : false;
                    return isValidSentence;

                case _DOLLAR: // sentence begin
                    _curTermNumber = 0;
                    _parity = 0;
                    _term.Clear();
                    _msg.Clear();
                    _msg.Append(ch);
                    _processArgument = Unknown_NMEA_Message;
                    _isChecksumTerm = false;
                    _sentenceHasFix = false;
                    return false;

                default: // ordinary characters
                    if (_term.Available > 0)
                    {
                        _term.Append(ch);
                        _msg.Append(ch);
                    }
                    if (!_isChecksumTerm)
                        _parity ^= ch;
                    return false;
            }
        }

        private bool EndOfMessage()
        {
            // If it's the checksum term, and the checksum checks out, commit
            if (_isChecksumTerm)
            {
                byte checksum = (byte) (16 * Conversions.HexCharacterToInteger(_term[0]) + 
                                             Conversions.HexCharacterToInteger(_term[1]));
                if (checksum == _parity)
                {
                    _passedChecksumCount++;
                    if (_sentenceHasFix)
                        ++_sentencesWithFixCount;
                    _processArgument(_END_OF_LINE);
                    return true;
                }
                else
                    ++_failedChecksumCount;
                return false;
            }

            // the first term determines the sentence type
            if (_curTermNumber == 0)
            {
                if (_term == _GPRMC)
                    _processArgument = Process_RMC;
                else if (_term == _GPGGA)
                    _processArgument = Process_GGA;
                else
                    _processArgument = Unknown_NMEA_Message;
                return false;
            }

            if (_term.Length > 0)
                _processArgument(_curTermNumber);

            return false;
        }

        public uint     CharsProcessed()   { return _encodedCharCount; }
        public uint     SentencesWithFix() { return _sentencesWithFixCount; }
        public uint     FailedChecksum()   { return _failedChecksumCount; }
        public uint     PassedChecksum()   { return _passedChecksumCount; }
    }
}
