/* SPDX-License-Identifier: LGPL-2.1-only */
/*
 * qca.c - for mac80211 based Qualcomm drivers.
 *
 * Copyright (C) 2020-2024 iopsys Software Solutions AB. All rights reserved.
 */
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <net/if.h>
#include <time.h>
#include <syslog.h>
#include <unistd.h>
#include <stdlib.h>
#include <dirent.h>
#include <ctype.h>

#include <easy/easy.h>
#include "wifi.h"
#include "wifiutils.h"
#include "nlwifi.h"
#include "hostapd_ctrl.h"
#include "supplicant_ctrl.h"
#include "debug.h"
#include "drivers.h"


static int qca_simulate_radar_debugfs(const char *phyname, struct wifi_radar_args *radar)
{
	char rdfile[512] = {0};
	char path[512] = {0};
	struct stat statbuf;
	struct dirent *p;
	int fd;
	DIR *d;

	/* Trigger radar
	 * echo 1 > /sys/kernel/debug/ieee80211/phyX/ath12k_hwY/dfs_simulate_radar
	 */

	snprintf(path, sizeof(path), "/sys/kernel/debug/ieee80211/%s", phyname);
	d = opendir(path);
	if (!d)
		return -1;

	while ((p = readdir(d))) {

		if (!strcmp(p->d_name, ".") ||
		    !strcmp(p->d_name, "..") ||
		    strncmp(p->d_name, "ath12k_", 7)) {
			continue;
		}

		snprintf(rdfile, sizeof(rdfile) - 1,
			 "/sys/kernel/debug/ieee80211/%s/%s/dfs_simulate_radar",
			 phyname, p->d_name);

		if (!stat(rdfile, &statbuf)) {
			fd = open(rdfile, O_WRONLY);
			if (fd < 0)
				break;

			write(fd, "1", 1);
			close(fd);
			break;
		}
	}

	closedir(d);
	return 0;
}

int radio_simulate_radar(const char *name, struct wifi_radar_args *radar)
{
	if (WARN_ON(!radar))
		return -1;

	libwifi_dbg("[%s] %s called ch:%d, bandwidth:%d, type:0x%x, subband:0x%x\n",
		    name, __func__, radar->channel, radar->bandwidth, radar->type,
		    radar->subband_mask);

	return qca_simulate_radar_debugfs(name, radar);
}

static int radio_is_multiband(const char *ifname, bool *res)
{
	/* Report early without NL80211 calls */
	*res = true;
	return 0;
}

static uint64_t read_int_from_file(const char *fmt, ...)
{
	char str[512], *tmp;
	uint64_t res;
	va_list ap;
	FILE *f;

	va_start(ap, fmt);
	vsnprintf(str, sizeof(str), fmt, ap); /* Flawfinder: ignore */
	va_end(ap);

	if ((f = fopen(str, "r")) == NULL)
		return 0;

	tmp = fgets(str, sizeof(str), f);
	fclose(f);
	if (tmp == NULL)
		return 0;

	res = strtoll(str, &tmp, 10);
	if (tmp == str || (*tmp != 0 && !isspace(*tmp)))
		return 0;

	return res;
}

static int qca_get_mlo_links(const char *ifname, enum wifi_band band,
			     struct wifi_mlo_link *link, int *num)
{
	libwifi_dbg("[%s] %s called\n", ifname, __func__);

	return nlwifi_get_mlo_links(ifname, band, link, num);
}

int qca_get_ctrl_interface_band(const char *ifname, enum wifi_band band, char **ctrl_interface)
{
	struct wifi_mlo_link link[8] = {0};
	char ctrl_iface[512] = {0};
	int num = 8;
	int ret;

	ret = qca_get_mlo_links(ifname, band, link, &num);
	if (ret) {
		return -1;
	}

	switch (link[0].mode) {
		case WIFI_MODE_AP:
			if (num > 0)
				snprintf(ctrl_iface, sizeof(ctrl_iface), "%s_link%u", ifname, link[0].id);
			else
				strncpy(ctrl_iface, ifname, sizeof(ctrl_iface));
			break;
		default:
			strncpy(ctrl_iface, ifname, sizeof(ctrl_iface));
			break;
	}

	*ctrl_interface = strdup(ctrl_iface);
	return 0;
}

int qca_get_ctrl_interface(const char *ifname, char **ctrl_interface)
{
	return qca_get_ctrl_interface_band(ifname, BAND_ANY, ctrl_interface);
}

int qca_iface_get_stats(const char *ifname, struct wifi_ap_stats *s)
{
	struct wifi_iface ifaces[WIFI_IFACE_MAX_NUM];
	uint8_t num = WIFI_IFACE_MAX_NUM, i;
	char parent[32] = {};
	char phyname[16] = {0};
	ifstatus_t flags = 0;
	int ret;

	memset(s, 0, sizeof(*s));

	default_wifi_driver.radio.get_ifstatus(ifname, &flags);
	if (!(flags & IFF_UP)) {
		libwifi_dbg("%s: %s not UP\n", __func__, ifname);
		return -1;
	}

	ret = nlwifi_get_phyname(ifname, phyname);
	if (ret || !strlen(phyname))
		return -1;

	if (nlwifi_get_phy_wifi_ifaces(ifname, BAND_ANY, ifaces, &num) < 0) {
		libwifi_dbg("Failed to get list of interfaces of %s\n", ifname);
		return -1;
	}

	for (i = 0; i < num; i++) {
		if (ifaces[i].mode != WIFI_MODE_AP_VLAN &&
		    ifaces[i].mode != WIFI_MODE_AP)
			continue;

		memset(parent, 0, sizeof(parent));
		wifi_get_4addr_parent(ifaces[i].name, parent);

		libwifi_dbg("[%s] %s check iface %s parent %s\n", ifname, __func__, ifaces[i].name, parent);

		if (strcmp(ifname, ifaces[i].name) &&
		    strcmp(ifname, parent))
			continue;

		libwifi_dbg("[%s] %s add iface stats\n", ifaces[i].name, __func__);
		s->tx_bytes += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/tx_bytes", phyname, ifaces[i].name);
		s->rx_bytes += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/rx_bytes", phyname, ifaces[i].name);
		s->tx_pkts += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/tx_packets", phyname, ifaces[i].name);
		s->rx_pkts += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/rx_packets", phyname, ifaces[i].name);
		s->tx_err_pkts += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/tx_errors", phyname, ifaces[i].name);
		s->rx_err_pkts += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/rx_errors", phyname, ifaces[i].name);
		s->tx_dropped_pkts += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/tx_dropped", phyname, ifaces[i].name);
		s->rx_dropped_pkts += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/rx_dropped", phyname, ifaces[i].name);
		s->rx_unknown_pkts += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/rx_nohandler", phyname, ifaces[i].name);
	}

	libwifi_dbg("[%s] %s ret %d\n", ifname, __func__, 0);
	return 0;
}

static int radio_get_band_stats(const char *name, enum wifi_band band, struct wifi_radio_stats *s)
{
	struct wifi_iface iface[WIFI_IFACE_MAX_NUM] = {0};
	uint8_t num = WIFI_IFACE_MAX_NUM;
	int i;

	if (nlwifi_get_phy_wifi_ifaces(name, band, iface, &num) < 0) {
		libwifi_dbg("Failed to get list of interfaces of %s\n", name);
		return -1;
	}

	memset(s, 0, sizeof(*s));
	for (i = 0; i < num; i++) {
		s->tx_bytes += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/tx_bytes", name, iface[i].name);
		s->rx_bytes += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/rx_bytes", name, iface[i].name);
		s->tx_pkts += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/tx_packets", name, iface[i].name);
		s->rx_pkts += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/rx_packets", name, iface[i].name);
		s->tx_err_pkts += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/tx_errors", name, iface[i].name);
		s->rx_err_pkts += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/rx_errors", name, iface[i].name);
		s->tx_dropped_pkts += read_int_from_file("/sys/class/ieee80211/%s/device/net/%s/statistics/tx_dropped", name, iface[i].name);
		s->rx_dropped_pkts += read_int_from_file("/sys/class/ieee80211/phy0/device/net/%s/statistics/rx_dropped", name, iface[i].name);
		s->rx_unknown_pkts += read_int_from_file("/sys/class/ieee80211/phy0/device/net/%s/statistics/rx_nohandler", name, iface[i].name);
	}

	if (num)
		nlwifi_get_band_noise(iface[0].name, band, &s->noise);

	return 0;
}

int radio_info_band(const char *name, enum wifi_band band, struct wifi_radio *radio)
{
	int ret;

	if (!default_wifi_driver.radio.info_band)
		return -ENOTSUP;

	ret = default_wifi_driver.radio.info_band(name, band, radio);

	/* FIXME: Before qca fix report sta/ap ml caps same */
	memcpy(&radio->sta_caps.ml, &radio->ap_caps.ml, sizeof(radio->sta_caps.ml));

	radio->max_iface_sta = 1;
	radio->max_iface_ap = 8;

	return ret;
}

static int qca_iface_block_sta(const char *ifname, enum wifi_band band, uint8_t *sta, int block)
{
	struct wifi_mlo_link link[4];
	int num = ARRAY_SIZE(link);
	int ret = 0;
	int i;

	if (band < BAND_2 || band > BAND_6)
		band = BAND_ANY;

	ret = nlwifi_get_mlo_links(ifname, band, link, &num);
	if (ret || num == 0)
		return default_wifi_driver.iface.block_sta(ifname, band, sta, block);

	for (i = 0; i < num; i++)
		ret |=  default_wifi_driver.iface.block_sta(ifname, link[i].band, sta, block);

	if (ret) {
		for (i--; i >= 0; i--)
			default_wifi_driver.iface.block_sta(ifname, link[i].band, sta, !block);
	}

	return ret;
}

const struct wifi_driver qca_driver = {
	.name = "wlan,phy",

	/* Radio/phy callbacks */
	.radio.info_band = radio_info_band,
	.radio.is_multiband = radio_is_multiband,
	.simulate_radar = radio_simulate_radar,
	.radio.get_band_stats = radio_get_band_stats,

	/* Interface/vif common callbacks */
	.iface.get_stats = qca_iface_get_stats,
	.get_mlo_links = qca_get_mlo_links,
	.iface.get_ctrl_interface = qca_get_ctrl_interface,
	.iface.get_ctrl_interface_band = qca_get_ctrl_interface_band,

	/* Interface/vif ap callbacks */
	.iface.block_sta = qca_iface_block_sta,

	/* Interface/vif sta callbacks */

	/* use fallback driver ops */
	.fallback = &default_wifi_driver,
};
