/*
 * Copyright (c) 2019-2021 The Linux Foundation. All rights reserved.
 * Copyright (c) 2022-2023 Qualcomm Innovation Center, Inc. All rights reserved.
 *
 * Permission to use, copy, modify, and/or distribute this software for
 * any purpose with or without fee is hereby granted, provided that the
 * above copyright notice and this permission notice appear in all
 * copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
 * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
 * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 * PERFORMANCE OF THIS SOFTWARE.
 */

/**
 * DOC:  wlan_hdd_bcn_recv.c
 * Feature for receiving beacons of connected AP and sending select
 * params to upper layer via vendor event
 */

#include <wlan_hdd_includes.h>
#include <net/cfg80211.h>
#include "wlan_osif_priv.h"
#include "qdf_trace.h"
#include "wlan_hdd_main.h"
#include "osif_sync.h"
#include "wlan_hdd_bcn_recv.h"
#include <linux/limits.h>
#include <wlan_hdd_object_manager.h>

#define SET_BIT(value, mask) ((value) |= (1 << (mask)))

#define BOOTTIME QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_BOOTTIME_WHEN_RECEIVED

#ifndef CHAR_BIT
#define CHAR_BIT 8	/* Normally in <limits.h> */
#endif

const struct nla_policy
	beacon_reporting_params_policy
	[QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_MAX + 1] = {
	[QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_OP_TYPE] = {.type = NLA_U8},
	[QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_ACTIVE_REPORTING] = {.type =
								     NLA_FLAG},
	[QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_PERIOD] = {.type = NLA_U8},
	[QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_DO_NOT_RESUME] = {.type =
								  NLA_FLAG},
};

/**
 * get_beacon_report_data_len() - Calculate length for beacon
 * report to allocate skb buffer
 * @report: beacon report structure
 *
 * Return: skb buffer length
 */
static
int get_beacon_report_data_len(struct wlan_beacon_report *report)
{
	uint32_t data_len = NLMSG_HDRLEN;

	/* QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_OP_TYPE */
	data_len += nla_total_size(sizeof(u32));

	/* QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_SSID */
	data_len += nla_total_size(report->ssid.length);

	/* QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_BSSID */
	data_len += nla_total_size(ETH_ALEN);

	/* QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_FREQ */
	data_len += nla_total_size(sizeof(u32));

	/* QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_BI */
	data_len += nla_total_size(sizeof(u16));

	/* QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_TSF */
	data_len += nla_total_size(sizeof(uint64_t));

	/* QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_BOOTTIME_WHEN_RECEIVED */
	data_len += nla_total_size(sizeof(uint64_t));

	return data_len;
}

/**
 * get_pause_ind_data_len() - Calculate skb buffer length
 * @is_disconnected: Connection state
 *
 * Calculate length for pause indication to allocate skb buffer
 *
 * Return: skb buffer length
 */
static int get_pause_ind_data_len(bool is_disconnected)
{
	uint32_t data_len = NLMSG_HDRLEN;

	/* QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_OP_TYPE */
	data_len += nla_total_size(sizeof(u32));

	/* QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_PAUSE_REASON */
	data_len += nla_total_size(sizeof(u32));

	/* QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_AUTO_RESUMES */
	if (!is_disconnected)
		data_len += nla_total_size(sizeof(u8));

	return data_len;
}

/**
 * hdd_send_bcn_recv_info() - Send beacon info to userspace for
 * connected AP
 * @hdd_handle: hdd_handle to get hdd_adapter
 * @beacon_report: Required beacon report
 *
 * Send beacon info to userspace for connected AP through a vendor event:
 * QCA_NL80211_VENDOR_SUBCMD_BEACON_REPORTING.
 */
static QDF_STATUS hdd_send_bcn_recv_info(hdd_handle_t hdd_handle,
					 struct wlan_beacon_report
					 *beacon_report)
{
	struct sk_buff *vendor_event;
	struct hdd_context *hdd_ctx = hdd_handle_to_context(hdd_handle);
	uint32_t data_len;
	int flags = cds_get_gfp_flags();
	struct hdd_adapter *adapter;
	struct wlan_hdd_link_info *link_info;
	enum qca_nl80211_vendor_subcmds_index index =
		QCA_NL80211_VENDOR_SUBCMD_BEACON_REPORTING_INDEX;

	if (wlan_hdd_validate_context(hdd_ctx))
		return QDF_STATUS_E_FAILURE;

	data_len = get_beacon_report_data_len(beacon_report);

	link_info = hdd_get_link_info_by_vdev(hdd_ctx, beacon_report->vdev_id);
	if (!link_info || hdd_validate_adapter(link_info->adapter))
		return QDF_STATUS_E_FAILURE;

	adapter = link_info->adapter;
	vendor_event = wlan_cfg80211_vendor_event_alloc(hdd_ctx->wiphy,
							&adapter->wdev,
							data_len, index, flags);
	if (!vendor_event) {
		hdd_err("wlan_cfg80211_vendor_event_alloc failed");
		return QDF_STATUS_E_FAILURE;
	}

	if (nla_put_u32(vendor_event,
			QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_OP_TYPE,
			QCA_WLAN_VENDOR_BEACON_REPORTING_OP_BEACON_INFO) ||
	    nla_put(vendor_event, QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_SSID,
		    beacon_report->ssid.length, beacon_report->ssid.ssid) ||
	    nla_put(vendor_event, QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_BSSID,
		    ETH_ALEN, beacon_report->bssid.bytes) ||
	    nla_put_u32(vendor_event,
			QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_FREQ,
			beacon_report->frequency) ||
	    nla_put_u16(vendor_event,
			QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_BI,
			beacon_report->beacon_interval) ||
	    wlan_cfg80211_nla_put_u64(vendor_event,
				      QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_TSF,
				      beacon_report->time_stamp) ||
	    wlan_cfg80211_nla_put_u64(vendor_event, BOOTTIME,
				      beacon_report->boot_time)) {
		hdd_err("QCA_WLAN_VENDOR_ATTR put fail");
		wlan_cfg80211_vendor_free_skb(vendor_event);
		return QDF_STATUS_E_FAILURE;
	}

	wlan_cfg80211_vendor_event(vendor_event, flags);
	return QDF_STATUS_SUCCESS;
}

/**
 * hdd_handle_beacon_reporting_start_op() - Process bcn recv start op
 * @hdd_ctx: Pointer to hdd context
 * @adapter: Pointer to network adapter
 * @active_report: Active reporting flag
 * @nth_value: Beacon report period
 * @do_not_resume: beacon reporting resume after a pause is completed
 *
 * This function process beacon reporting start operation.
 */
static int hdd_handle_beacon_reporting_start_op(struct hdd_context *hdd_ctx,
						struct hdd_adapter *adapter,
						bool active_report,
						uint32_t nth_value,
						bool do_not_resume)
{
	QDF_STATUS qdf_status = QDF_STATUS_SUCCESS;
	int errno;
	uint32_t mask = 0;

	if (active_report) {
		/* Register beacon report callback */
		qdf_status =
			sme_register_bcn_report_pe_cb(hdd_ctx->mac_handle,
						      hdd_send_bcn_recv_info);
		if (QDF_IS_STATUS_ERROR(qdf_status)) {
			hdd_err("bcn recv info cb reg failed = %d", qdf_status);
			errno = qdf_status_to_os_return(qdf_status);
			return errno;
		}

		/* Register pause indication callback */
		qdf_status =
			sme_register_bcn_recv_pause_ind_cb(hdd_ctx->mac_handle,
					hdd_beacon_recv_pause_indication);
		if (QDF_IS_STATUS_ERROR(qdf_status)) {
			hdd_err("pause_ind_cb reg failed = %d", qdf_status);
			errno = qdf_status_to_os_return(qdf_status);
			return errno;
		}
		/* Update Beacon report period in case of active reporting */
		nth_value = 1;
		/*
		 * Set MSB which indicates fw to don't wakeup host in wow
		 * mode in case of active beacon report.
		 */
		mask = (sizeof(uint32_t) * CHAR_BIT) - 1;
		SET_BIT(nth_value, mask);
	}
	/* Handle beacon receive start indication */
	qdf_status = sme_handle_bcn_recv_start(hdd_ctx->mac_handle,
					       adapter->deflink->vdev_id,
					       nth_value, do_not_resume);
	if (QDF_IS_STATUS_ERROR(qdf_status)) {
		hdd_err("bcn rcv start failed with status=%d", qdf_status);
		if (sme_register_bcn_report_pe_cb(hdd_ctx->mac_handle, NULL))
			hdd_err("bcn report cb deregistration failed");
		if (sme_register_bcn_recv_pause_ind_cb(hdd_ctx->mac_handle,
						       NULL))
			hdd_err("bcn pause ind cb deregistration failed");
		errno = qdf_status_to_os_return(qdf_status);
		return errno;
	}

	errno = qdf_status_to_os_return(qdf_status);

	return errno;
}

/**
 * hdd_handle_beacon_reporting_stop_op() - Process bcn recv stop op
 * @hdd_ctx: Pointer to hdd context
 * @adapter: Pointer to network adapter
 *
 * This function process beacon reporting stop operation.
 */
static int hdd_handle_beacon_reporting_stop_op(struct hdd_context *hdd_ctx,
					       struct hdd_adapter *adapter)
{
	QDF_STATUS qdf_status = QDF_STATUS_SUCCESS;
	int errno;

	/* Reset bcn recv start flag */
	sme_stop_beacon_report(hdd_ctx->mac_handle, adapter->deflink->vdev_id);

	/* Deregister beacon report callback */
	qdf_status = sme_register_bcn_report_pe_cb(hdd_ctx->mac_handle, NULL);
	if (QDF_IS_STATUS_ERROR(qdf_status)) {
		hdd_err("Callback de-registration failed = %d", qdf_status);
		errno = qdf_status_to_os_return(qdf_status);
		return errno;
	}

	/* Deregister pause indication callback */
	qdf_status = sme_register_bcn_recv_pause_ind_cb(hdd_ctx->mac_handle,
							NULL);
	if (QDF_IS_STATUS_ERROR(qdf_status)) {
		hdd_err("scan even deregister failed = %d", qdf_status);
		errno = qdf_status_to_os_return(qdf_status);
		return errno;
	}

	if (hdd_cm_is_vdev_associated(adapter->deflink))
		/* Add beacon filter */
		if (hdd_add_beacon_filter(adapter)) {
			hdd_err("Beacon filter addition failed");
			return -EINVAL;
		}

	errno = qdf_status_to_os_return(qdf_status);

	return errno;
}

/**
 * __wlan_hdd_cfg80211_bcn_rcv_op() - enable/disable beacon reporting
 * indication
 * @wiphy: Pointer to wireless phy
 * @wdev: Pointer to wireless device
 * @data: Pointer to data
 * @data_len: Length of @data
 *
 * This function is used to enable/disable asynchronous beacon
 * reporting feature using vendor commands.
 *
 * Return: 0 on success, negative errno on failure
 */
static int __wlan_hdd_cfg80211_bcn_rcv_op(struct wiphy *wiphy,
					  struct wireless_dev *wdev,
					  const void *data, int data_len)
{
	struct hdd_context *hdd_ctx = wiphy_priv(wiphy);
	struct net_device *dev = wdev->netdev;
	struct hdd_adapter *adapter = WLAN_HDD_GET_PRIV_PTR(dev);
	struct nlattr *tb[QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_MAX + 1];
	uint32_t bcn_report, nth_value = 1;
	int errno;
	bool active_report, do_not_resume;
	struct wlan_objmgr_vdev *vdev;
	enum scm_scan_status scan_req_status;

	hdd_enter_dev(dev);

	if (QDF_GLOBAL_FTM_MODE == hdd_get_conparam()) {
		hdd_err("Command not allowed in FTM mode");
		return -EPERM;
	}

	errno = hdd_validate_adapter(adapter);
	if (errno)
		return errno;

	if (adapter->device_mode != QDF_STA_MODE) {
		hdd_err("Command not allowed as device not in STA mode");
		return -EINVAL;
	}

	if (!hdd_cm_is_vdev_associated(adapter->deflink)) {
		hdd_err("STA not in connected state");
		return -EINVAL;
	}

	vdev = hdd_objmgr_get_vdev_by_user(adapter->deflink, WLAN_OSIF_SCAN_ID);
	if (!vdev)
		return -EINVAL;

	scan_req_status = ucfg_scan_get_pdev_status(wlan_vdev_get_pdev(vdev));
	wlan_objmgr_vdev_release_ref(vdev, WLAN_OSIF_SCAN_ID);

	if (scan_req_status != SCAN_NOT_IN_PROGRESS) {
		hdd_debug("Scan in progress: %d, bcn rpt start OP not allowed",
			  scan_req_status);
		return -EBUSY;
	}

	errno =
	   wlan_cfg80211_nla_parse(tb,
				   QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_MAX,
				   data,
				   data_len, beacon_reporting_params_policy);
	if (errno) {
		hdd_err("Failed to parse the beacon reporting params %d",
			errno);
		return errno;
	}

	/* Parse and fetch OP Type */
	if (!tb[QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_OP_TYPE]) {
		hdd_err("attr beacon report OP type failed");
		return -EINVAL;
	}
	bcn_report =
		nla_get_u8(tb[QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_OP_TYPE]);
	hdd_debug("Bcn Report: OP type:%d", bcn_report);

	switch (bcn_report) {
	case QCA_WLAN_VENDOR_BEACON_REPORTING_OP_START:
		active_report =
			!!tb[QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_ACTIVE_REPORTING];
		hdd_debug("attr active_report %d", active_report);

		do_not_resume =
			!!tb[QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_DO_NOT_RESUME];
		hdd_debug("Attr beacon report do not resume %d", do_not_resume);

		if (tb[QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_PERIOD])
			nth_value =
				nla_get_u8(tb[QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_PERIOD]);
		hdd_debug("Beacon Report: Period: %d", nth_value);

		if (sme_is_beacon_report_started(hdd_ctx->mac_handle,
						 adapter->deflink->vdev_id)) {
			hdd_debug("Start cmd already in progress, issue the stop to FW, before new start");
			if (hdd_handle_beacon_reporting_stop_op(hdd_ctx,
								adapter)) {
				hdd_err("Failed to stop the beacon reporting before starting new start");
				return -EAGAIN;
			}
		}
		errno = hdd_handle_beacon_reporting_start_op(hdd_ctx,
							     adapter,
							     active_report,
							     nth_value,
							     do_not_resume);
		if (errno) {
			hdd_err("Failed to start beacon reporting %d,", errno);
			break;
		}
		break;
	case QCA_WLAN_VENDOR_BEACON_REPORTING_OP_STOP:
		if (sme_is_beacon_report_started(hdd_ctx->mac_handle,
						 adapter->deflink->vdev_id)) {
			errno = hdd_handle_beacon_reporting_stop_op(hdd_ctx,
								    adapter);
			if (errno) {
				hdd_err("Failed to stop the beacon report, %d",
					errno);
			}
		} else {
			hdd_err_rl("BCN_RCV_STOP rej as no START CMD active");
			errno = -EINVAL;
		}
		break;
	default:
		hdd_debug("Invalid bcn report type %d", bcn_report);
	}

	return errno;
}

void hdd_beacon_recv_pause_indication(hdd_handle_t hdd_handle,
				      uint8_t vdev_id,
				      enum scan_event_type type,
				      bool is_disconnected)
{
	struct hdd_context *hdd_ctx = hdd_handle_to_context(hdd_handle);
	struct hdd_adapter *adapter;
	struct sk_buff *vendor_event;
	uint32_t data_len;
	int flags;
	uint32_t abort_reason;
	bool do_not_resume;
	struct wlan_hdd_link_info *link_info;

	if (wlan_hdd_validate_context(hdd_ctx))
		return;

	link_info = hdd_get_link_info_by_vdev(hdd_ctx, vdev_id);
	if (!link_info || hdd_validate_adapter(link_info->adapter))
		return;

	adapter = link_info->adapter;
	data_len = get_pause_ind_data_len(is_disconnected);
	flags = cds_get_gfp_flags();

	vendor_event =
		wlan_cfg80211_vendor_event_alloc(
			hdd_ctx->wiphy, &(adapter->wdev),
			data_len,
			QCA_NL80211_VENDOR_SUBCMD_BEACON_REPORTING_INDEX,
			flags);
	if (!vendor_event) {
		hdd_err("wlan_cfg80211_vendor_event_alloc failed");
		return;
	}

	do_not_resume =
		sme_is_beacon_reporting_do_not_resume(hdd_ctx->mac_handle,
						      link_info->vdev_id);

	if (is_disconnected) {
		abort_reason =
		     QCA_WLAN_VENDOR_BEACON_REPORTING_PAUSE_REASON_DISCONNECTED;
		/* Deregister callbacks and Reset bcn recv start flag */
		if (sme_is_beacon_report_started(hdd_ctx->mac_handle,
						 link_info->vdev_id))
			hdd_handle_beacon_reporting_stop_op(hdd_ctx, adapter);
	} else {
		/*
		 * In case of scan, Check that auto resume of beacon reporting
		 * is allowed or not.
		 * If not allowed:
		 * Deregister callbacks and Reset bcn recv start flag in order
		 * to make sure host should not send beacon report to userspace
		 * further.
		 * If Auto resume allowed:
		 * Send pause indication to userspace and continue sending
		 * connected AP's beacon to userspace.
		 */
		if (do_not_resume)
			hdd_handle_beacon_reporting_stop_op(hdd_ctx, adapter);

		switch (type) {
		case SCAN_EVENT_TYPE_STARTED:
			abort_reason =
		     QCA_WLAN_VENDOR_BEACON_REPORTING_PAUSE_REASON_SCAN_STARTED;
			break;
		default:
			abort_reason =
		      QCA_WLAN_VENDOR_BEACON_REPORTING_PAUSE_REASON_UNSPECIFIED;
		}
	}
	/* Send vendor event to user space to inform ABORT */
	if (nla_put_u32(vendor_event,
			QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_OP_TYPE,
			QCA_WLAN_VENDOR_BEACON_REPORTING_OP_PAUSE) ||
	    nla_put_u32(vendor_event,
			QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_PAUSE_REASON,
			abort_reason)) {
		hdd_err("QCA_WLAN_VENDOR_ATTR put fail");
		wlan_cfg80211_vendor_free_skb(vendor_event);
		return;
	}

	/*
	 * Send auto resume flag to user space to specify the driver will
	 * automatically resume reporting beacon events only in case of
	 * pause indication due to scan started.
	 * If do_not_resume flag is set in the recent
	 * QCA_WLAN_VENDOR_BEACON_REPORTING_OP_START command, then in the
	 * subsequent QCA_WLAN_VENDOR_BEACON_REPORTING_OP_PAUSE event (if any)
	 * the QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_AUTO_RESUMES shall not be
	 * set by the driver.
	 */
	if (!is_disconnected && !do_not_resume)
		if (nla_put_flag(vendor_event,
			QCA_WLAN_VENDOR_ATTR_BEACON_REPORTING_AUTO_RESUMES)) {
			hdd_err("QCA_WLAN_VENDOR_ATTR put fail");
			wlan_cfg80211_vendor_free_skb(vendor_event);
			return;
		}

	wlan_cfg80211_vendor_event(vendor_event, flags);
}

int wlan_hdd_cfg80211_bcn_rcv_op(struct wiphy *wiphy,
				 struct wireless_dev *wdev,
				 const void *data, int data_len)
{
	int errno;
	struct osif_vdev_sync *vdev_sync;

	errno = osif_vdev_sync_op_start(wdev->netdev, &vdev_sync);
	if (errno)
		return errno;

	errno = __wlan_hdd_cfg80211_bcn_rcv_op(wiphy, wdev,
					       data, data_len);

	osif_vdev_sync_op_stop(vdev_sync);

	return errno;
}
