#ifndef PACKETPP_SOMEIPSD_LAYER
#define PACKETPP_SOMEIPSD_LAYER

#include "EndianPortable.h"
#include "IpAddress.h"
#include "Layer.h"
#include "SomeIpLayer.h"
#include <cstring>
#include <iterator>
#include <map>
#include <memory>
#include <stdexcept>
#include <vector>

/// @file

/**
 * \namespace pcpp
 * \brief The main namespace for the PcapPlusPlus lib
 */
namespace pcpp
{
/**
 * Types of protocols that can be referenced in SOME/IP-SD
 */
enum SomeIpSdProtocolType : uint8_t
{
	/** TCP */
	SD_TCP = 0x06,
	/** UDP */
	SD_UDP = 0x11
};

class SomeIpSdLayer;

/**
 * @class SomeIpSdOption
 * Base class of the SOME/IP-SD options. Cannot be instantiated.
 */
class SomeIpSdOption
{
public:
	friend class SomeIpSdLayer;

	/**
	 * Types of options currently available for the SOME/IP-SD protocol
	 */
	enum class OptionType : uint8_t
	{
		/** Unknown Option Type */
		Unknown = 0x00,
		/** Configuration Option */
		ConfigurationString = 0x01,
		/** Load Balancing Option */
		LoadBalancing = 0x02,
		/** IPv4 Endpoint Option */
		IPv4Endpoint = 0x04,
		/** IPv6 Endpoint Option */
		IPv6Endpoint = 0x06,
		/** IPv4 Multicast Option */
		IPv4Multicast = 0x14,
		/** IPv6 Multicast Option */
		IPv6Multicast = 0x16,
		/** IPv4 SD Endpoint Option */
		IPv4SdEndpoint = 0x24,
		/** IPv6 SD Endpoint Option */
		IPv6SdEndpoint = 0x26
	};

	/**
	 * @struct someipsdhdroptionsbase
	 * Represents the common base for SOME/IP-SD header options
	 */
#pragma pack(push, 1)
	struct someipsdhdroptionsbase
	{
		/** Length - excluding the 16 bit Length field and the 8 bit type flag */
		uint16_t length;
		/** Type */
		uint8_t type;
		/** Reserved */
		uint8_t reserved;
	};
#pragma pack(pop)

	/**
	 * Destroy the SOME/IP-SD Option object and delete allocated data if it has been allocated by a constructor
	 */
	virtual ~SomeIpSdOption();

	/**
	 * Get the Option Type
	 * @return OptionType
	 */
	OptionType getType() const;

	/**
	 * Get the Length of the SOME/IP-SD option
	 * @return size_t
	 */
	size_t getLength() const { return m_DataLen; }

	/**
	 * Get the internal data of the SOME/IP-SD Option
	 * @return uint8_t*
	 */
	uint8_t *getDataPtr() const;

	/**
	 * Get a pointer to the SOME/IP-SD Option base header
	 * @return someipsdhdroptionsbase*
	 */
	someipsdhdroptionsbase *getSomeIpSdOptionHeader() const;

protected:
	const IDataContainer *m_DataContainer;
	size_t m_Offset;
	uint8_t *m_ShadowData;
	size_t m_DataLen;

	SomeIpSdOption() : m_DataContainer(nullptr), m_Offset(0), m_ShadowData(nullptr), m_DataLen(0) {}

	SomeIpSdOption(const IDataContainer *dataContainer, size_t offset)
		: m_DataContainer(dataContainer), m_Offset(offset), m_ShadowData(nullptr), m_DataLen(0) {}

	void initStdFields(OptionType type);

	SomeIpSdOption(const SomeIpSdOption &) = delete;
	SomeIpSdOption &operator=(const SomeIpSdOption &) = delete;
};

/**
 * @class SomeIpSdIPv4Option
 * Implements the following SOME/IP-SD Options: IPv4 Endpoint, IPv4 Multicast, IPv4 SD Endpoint
 */
class SomeIpSdIPv4Option : public SomeIpSdOption
{
public:
	friend class SomeIpSdLayer;

	/**
	 * Types of options which are implemented with this class
	 */
	enum IPv4OptionType
	{
		/** IPv4 Endpoint Option */
		IPv4Endpoint,
		/** IPv4 Multicast Option */
		IPv4Multicast,
		/** IPv4 SD Endpoint Option */
		IPv4SdEndpoint,
	};

	/**
	 * Construct a new SomeIpSdIPv4 Option object
	 * @param[in] type IPv4 Option type
	 * @param[in] ipAddress Ipv4 address to use
	 * @param[in] port Port to use
	 * @param[in] l4Protocol Protocol to use
	 */
	SomeIpSdIPv4Option(IPv4OptionType type, IPv4Address ipAddress, uint16_t port, SomeIpSdProtocolType l4Protocol);

	/**
	 * Construct a new SomeIpSdIPv4 Option object from already existing memory
	 * @param[in] dataContainer Data containing the SomeIpSdIPv4 Option object
	 * @param[in] offset Offset for dataContainer
	 */
	SomeIpSdIPv4Option(const IDataContainer *dataContainer, size_t offset);

	/**
	 * Get the Ip Address
	 * @return IPv4Address
	 */
	IPv4Address getIpAddress() const;

	/**
	 * Get the Port
	 * @return uint16_t
	 */
	uint16_t getPort() const;

	/**
	 * Get the Protocol
	 * @return SomeIpSdProtocolType
	 */
	SomeIpSdProtocolType getProtocol() const;

private:
	/**
	 * @struct someipsdhdroptionsipv4
	 * Represents the IPv4 option types for the SOME/IP-SD header
	 */
#pragma pack(push, 1)
	struct someipsdhdroptionsipv4 : someipsdhdroptionsbase
	{
		/* IPv4-Address field */
		uint32_t ipv4Address;
		/* Reserved */
		// cppcheck-suppress duplInheritedMember
		uint8_t reserved;
		/* Layer 4 Protocol field (L4-Proto) - Either UDP or TCP */
		SomeIpSdProtocolType l4Protocol;
		/* Port number of UDP or TCP */
		uint16_t portNumber;
	};
#pragma pack(pop)
};

/**
 * @class SomeIpSdIPv6Option
 * Implements the following SOME/IP-SD Options: IPv6 Endpoint, IPv6 Multicast, IPv6 SD Endpoint
 */
class SomeIpSdIPv6Option : public SomeIpSdOption
{
public:
	friend class SomeIpSdLayer;

	/**
	 * Types of options which are implemented with this class
	 */
	enum IPv6OptionType
	{
		/** IPv6 Endpoint Option */
		IPv6Endpoint,
		/** IPv6 Multicast Option */
		IPv6Multicast,
		/** IPv6 SD Endpoint Option */
		IPv6SdEndpoint,
	};

	/**
	 * Construct a new SomeIpSdIPv6 Option object
	 * @param[in] type IPv6 Option type
	 * @param[in] ipAddress Ipv6 address to use
	 * @param[in] port Port to use
	 * @param[in] l4Protocol Protocol to use
	 */
	SomeIpSdIPv6Option(IPv6OptionType type, IPv6Address ipAddress, uint16_t port, SomeIpSdProtocolType l4Protocol);

	/**
	 * Construct a new SomeIpSdIPv6 Option object from already existing memory
	 * @param[in] dataContainer Data containing the SomeIpSdIPv6 Option object
	 * @param[in] offset Offset for dataContainer
	 */
	SomeIpSdIPv6Option(const IDataContainer *dataContainer, size_t offset);

	/**
	 * Get the Ip Address
	 * @return IPv6Address
	 */
	IPv6Address getIpAddress() const;

	/**
	 * Get the Port
	 * @return uint16_t
	 */
	uint16_t getPort() const;

	/**
	 * Get the Protocol
	 * @return SomeIpSdProtocolType
	 */
	SomeIpSdProtocolType getProtocol() const;

private:
	/**
	 * @struct someipsdhdroptionsipv6
	 * Represents the IPv6 option types for the SOME/IP-SD header
	 */
#pragma pack(push, 1)
	struct someipsdhdroptionsipv6 : someipsdhdroptionsbase
	{
		/* IPv6-Address field */
		uint8_t ipv6Address[16];
		/* Reserved */
		// cppcheck-suppress duplInheritedMember
		uint8_t reserved;
		/* Layer 4 Protocol field (L4-Proto) - Either UDP or TCP */
		SomeIpSdProtocolType l4Protocol;
		/* Port number of UDP or TCP */
		uint16_t portNumber;
	};
#pragma pack(pop)
};

/**
 * @class SomeIpSdConfigurationOption
 * Implements the Configuration option of SOME/IP-SD protocol
 */
class SomeIpSdConfigurationOption : public SomeIpSdOption
{
public:
	friend class SomeIpSdLayer;

	/**
	 * Construct a new Configuration Option object
	 * @param[in] configurationString the configuration string
	 */
	explicit SomeIpSdConfigurationOption(const std::string &configurationString);

	/**
	 * Construct a new Configuration Option object from already existing memory
	 * @param[in] dataContainer Data containing the Configuration Option object
	 * @param[in] offset Offset for dataContainer
	 */
	SomeIpSdConfigurationOption(const IDataContainer *dataContainer, size_t offset);

	/**
	 * Get the configuration string
	 * @return std::string
	 */
	std::string getConfigurationString() const;
};

/**
 * @class SomeIpSdLoadBalancingOption
 * Implements the Load Balancing option of SOME/IP-SD protocol
 */
class SomeIpSdLoadBalancingOption : public SomeIpSdOption
{
public:
	friend class SomeIpSdLayer;

	/**
	 * Construct a new Load Balancing object
	 * @param[in] priority Priority of this instance
	 * @param[in] weight Weight of this instance
	 */
	SomeIpSdLoadBalancingOption(uint16_t priority, uint16_t weight);

	/**
	 * Construct a new Option object from already existing memory
	 * @param[in] dataContainer Data containing the option object
	 * @param[in] offset Offset for dataContainer
	 */
	SomeIpSdLoadBalancingOption(const IDataContainer *dataContainer, size_t offset);

	/**
	 * Get the priority fild
	 * @return uint16_t
	 */
	uint16_t getPriority() const;

	/**
	 * Get the weight field
	 * @return uint16_t
	 */
	uint16_t getWeight() const;

private:
	/**
	 * @struct someipsdhdroptionsload
	 * Represents the Load Balancing option header for SOME/IP-SD
	 */
#pragma pack(push, 1)
	struct someipsdhdroptionsload : someipsdhdroptionsbase
	{
		/* Priority field */
		uint16_t priority;
		/* Weight field */
		uint16_t weight;
	};
#pragma pack(pop)
};

/**
 * @class SomeIpSdEntry
 * Implementation of the SOME/IP-SD Service Entry and Eventgroup Entry Type
 */
class SomeIpSdEntry
{
public:
	friend class SomeIpSdLayer;

	/**
	 * Types of entries that can occur in SOME/IP-SD
	 */
	enum class EntryType : uint8_t
	{
		/** Find Service */
		FindService,
		/** Offer Service */
		OfferService,
		/** Stop Offer Service */
		StopOfferService,
		/** Subscribe Eventgroup */
		SubscribeEventgroup,
		/** Stop Subscribe Eventgroup */
		StopSubscribeEventgroup,
		/** Subscribe Eventgroup Acknowledgment */
		SubscribeEventgroupAck,
		/** Subscribe Eventgroup Negative Acknowledgement */
		SubscribeEventgroupNack,
		/** Unknown Entry Type */
		UnknownEntryType
	};

	/**
	 * @struct someipsdhdrentry
	 * Represents the Service Entry Type and Eventgroup Entry Type
	 */
#pragma pack(push, 1)
	struct someipsdhdrentry
	{
		/** Type */
		uint8_t type;
		/** Index 1st option */
		uint8_t indexFirstOption;
		/** Index 2nd option */
		uint8_t indexSecondOption;
#if (BYTE_ORDER == LITTLE_ENDIAN)
		uint8_t
			/** Numbers of Option #2 (4bit) */
			nrOpt2 : 4,
			/** Numbers of Option #1 (4bit) */
			nrOpt1 : 4;
#else
		uint8_t
			/** Numbers of Option #1 (4bit) */
			nrOpt1 : 4,
			/** Numbers of Option #2 (4bit) */
			nrOpt2 : 4;
#endif
		/** Service ID */
		uint16_t serviceID;
		/** Instance ID */
		uint16_t instanceID;
		/** Major Version (8 bit) + TTL (24 bit) */
		uint32_t majorVersion_ttl;
		/** Minor Version (Service Entry Type) or Counter + Eventgroup ID (Eventgroup Entry Type) */
		uint32_t data;
	};
#pragma pack(pop)

	/**
	 * Construct a new SOME/IP-SD Service Entry Type
	 * @param[in] type Type to create
	 * @param[in] serviceID ServiceID to use
	 * @param[in] instanceID InstanceID to use
	 * @param[in] majorVersion MajorVersion to use
	 * @param[in] TTL TTL to use. Has to be 0 for all Stop* entry types
	 * @param[in] minorVersion MinorVersion to use
	 */
	SomeIpSdEntry(EntryType type, uint16_t serviceID, uint16_t instanceID, uint8_t majorVersion, uint32_t TTL,
				  uint32_t minorVersion);

	/**
	 * Construct a new SOME/IP-SD Eventgroup Entry Type
	 * @param[in] type Type to create
	 * @param[in] serviceID ServiceID to use
	 * @param[in] instanceID InstanceID to use
	 * @param[in] majorVersion MajorVersion to use
	 * @param[in] TTL TTL to use. Has to be 0 for all Stop* entry types
	 * @param[in] counter Counter value to use
	 * @param[in] eventGroupID EventgroupId to use
	 */
	SomeIpSdEntry(EntryType type, uint16_t serviceID, uint16_t instanceID, uint8_t majorVersion, uint32_t TTL,
				  uint8_t counter, uint16_t eventGroupID);

	/**
	 * Construct a new SomeIpSdEntry object from existing data
	 * @param[in] pSomeIpSdLayer Layer that this entry is created for
	 * @param[in] offset Offset for pSomeIpSdLayer
	 */
	SomeIpSdEntry(const SomeIpSdLayer *pSomeIpSdLayer, size_t offset);

	/**
	 * Destroy the SomeIpSd Entry object and delete allocated data if it has been allocated by a constructor
	 */
	~SomeIpSdEntry();

	/**
	 * Get the internal data of the SOME/IP-SD Entry
	 * @return uint8_t*
	 */
	uint8_t *getDataPtr() const;

	/**
	 * Get a pointer to the SOME/IP-SD Entry header
	 * @return someipsdhdrentry*
	 */
	someipsdhdrentry *getSomeIpSdEntryHeader() const;

	/**
	 * Get the Entry Type
	 * @return EntryType
	 */
	EntryType getType() const { return m_EntryType; }

	/**
	 * Get the Length of the SomeIpSd Entry
	 * @return size_t
	 */
	size_t getLength() const { return sizeof(someipsdhdrentry); }

	/**
	 * Get the number of Options of this Entry
	 * @return uint32_t
	 */
	uint32_t getNumOptions() const;

	/**
	 * Get the Service Id in host endianness
	 * @return uint16_t
	 */
	uint16_t getServiceId() const;

	/**
	 * Set the Service Id
	 * @param[in] serviceId
	 */
	void setServiceId(uint16_t serviceId);

	/**
	 * Get the Instance Id in host endianness
	 * @return uint16_t
	 */
	uint16_t getInstanceId() const;

	/**
	 * Set the Instance Id
	 * @param[in] instanceId
	 */
	void setInstanceId(uint16_t instanceId);

	/**
	 * Get the Major version field in host endianness
	 * @return uint16_t
	 */
	uint8_t getMajorVersion() const;

	/**
	 * Set the Major Version
	 * @param[in] majorVersion
	 */
	void setMajorVersion(uint8_t majorVersion);

	/**
	 * Get the Ttl field
	 * @return uint32_t
	 */
	uint32_t getTtl() const;

	/**
	 * Set the Ttl field
	 * @param[in] ttl
	 */
	void setTtl(uint32_t ttl);

	/**
	 * Get the minor version
	 * @return uint32_t
	 */
	uint32_t getMinorVersion() const;

	/**
	 * Set the minor version
	 * @param[in] minorVersion
	 */
	void setMinorVersion(uint32_t minorVersion);

	/**
	 * Get the counter value
	 * @return uint32_t
	 */
	uint8_t getCounter() const;

	/**
	 * Set the counter value
	 * @param[in] counter
	 */
	void setCounter(uint8_t counter);

	/**
	 * Get the eventgroup id
	 * @return uint32_t
	 */
	uint16_t getEventgroupId() const;

	/**
	 * Set the eventgroup id
	 * @param[in] eventgroupID
	 */
	void setEventgroupId(uint16_t eventgroupID);

private:
	/**
	 * These are the entry types used by SOME/IP-SD. They cannot be used for parameter passing since the values
	 * are not unique.
	 */
	enum class TypeInternal : uint8_t
	{
		/** Find Service */
		FindService_Internal = 0x00,
		/** Offer Service / Stop Offer Service */
		OfferService_Internal = 0x01,
		/** Subscribe Eventgroup & Stop Subscribe Eventgroup */
		SubscribeEventgroup_Internal = 0x06,
		/** Subscribe Eventgroup Acknowledgment / Negative Acknowledgement */
		SubscribeEventgroupAck_Internal = 0x07,
	};

	EntryType m_EntryType;
	const SomeIpSdLayer *m_Layer;
	size_t m_Offset;
	uint8_t *m_ShadowData;

	void initStdFields(EntryType type, uint16_t serviceID, uint16_t instanceID, uint8_t majorVersion, uint32_t TTL);

	SomeIpSdEntry(const SomeIpSdEntry &) = delete;
	SomeIpSdEntry &operator=(const SomeIpSdEntry &) = delete;

	static const uint32_t SOMEIPSD_HDR_ENTRY_MASK_TTL = 0x00FFFFFF;
};

/**
 * @class SomeIpSdLayer
 * Implementation of the SOME/IP-SD protocol
 */
class SomeIpSdLayer : public SomeIpLayer
{
public:
	friend class SomeIpSdEntry;

	typedef SomeIpSdEntry* EntryPtr;
	typedef std::vector<EntryPtr> EntriesVec;
	typedef SomeIpSdOption* OptionPtr;
	typedef std::vector<OptionPtr> OptionsVec;

	/**
	 * 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
	 */
	SomeIpSdLayer(uint8_t *data, size_t dataLen, Layer *prevLayer, Packet *packet);

	/**
	 * Construct a new SomeIpSdLayer object
	 * @param[in] serviceID Service ID
	 * @param[in] methodID Method ID
	 * @param[in] clientID Client ID
	 * @param[in] sessionID Session ID
	 * @param[in] interfaceVersion Interface Version
	 * @param[in] type Type of the message
	 * @param[in] returnCode Return Code
	 * @param[in] flags Flags that shall be used in the header
	 */
	SomeIpSdLayer(uint16_t serviceID, uint16_t methodID, uint16_t clientID, uint16_t sessionID,
				  uint8_t interfaceVersion, MsgType type, uint8_t returnCode, uint8_t flags);

	/**
	 * Destroy the layer object
	 */
	~SomeIpSdLayer() {}

	/**
	 * Checks if given port is a SOME/IP-SD protocol port
	 * @param[in] port Port to check
	 * @return true if SOME/IP-SD protocol port, false if not
	 */
	static bool isSomeIpSdPort(uint16_t port) { return port == 30490; }

	/**
	* The static method makes validation of input data
	* @param[in] data The pointer to the beginning of byte stream of IP packet
	* @param[in] dataLen The length of byte stream
	* @return True if the data is valid and can represent the packet
	*/
	static bool isDataValid(const uint8_t* data, size_t dataLen);

	/**
	 * Get the Flags of the layer
	 * @return uint8_t Flags
	 */
	uint8_t getFlags() const;

	/**
	 * Set the Flags of the layer
	 * @param[in] flags Flags to set
	 */
	void setFlags(uint8_t flags);

	/**
	 * Get the number of entries in this layer
	 * @return uint32_t
	 */
	uint32_t getNumEntries() const;

	/**
	 * Get the number of options in this layer
	 * @return uint32_t
	 */
	uint32_t getNumOptions() const;

	/**
	 * Get the Entries from this layer
	 * @return EntriesVec Vector holding pointers to the options
	 */
	const EntriesVec getEntries() const;

	/**
	 * Get the Options from this layer
	 * @return OptionsVec Vector holding pointers to the options
	 */
	const OptionsVec getOptions() const;

	/**
	 * Get the Options from a specific Entry
	 * @param[in] index Index of the Entry, starting with 0.
	 * @return OptionsVec Vector holding pointers to the options
	 */
	const OptionsVec getOptionsFromEntry(uint32_t index) const;

	/**
	 * Adds a given entry to the layer and returns the index of the entry
	 * @param[in] entry Pointer to the entry that shall be added to the layer
	 * @return uint32_t Returns the index of the entry starting with 0
	 */
	uint32_t addEntry(const SomeIpSdEntry &entry);

	/**
	 * Adds an option to an entry that has already been added to the layer by using addEntry(). The option
	 * is also added to the layer itself. If the option cannot by assigned to the entry, the option is not
	 * copied into the layer.
	 * @param[in] indexEntry Index of the entry where the option shall be added. First Entry has index 0
	 * @param[in] option Pointer to the option that shall be added
	 * @return True if the option could be assigned to the entry and was copied into the layer, false otherwise
	 */
	bool addOptionTo(uint32_t indexEntry, const SomeIpSdOption &option);

	/**
	 * Does nothing for this layer
	 */
	void computeCalculateFields() {};

	/**
	 * @return The string representation of the SOME/IP-SD layer
	 */
	std::string toString() const;

private:
	/**
	 * @struct someipsdhdr
	 * Represents an SOME/IP-SD protocol header
	 */
#pragma pack(push, 1)
	struct someipsdhdr : someiphdr
	{
		/** Flags (8 bit) */
		uint8_t flags;
		/** Reserved1 field (Bits 0-7 of 24-bits reserved field) */
		uint8_t reserved1;
		/** Reserved2 field (Bits 8-15 of 24-bits reserved field) */
		uint8_t reserved2;
		/** Reserved3 field (Bits 16-23 of 24-bits reserved field) */
		uint8_t reserved3;
	};
#pragma pack(pop)

	uint32_t m_NumOptions;

	static bool countOptions(uint32_t& count, const uint8_t* data);
	uint32_t findOption(const SomeIpSdOption &option);
	void addOption(const SomeIpSdOption &option);
	bool addOptionIndex(uint32_t indexEntry, uint32_t indexOffset);
	OptionPtr parseOption(SomeIpSdOption::OptionType type, size_t offset) const;

	static size_t getLenEntries(const uint8_t* data);
	size_t getLenEntries() const;
	static size_t getLenOptions(const uint8_t* data);
	size_t getLenOptions() const;
	void setLenEntries(uint32_t length);
	void setLenOptions(uint32_t length);
};

} // namespace pcpp
#endif /* PACKETPP_SOMEIPSD_LAYER */
