/*
This file is part of PacketDotNet

PacketDotNet is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

PacketDotNet is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with PacketDotNet.  If not, see <http://www.gnu.org/licenses/>.
*/
/*
 * Copyright 2010 Chris Morgan <chmorgan@gmail.com>
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.IO;
using System.Text;
using System.Linq;
using PacketDotNet.Utils;
using MiscUtil.Conversion;

namespace PacketDotNet
{
    namespace Ieee80211
    {
        /// <summary>
        /// .Net analog of a ieee80211_radiotap_header from airpcap.h
        /// </summary>
        public class RadioPacket : InternetLinkLayerPacket
        {
        // NOTE: No need to warn about lack of use, the compiler won't
        //       put any calls to 'log' here but we need 'log' to exist to compile
#pragma warning disable 0169, 0649
        private static readonly ILogInactive log;
#pragma warning restore 0169, 0649

            /// <summary>
            /// Version 0. Only increases for drastic changes, introduction of compatible
            /// new fields does not count.
            /// </summary>
            public byte Version { get; set; }
            
            private byte VersionBytes
            {
                get
                {
                    return header.Bytes[header.Offset + RadioFields.VersionPosition];
                }

                set
                {
                    header.Bytes[header.Offset + RadioFields.VersionPosition] = value;
                }
            }

            /// <summary>
            /// Length of the whole header in bytes, including it_version, it_pad, it_len
            /// and data fields
            /// </summary>
            public UInt16 Length {get; set;}
            
            private UInt16 LengthBytes
            {
                get
                {
                    return EndianBitConverter.Little.ToUInt16(header.Bytes,
                                                              header.Offset + RadioFields.LengthPosition);
                }

                set
                {
                    EndianBitConverter.Little.CopyBytes(value,
                                                        header.Bytes,
                                                        header.Offset + RadioFields.LengthPosition);
                }
            }

            /// <summary>
            /// Returns an array of UInt32 bitmap entries. Each bit in the bitmap indicates
            /// which fields are present. Set bit 31 (0x8000000)
            /// to extend the bitmap by another 32 bits. Additional extensions are made
            /// by setting bit 31.
            /// </summary>
            private UInt32[] Present {get; set;}
                
            private UInt32[] ReadPresentFields()
            {
                // make an array of the bitmask fields
                // the highest bit indicates whether other bitmask fields follow
                // the current field
                var bitmaskFields = new List<UInt32>();
                UInt32 bitmask = EndianBitConverter.Little.ToUInt32(header.Bytes,
                                                                    header.Offset + RadioFields.PresentPosition);
                bitmaskFields.Add(bitmask);
                int bitmaskOffsetInBytes = 4;
                while ((bitmask & (1 << 31)) == 1)
                {
                    // retrieve the next field
                    bitmask = EndianBitConverter.Little.ToUInt32(header.Bytes,
                                                                 header.Offset + RadioFields.PresentPosition + bitmaskOffsetInBytes);
                    bitmaskFields.Add(bitmask);
                    bitmaskOffsetInBytes += 4;
                }

                return bitmaskFields.ToArray();
            }
   
            /// <summary>
            /// Initializes a new instance of the <see cref="PacketDotNet.Ieee80211.RadioPacket"/> class.
            /// </summary>
            public RadioPacket()
            {
                Present = new UInt32[1];
                RadioTapFields = new SortedDictionary<RadioTapType, RadioTapField>();
                Length = (ushort)RadioFields.DefaultHeaderLength;
            }
            
            internal RadioPacket (ByteArraySegment bas)
            {
                log.Debug ("");

                // slice off the header portion
                header = new ByteArraySegment (bas);
                header.Length = RadioFields.DefaultHeaderLength;
                Version = VersionBytes;
                Length = LengthBytes;
                
                // update the header size based on the headers packet length
                header.Length = Length;
                Present = ReadPresentFields();
                RadioTapFields = ReadRadioTapFields();
    
                //Before we attempt to parse the payload we need to work out if 
                //the FCS was valid and if it will be present at the end of the frame
                FlagsRadioTapField flagsField = this[RadioTapType.Flags] as FlagsRadioTapField;
		        payloadPacketOrData = ParseEncapsulatedBytes(header.EncapsulatedBytes(), flagsField);
            }

            /// <summary cref="Packet.ToString(StringOutputType)" />
            public override string ToString(StringOutputType outputFormat)
            {
                var buffer = new StringBuilder();
                string color = "";
                string colorEscape = "";

                if (outputFormat == StringOutputType.Colored || outputFormat == StringOutputType.VerboseColored)
                {
                    color = Color;
                    colorEscape = AnsiEscapeSequences.Reset;
                }

                if (outputFormat == StringOutputType.Normal || outputFormat == StringOutputType.Colored)
                {
                    // build the output string
                    buffer.AppendFormat("{0}[Ieee80211RadioPacket: Version={2}, Length={3}, Present[0]=0x{4:x}]{1}",
                        color,
                        colorEscape,
                        Version,
                        Length,
                        Present[0]);
                }

                if (outputFormat == StringOutputType.Verbose || outputFormat == StringOutputType.VerboseColored)
                {
                    // collect the properties and their value
                    Dictionary<string, string> properties = new Dictionary<string, string>();
                    properties.Add("version", Version.ToString());
                    properties.Add("length", Length.ToString());
                    properties.Add("present", " (0x" + Present[0].ToString("x") + ")");

                    var radioTapFields = this.RadioTapFields;

                    foreach (var r in radioTapFields)
                    {
                        properties.Add(r.Value.FieldType.ToString(),
                                       r.Value.ToString());
                    }

                    // calculate the padding needed to right-justify the property names
                    int padLength = Utils.RandomUtils.LongestStringLength(new List<string>(properties.Keys));

                    // build the output string
                    buffer.AppendLine("Ieee80211RadioPacket");
                    foreach (var property in properties)
                    {
                        buffer.AppendLine("TAP: " + property.Key.PadLeft(padLength) + " = " + property.Value);
                    }
                    buffer.AppendLine("TAP:");
                }

                // append the base output
                buffer.Append(base.ToString(outputFormat));

                return buffer.ToString();
            }
   
            /// <summary>
            /// Add the specified field to the packet.
            /// </summary>
            /// <param name='field'>
            /// Field to be added
            /// </param>
            public void Add(RadioTapField field)
            {
                RadioTapFields[field.FieldType] = field;
                Length += field.Length;
                var presenceBit = (int)field.FieldType;
                var presenceField = (presenceBit / 32);
                if(Present.Length <= presenceField)
                {
                    var newPresentFields = new UInt32[presenceField];
                    Array.Copy(Present, newPresentFields, Present.Length);
                    //set bit 31 to true for every present field except the last one
                    for(int i = 0; i < newPresentFields.Length - 1; i++)
                    {
                        newPresentFields[i] |= 0x80000000;
                    }
                    Present = newPresentFields;
                }
                Present[presenceField] |= (UInt32)(1 << presenceBit);
            }
            
            /// <summary>
            /// Removes a field of the specified type if one is present in the packet.
            /// </summary>
            /// <param name='fieldType'>
            /// Field type.
            /// </param>
            public void Remove(RadioTapType fieldType)
            {
                RadioTapField field;
                if(RadioTapFields.TryGetValue(fieldType, out field))
                {
                    RadioTapFields.Remove(fieldType);
                    Length -= field.Length;
                    var presenceBit = (int)field.FieldType;
                    var presenceField = (presenceBit / 32);
                    Present[presenceField] &= (UInt32)~(1 << presenceBit);
                }
            }
            
            /// <summary>
            /// Checks for the presence of a field of the specified type in the packet.
            /// </summary>
            /// <param name='fieldType'>
            /// The field type to check for.
            /// </param>
            /// <returns><c>true</c> if the packet contains a field of the specified type; otherwise, <c>false</c>.</returns>
            public bool Contains(RadioTapType fieldType)
            {
                return RadioTapFields.ContainsKey(fieldType);
            }
            
            /// <summary>
            /// Gets the <see cref="PacketDotNet.Ieee80211.RadioTapField"/> with the specified type, or null if the
            /// field is not in the packet.
            /// </summary>
            /// <param name='type'>
            /// Radio Tap field type
            /// </param>
            public RadioTapField this[RadioTapType type]
            {
                get
                {
                    RadioTapField field;
                    RadioTapFields.TryGetValue(type, out field);
                    return field;
                }
            }
            
            /// <summary>
            /// Array of radio tap fields
            /// </summary>
            private SortedDictionary<RadioTapType, RadioTapField> RadioTapFields { get; set; }
            
            private byte[] UnhandledFieldBytes {get; set;}
            
            private SortedDictionary<RadioTapType, RadioTapField> ReadRadioTapFields()
            {
                var bitmasks = Present;

                var retval = new SortedDictionary<RadioTapType, RadioTapField>();

                int bitIndex = 0;

                // create a binary reader that points to the memory immediately after the bitmasks
                var offset = header.Offset +
                             RadioFields.PresentPosition +
                             (bitmasks.Length) * Marshal.SizeOf (typeof(UInt32));
                var br = new BinaryReader (new MemoryStream (header.Bytes,
                                                           offset,
                                                           (int)(Length - offset)));

                // now go through each of the bitmask fields looking at the least significant
                // bit first to retrieve each field
                foreach (var bmask in bitmasks)
                {
                    int[] bmaskArray = new int[1];
                    bmaskArray [0] = (int)bmask;
                    var ba = new BitArray (bmaskArray);
                    
                    bool unhandledFieldFound = false;

                    // look at all of the bits, note we don't want to consider the
                    // highest bit since that indicates another bitfield that follows
                    for (int x = 0; x < 31; x++)
                    {
                        if (ba [x] == true)
                        {
                            var field = RadioTapField.Parse (bitIndex, br);
                            if (field != null)
                            {
                                retval[field.FieldType] = field;
                            }
                            else
                            {
                                //We have found a field that we dont handle. As we dont know how big
                                //it is we can't handle any fields after it. We will just copy
                                //the rest of the data around as a lump
                                unhandledFieldFound = true;
                                break;
                            }
                        }
                        bitIndex++;
                    }
                    
                    if(unhandledFieldFound)
                    {
                        break;
                    }
                }
                
                //this will read the rest of the bytes. We pass in max value because we dont know how
                //much there is but this will ensure we get up to the end of the buffer
                UnhandledFieldBytes = br.ReadBytes(UInt16.MaxValue);

                return retval;
            }
   
            /// <summary>
            /// Called to ensure that field values are updated before
            /// the packet bytes are retrieved
            /// </summary>
            public override void UpdateCalculatedValues()
            {
                if ((header == null) || (header.Length < Length))
                {
                    //the backing buffer isnt big enough to accommodate the info elements so we need to resize it
                    header = new ByteArraySegment (new Byte[Length]);
                }
                
                VersionBytes = Version;
                LengthBytes = Length;
                var index = RadioFields.PresentPosition;
                foreach(var presentField in Present)
                {
                    EndianBitConverter.Little.CopyBytes(presentField,
                                                        header.Bytes,
                                                        header.Offset + index);
                    index += RadioFields.PresentLength;
                }
                
                foreach(var field in RadioTapFields)
                {
                    //then copy the field data to the appropriate index
                    field.Value.CopyTo(header.Bytes, header.Offset + index);
                    index += field.Value.Length;
                }
                
                if((UnhandledFieldBytes != null) && (UnhandledFieldBytes.Length > 0))
                {
                    Array.Copy(UnhandledFieldBytes, 0, header.Bytes, header.Offset + index, UnhandledFieldBytes.Length);
                }
            }

            internal static PacketOrByteArraySegment ParseEncapsulatedBytes (ByteArraySegment payload, FlagsRadioTapField flagsField)
            {
                var payloadPacketOrData = new PacketOrByteArraySegment ();
                MacFrame frame = null;
                
                if (flagsField != null)
                {
                    bool fcsPresent = ((flagsField.Flags & RadioTapFlags.FcsIncludedInFrame) == RadioTapFlags.FcsIncludedInFrame);
                    
                    if (fcsPresent)
                    {
                        frame = MacFrame.ParsePacketWithFcs (payload);
                    }
                    else
                    {
                        frame = MacFrame.ParsePacket (payload);
                    }
                }
                else
                {
                    frame = MacFrame.ParsePacket (payload);
                }
                
                if (frame == null)
                {
                    payloadPacketOrData.TheByteArraySegment = payload;
                }
                else
                {
                    payloadPacketOrData.ThePacket = frame;
                }
                
                return payloadPacketOrData;
            }
        } 
    }
}
