/*
 * Copyright (c) 2015 - 2016, The Linux Foundation. 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.
 */

#include <linux/ethtool.h>
#include <linux/netdevice.h>
#include <linux/string.h>
#include <linux/version.h>
#include "edma.h"

struct edma_ethtool_stats {
	uint8_t stat_string[ETH_GSTRING_LEN];
	uint32_t stat_offset;
};

#define EDMA_STAT(m)    offsetof(struct edma_ethtool_statistics, m)
#define DRVINFO_LEN	32

/* Array of strings describing statistics
 */
static const struct edma_ethtool_stats edma_gstrings_stats[] = {
	{"tx_q0_pkt", EDMA_STAT(tx_q0_pkt)},
	{"tx_q1_pkt", EDMA_STAT(tx_q1_pkt)},
	{"tx_q2_pkt", EDMA_STAT(tx_q2_pkt)},
	{"tx_q3_pkt", EDMA_STAT(tx_q3_pkt)},
	{"tx_q4_pkt", EDMA_STAT(tx_q4_pkt)},
	{"tx_q5_pkt", EDMA_STAT(tx_q5_pkt)},
	{"tx_q6_pkt", EDMA_STAT(tx_q6_pkt)},
	{"tx_q7_pkt", EDMA_STAT(tx_q7_pkt)},
	{"tx_q8_pkt", EDMA_STAT(tx_q8_pkt)},
	{"tx_q9_pkt", EDMA_STAT(tx_q9_pkt)},
	{"tx_q10_pkt", EDMA_STAT(tx_q10_pkt)},
	{"tx_q11_pkt", EDMA_STAT(tx_q11_pkt)},
	{"tx_q12_pkt", EDMA_STAT(tx_q12_pkt)},
	{"tx_q13_pkt", EDMA_STAT(tx_q13_pkt)},
	{"tx_q14_pkt", EDMA_STAT(tx_q14_pkt)},
	{"tx_q15_pkt", EDMA_STAT(tx_q15_pkt)},
	{"tx_q0_byte", EDMA_STAT(tx_q0_byte)},
	{"tx_q1_byte", EDMA_STAT(tx_q1_byte)},
	{"tx_q2_byte", EDMA_STAT(tx_q2_byte)},
	{"tx_q3_byte", EDMA_STAT(tx_q3_byte)},
	{"tx_q4_byte", EDMA_STAT(tx_q4_byte)},
	{"tx_q5_byte", EDMA_STAT(tx_q5_byte)},
	{"tx_q6_byte", EDMA_STAT(tx_q6_byte)},
	{"tx_q7_byte", EDMA_STAT(tx_q7_byte)},
	{"tx_q8_byte", EDMA_STAT(tx_q8_byte)},
	{"tx_q9_byte", EDMA_STAT(tx_q9_byte)},
	{"tx_q10_byte", EDMA_STAT(tx_q10_byte)},
	{"tx_q11_byte", EDMA_STAT(tx_q11_byte)},
	{"tx_q12_byte", EDMA_STAT(tx_q12_byte)},
	{"tx_q13_byte", EDMA_STAT(tx_q13_byte)},
	{"tx_q14_byte", EDMA_STAT(tx_q14_byte)},
	{"tx_q15_byte", EDMA_STAT(tx_q15_byte)},
	{"rx_q0_pkt", EDMA_STAT(rx_q0_pkt)},
	{"rx_q1_pkt", EDMA_STAT(rx_q1_pkt)},
	{"rx_q2_pkt", EDMA_STAT(rx_q2_pkt)},
	{"rx_q3_pkt", EDMA_STAT(rx_q3_pkt)},
	{"rx_q4_pkt", EDMA_STAT(rx_q4_pkt)},
	{"rx_q5_pkt", EDMA_STAT(rx_q5_pkt)},
	{"rx_q6_pkt", EDMA_STAT(rx_q6_pkt)},
	{"rx_q7_pkt", EDMA_STAT(rx_q7_pkt)},
	{"rx_q0_byte", EDMA_STAT(rx_q0_byte)},
	{"rx_q1_byte", EDMA_STAT(rx_q1_byte)},
	{"rx_q2_byte", EDMA_STAT(rx_q2_byte)},
	{"rx_q3_byte", EDMA_STAT(rx_q3_byte)},
	{"rx_q4_byte", EDMA_STAT(rx_q4_byte)},
	{"rx_q5_byte", EDMA_STAT(rx_q5_byte)},
	{"rx_q6_byte", EDMA_STAT(rx_q6_byte)},
	{"rx_q7_byte", EDMA_STAT(rx_q7_byte)},
	{"tx_desc_error", EDMA_STAT(tx_desc_error)},
	{"rx_alloc_fail_ctr", EDMA_STAT(rx_alloc_fail_ctr)},
};

#define EDMA_STATS_LEN ARRAY_SIZE(edma_gstrings_stats)

/* edma_get_strset_count()
 *	Get strset count
 */
static int edma_get_strset_count(struct net_device *netdev,
				 int sset)
{
	switch (sset) {
	case ETH_SS_STATS:
		return EDMA_STATS_LEN;
	default:
		netdev_dbg(netdev, "%s: Invalid string set", __func__);
		return -EOPNOTSUPP;
	}
}


/* edma_get_strings()
 *	get stats string
 */
static void edma_get_strings(struct net_device *netdev, uint32_t stringset,
			     uint8_t *data)
{
	uint8_t *p = data;
	uint32_t i;

	switch (stringset) {
	case ETH_SS_STATS:
		for (i = 0; i < EDMA_STATS_LEN; i++) {
			memcpy(p, edma_gstrings_stats[i].stat_string,
				min((size_t)ETH_GSTRING_LEN,
				    strlen(edma_gstrings_stats[i].stat_string)
				    + 1));
			p += ETH_GSTRING_LEN;
		}
		break;
	}
}

/* edma_get_ethtool_stats()
 *	Get ethtool statistics
 */
static void edma_get_ethtool_stats(struct net_device *netdev,
				   struct ethtool_stats *stats, uint64_t *data)
{
	struct edma_adapter *adapter = netdev_priv(netdev);
	struct edma_common_info *edma_cinfo = adapter->edma_cinfo;
	int i;
	uint8_t *p = NULL;

	edma_read_append_stats(edma_cinfo);

	for(i = 0; i < EDMA_STATS_LEN; i++) {
		p = (uint8_t *)&(edma_cinfo->edma_ethstats) +
			edma_gstrings_stats[i].stat_offset;
		data[i] = *(uint32_t *)p;
	}
}

/* edma_get_drvinfo()
 *	get edma driver info
 */
static void edma_get_drvinfo(struct net_device *dev,
			     struct ethtool_drvinfo *info)
{
	strlcpy(info->driver, "ess_edma", DRVINFO_LEN);
	strlcpy(info->bus_info, "axi", ETHTOOL_BUSINFO_LEN);
}

/* edma_nway_reset()
 *	Reset the phy, if available.
 */
static int edma_nway_reset(struct net_device *netdev)
{
	return -EINVAL;
}

/* edma_get_wol()
 *	get wake on lan info
 */
static void edma_get_wol(struct net_device *netdev,
			 struct ethtool_wolinfo *wol)
{
	wol->supported = 0;
	wol->wolopts = 0;
}

/* edma_get_msglevel()
 *	get message level.
 */
static uint32_t edma_get_msglevel(struct net_device *netdev)
{
	return 0;
}

/* edma_get_settings()
 *	Get edma settings
 */
static int edma_get_settings(struct net_device *netdev,
			     struct ethtool_link_ksettings *cmd)
{
	struct edma_adapter *adapter = netdev_priv(netdev);

	if (adapter->poll_required) {
		if ((adapter->forced_speed != SPEED_UNKNOWN)
			&& !(adapter->poll_required))
			return -EPERM;

		phy_ethtool_ksettings_get(adapter->phydev, cmd);
		if (linkmode_test_bit(ETHTOOL_LINK_MODE_FIBRE_BIT, adapter->phydev->advertising))
			cmd->base.port = PORT_FIBRE;
		else
			cmd->base.port = PORT_TP;
	} else {
		/* If the speed/duplex for this GMAC is forced and we
		 * are not polling for link state changes, return the
		 * values as specified by platform. This will be true
		 * for GMACs connected to switch, and interfaces that
		 * do not use a PHY.
		 */
		if (!(adapter->poll_required)) {
			if (adapter->forced_speed != SPEED_UNKNOWN) {
				/* set speed and duplex */
				cmd->base.speed = SPEED_1000;
				cmd->base.duplex = DUPLEX_FULL;

				/* Populate capabilities advertised by self */
				linkmode_zero(cmd->link_modes.advertising);
				cmd->base.autoneg = 0;
				cmd->base.port = PORT_TP;
				cmd->base.transceiver = XCVR_EXTERNAL;
			} else {
				/* non link polled and non
				 * forced speed/duplex interface
				 */
				return -EIO;
			}
		}
	}

	return 0;
}

/* edma_set_settings()
 *	Set EDMA settings
 */
static int edma_set_settings(struct net_device *netdev,
			    const struct ethtool_link_ksettings *cmd)
{
	struct edma_adapter *adapter = netdev_priv(netdev);

	if ((adapter->forced_speed != SPEED_UNKNOWN) &&
	     !adapter->poll_required)
		return -EPERM;

	return phy_ethtool_ksettings_set(adapter->phydev, cmd);
}

/* edma_get_coalesce
 *	get interrupt mitigation
 */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,15,0)
static int edma_get_coalesce(struct net_device *dev, struct ethtool_coalesce *ec,
			     struct kernel_ethtool_coalesce *kernel_coal,
			     struct netlink_ext_ack *extack)
#else
static int edma_get_coalesce(struct net_device *netdev,
			     struct ethtool_coalesce *ec)
#endif
{
	u32 reg_val;

	edma_get_tx_rx_coalesce(&reg_val);

	/* We read the Interrupt Moderation Timer(IMT) register value,
	 * use lower 16 bit for rx and higher 16 bit for Tx. We do a
	 * left shift by 1, because IMT resolution timer is 2usecs.
	 * Hence the value given by the register is multiplied by 2 to
	 * get the actual time in usecs.
	 */
	ec->tx_coalesce_usecs = (((reg_val >> 16) & 0xffff) << 1);
	ec->rx_coalesce_usecs = ((reg_val & 0xffff) << 1);

	return 0;
}

/* edma_set_coalesce
 *	set interrupt mitigation
 */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,15,0)
static int edma_set_coalesce(struct net_device *dev, struct ethtool_coalesce *ec,
			     struct kernel_ethtool_coalesce *kernel_coal,
			     struct netlink_ext_ack *extack)
#else
static int edma_set_coalesce(struct net_device *netdev,
			     struct ethtool_coalesce *ec)
#endif
{
	if (ec->tx_coalesce_usecs)
		edma_change_tx_coalesce(ec->tx_coalesce_usecs);
	if (ec->rx_coalesce_usecs)
		edma_change_rx_coalesce(ec->rx_coalesce_usecs);

	return 0;
}

/* edma_set_priv_flags()
 *	Set EDMA private flags
 */
static int edma_set_priv_flags(struct net_device *netdev, u32 flags)
{
	return 0;
}

/* edma_get_priv_flags()
 *	get edma driver flags
 */
static u32 edma_get_priv_flags(struct net_device *netdev)
{
	return 0;
}

/* edma_get_ringparam()
 *	get ring size
 */
static void edma_get_ringparam(struct net_device *netdev,
			       struct ethtool_ringparam *ring)
{
	struct edma_adapter *adapter = netdev_priv(netdev);
	struct edma_common_info *edma_cinfo = adapter->edma_cinfo;

	ring->tx_max_pending = edma_cinfo->tx_ring_count;
	ring->rx_max_pending = edma_cinfo->rx_ring_count;
}

/* Ethtool operations
 */
static const struct ethtool_ops edma_ethtool_ops = {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,7,0)
	.supported_coalesce_params = ETHTOOL_COALESCE_USECS,
#endif
	.get_drvinfo = &edma_get_drvinfo,
	.get_link = &ethtool_op_get_link,
	.get_msglevel = &edma_get_msglevel,
	.nway_reset = &edma_nway_reset,
	.get_wol = &edma_get_wol,
	.get_link_ksettings = &edma_get_settings,
	.set_link_ksettings = &edma_set_settings,
	.get_strings = &edma_get_strings,
	.get_sset_count = &edma_get_strset_count,
	.get_ethtool_stats = &edma_get_ethtool_stats,
	.get_coalesce = &edma_get_coalesce,
	.set_coalesce = &edma_set_coalesce,
	.get_priv_flags = edma_get_priv_flags,
	.set_priv_flags = edma_set_priv_flags,
	.get_ringparam = edma_get_ringparam,
};

/* edma_set_ethtool_ops
 *	Set ethtool operations
 */
void edma_set_ethtool_ops(struct net_device *netdev)
{
	netdev->ethtool_ops = &edma_ethtool_ops;
}
