/*
 * ecnt_prvt.c - Econet switch private utilities
 *
 * Copyright (C) 2022 iopsys Software Solutions AB. All rights reserved.
 *
 * Author: maxim.menshikov@iopsys.eu
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 */
#include <ctype.h>
#include <fcntl.h>
#include <linux/mii.h>
#include <net/if.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <easy/easy.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <linux/sockios.h>

#include "../ethernet.h"

#include "ecnt_prvt.h"

#ifdef STATIC_ANALYSIS
#include "stub_libapi_lib_switchmgr.h"
#else
#include "libapi_lib_switchmgr.h"
#endif

#include "libapi_lib_fe.h"
#define TC3162_MAX_LINE_LEN     (100)
#define TC3162_DUPLEX_MODE_LEN  (32)
#define TC3162_SPEED_MODE_LEN   (32)
#define TC3162_ETH_PORTMAP_PATH "/proc/tc3162/eth_portmap"
#define IFNAME_ETH0             "eth0."
#define IFNAME_NAS              "nas"
#define IFNAME_AE_WAN           "ae_wan"
#define IFNAME_PON              "pon"
#define DRIVER_NAME "hsgmii_lan"
#define DRIVER_NAME_LEN 20

struct hsgmii_lookup_table {
	const unsigned int idx;
	char *ifnames[IFNAMSIZ];
	const char iftype[IFNAMSIZ];
};

const struct hsgmii_lookup_table hsgmii_lookup_tbl[] = {
	{ 2, { "eth0.5", "eth1", NULL }, "usb" },
	{ 0, { "eth0.6", "eth2", NULL }, "pcie0" },
	{ 1, { "eth0.7", "eth3", NULL }, "pcie1" },
	{ 3, { "eth0.8", "eth4", NULL }, "eth" },
};


/* Not defined in Econet library */
ECNT_SWITCHMGR_RET switchmgr_lib_get_port_link_state(u8 port,
	ECNT_SWITCHMGR_LINK_STATE *p_link_state,
	ECNT_SWITCHMGR_LINK_SPEED *p_speed);

static int get_drv_info_by_ifname(char *ifname, char *buffer)
{
	int                     fd;
	int                     ret = -1;
	struct ifreq            ifr;
	struct ethtool_drvinfo  info;

	if (ifname == NULL || buffer == NULL)
		return ret;

	fd = socket(AF_INET, SOCK_DGRAM, 0);
	if (fd < 0)
		return ret;

	memset(&info, 0, sizeof(info));
	info.cmd = ETHTOOL_GDRVINFO;

	memset(&ifr, 0, sizeof(ifr));
	strncpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name) - 1);
	ifr.ifr_data = (void *)&info;

	if (ioctl(fd, SIOCETHTOOL, &ifr) != 0)
		goto exit;

	strncpy(buffer, info.driver, DRIVER_NAME_LEN - 1);
	ret = 0;

exit:
	close(fd);
	return ret;
}

static int get_pause_stats_from_proc(const char *ifprefix, const char *iftype,
				     uint64_t *rx_pause_packets, uint64_t *tx_pause_packets)
{
	uint64_t rx_pause_on, rx_pause_off, tx_pause_on, tx_pause_off;
	char cmdbuf[512] = {0};
	int ret;
	chrCmd(cmdbuf, sizeof(cmdbuf),
		"cat /proc/tc3162/%s%s_mac_dbg | grep PAUSE",
		ifprefix, iftype);

	if (cmdbuf[0] == '\0')
		return -1;

	ret = sscanf(cmdbuf, /* Flawfinder: ignore */
		"TXMBI_PAUSEON_CNT\t\t= %" SCNx64 ", TXMBI_PAUSEOFF_CNT\t\t= %" SCNx64
		" RX_PAUSEON_CNT\t\t\t= %" SCNx64 ", RX_PAUSEOFF_CNT\t\t\t= %" SCNx64,
		&tx_pause_on, &tx_pause_off, &rx_pause_on, &rx_pause_off);

	if (ret < 4)
		return -1;

	*tx_pause_packets = tx_pause_on + tx_pause_off;
	*rx_pause_packets = rx_pause_on + rx_pause_off;

	return 0;
}

static void fill_stats_tx_from_gdma(struct eth_stats *stats, struct eth_rmon_stats *rstats,
				    ECNT_FEMGR_GDMA2_TX_STATISTICS *tx_stats)
{
	if (tx_stats == NULL)
		return;

	if (stats != NULL) {
		stats->tx_bytes = tx_stats->frame_len;
		stats->tx_packets = tx_stats->frame_cnt;
		stats->tx_errors = 0;
		stats->tx_ucast_packets = tx_stats->frame_cnt - (tx_stats->broadcast + tx_stats->multicast);
		stats->tx_bcast_packets = tx_stats->broadcast;
		stats->tx_mcast_packets = tx_stats->multicast;
		stats->tx_discard_packets = tx_stats->drop_cnt;
	}

	if (rstats != NULL) {
		rstats->tx.packets = tx_stats->frame_cnt;
		rstats->tx.bytes = tx_stats->frame_len;
		rstats->tx.bcast_packets = tx_stats->broadcast;
		rstats->tx.mcast_packets = tx_stats->multicast;
		rstats->tx.crc_err_packets = 0;
		rstats->tx.under_sz_packets = 0;
		rstats->tx.over_sz_packets = 0;
		rstats->tx.packets_64bytes = tx_stats->eq_64;
		rstats->tx.packets_65to127bytes = tx_stats->from_65_to_127;
		rstats->tx.packets_256to511bytes = tx_stats->from_256_to_511;
		rstats->tx.packets_512to1023bytes = tx_stats->from_512_to_1023;
		rstats->tx.packets_1024to1518bytes = tx_stats->from_1024_to_1518;
	}
}

static void fill_stats_rx_from_gdma(struct eth_stats *stats, struct eth_rmon_stats *rstats,
				    ECNT_FEMGR_GDMA2_RX_STATISTICS *rx_stats, bool oversize_is_ok)
{
	if (rx_stats == NULL)
		return;

	if (stats != NULL) {
		stats->rx_bytes = rx_stats->frame_len;
		stats->rx_packets = rx_stats->frame_cnt;
		stats->rx_errors = rx_stats->crc + rx_stats->jabber + rx_stats->fragment + rx_stats->undersize + (oversize_is_ok == false ? rx_stats->oversize : 0);
		stats->rx_ucast_packets = rx_stats->frame_cnt - (rx_stats->broadcast + rx_stats->multicast);
		stats->rx_bcast_packets = rx_stats->broadcast;
		stats->rx_mcast_packets = rx_stats->multicast;
		stats->rx_discard_packets = rx_stats->drop_cnt;
		stats->rx_unknown_packets = 0;
	}

	if (rstats != NULL) {
		rstats->rx.packets = rx_stats->frame_cnt;
		rstats->rx.bytes = rx_stats->frame_len;
		rstats->rx.bcast_packets = rx_stats->broadcast;
		rstats->rx.mcast_packets = rx_stats->multicast;
		rstats->rx.crc_err_packets = rx_stats->crc;
		rstats->rx.under_sz_packets = rx_stats->undersize;
		rstats->rx.over_sz_packets = rx_stats->oversize;
		rstats->rx.packets_64bytes = rx_stats->eq_64;
		rstats->rx.packets_65to127bytes = rx_stats->from_65_to_127;
		rstats->rx.packets_256to511bytes = rx_stats->from_256_to_511;
		rstats->rx.packets_512to1023bytes = rx_stats->from_512_to_1023;
		rstats->rx.packets_1024to1518bytes = rx_stats->from_1024_to_1518;
	}
}

int hsgmii_lan_prvt_get_port_statistics(char *ifname, struct eth_stats *stats, struct eth_rmon_stats *rstats) {
	ECNT_FEMGR_GDMA2_TX_STATISTICS tx_stats;
	ECNT_FEMGR_GDMA2_RX_STATISTICS rx_stats;

	char driver_name[DRIVER_NAME_LEN] = {0};
	if (get_drv_info_by_ifname(ifname, driver_name))
		return -1;

	if (!strncmp(driver_name, DRIVER_NAME, DRIVER_NAME_LEN)) {
		int i = 0, hsgmii_index = -1;
		const char *hsgmii_iftype;
		char cmdbuf[512] = {0};

		for (; i < ARRAY_SIZE(hsgmii_lookup_tbl); i++) {
			int j = 0;

			while (hsgmii_lookup_tbl[i].ifnames[j] != NULL) {
				if (!strcmp(ifname, hsgmii_lookup_tbl[i].ifnames[j])) {
					hsgmii_index = hsgmii_lookup_tbl[i].idx;
					hsgmii_iftype = hsgmii_lookup_tbl[i].iftype;
					break;
				}
				j++;
			}

			if (hsgmii_index != -1)
				break;
		}

		if (hsgmii_index == -1)
			return -1;

		memset(&tx_stats, 0, sizeof(ECNT_FEMGR_GDMA2_TX_STATISTICS));
		memset(&rx_stats, 0, sizeof(ECNT_FEMGR_GDMA2_RX_STATISTICS));

		/* Handle unbound interface statistics */
		chrCmd(cmdbuf, sizeof(cmdbuf), "cat /proc/tc3162/%s_rebind | awk '{ print $3 }'", ifname);
		if (cmdbuf[0] != '\0' && strtoul(cmdbuf, NULL, 0) == 1)
			return 0;

		if (!fe_lib_get_hsgmii_tx_statistics(&tx_stats, hsgmii_index)) {
			fill_stats_tx_from_gdma(stats, rstats, &tx_stats);
		}
		if (!fe_lib_get_hsgmii_rx_statistics(&rx_stats, hsgmii_index)) {
			fill_stats_rx_from_gdma(stats, rstats, &rx_stats, false);
		}

		if (rstats != NULL) {
			get_pause_stats_from_proc("hsgmii_", hsgmii_iftype, &rstats->rx.pause_packets,
						  &rstats->tx.pause_packets);
		}

		return 0;
	}
	return -1;
}

int ae_wan_prvt_get_port_statistics(struct eth_stats *stats, struct eth_rmon_stats *rstats) {
	char cmdbuf[512] = {0};
	ECNT_FEMGR_GDMA2_TX_STATISTICS tx_stats;
	ECNT_FEMGR_GDMA2_RX_STATISTICS rx_stats;

	memset(&tx_stats, 0, sizeof(ECNT_FEMGR_GDMA2_TX_STATISTICS));
	memset(&rx_stats, 0, sizeof(ECNT_FEMGR_GDMA2_RX_STATISTICS));

	chrCmd(cmdbuf, sizeof(cmdbuf), "cat /proc/tc3162/ae_wan_switch_hsgmii_lan");
	if (cmdbuf[0] == '\0')  {
		return -1;
	} else if (strcmp(cmdbuf, "pon") != 0) {
		int i = 0, hsgmii_index = -1;
		for (; i < ARRAY_SIZE(hsgmii_lookup_tbl); i++) {
			if (!strcmp(cmdbuf, hsgmii_lookup_tbl[i].iftype)) {
				hsgmii_index = hsgmii_lookup_tbl[i].idx;
				break;
			}
		}

		if (hsgmii_index == -1)
			return -1;

		if (!fe_lib_get_hsgmii_tx_statistics(&tx_stats, hsgmii_index)) {
			fill_stats_tx_from_gdma(stats, rstats, &tx_stats);
		}
		if (!fe_lib_get_hsgmii_rx_statistics(&rx_stats, hsgmii_index)) {
			fill_stats_rx_from_gdma(stats, rstats, &rx_stats, false);
		}

		if (rstats != NULL) {
			get_pause_stats_from_proc("hsgmii_", cmdbuf, &rstats->rx.pause_packets,
						  &rstats->tx.pause_packets);
		}
	} else {
		if (!fe_lib_get_gdma2_tx_statistics(&tx_stats)) {
			fill_stats_tx_from_gdma(stats, rstats, &tx_stats);
		}

		if (!fe_lib_get_gdma2_rx_statistics(&rx_stats)) {
			fill_stats_rx_from_gdma(stats, rstats, &rx_stats, false);
		}

		if (rstats != NULL) {
			get_pause_stats_from_proc("", "ae_wan", &rstats->rx.pause_packets,
						  &rstats->tx.pause_packets);
		}
	}

	return 0;
}

int pon_prvt_get_port_statistics(struct eth_stats *stats, struct eth_rmon_stats *rstats) {
	ECNT_FEMGR_GDMA2_TX_STATISTICS tx_stats;
	ECNT_FEMGR_GDMA2_RX_STATISTICS rx_stats;
	char driver_name[DRIVER_NAME_LEN] = {0};

	if (get_drv_info_by_ifname(IFNAME_PON, driver_name))
		return -1;

	memset(&tx_stats, 0, sizeof(ECNT_FEMGR_GDMA2_TX_STATISTICS));
	memset(&rx_stats, 0, sizeof(ECNT_FEMGR_GDMA2_RX_STATISTICS));

	if (!fe_lib_get_gdma2_tx_statistics(&tx_stats)) {
		fill_stats_tx_from_gdma(stats, rstats, &tx_stats);
	}

	if (!fe_lib_get_gdma2_rx_statistics(&rx_stats)) {
		fill_stats_rx_from_gdma(stats, rstats, &rx_stats, true);
	}

	return 0;
}

int ecnt_prvt_get_port_statistics(uint32_t port,
								  struct eth_stats *stats,
								  struct eth_rmon_stats *rstats)
{
	int ret;
	ECNT_SWITCHMGR_PORT_STATISTICS portcnt;

	if (port == ECNT_PRVT_PORT_NUM_INVALID ||
		(stats == NULL && rstats == NULL)) {
		return -1;
	}

	if (port >= 256) {
		return -1;
	}

	memset(&portcnt, 0, sizeof(portcnt));

	ret = switchmgr_lib_get_port_statistics((uint8_t)port, &portcnt);
	if (ret != ECNT_SWITCHMGR_SUCCESS) {
		return -1;
	}

	if (stats != NULL) {
		stats->tx_bytes = (((uint64_t)portcnt.TxBytesCnt_Hi) << 32) + portcnt.TxBytesCnt_Lo;
		stats->rx_bytes = (((uint64_t)portcnt.RxBytesCnt_Hi) << 32) + portcnt.RxBytesCnt_Lo;
		stats->tx_packets = portcnt.TxPktsCnt;
		stats->rx_packets = portcnt.RxPktsCnt;
		stats->tx_errors = 0;
		stats->rx_errors = portcnt.RxAlignmentErrorCnt + portcnt.RxCRCFramesCnt + portcnt.RxUnderSizePktsCnt + portcnt.RxFragmentErrorCnt + portcnt.RxOverSizePktsCnt;
		stats->tx_ucast_packets = portcnt.TxUniPktsCnt;
		stats->rx_ucast_packets = portcnt.RxUniPktsCnt;
		stats->tx_mcast_packets = portcnt.TxMultiPktsCnt;
		stats->rx_mcast_packets = portcnt.RxMultiPktsCnt;
		stats->tx_bcast_packets = portcnt.TxBroadPktsCnt;
		stats->rx_bcast_packets = portcnt.RxBroadPktsCnt;
		stats->tx_discard_packets = portcnt.TxDropFramesCnt;
		stats->rx_discard_packets = portcnt.RxDropFramesCnt;
		stats->rx_unknown_packets = 0;
	}

	if (rstats != NULL) {
#define FILL_RSTATS_FOR_DIRECTION(__rmon_field, __ecnt_prefix) \
		do { \
			rstats->__rmon_field.drop_events = \
				portcnt.__ecnt_prefix ## DropFramesCnt; \
			rstats->__rmon_field.bytes = \
				(((uint64_t)portcnt.__ecnt_prefix ## BytesCnt_Hi) << 32) \
				+ portcnt.__ecnt_prefix ## BytesCnt_Lo; \
			rstats->__rmon_field.packets = portcnt.__ecnt_prefix ## PktsCnt;\
			rstats->__rmon_field.bcast_packets = \
				portcnt.__ecnt_prefix ## BroadPktsCnt; \
			rstats->__rmon_field.mcast_packets = \
				portcnt.__ecnt_prefix ## MultiPktsCnt; \
			rstats->__rmon_field.crc_err_packets = \
				portcnt.__ecnt_prefix ## CRCFramesCnt; \
			rstats->__rmon_field.under_sz_packets = \
				portcnt.__ecnt_prefix ## UnderSizePktsCnt; \
			rstats->__rmon_field.over_sz_packets = \
				portcnt.__ecnt_prefix ## OverSizePktsCnt; \
			rstats->__rmon_field.pause_packets = \
				portcnt.__ecnt_prefix ## PauseFramesCnt; \
			rstats->__rmon_field.packets_64bytes = \
				portcnt.__ecnt_prefix ## 64BytePktsCnt; \
			rstats->__rmon_field.packets_65to127bytes = \
				portcnt.__ecnt_prefix ## 65_127BytePktsCnt; \
			rstats->__rmon_field.packets_256to511bytes = \
				portcnt.__ecnt_prefix ## 256_511BytePktsCnt; \
			rstats->__rmon_field.packets_512to1023bytes = \
				portcnt.__ecnt_prefix ## 512_1023BytePktsCnt; \
			rstats->__rmon_field.packets_1024to1518bytes = \
				portcnt.__ecnt_prefix ## 1024_1518BytePktsCnt; \
		} while (0)

		FILL_RSTATS_FOR_DIRECTION(tx, Tx);
		FILL_RSTATS_FOR_DIRECTION(rx, Rx);

#undef FILL_RSTATS_FOR_DIRECTION
	}

	return 0;
}

/* Read port number from proc interface */
static uint32_t read_port_from_proc(bool wan, uint32_t eth_port_num)
{
	FILE    *f;
	int      tmp;
	int      ret;
	char     buf[TC3162_MAX_LINE_LEN];
	uint32_t new_port_num = ECNT_PRVT_PORT_NUM_INVALID;

	f = fopen(TC3162_ETH_PORTMAP_PATH, "r");
	if (f == NULL) {
		return ECNT_PRVT_PORT_NUM_INVALID;
	}

	if (wan) {
		/* WAN is the very first in the proc dump */
		ret = fscanf(f, "%d", &tmp);
		if (ret != 1) {
			goto out;
		}

		new_port_num = (uint32_t)tmp;
	} else {
		char   *retstr;
		bool    inside_map = false;

		/* First we search for the beginning of switch port map */
		while (true) {
			retstr = fgets(buf, sizeof(buf), f);
			if (retstr == NULL) {
				break;
			}

			inside_map = strstr(buf, "switch_port_map") != NULL;
			if (inside_map) {
				break;
			}
		}

		if (!inside_map) {
			goto out;
		}

		/*
		 * Then we sequentially read all ports until the needed port line is
		 * found
		 * Line format: <virtual port> <switch port>
		 */
		while (true) {
			int port;
			int swport;

			if (fscanf(f, "%d %d", &port, &swport) != 2) {
				break;
			}

			if (port == eth_port_num) {
				new_port_num = (uint32_t)swport;
				goto out;
			}
		}
	}

out:
	fclose(f);

	return new_port_num;
}

uint32_t ecnt_prvt_get_port_num(const char *ifname)
{
	bool     is_wan;
	uint32_t eth_port_num = 0;
	/*
	 * This algorithm is not accurate if numbers differ for target hardware.
	 * There must be a generic way to get port IDs reliably based on interface
	 * names.
	 */
	if (strncmp(ifname, IFNAME_ETH0, strlen(IFNAME_ETH0)) == 0) {
		char *end;

		is_wan = false;
		eth_port_num = (uint32_t)strtol(&ifname[strlen(IFNAME_ETH0)], &end, 10);

		if (end == NULL || *end != '\0') {
			return ECNT_PRVT_PORT_NUM_INVALID;
		}
	} else if (strncmp(ifname, IFNAME_NAS, strlen(IFNAME_NAS)) == 0 ||
			   strncmp(ifname, IFNAME_AE_WAN, strlen(IFNAME_AE_WAN)) == 0 ||
			   strncmp(ifname, IFNAME_PON, strlen(IFNAME_PON)) == 0) {
		is_wan = true;
	} else {
		return ECNT_PRVT_PORT_NUM_INVALID;
	}

	return read_port_from_proc(is_wan, eth_port_num);
}


enum eth_speed
ecnt_prvt_link_speed2lib(int speed)
{
	switch (speed)
	{
#define MAP_ECNT_LINK_SPEED(__val, __speed, __duplex) \
		case (__val): \
		{ \
			return (__speed); \
		}
#include "map_link_speed.def"
#undef MAP_ECNT_LINK_SPEED

		default:
			return ETH_10_Half;
	}
}

uint32_t
ecnt_prvt_bitrate2capability(char *speed_str, char *duplex_mode_str)
{
	int   speed;
	bool  full_duplex;
	char *end;

	full_duplex = strcmp(duplex_mode_str, "Full") == 0;

	if (strcmp(speed_str, "Auto") == 0) {
		goto err;
	}

	speed = strtol(speed_str, &end, 10);
	if (end == NULL || *end != '\0') {
		goto err;
	}

	/* Convert bitrate and duplex mode to actual capability */
#define MAP_ECNT_BITRATE_RANGE_BOUNDARY(__bound, __full_cap, __half_cap) \
	do { \
		if (speed >= (__bound)) { \
			if (full_duplex) { \
				return __full_cap; \
			} else { \
				return __half_cap; \
			} \
		} \
	} while (0);
#include "map_bitrate.def"
#undef MAP_ECNT_BITRATE_RANGE_BOUNDARY

err:
	/* Fail-safe defaults */
	return full_duplex ? ETH_100_Full : ETH_100_Half;
}

int ecnt_prvt_get_link_settings(uint32_t port_num, struct eth_link *link)
{
	uint8_t  up;
	uint8_t  autoneg;
	char     duplex_mode[TC3162_DUPLEX_MODE_LEN];
	char     speed_str[TC3162_SPEED_MODE_LEN];
	ECNT_SWITCHMGR_LINK_STATE link_state;
	ECNT_SWITCHMGR_LINK_SPEED link_speed;

	if (port_num >= 256) {
		return -1;
	}

	if (switchmgr_lib_get_port_admin((uint8_t)port_num, &up) !=
			ECNT_SWITCHMGR_SUCCESS ||
		switchmgr_lib_get_port_autoneg_enable((uint8_t)port_num, &autoneg) !=
			ECNT_SWITCHMGR_SUCCESS ||
		switchmgr_lib_get_port_link_state((uint8_t)port_num, &link_state, &link_speed) !=
			ECNT_SWITCHMGR_SUCCESS ||
		switchmgr_lib_get_port_duplex((uint8_t)port_num, duplex_mode) !=
			ECNT_SWITCHMGR_SUCCESS ||
		switchmgr_lib_get_port_max_bitrate((uint8_t)port_num, speed_str) !=
			ECNT_SWITCHMGR_SUCCESS) {
		return -1;
	}

	link->portid = (int)port_num;
	link->capability = ecnt_prvt_bitrate2capability(speed_str, duplex_mode);
	link->autoneg = autoneg;
	link->speed = ecnt_prvt_link_speed2lib(link_speed);
	link->fullduplex = (strcmp(duplex_mode, "Full") == 0) ? true : false;
	link->down = (link_state == ECNT_SWITCHMGR_LINK_UP) ? false : true;
	link->prio_tagged = false;

	return 0;
}

int ecnt_prvt_set_port_state(uint32_t port_num, bool state)
{
	int ret;

	if (port_num >= 256) {
		return -1;
	}

	ret = switchmgr_lib_set_port_admin((uint8_t)port_num, state ? 1 : 0);

	return (ret == ECNT_SWITCHMGR_SUCCESS) ? 0 : (-1);
}