/*  Bluetooth Mesh */

/*
 * Copyright (c) 2017 Intel Corporation
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include "syscfg/syscfg.h"

#define BLE_NPL_LOG_MODULE BLE_MESH_BEACON_LOG
#include <nimble/nimble_npl_log.h>


#include <errno.h>
#include <assert.h>
#include "os/os_mbuf.h"
#include "mesh/mesh.h"

#include "adv.h"
#include "mesh_priv.h"
#include "net.h"
#include "prov.h"
#include "crypto.h"
#include "beacon.h"
#include "foundation.h"
#include "atomic.h"

#define PROVISIONED_INTERVAL      (K_SECONDS(10))

#define BEACON_TYPE_UNPROVISIONED  0x00
#define BEACON_TYPE_SECURE         0x01

/* 3 transmissions, 20ms interval */
#define UNPROV_XMIT                BT_MESH_TRANSMIT(2, 20)

/* 1 transmission, 20ms interval */
#define PROV_XMIT                  BT_MESH_TRANSMIT(0, 20)

static struct k_work_delayable beacon_timer;

static int cache_check(struct bt_mesh_subnet *sub, void *beacon_data)
{
	return !memcmp(sub->beacon_cache, beacon_data, 21);
}

static void cache_add(uint8_t data[21], struct bt_mesh_subnet *sub)
{
	memcpy(sub->beacon_cache, data, 21);
}

static void beacon_complete(int err, void *user_data)
{
	struct bt_mesh_subnet *sub = user_data;

	BT_DBG("err %d", err);

	sub->beacon_sent = k_uptime_get_32();
}

void bt_mesh_beacon_create(struct bt_mesh_subnet *sub,
			   struct os_mbuf *buf)
{
	uint8_t flags = bt_mesh_net_flags(sub);
	struct bt_mesh_subnet_keys *keys;

	net_buf_simple_add_u8(buf, BEACON_TYPE_SECURE);

	keys = &sub->keys[SUBNET_KEY_TX_IDX(sub)];

	net_buf_simple_add_u8(buf, flags);

	/* Network ID */
	net_buf_simple_add_mem(buf, keys->net_id, 8);

	/* IV Index */
	net_buf_simple_add_be32(buf, bt_mesh.iv_index);

	net_buf_simple_add_mem(buf, sub->auth, 8);

	BT_DBG("net_idx 0x%04x flags 0x%02x NetID %s", sub->net_idx,
	       flags, bt_hex(keys->net_id, 8));
	BT_DBG("IV Index 0x%08x Auth %s", (unsigned) bt_mesh.iv_index,
	       bt_hex(sub->auth, 8));
}

/* If the interval has passed or is within 5 seconds from now send a beacon */
#define BEACON_THRESHOLD(sub) (K_SECONDS(10 * ((sub)->beacons_last + 1)) - \
			       K_SECONDS(5))

static int secure_beacon_send(struct bt_mesh_subnet *sub, void *cb_data)
{
	static const struct bt_mesh_send_cb send_cb = {
		.end = beacon_complete,
	};
	uint32_t now = k_uptime_get_32();
	struct os_mbuf *buf;
	uint32_t time_diff;

	BT_DBG("");

	time_diff = now - sub->beacon_sent;
	if (time_diff < (600 * MSEC_PER_SEC) &&
		time_diff < BEACON_THRESHOLD(sub)) {
		return 0;
	}

	buf = bt_mesh_adv_create(BT_MESH_ADV_BEACON, PROV_XMIT, K_NO_WAIT);
	if (!buf) {
		BT_ERR("Unable to allocate beacon buffer");
		return -ENOMEM;
	}

	bt_mesh_beacon_create(sub, buf);

	bt_mesh_adv_send(buf, &send_cb, sub);
	net_buf_unref(buf);

	return 0;
}

static int unprovisioned_beacon_send(void)
{
	const struct bt_mesh_prov *prov;
	uint8_t uri_hash[16] = { 0 };
	struct os_mbuf *buf;
	uint16_t oob_info;

	BT_DBG("unprovisioned_beacon_send");

	buf = bt_mesh_adv_create(BT_MESH_ADV_BEACON, UNPROV_XMIT, K_NO_WAIT);
	if (!buf) {
		BT_ERR("Unable to allocate beacon buffer");
		return -ENOBUFS;
	}

	prov = bt_mesh_prov_get();

	net_buf_add_u8(buf, BEACON_TYPE_UNPROVISIONED);
	net_buf_add_mem(buf, prov->uuid, 16);

	if (prov->uri && bt_mesh_s1(prov->uri, uri_hash) == 0) {
		oob_info = prov->oob_info | BT_MESH_PROV_OOB_URI;
	} else {
		oob_info = prov->oob_info;
	}

	net_buf_add_be16(buf, oob_info);
	net_buf_add_mem(buf, uri_hash, 4);

	bt_mesh_adv_send(buf, NULL, NULL);
	net_buf_unref(buf);

	if (prov->uri) {
		size_t len;

		buf = bt_mesh_adv_create(BT_MESH_ADV_URI, UNPROV_XMIT,
					 K_NO_WAIT);
		if (!buf) {
			BT_ERR("Unable to allocate URI buffer");
			return -ENOBUFS;
		}

		len = strlen(prov->uri);
		if (net_buf_tailroom(buf) < len) {
			BT_WARN("Too long URI to fit advertising data");
		} else {
			net_buf_add_mem(buf, prov->uri, len);
			bt_mesh_adv_send(buf, NULL, NULL);
		}

		net_buf_unref(buf);
	}

	return 0;
}

static void unprovisioned_beacon_recv(struct os_mbuf *buf)
{
	const struct bt_mesh_prov *prov;
	uint8_t *uuid;
	uint16_t oob_info;
	uint32_t uri_hash_val;
	uint32_t *uri_hash = NULL;

	if (buf->om_len != 18 && buf->om_len != 22) {
		BT_ERR("Invalid unprovisioned beacon length (%u)", buf->om_len);
		return;
	}

	uuid = net_buf_simple_pull_mem(buf, 16);
	oob_info = net_buf_simple_pull_be16(buf);

	if (buf->om_len == 4) {
		uri_hash_val = net_buf_simple_pull_be32(buf);
		uri_hash = &uri_hash_val;
	}

	BT_DBG("uuid %s", bt_hex(uuid, 16));

	prov = bt_mesh_prov_get();

	if (prov->unprovisioned_beacon) {
		prov->unprovisioned_beacon(uuid,
					   (bt_mesh_prov_oob_info_t)oob_info,
					   uri_hash);
	}
}

static void sub_update_beacon_observation(struct bt_mesh_subnet *sub)
{
	sub->beacons_last = sub->beacons_cur;
	sub->beacons_cur = 0U;
}

static void update_beacon_observation(void)
{
	static bool first_half;

	/* Observation period is 20 seconds, whereas the beacon timer
	 * runs every 10 seconds. We process what's happened during the
	 * window only after the seconnd half.
	 */
	first_half = !first_half;
	if (first_half) {
		return;
	}

	bt_mesh_subnet_foreach(sub_update_beacon_observation);
}

static void beacon_send(struct ble_npl_event *work)
{

	BT_DBG("");

	if (bt_mesh_is_provisioned()) {
		if (!bt_mesh_beacon_enabled() &&
		!atomic_test_bit(bt_mesh.flags, BT_MESH_IVU_INITIATOR)) {
			return;
		}

		update_beacon_observation();
		(void)bt_mesh_subnet_find(secure_beacon_send, NULL);

		k_work_schedule(&beacon_timer, PROVISIONED_INTERVAL);

		return;
	}

	if (IS_ENABLED(BLE_MESH_PB_ADV)) {
		/* Don't send anything if we have an active provisioning link */
		if (!bt_mesh_prov_active()) {
			unprovisioned_beacon_send();
		}

		k_work_schedule(&beacon_timer, K_SECONDS(MYNEWT_VAL(BLE_MESH_UNPROV_BEACON_INT)));
	}
}

struct beacon_params {
	const uint8_t *net_id;
	const uint8_t *auth;
	uint32_t iv_index;
	uint8_t flags;

	bool new_key;
};

static bool auth_match(struct bt_mesh_subnet_keys *keys,
		       const struct beacon_params *params)
{
	uint8_t net_auth[8];

	if (memcmp(params->net_id, keys->net_id, 8)) {
		return false;
	}

	bt_mesh_beacon_auth(keys->beacon, params->flags, keys->net_id,
			    params->iv_index, net_auth);

	if (memcmp(params->auth, net_auth, 8)) {
		BT_WARN("Authentication Value %s != %s",
			bt_hex(params->auth, 8), bt_hex(net_auth, 8));
		return false;
	}

	return true;
}

static int subnet_by_id(struct bt_mesh_subnet *sub, void *cb_data)
{
	struct beacon_params *params = cb_data;

	for (int i = 0; i < ARRAY_SIZE(sub->keys); i++) {
		if (sub->keys[i].valid && auth_match(&sub->keys[i], params)) {
			params->new_key = (i > 0);
			return true;
		}
	}

	return false;
}

static void secure_beacon_recv(struct os_mbuf *buf)
{
	struct beacon_params params;
	struct bt_mesh_subnet *sub;
	uint8_t *data;

	if (buf->om_len < 21) {
		BT_ERR("Too short secure beacon (len %u)", buf->om_len);
		return;
	}

	sub = bt_mesh_subnet_find(cache_check, buf->om_data);
	if (sub) {
		/* We've seen this beacon before - just update the stats */
		goto update_stats;
	}

	/* So we can add to the cache if auth matches */
	data = buf->om_data;

	params.flags = net_buf_simple_pull_u8(buf);
	params.net_id = net_buf_simple_pull_mem(buf, 8);
	params.iv_index = net_buf_simple_pull_be32(buf);
	params.auth = buf->om_data;

	BT_DBG("flags 0x%02x id %s iv_index 0x%08x",
	       params.flags, bt_hex(params.net_id, 8), params.iv_index);

	sub = bt_mesh_subnet_find(subnet_by_id, &params);
	if (!sub) {
		BT_DBG("No subnet that matched beacon");
		return;
	}

	if (sub->kr_phase == BT_MESH_KR_PHASE_2 && !params.new_key) {
		BT_WARN("Ignoring Phase 2 KR Update secured using old key");
		return;
	}

	cache_add(data, sub);

	bt_mesh_kr_update(sub, BT_MESH_KEY_REFRESH(params.flags),
			  params.new_key);

	/* If we have NetKey0 accept initiation only from it */
	if (bt_mesh_subnet_get(BT_MESH_KEY_PRIMARY) &&
	    sub->net_idx != BT_MESH_KEY_PRIMARY) {
		BT_WARN("Ignoring secure beacon on non-primary subnet");
		goto update_stats;
	}

	BT_DBG("net_idx 0x%04x iv_index 0x%08x, current iv_index 0x%08x",
	       sub->net_idx, params.iv_index, bt_mesh.iv_index);

	if (atomic_test_bit(bt_mesh.flags, BT_MESH_IVU_INITIATOR) &&
	    (atomic_test_bit(bt_mesh.flags, BT_MESH_IVU_IN_PROGRESS) ==
	     BT_MESH_IV_UPDATE(params.flags))) {
		bt_mesh_beacon_ivu_initiator(false);
	}

	bt_mesh_net_iv_update(params.iv_index, BT_MESH_IV_UPDATE(params.flags));

update_stats:
	if (bt_mesh_beacon_enabled() &&
	    sub->beacons_cur < 0xff) {
		sub->beacons_cur++;
	}
}

void bt_mesh_beacon_recv(struct os_mbuf *buf)
{
	uint8_t type;

	BT_DBG("%u bytes: %s", buf->om_len, bt_hex(buf->om_data, buf->om_len));

	if (buf->om_len < 1) {
		BT_ERR("Too short beacon");
		return;
	}

	type = net_buf_simple_pull_u8(buf);
	switch (type) {
	case BEACON_TYPE_UNPROVISIONED:
		if (IS_ENABLED(BLE_MESH_PB_ADV)) {
			unprovisioned_beacon_recv(buf);
		}
		break;
	case BEACON_TYPE_SECURE:
		secure_beacon_recv(buf);
		break;
	default:
		BT_WARN("Unknown beacon type 0x%02x", type);
		break;
	}
}

void bt_mesh_beacon_update(struct bt_mesh_subnet *sub)
{
	uint8_t flags = bt_mesh_net_flags(sub);
	struct bt_mesh_subnet_keys *keys;
	int err;

	keys = &sub->keys[SUBNET_KEY_TX_IDX(sub)];

	BT_DBG("NetIndex 0x%03x Using %s key", sub->net_idx,
	       SUBNET_KEY_TX_IDX(sub) ? "new" : "current");
	BT_DBG("flags 0x%02x, IVI 0x%08x", flags, bt_mesh.iv_index);

	err = bt_mesh_beacon_auth(keys->beacon, flags, keys->net_id,
				   bt_mesh.iv_index, sub->auth);
	if (err) {
		BT_ERR("Failed updating net beacon for 0x%03x", sub->net_idx);
	}
}

static void subnet_evt(struct bt_mesh_subnet *sub, enum bt_mesh_key_evt evt)
{
	if (evt != BT_MESH_KEY_DELETED) {
		bt_mesh_beacon_update(sub);
	}
}

void bt_mesh_beacon_init(void)
{
	if (!bt_mesh_subnet_cb_list[1]) {
		bt_mesh_subnet_cb_list[1] = subnet_evt;
	}

	k_work_init_delayable(&beacon_timer, beacon_send);
}

void bt_mesh_beacon_ivu_initiator(bool enable)
{
	atomic_set_bit_to(bt_mesh.flags, BT_MESH_IVU_INITIATOR, enable);

	/* Fire the beacon handler straight away if it's not already pending -
	 * in which case we'll fire according to the ongoing periodic sending.
	 * If beacons are disabled, the handler will exit early.
	 *
	 * An alternative solution would be to check whether beacons are enabled
	 * here, and cancel if not. As the cancel operation may fail, we would
	 * still have to implement an early exit mechanism, so we might as well
	 * just use this every time.
	 */
	k_work_schedule(&beacon_timer, K_NO_WAIT);
}

static void subnet_beacon_enable(struct bt_mesh_subnet *sub)
{
	sub->beacons_last = 0U;
	sub->beacons_cur = 0U;

	bt_mesh_beacon_update(sub);
}

void bt_mesh_beacon_enable(void)
{
	if (bt_mesh_is_provisioned()) {
		bt_mesh_subnet_foreach(subnet_beacon_enable);
	}

	k_work_reschedule(&beacon_timer, K_NO_WAIT);
}

void bt_mesh_beacon_disable(void)
{
	if (!atomic_test_bit(bt_mesh.flags, BT_MESH_IVU_INITIATOR)) {
		/* If this fails, we'll do an early exit in the work handler. */
		(void)k_work_cancel_delayable(&beacon_timer);
	}
}
