#ifndef PACKETPP_RADIUS_LAYER
#define PACKETPP_RADIUS_LAYER

#include "Layer.h"
#include "TLVData.h"

/// @file

/**
 * \namespace pcpp
 * \brief The main namespace for the PcapPlusPlus lib
 */
namespace pcpp
{

	/**
	 * @struct radius_header
	 * Represents a RADIUS protocol header
	 */
#pragma pack(push, 1)
	struct radius_header
	{
		/** RADIUS message code */
		uint8_t code;
		/** RADIUS message ID */
		uint8_t id;
		/** RADIUS message length */
		uint16_t length;
		/** Used to authenticate the reply from the RADIUS server and to encrypt passwords */
		uint8_t authenticator[16];
	};
#pragma pack(pop)


	/**
	 * @class RadiusAttribute
	 * A wrapper class for RADIUS attributes. This class does not create or modify RADIUS attribute records, but rather
	 * serves as a wrapper and provides useful methods for retrieving data from them
	 */
	class RadiusAttribute : public TLVRecord<uint8_t, uint8_t>
	{
	public:

		/**
		 * A c'tor for this class that gets a pointer to the attribute raw data (byte array)
		 * @param[in] attrRawData A pointer to the attribute raw data
		 */
		explicit RadiusAttribute(uint8_t* attrRawData) : TLVRecord(attrRawData) { }

		/**
		 * A d'tor for this class, currently does nothing
		 */
		virtual ~RadiusAttribute() { }

		// implement abstract methods

		size_t getTotalSize() const
		{
			if (m_Data == nullptr)
				return 0;

			return (size_t)m_Data->recordLen;
		}

		size_t getDataSize() const
		{
			if (m_Data == nullptr)
				return 0;

			return (size_t)m_Data->recordLen - 2*sizeof(uint8_t);
		}
	};


	/**
	 * @class RadiusAttributeBuilder
	 * A class for building RADIUS attributes. This builder receives the attribute parameters in its c'tor,
	 * builds the RADIUS attribute raw buffer and provides a build() method to get a RadiusAttribute object out of it
	 */
	class RadiusAttributeBuilder : public TLVRecordBuilder
	{
	public:

		/**
		 * A c'tor for building RADIUS attributes which their value is a byte array. The RadiusAttribute object can later
		 * be retrieved by calling build()
		 * @param[in] attrType RADIUS attribute type
		 * @param[in] attrValue A buffer containing the attribute value. This buffer is read-only and isn't modified in any way
		 * @param[in] attrValueLen Attribute value length in bytes
		 */
		RadiusAttributeBuilder(uint8_t attrType, const uint8_t* attrValue, uint8_t attrValueLen) :
			TLVRecordBuilder(attrType, attrValue, attrValueLen) { }

		/**
		 * A c'tor for building RADIUS attributes which have a 1-byte value. The RadiusAttribute object can later be retrieved
		 * by calling build()
		 * @param[in] attrType RADIUS attribute type
		 * @param[in] attrValue A 1-byte attribute value
		 */
		RadiusAttributeBuilder(uint8_t attrType, uint8_t attrValue) :
			TLVRecordBuilder(attrType, attrValue) { }

		/**
		 * A c'tor for building RADIUS attributes which have a 2-byte value. The RadiusAttribute object can later be retrieved
		 * by calling build()
		 * @param[in] attrType RADIUS attribute type
		 * @param[in] attrValue A 2-byte attribute value
		 */
		RadiusAttributeBuilder(uint8_t attrType, uint16_t attrValue) :
			TLVRecordBuilder(attrType, attrValue) { }

		/**
		 * A c'tor for building RADIUS attributes which have a 4-byte value. The RadiusAttribute object can later be retrieved
		 * by calling build()
		 * @param[in] attrType RADIUS attribute type
		 * @param[in] attrValue A 4-byte attribute value
		 */
		RadiusAttributeBuilder(uint8_t attrType, uint32_t attrValue) :
			TLVRecordBuilder(attrType, attrValue) { }

		/**
		 * A c'tor for building RADIUS attributes which have an IPv4Address value. The RadiusAttribute object can later be
		 * retrieved by calling build()
		 * @param[in] attrType RADIUS attribute type
		 * @param[in] attrValue The IPv4 address attribute value
		 */
		RadiusAttributeBuilder(uint8_t attrType, const IPv4Address& attrValue) :
			TLVRecordBuilder(attrType, attrValue) { }

		/**
		 * A c'tor for building RADIUS attributes which have a string value. The RadiusAttribute object can later be retrieved
		 * by calling build()
		 * @param[in] attrType RADIUS attribute type
		 * @param[in] attrValue The string attribute value
		 */
		RadiusAttributeBuilder(uint8_t attrType, const std::string& attrValue) :
			TLVRecordBuilder(attrType, attrValue) { }

		/**
		 * A copy c'tor which copies all the data from another instance of RadiusAttributeBuilder
		 * @param[in] other The instance to copy from
		 */
		RadiusAttributeBuilder(const RadiusAttributeBuilder& other) :
			TLVRecordBuilder(other) { }

		/**
		 * Assignment operator that copies all data from another instance of RadiusAttributeBuilder
		 * @param[in] other The instance to assign from
		 */
		RadiusAttributeBuilder& operator=(const RadiusAttributeBuilder& other)
		{
			TLVRecordBuilder::operator=(other);
			return *this;
		}

		/**
		 * Build the RadiusAttribute object out of the parameters defined in the c'tor
		 * @return The RadiusAttribute object
		 */
		RadiusAttribute build() const;
	};


	/**
	 * @class RadiusLayer
	 * Represents a RADIUS (Remote Authentication Dial-In User Service) protocol layer
	 */
	class RadiusLayer : public Layer
	{
	private:

		TLVRecordReader<RadiusAttribute> m_AttributeReader;

		uint8_t* getAttributesBasePtr() const { return m_Data + sizeof(radius_header); }

		RadiusAttribute addAttrAt(const RadiusAttributeBuilder& attrBuilder, int offset);

	public:

		/**
		 * A constructor that creates the layer from an existing packet raw data
		 * @param[in] data A pointer to the raw data
		 * @param[in] dataLen Size of the data in bytes
		 * @param[in] prevLayer A pointer to the previous layer
		 * @param[in] packet A pointer to the Packet instance where layer will be stored in
		 */
		RadiusLayer(uint8_t* data, size_t dataLen, Layer* prevLayer, Packet* packet) :
			Layer(data, dataLen, prevLayer, packet)
			{ m_Protocol = Radius; }

		/**
		 * A constructor that creates a new layer from scratch
		 * @param[in] code The RADIUS message code
		 * @param[in] id The RADIUS message ID
		 * @param[in] authenticator A pointer to a byte array containing the authenticator value
		 * @param[in] authenticatorArrSize The authenticator byte array size. A valid size of the authenticator field is
		 * 16 bytes. If the provided size is less than that then the byte array will be copied to the packet but the missing
		 * bytes will stay zero. If the size is more than 16 bytes, only the first 16 bytes will be copied to the packet
		 */
		RadiusLayer(uint8_t code, uint8_t id, const uint8_t* authenticator, uint8_t authenticatorArrSize);

		/**
		 * A constructor that creates a new layer from scratch
		 * @param[in] code The RADIUS message code
		 * @param[in] id The RADIUS message ID
		 * @param[in] authenticator A hex string representing the authenticator value. A valid size of the authenticator
		 * field is 16 bytes. If the hex string represents an array that is smaller than this then the missing bytes in the
		 * packet's authenticator field will stay zero. If the hex string represents an array that is larger than 16 bytes,
		 * only the first 16 bytes will be copied to the packet
		 */
		RadiusLayer(uint8_t code, uint8_t id, const std::string &authenticator);

		/**
		 * A d'tor for this layer, currently does nothing
		 */
		~RadiusLayer() {}

		/**
		 * Get a pointer to the RADIUS header. Notice this points directly to the data, so every change will change the actual packet data
		 * @return A pointer to the radius_header object
		 */
		radius_header* getRadiusHeader() const { return (radius_header*)m_Data; }

		/**
		 * @return A hex string representation of the radius_header#authenticator byte array value
		 */
		std::string getAuthenticatorValue() const;

		/**
		 * Setter for radius_header#authenticator
		 * @param[in] authValue A hex string representing the requested authenticator value
		 */
		void setAuthenticatorValue(const std::string& authValue);

		/**
		 * A static method that returns the RADIUS message string for a give message code. For example: the string
		 * "Access-Request" will be returned for code 1
		 * @param[in] radiusMessageCode RADIUS message code
		 * @return RADIUS message string
		 */
		static std::string getRadiusMessageString(uint8_t radiusMessageCode);

		/**
		 * @return The first RADIUS attribute in the packet. If there are no attributes the returned value will contain
		 * a logical NULL (RadiusAttribute#isNull() == true)
		 */
		RadiusAttribute getFirstAttribute() const;

		/**
		 * Get the RADIUS attribute that comes after a given attribute. If the given attribute was the last one, the
		 * returned value will contain a logical NULL (RadiusAttribute#isNull() == true)
		 * @param[in] attr A given attribute
		 * @return A RadiusAttribute object containing the attribute data that comes next, or logical NULL if the given
		 * attribute: (1) was the last one; (2) contains a logical NULL or (3) doesn't belong to this packet
		 */
		RadiusAttribute getNextAttribute(RadiusAttribute& attr) const;

		/**
		 * Get a RADIUS attribute by attribute type
		 * @param[in] attrType RADIUS attribute type
		 * @return A RadiusAttribute object containing the first attribute data that matches this type, or logical NULL
		 * (RadiusAttribute#isNull() == true) if no such attribute found
		 */
		RadiusAttribute getAttribute(uint8_t attrType) const;

		/**
		 * @return The number of RADIUS attributes in the packet
		 */
		size_t getAttributeCount() const;

		/**
		 * Add a new RADIUS attribute at the end of the layer
		 * @param[in] attrBuilder A RadiusAttributeBuilder object that contains the requested attribute data to add
		 * @return A RadiusAttribute object containing the newly added RADIUS attribute data or logical NULL
		 * (RadiusAttribute#isNull() == true) if addition failed
		 */
		RadiusAttribute addAttribute(const RadiusAttributeBuilder& attrBuilder);

		/**
		 * Add a new RADIUS attribute after an existing one
		 * @param[in] attrBuilder A RadiusAttributeBuilder object that contains the requested attribute data to add
		 * @param[in] prevAttrType The RADIUS attribute which the newly added attribute will come after
		 * @return A RadiusAttribute object containing the newly added RADIUS attribute data or logical NULL
		 * (RadiusAttribute#isNull() == true) if addition failed
		 */
		RadiusAttribute addAttributeAfter(const RadiusAttributeBuilder& attrBuilder, uint8_t prevAttrType);

		/**
		 * Remove an existing RADIUS attribute from the layer
		 * @param[in] attrType The RADIUS attribute type to remove
		 * @return True if the RADIUS attribute was successfully removed or false if type wasn't found or if removal failed
		 */
		bool removeAttribute(uint8_t attrType);

		/**
		 * Remove all RADIUS attributes in this layer
		 * @return True if all attributes were successfully removed or false if removal failed for some reason
		 */
		bool removeAllAttributes();

		/**
		 * The static method makes validation of UDP data
		 * @param[in] udpData The pointer to the UDP payload data. It points to the first byte of RADIUS header.
		 * @param[in] udpDataLen The payload data size
		 * @return True if the data is valid and can represent the RADIUS packet
		 */
		static bool isDataValid(const uint8_t* udpData, size_t udpDataLen);

		/**
		 * A static method that checks whether the port is considered as RADIUS
		 * @param[in] port The port number to be checked
		 */
		static inline bool isRadiusPort(uint16_t port);

		// implement abstract methods

		/**
		 * @return The size written in radius_header#length
		 */
		size_t getHeaderLen() const;

		/**
		 * Does nothing for this layer, RADIUS is always last
		 */
		void parseNextLayer() {}

		/**
		 * Calculate and store the value of radius_header#length according to the layer size
		 */
		void computeCalculateFields();

		std::string toString() const;

		OsiModelLayer getOsiModelLayer() const { return OsiModelApplicationLayer; }
	};


	// implementation of inline methods

	bool RadiusLayer::isRadiusPort(uint16_t port)
	{
		switch (port)
		{
		case 1812:
		case 1813:
		case 3799:
			return true;
		default:
			return false;
		}
	} // isRadiusPort

} // namespace pcpp

#endif // PACKETPP_RADIUS_LAYER
