/*
 * \brief  DHCP client state model
 * \author Martin Stein
 * \date   2016-08-24
 */

/*
 * Copyright (C) 2016-2017 Genode Labs GmbH
 *
 * This file is part of the Genode OS framework, which is distributed
 * under the terms of the GNU Affero General Public License version 3.
 */

/* local includes */
#include <dhcp_client.h>
#include <interface.h>
#include <domain.h>
#include <configuration.h>
#include <size_guard.h>

using namespace Genode;
using namespace Net;
using Message_type       = Dhcp_packet::Message_type;
using Drop_packet_inform = Net::Interface::Drop_packet_inform;


Configuration &Dhcp_client::_config() { return _domain().config(); };


Domain &Dhcp_client::_domain() { return _interface.domain(); }


Dhcp_client::Dhcp_client(Genode::Allocator &alloc,
                         Timer::Connection &timer,
                         Interface         &interface)
:
	_alloc(alloc), _interface(interface),
	_timeout(timer, *this, &Dhcp_client::_handle_timeout)
{ }


void Dhcp_client::discover()
{
	_set_state(State::SELECT, _config().dhcp_discover_timeout());
	_send(Message_type::DISCOVER, Ipv4_address(), Ipv4_address());
}


void Dhcp_client::_rerequest(State next_state)
{
	_set_state(next_state, _rerequest_timeout(2));
	_send(Message_type::REQUEST, _domain().ip_config().interface.address,
	      Ipv4_address());
}


void Dhcp_client::_set_state(State state, Microseconds timeout)
{
	_state = state;
	_timeout.schedule(timeout);
}


Microseconds Dhcp_client::_rerequest_timeout(unsigned lease_time_div_log2)
{
	/* FIXME limit the time because of shortcomings in timeout framework */
	enum { MAX_TIMEOUT_SEC = 3600 };
	unsigned long timeout_sec = _lease_time_sec >> lease_time_div_log2;

	if (timeout_sec > MAX_TIMEOUT_SEC) {
		timeout_sec = MAX_TIMEOUT_SEC;
		warning("Had to prune the state timeout of DHCP client");
	}
	return Microseconds(timeout_sec * 1000UL * 1000UL);
}


void Dhcp_client::_handle_timeout(Duration)
{
	switch (_state) {
	case State::BOUND:  _rerequest(State::RENEW);  break;
	case State::RENEW:  _rerequest(State::REBIND); break;
	case State::REBIND: _domain().discard_ip_config();
	default:            discover();
	}
}


void Dhcp_client::handle_ip(Ethernet_frame &eth, size_t eth_size)
{
	if (eth.dst() != _interface.router_mac() &&
	    eth.dst() != Mac_address(0xff))
	{
		throw Drop_packet_inform("DHCP client expects Ethernet targeting the router");
	}
	Ipv4_packet &ip = *eth.data<Ipv4_packet>(eth_size - sizeof(Ethernet_frame));

	if (ip.protocol() != Ipv4_packet::Protocol::UDP) {
		throw Drop_packet_inform("DHCP client expects UDP packet");
	}
	Udp_packet &udp = *ip.data<Udp_packet>(eth_size - sizeof(Ethernet_frame)
	                                                - sizeof(Ipv4_packet));

	if (!Dhcp_packet::is_dhcp(&udp)) {
		throw Drop_packet_inform("DHCP client expects DHCP packet");
	}
	Dhcp_packet &dhcp = *udp.data<Dhcp_packet>(eth_size - sizeof(Ethernet_frame)
	                                                    - sizeof(Ipv4_packet)
	                                                    - sizeof(Udp_packet));

	if (dhcp.op() != Dhcp_packet::REPLY) {
		throw Drop_packet_inform("DHCP client expects DHCP reply");
	}
	if (dhcp.client_mac() != _interface.router_mac()) {
		throw Drop_packet_inform("DHCP client expects DHCP targeting the router");
	}
	try { _handle_dhcp_reply(dhcp); }
	catch (Dhcp_packet::Option_not_found) {
		throw Drop_packet_inform("DHCP client misses DHCP option");
	}
}


void Dhcp_client::_handle_dhcp_reply(Dhcp_packet &dhcp)
{
	Message_type const msg_type =
		dhcp.option<Dhcp_packet::Message_type_option>().value();

	switch (_state) {
	case State::SELECT:

		if (msg_type != Message_type::OFFER) {
			throw Drop_packet_inform("DHCP client expects an offer");
		}
		_set_state(State::REQUEST, _config().dhcp_request_timeout());
		_send(Message_type::REQUEST, dhcp.yiaddr(),
		      dhcp.option<Dhcp_packet::Server_ipv4>().value());
		break;

	case State::REQUEST:

		if (msg_type != Message_type::ACK) {
			throw Drop_packet_inform("DHCP client expects an acknowledgement");
		}
		_lease_time_sec = dhcp.option<Dhcp_packet::Ip_lease_time>().value();
		_set_state(State::BOUND, _rerequest_timeout(1));
		_domain().ip_config(dhcp.yiaddr(),
		                    dhcp.option<Dhcp_packet::Subnet_mask>().value(),
		                    dhcp.option<Dhcp_packet::Router_ipv4>().value());
		break;

	case State::RENEW:
	case State::REBIND:

		if (msg_type != Message_type::ACK) {
			throw Drop_packet_inform("DHCP client expects an acknowledgement");
		}
		_set_state(State::BOUND, _rerequest_timeout(1));
		_lease_time_sec = dhcp.option<Dhcp_packet::Ip_lease_time>().value();
		break;

	default: throw Drop_packet_inform("DHCP client doesn't expect a packet");
	}
}


void Dhcp_client::_send(Message_type msg_type,
                        Ipv4_address client_ip,
                        Ipv4_address server_ip)
{
	enum { PKT_SIZE = 1024 };
	using Size_guard = Size_guard_tpl<PKT_SIZE, Interface::Dhcp_msg_buffer_too_small>;
	Mac_address client_mac = _interface.router_mac();
	_interface.send(PKT_SIZE, [&] (void *pkt_base) {

		/* create ETH header of the request */
		Size_guard size;
		size.add(sizeof(Ethernet_frame));
		Ethernet_frame &eth = *reinterpret_cast<Ethernet_frame *>(pkt_base);
		eth.dst(Mac_address(0xff));
		eth.src(client_mac);
		eth.type(Ethernet_frame::Type::IPV4);

		/* create IP header of the request */
		enum { IPV4_TIME_TO_LIVE = 64 };
		size_t const ip_off = size.curr();
		Ipv4_packet &ip = *eth.data<Ipv4_packet>(size.left());
		size.add(sizeof(Ipv4_packet));
		ip.header_length(sizeof(Ipv4_packet) / 4);
		ip.version(4);
		ip.diff_service(0);
		ip.identification(0);
		ip.flags(0);
		ip.fragment_offset(0);
		ip.time_to_live(IPV4_TIME_TO_LIVE);
		ip.protocol(Ipv4_packet::Protocol::UDP);
		ip.src(client_ip);
		ip.dst(Ipv4_address(0xff));

		/* create UDP header of the request */
		size_t const udp_off = size.curr();
		Udp_packet &udp = *ip.data<Udp_packet>(size.left());
		size.add(sizeof(Udp_packet));
		udp.src_port(Port(Dhcp_packet::BOOTPC));
		udp.dst_port(Port(Dhcp_packet::BOOTPS));

		/* create mandatory DHCP fields of the request  */
		size_t const dhcp_off = size.curr();
		Dhcp_packet &dhcp = *udp.data<Dhcp_packet>(size.left());
		size.add(sizeof(Dhcp_packet));
		dhcp.op(Dhcp_packet::REQUEST);
		dhcp.htype(Dhcp_packet::Htype::ETH);
		dhcp.hlen(sizeof(Mac_address));
		dhcp.hops(0);
		dhcp.xid(0x12345678);
		dhcp.secs(0);
		dhcp.flags(0);
		dhcp.ciaddr(client_ip);
		dhcp.yiaddr(Ipv4_address());
		dhcp.siaddr(Ipv4_address());
		dhcp.giaddr(Ipv4_address());
		dhcp.client_mac(client_mac);
		dhcp.zero_fill_sname();
		dhcp.zero_fill_file();
		dhcp.default_magic_cookie();

		/* append DHCP option fields to the request */
		Dhcp_packet::Options_aggregator<Size_guard>
			dhcp_opts(dhcp, size);
		dhcp_opts.append_option<Dhcp_packet::Message_type_option>(msg_type);

		switch (msg_type) {
		case Message_type::DISCOVER:
			dhcp_opts.append_option<Dhcp_packet::Client_id>(client_mac);
			dhcp_opts.append_option<Dhcp_packet::Max_msg_size>(PKT_SIZE - dhcp_off);
			break;

		case Message_type::REQUEST:
			dhcp_opts.append_option<Dhcp_packet::Client_id>(client_mac);
			dhcp_opts.append_option<Dhcp_packet::Max_msg_size>(PKT_SIZE - dhcp_off);
			if (_state == State::REQUEST) {
				dhcp_opts.append_option<Dhcp_packet::Requested_addr>(client_ip);
				dhcp_opts.append_option<Dhcp_packet::Server_ipv4>(server_ip);
			}
			break;

		default:
			throw Interface::Bad_send_dhcp_args();
		}
		dhcp_opts.append_option<Dhcp_packet::Options_end>();

		/* fill in header values that need the packet to be complete already */
		udp.length(size.curr() - udp_off);
		udp.update_checksum(ip.src(), ip.dst());
		ip.total_length(size.curr() - ip_off);
		ip.checksum(Ipv4_packet::calculate_checksum(ip));
	});
}
