using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using TinyRadius.Net.Dictionaries;
using TinyRadius.Net.Util;

namespace TinyRadius.Net.Attributes
{
    /// <summary>
    /// This class represents a "Vendor-Specific" attribute.
    /// </summary>
    public class VendorSpecificAttribute : RadiusAttribute
    {
        /// <summary>
        /// Radius attribute type code for Vendor-Specific
        /// </summary>
        public static readonly int VENDOR_SPECIFIC = 26;

        /// <summary>
        /// Sub attributes. Only set if isRawData == false.
        /// </summary>
        private List<RadiusAttribute> subAttributes = new List<RadiusAttribute>();

        /// <summary>
        /// Constructs an empty Vendor-Specific attribute that can be read from a
        /// Radius packet.
        /// </summary>
        public VendorSpecificAttribute()
        {
        }

        /// <summary>
        /// Constructs a new Vendor-Specific attribute to be sent.
        /// @param vendorId vendor ID of the sub-attributes
        /// </summary>
        public VendorSpecificAttribute(int vendorId)
        {
            Type = VENDOR_SPECIFIC;
            ChildVendorId = vendorId;
        }

        /// <summary>
        /// Gets or sets  the vendor ID of the child attributes.
        /// @param childVendorId
        /// </summary>
        public int ChildVendorId { get; set; }

        public override IWritableDictionary Dictionary
        {
            get { return base.Dictionary; }
            set
            {
                base.Dictionary = value;
                foreach (RadiusAttribute a in subAttributes)
                {
                    a.Dictionary = value;
                }
            }
        }

        /// <summary>
        /// Returns the list of sub-attributes.
        /// @return ArrayList of RadiusAttribute objects
        /// </summary>
        public List<RadiusAttribute> SubAttributes
        {
            get { return subAttributes; }
        }

        /// <summary>
        /// Adds a sub-attribute to this attribute.
        /// @param attribute sub-attribute to add
        /// </summary>
        public void AddSubAttribute(RadiusAttribute attribute)
        {
            if (attribute.VendorId != ChildVendorId)
                throw new ArgumentException(
                    "sub attribut has incorrect vendor ID");

            subAttributes.Add(attribute);
        }

        /// <summary>
        /// Adds a sub-attribute with the specified name to this attribute.
        /// @param name name of the sub-attribute
        /// @param value value of the sub-attribute
        /// @exception ArgumentException invalid sub-attribute name or value
        /// </summary>
        public void AddSubAttribute(String name, String value)
        {
            if (string.IsNullOrEmpty(name))
                throw new ArgumentException("type name is empty");
            if (string.IsNullOrEmpty(value))
                throw new ArgumentException("value is empty");

            AttributeType type = Dictionary.GetAttributeTypeByName(name);
            if (type == null)
                throw new ArgumentException("unknown attribute type '"
                                            + name + "'");
            if (type.VendorId == -1)
                throw new ArgumentException("attribute type '" + name
                                            + "' is not a Vendor-Specific sub-attribute");
            if (type.VendorId != ChildVendorId)
                throw new ArgumentException("attribute type '" + name
                                            + "' does not belong to vendor ID " + ChildVendorId);

            RadiusAttribute attribute = CreateRadiusAttribute(Dictionary,
                                                              ChildVendorId, type.TypeCode);
            attribute.Value = value;
            AddSubAttribute(attribute);
        }

        /// <summary>
        /// Removes the specified sub-attribute from this attribute.
        /// @param attribute RadiusAttribute to remove
        /// </summary>
        public void RemoveSubAttribute(RadiusAttribute attribute)
        {
            if (!subAttributes.Remove(attribute))
                throw new ArgumentException("no such attribute");
        }

        /// <summary>
        /// Returns all sub-attributes of this attribut which have the given type.
        /// @param attributeType type of sub-attributes to get
        /// @return list of RadiusAttribute objects, does not return null
        /// </summary>
        public List<RadiusAttribute> GetSubAttributes(int attributeType)
        {
            if (attributeType < 1 || attributeType > 255)
                throw new ArgumentException(
                    "sub-attribute type out of bounds");

            var result = new List<RadiusAttribute>();
            foreach (RadiusAttribute a in subAttributes)
            {
                if (attributeType == a.Type)
                    result.Add(a);
            }
            return result;
        }

        /// <summary>
        /// Returns a sub-attribute of the given type which may only occur once in
        /// this attribute.
        /// @param type sub-attribute type
        /// @return RadiusAttribute object or null if there is no such sub-attribute
        /// @throws NotImplementedException if there are multiple occurences of the
        /// requested sub-attribute type
        /// </summary>
        public RadiusAttribute GetSubAttribute(int type)
        {
            List<RadiusAttribute> attrs = GetSubAttributes(type);
            if (attrs.Count > 1)
                throw new NotImplementedException(
                    "multiple sub-attributes of requested type " + type);
            else if (attrs.Count == 0)
                return null;
            else
                return attrs[0];
        }

        /// <summary>
        /// Returns a single sub-attribute of the given type name.
        /// @param type attribute type name
        /// @return RadiusAttribute object or null if there is no such attribute
        /// @throws NotImplementedException if the attribute occurs multiple times
        /// </summary>
        public RadiusAttribute GetSubAttribute(String type)
        {
            if (string.IsNullOrEmpty(type))
                throw new ArgumentException("type name is empty");


            AttributeType t = Dictionary.GetAttributeTypeByName(type);
            if (t == null)
                throw new ArgumentException("unknown attribute type name '"
                                            + type + "'");
            if (t.VendorId != ChildVendorId)
                throw new ArgumentException("vendor ID mismatch");

            return GetSubAttribute(t.TypeCode);
        }

        /// <summary>
        /// Returns the value of the Radius attribute of the given type or null if
        /// there is no such attribute.
        /// @param type attribute type name
        /// @return value of the attribute as a string or null if there is no such
        /// attribute
        /// @throws ArgumentException if the type name is unknown
        /// @throws NotImplementedException attribute occurs multiple times
        /// </summary>
        public String GetSubAttributeValue(String type)
        {
            RadiusAttribute attr = GetSubAttribute(type);
            if (attr == null)
                return null;
            else
                return attr.Value;
        }

        /// <summary>
        /// Renders this attribute as a byte array.
        /// @see TinyRadius.attribute.RadiusAttribute#writeAttribute()
        /// </summary>
        public override byte[] WriteAttribute()
        {
            // write vendor ID
            var bos = new MemoryStream(255);
            bos.WriteByte(Convert.ToByte(ChildVendorId >> 24 & 0x0ff));
            bos.WriteByte(Convert.ToByte(ChildVendorId >> 16 & 0x0ff));
            bos.WriteByte(Convert.ToByte(ChildVendorId >> 8 & 0x0ff));
            bos.WriteByte(Convert.ToByte(ChildVendorId & 0x0ff));

            // write sub-attributes
            try
            {
                foreach (RadiusAttribute a in subAttributes)
                {
                    byte[] c = a.WriteAttribute();
                    bos.Write(c, 0, c.Length);
                }
            }
            catch (IOException ioe)
            {
                // occurs never
                throw new NotImplementedException("error writing data", ioe);
            }

            // check data Length
            byte[] attrData = bos.ToArray();
            int len = attrData.Length;
            if (len > 253)
                throw new NotImplementedException("Vendor-Specific attribute too long: "
                                                  + len);

            // compose attribute
            var attr = new byte[len + 2];
            attr[0] = Convert.ToByte(VENDOR_SPECIFIC); // code
            attr[1] = (byte) (len + 2); // Length
            Array.Copy(attrData, 0, attr, 2, len);
            return attr;
        }

        /// <summary>
        /// Reads a Vendor-Specific attribute and decodes the internal sub-attribute
        /// structure.
        /// @see TinyRadius.attribute.RadiusAttribute#readAttribute(byte[], int,
        /// int)
        /// </summary>
        public override void ReadAttribute(byte[] data, int offset, int length)
        {
            // check Length
            if (length < 6)
                throw new RadiusException("Vendor-Specific attribute too short: "
                                          + length);

            int vsaCode = data[offset];
            int vsaLen = (data[offset + 1] & 0x000000ff) - 6;

            if (vsaCode != VENDOR_SPECIFIC)
                throw new RadiusException("not a Vendor-Specific attribute");

            // read vendor ID and vendor data
            /*
             * int vendorId = (data[offset + 2] << 24 | data[offset + 3] << 16 |
             * data[offset + 4] << 8 | ((int)data[offset + 5] & 0x000000ff));
             */
            int vendorId = (UnsignedByteToInt(data[offset + 2]) << 24
                            | UnsignedByteToInt(data[offset + 3]) << 16
                            | UnsignedByteToInt(data[offset + 4]) << 8 | UnsignedByteToInt(data[offset + 5]));
            ChildVendorId = vendorId;

            // validate sub-attribute structure
            int pos = 0;
            int count = 0;
            while (pos < vsaLen)
            {
                if (pos + 1 >= vsaLen)
                    throw new RadiusException("Vendor-Specific attribute malformed");
                // int vsaSubType = data[(offset + 6) + pos] & 0x0ff;
                int vsaSubLen = data[(offset + 6) + pos + 1] & 0x0ff;
                pos += vsaSubLen;
                count++;
            }
            if (pos != vsaLen)
                throw new RadiusException("Vendor-Specific attribute malformed");

            subAttributes = new List<RadiusAttribute>(count);
            pos = 0;
            while (pos < vsaLen)
            {
                int subtype = data[(offset + 6) + pos] & 0x0ff;
                int sublength = data[(offset + 6) + pos + 1] & 0x0ff;
                RadiusAttribute a = CreateRadiusAttribute(Dictionary,
                                                          vendorId, subtype);
                a.ReadAttribute(data, (offset + 6) + pos, sublength);
                subAttributes.Add(a);
                pos += sublength;
            }
        }

        private static int UnsignedByteToInt(byte b)
        {
            return b & 0xFF;
        }

        /// <summary>
        /// Returns a string representation for debugging.
        /// @see TinyRadius.attribute.RadiusAttribute#toString()
        /// </summary>
        public override String ToString()
        {
            var sb = new StringBuilder();
            sb.Append("Vendor-Specific: ");
            int vendorId = ChildVendorId;
            String vendorName = Dictionary.GetVendorName(vendorId);
            if (vendorName != null)
            {
                sb.Append(vendorName)
                    .Append(" (")
                    .Append(vendorId)
                    .Append(")");
            }
            else
            {
                sb.Append("vendor ID ");
                sb.Append(vendorId);
            }
            foreach (RadiusAttribute attr in SubAttributes)
            {
                sb.Append("\n");
                sb.Append(attr.ToString());
            }
            return sb.ToString();
        }
    }
}