using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace LibADSB
{
    public class AirbornePositionMsg : ExtendedSquitter
    {

        [Browsable(false)]
        [DescriptionAttribute("Position is availble in Airborne Position Message")]
        public bool HasValidPosition
        {
            get
            {
                return horizontal_position_available;
            }
        }

        [Browsable(false)]
        [DescriptionAttribute("Altitude is availble in Airborne Position Message")]
        public bool HasValidAltitude
        {
            get
            {
                return altitude_available;
            }
        }

        [Browsable(false)]
        [DescriptionAttribute("Altitude in Airborne Position Message [ft]")]
        public int Altitude
        {
            get
            {
		        bool Qbit = (altitude_encoded&0x10)!=0;
		        int N;
		        if (Qbit) { // altitude reported in 25ft increments
			        N = (altitude_encoded&0xF) | ((altitude_encoded&0xFE0)>>1);
			        return (int)((25*N-1000)+ 0.5);
		        }
		        else { // altitude is above 50175ft, so we use 100ft increments

			        // it's decoded using the Gillham code
			        int C1 = (0x800&altitude_encoded)>>11;
			        int A1 = (0x400&altitude_encoded)>>10;
			        int C2 = (0x200&altitude_encoded)>>9;
			        int A2 = (0x100&altitude_encoded)>>8;
			        int C4 = (0x080&altitude_encoded)>>7;
			        int A4 = (0x040&altitude_encoded)>>6;
			        int B1 = (0x020&altitude_encoded)>>5;
			        int B2 = (0x008&altitude_encoded)>>3;
			        int D2 = (0x004&altitude_encoded)>>2;
			        int B4 = (0x002&altitude_encoded)>>1;
			        int D4 = (0x001&altitude_encoded);

			        // this is standard gray code
			        int N500 = grayToBin(D2<<7|D4<<6|A1<<5|A2<<4|A4<<3|B1<<2|B2<<1|B4, 8);

			        // 100-ft steps must be converted
			        int N100 = grayToBin(C1<<2|C2<<1|C4, 3)-1;
			        if (N100 == 6) N100=4;
			        if (N500%2 != 0) N100=4-N100; // invert it

			        return (int)((-1200+N500*500+N100*100) + 0.5);
		        }
            }
        }

        [Browsable(false)]
        [DescriptionAttribute("Surveillance status of Airborne Position Message")]
        public byte SurveillanceStatus
        {
            get
            {
                return surveillance_status;
            }
        }

        [Browsable(false)]
        [DescriptionAttribute("Synchronization status of Time of Applicability of Airborne Position Message")]
        public bool IsUTCTime
        {
            get
            {
                return time_flag;
            }
        }

        [Browsable(false)]
        [DescriptionAttribute("Indicates ODD format of Airborne Position Message")]
        public bool IsOddFormat
        {
            get
            {
                return cpr_format;
            }
        }

        [Browsable(false)]
        [DescriptionAttribute("Indicates barometric measuremnet of altitude of Airborne Position Message")]
        public bool IsBarometricAltitude
        {
            get
            {
    		    return this.FormatTypeCode < 20;
            }
        }

        [Browsable(false)]
        [DescriptionAttribute("CPR-encoded Latitude of Airborne Position Message")]
        public int CPREncodedLat
        {
            get
            {
                return cpr_encoded_lat;
            }
        }

        [Browsable(false)]
        [DescriptionAttribute("CPR-encoded Longitude of Airborne Position Message")]
        public int CPREncodedLon
        {
            get
            {
                return cpr_encoded_lon;
            }
        }

        [Browsable(false)]
        [DescriptionAttribute("Horizontal containment radius limit of Airborne Position Message")]
        public double HorizontalContainmentRadiusLimit
        {
            get
            {
		        switch (FormatTypeCode) 
                {
		            case 0: case 18: case 22: return -1;
		            case 9: case 20: return 7.5;
		            case 10: case 21: return 25;
		            case 11:
			            if (nic_suppl_a) return 75;
			            else return 185.2;
		            case 12: return 370.4;
		            case 13:
			            if (!nic_suppl_b) return 926;
			            else if (nic_suppl_a) return 1111.2;
			            else return 555.6;
		            case 14: return 1852;
		            case 15: return 3704;
		            case 16:
			            if (nic_suppl_a) return 7408;
			            else return 14816;
		            case 17: return 37040;
		            default: return 0;
		        }
            }
        }

        [Browsable(false)]
        [DescriptionAttribute("")]
        public byte NavigationIntegrityCategory
        {
            get
            {
 		        switch (FormatTypeCode) 
                {
		            case 0: case 18: case 22: return 0;
		            case 9: case 20: return 11;
		            case 10: case 21: return 10;
		            case 11:
			            if (nic_suppl_a) return 9;
			            else return 8;
		            case 12: return 7;
		            case 13: return 6;
		            case 14: return 5;
		            case 15: return 4;
		            case 16:
			            if (nic_suppl_a) return 3;
			            else return 2;
		            case 17: return 1;
		            default: return 0;
		        }
           }
        }

        [Browsable(false)]
        [DescriptionAttribute("NIC supplement A of Airborne Position Message")]
        public bool NICSupplementA
        {
            get
            {
                return nic_suppl_a;
            }
            set
            {
                nic_suppl_a = value;
            }
        }

        [Browsable(false)]
        [DescriptionAttribute("NIC supplement B of Airborne Position Message")]
        public bool NICSupplementB
        {
            get
            {
                return nic_suppl_b;
            }
            set
            {
                nic_suppl_b = value;
            }
        }

	    private bool horizontal_position_available;
	    private bool altitude_available;
	    private byte surveillance_status;
	    private bool nic_suppl_b;
	    private short altitude_encoded;
	    private bool time_flag;
	    private bool cpr_format;
	    private int cpr_encoded_lat;
	    private int cpr_encoded_lon;
	    private bool nic_suppl_a;

	    /**
	     * @param raw_message raw ADS-B airborne position message as hex string
	     * @throws BadFormatException if message has wrong format
	     */
	    public AirbornePositionMsg(String raw_message)
            : base(raw_message)
        {

		    if (!(FormatTypeCode == 0 ||
				    (FormatTypeCode >= 9 && FormatTypeCode <= 18) ||
				    (FormatTypeCode >= 20 && FormatTypeCode <= 22))) 
			    throw new BadFormatException("This is not a position message! Wrong format type code ("+FormatTypeCode+"): " + raw_message);

		    byte[] msg = Message;

		    horizontal_position_available = FormatTypeCode != 0;

		    surveillance_status = (byte) ((msg[0]>>1)&0x3);
		    nic_suppl_b = (msg[0]&0x1) == 1;

		    altitude_encoded = (short) (((msg[1]<<4)|((msg[2]>>4)&0xF))&0xFFF);
		    altitude_available = altitude_encoded != 0;

		    time_flag = ((msg[2]>>3)&0x1) == 1;
		    cpr_format = ((msg[2]>>2)&0x1) == 1;
		    cpr_encoded_lat = (((msg[2]&0x3)<<15) | ((msg[3]&0xFF)<<7) | ((msg[4]>>1)&0x7F)) & 0x1FFFF;
		    cpr_encoded_lon = (((msg[4]&0x1)<<16) | ((msg[5]&0xFF)<<8) | (msg[6]&0xFF)) & 0x1FFFF;
	    }

	    /**
	     * This is a function of the surveillance status field in the position
	     * message.
	     * 
	     * @return surveillance status description as defines in DO-260B
	     */
	    public string getSurveillanceStatusDescription() 
        {
		    string[] desc = 
            {
				    "No condition information",
				    "Permanent alert (emergency condition)",
				    "Temporary alert (change in Mode A identity code oter than emergency condition)",
				    "SPI condition"
		    };

		    return desc[surveillance_status];
	    }

	    /**
	     * @param Rlat Even or odd Rlat value (CPR internal)
	     * @return the number of even longitude zones at a latitude
	     */
	    private double NL(double Rlat) 
        {
		    if (Rlat == 0) return 59;
		    else if (Math.Abs(Rlat) == 87) return 2;
		    else if (Math.Abs(Rlat) > 87) return 1;

		    double tmp = 1-(1-Math.Cos(Math.PI/(2.0*15.0)))/Math.Pow(Math.Cos(Math.PI/180.0*Math.Abs(Rlat)), 2);
		    return Math.Floor(2*Math.PI/Math.Acos(tmp));
	    }
	
	    /**
	     * Modulo operator in java has stupid behavior
	     */
	    private static double mod(double a, double b) {
		    return ((a%b)+b)%b;
	    }

	    /**
	     * This method can only be used if another position report with a different format (even/odd) is available
	     * and set with msg.setOtherFormatMsg(other).
	     * @param other airborne position message of the other format (even/odd). Note that the time between
	     *        both messages should be not longer than 10 seconds! 
	     * @return globally unambiguously decoded position tuple (latitude, longitude). The positional
	     *         accuracy maintained by the Airborne CPR encoding will be approximately 5.1 meters.
	     *         A message of the other format is needed for global decoding.
	     * @throws MissingInformationException if no position information is available in one of the messages
	     * @throws IllegalArgumentException if input message was emitted from a different transmitter
	     * @throws PositionStraddleError if position messages straddle latitude transition
	     * @throws BadFormatException other has the same format (even/odd)
	     */
	    public double[] getGlobalPosition(AirbornePositionMsg other) 
        {
			    if (!other.ICAO24.SequenceEqual(ICAO24))
				    throw new IllegalArgumentException(
						    string.Format("Transmitter of other message (%s) not equal to this (%s):",
						    BitConverter.ToString(other.ICAO24).Replace("-",String.Empty), BitConverter.ToString(ICAO24).Replace("-",String.Empty)));
		
		    if (other.IsOddFormat == IsOddFormat)
			    throw new BadFormatException("Expected "+ (IsOddFormat? "even":"odd") + " message format:" + other.ToString());

		    if (!horizontal_position_available)
			    throw new MissingInformationException("No position information available!");
		    if (!other.HasValidPosition)
			    throw new MissingInformationException("Other message has no position information.");

		    AirbornePositionMsg even = IsOddFormat?other:this;
		    AirbornePositionMsg odd = IsOddFormat?this:other;

		    // Helper for latitude single(Number of zones NZ is set to 15)
		    double Dlat0 = 360.0/60.0;
		    double Dlat1 = 360.0/59.0;

		    // latitude index
		    double j = Math.Floor((59.0*even.CPREncodedLat-60.0*odd.CPREncodedLat)/((double)(1<<17))+0.5);

		    // global latitudes
		    double Rlat0 = Dlat0 * (mod(j,60)+even.CPREncodedLat/((double)(1<<17)));
		    double Rlat1 = Dlat1 * (mod(j,59)+odd.CPREncodedLat/((double)(1<<17)));

		    // Southern hemisphere?
		    if (Rlat0 >= 270 && Rlat0 <= 360) Rlat0 -= 360;
		    if (Rlat1 >= 270 && Rlat1 <= 360) Rlat1 -= 360;

		    // Northern hemisphere?
		    if (Rlat0 <= -270 && Rlat0 >= -360) Rlat0 += 360;
		    if (Rlat1 <= -270 && Rlat1 >= -360) Rlat1 += 360;

		    // ensure that the number of even longitude zones are equal
		    if (NL(Rlat0) != NL(Rlat1))
			    throw new PositionStraddleException(
				    "The two given position straddle a transition latitude "+
				    "and cannot be decoded. Wait for positions where they are equal.");

		    // Helper for longitude
		    double Dlon0 = 360.0/Math.Max(1.0, NL(Rlat0));
		    double Dlon1 = 360.0/Math.Max(1.0, NL(Rlat1)-1);

		    // longitude index
		    double NL_helper = NL(IsOddFormat?Rlat1:Rlat0); // assuming that this is the newer message
		    double m = Math.Floor((even.CPREncodedLon*(NL_helper-1)-odd.CPREncodedLon*NL_helper)/((double)(1<<17))+0.5);

		    // global longitude
		    double Rlon0 = Dlon0 * (mod(m,Math.Max(1.0, NL(Rlat0))) + even.CPREncodedLon/((double)(1<<17)));
		    double Rlon1 = Dlon1 * (mod(m,Math.Max(1.0, NL(Rlat1)-1)) + odd.CPREncodedLon/((double)(1<<17)));

		    // correct longitude
		    if (Rlon0 < -180 && Rlon0 > -360) Rlon0 += 360;
		    if (Rlon1 < -180 && Rlon1 > -360) Rlon1 += 360;
		    if (Rlon0 > 180 && Rlon0 < 360) Rlon0 -= 360;
		    if (Rlon1 > 180 && Rlon1 < 360) Rlon1 -= 360;
		
		    return new double[] {IsOddFormat?Rlat1:Rlat0, IsOddFormat?Rlon1:Rlon0};
	    }
	
	    /**
	     * This method uses a locally unambiguous decoding for airborne position messages. It
	     * uses a reference position known to be within 180NM (= 333.36km) of the true target
	     * airborne position. the reference point may be a previously tracked position that has
	     * been confirmed by global decoding (see getGlobalPosition()).
	     * @param ref_lat latitude of reference position
	     *        ref_lon longitude of reference position
	     * @return decoded position as tuple (latitude, longitude). The positional
	     *         accuracy maintained by the Airborne CPR encoding will be approximately 5.1 meters.
	     * @throws MissingInformationException if no position information is available
	     */
	    public double[] getLocalPosition(double ref_lat, double ref_lon) 
        {
		    if (!horizontal_position_available)
			    throw new MissingInformationException("No position information available!");
		
		    // latitude zone size
		    double Dlat = IsOddFormat ? 360.0/59.0 : 360.0/60.0;
		
		    // latitude zone index
		    double j = Math.Floor(ref_lat/Dlat) + Math.Floor(0.5+(mod(ref_lat, Dlat))/Dlat-CPREncodedLat/((double)(1<<17)));
		
		    // decoded position latitude
		    double Rlat = Dlat*(j+CPREncodedLat/((double)(1<<17)));
		
		    // longitude zone size
		    double Dlon = 360.0/Math.Max(1.0, NL(Rlat)-(IsOddFormat?1.0:0.0));
		
		    // longitude zone coordinate
		    double m =
				    Math.Floor(ref_lon/Dlon) +
				    Math.Floor(0.5+(mod(ref_lon,Dlon))/Dlon-(float)CPREncodedLon/((double)(1<<17)));
		
		    // and finally the longitude
		    double Rlon = Dlon * (m + CPREncodedLon/((double)(1<<17)));
		
    //		System.out.println("Loc: EncLon: "+CPREncodedLon+
    //				" m: "+m+" Dlon: "+Dlon+ " Rlon2: "+Rlon2);
		
		    return new double[] {Rlat,Rlon}; 
	    }

	    /**
	     * This method converts a gray code encoded int to a standard decimal int
	     * @param gray gray code encoded int of length bitlength
	     *        bitlength bitlength of gray code
	     * @return radix 2 encoded integer
	     */
	    private static int grayToBin(int gray, int bitlength) {
		    int result = 0;
		    for (int i = bitlength-1; i >= 0; --i)
			    result = result|((((0x1<<(i+1))&result)>>1)^((1<<i)&gray));
		    return result;
	    }

	
	    public override string ToString() 
        {
		    return base.ToString()+"\n"+
				    "Position:\n"+
				    "\tFormat:\t\t"+(IsOddFormat?"odd":"even")+
				    "\tHas position:\t"+(HasValidPosition? "[Lat=" + CPREncodedLat.ToString("F8") + ",Lon=" + CPREncodedLon.ToString("F8") + "]" :"no")+
				    "\tHas altitude:\t"+(HasValidAltitude? "[Alt=" + Altitude.ToString("F8") + "]" :"no");
	    }
    }
}
