// SPDX-License-Identifier: GPL-2.0
/* Copyright (C) 2022 Gerhard Engleder <gerhard@engleder-embedded.com> */

#include "tsnep.h"

#define ETHER_TYPE_FULL_MASK ((__force __be16)~0)

static void tsnep_enable_rule(struct tsnep_adapter *adapter,
			      struct tsnep_rxnfc_rule *rule)
{
	u8 rx_assign;
	void __iomem *addr;

	rx_assign = TSNEP_RX_ASSIGN_ACTIVE;
	rx_assign |= (rule->queue_index << TSNEP_RX_ASSIGN_QUEUE_SHIFT) &
		     TSNEP_RX_ASSIGN_QUEUE_MASK;

	addr = adapter->addr + TSNEP_RX_ASSIGN_ETHER_TYPE +
	       TSNEP_RX_ASSIGN_ETHER_TYPE_OFFSET * rule->location;
	iowrite16(rule->filter.ether_type, addr);

	/* enable rule after all settings are done */
	addr = adapter->addr + TSNEP_RX_ASSIGN +
	       TSNEP_RX_ASSIGN_OFFSET * rule->location;
	iowrite8(rx_assign, addr);
}

static void tsnep_disable_rule(struct tsnep_adapter *adapter,
			       struct tsnep_rxnfc_rule *rule)
{
	void __iomem *addr;

	addr = adapter->addr + TSNEP_RX_ASSIGN +
	       TSNEP_RX_ASSIGN_OFFSET * rule->location;
	iowrite8(0, addr);
}

static struct tsnep_rxnfc_rule *tsnep_get_rule(struct tsnep_adapter *adapter,
					       int location)
{
	struct tsnep_rxnfc_rule *rule;

	list_for_each_entry(rule, &adapter->rxnfc_rules, list) {
		if (rule->location == location)
			return rule;
		if (rule->location > location)
			break;
	}

	return NULL;
}

static void tsnep_add_rule(struct tsnep_adapter *adapter,
			   struct tsnep_rxnfc_rule *rule)
{
	struct tsnep_rxnfc_rule *pred, *cur;

	tsnep_enable_rule(adapter, rule);

	pred = NULL;
	list_for_each_entry(cur, &adapter->rxnfc_rules, list) {
		if (cur->location >= rule->location)
			break;
		pred = cur;
	}

	list_add(&rule->list, pred ? &pred->list : &adapter->rxnfc_rules);
	adapter->rxnfc_count++;
}

static void tsnep_delete_rule(struct tsnep_adapter *adapter,
			      struct tsnep_rxnfc_rule *rule)
{
	tsnep_disable_rule(adapter, rule);

	list_del(&rule->list);
	adapter->rxnfc_count--;

	kfree(rule);
}

static void tsnep_flush_rules(struct tsnep_adapter *adapter)
{
	struct tsnep_rxnfc_rule *rule, *tmp;

	mutex_lock(&adapter->rxnfc_lock);

	list_for_each_entry_safe(rule, tmp, &adapter->rxnfc_rules, list)
		tsnep_delete_rule(adapter, rule);

	mutex_unlock(&adapter->rxnfc_lock);
}

int tsnep_rxnfc_get_rule(struct tsnep_adapter *adapter,
			 struct ethtool_rxnfc *cmd)
{
	struct ethtool_rx_flow_spec *fsp = &cmd->fs;
	struct tsnep_rxnfc_rule *rule = NULL;

	cmd->data = adapter->rxnfc_max;

	mutex_lock(&adapter->rxnfc_lock);

	rule = tsnep_get_rule(adapter, fsp->location);
	if (!rule) {
		mutex_unlock(&adapter->rxnfc_lock);

		return -ENOENT;
	}

	fsp->flow_type = ETHER_FLOW;
	fsp->ring_cookie = rule->queue_index;

	if (rule->filter.type == TSNEP_RXNFC_ETHER_TYPE) {
		fsp->h_u.ether_spec.h_proto = htons(rule->filter.ether_type);
		fsp->m_u.ether_spec.h_proto = ETHER_TYPE_FULL_MASK;
	}

	mutex_unlock(&adapter->rxnfc_lock);

	return 0;
}

int tsnep_rxnfc_get_all(struct tsnep_adapter *adapter,
			struct ethtool_rxnfc *cmd,
			u32 *rule_locs)
{
	struct tsnep_rxnfc_rule *rule;
	int count = 0;

	cmd->data = adapter->rxnfc_max;

	mutex_lock(&adapter->rxnfc_lock);

	list_for_each_entry(rule, &adapter->rxnfc_rules, list) {
		if (count == cmd->rule_cnt) {
			mutex_unlock(&adapter->rxnfc_lock);

			return -EMSGSIZE;
		}

		rule_locs[count] = rule->location;
		count++;
	}

	mutex_unlock(&adapter->rxnfc_lock);

	cmd->rule_cnt = count;

	return 0;
}

static int tsnep_rxnfc_find_location(struct tsnep_adapter *adapter)
{
	struct tsnep_rxnfc_rule *tmp;
	int location = 0;

	list_for_each_entry(tmp, &adapter->rxnfc_rules, list) {
		if (tmp->location == location)
			location++;
		else
			return location;
	}

	if (location >= adapter->rxnfc_max)
		return -ENOSPC;

	return location;
}

static void tsnep_rxnfc_init_rule(struct tsnep_rxnfc_rule *rule,
				  const struct ethtool_rx_flow_spec *fsp)
{
	INIT_LIST_HEAD(&rule->list);

	rule->queue_index = fsp->ring_cookie;
	rule->location = fsp->location;

	rule->filter.type = TSNEP_RXNFC_ETHER_TYPE;
	rule->filter.ether_type = ntohs(fsp->h_u.ether_spec.h_proto);
}

static int tsnep_rxnfc_check_rule(struct tsnep_adapter *adapter,
				  struct tsnep_rxnfc_rule *rule)
{
	struct net_device *dev = adapter->netdev;
	struct tsnep_rxnfc_rule *tmp;

	list_for_each_entry(tmp, &adapter->rxnfc_rules, list) {
		if (!memcmp(&rule->filter, &tmp->filter, sizeof(rule->filter)) &&
		    tmp->location != rule->location) {
			netdev_dbg(dev, "rule already exists\n");

			return -EEXIST;
		}
	}

	return 0;
}

int tsnep_rxnfc_add_rule(struct tsnep_adapter *adapter,
			 struct ethtool_rxnfc *cmd)
{
	struct net_device *netdev = adapter->netdev;
	struct ethtool_rx_flow_spec *fsp =
		(struct ethtool_rx_flow_spec *)&cmd->fs;
	struct tsnep_rxnfc_rule *rule, *old_rule;
	int retval;

	/* only EtherType is supported */
	if (fsp->flow_type != ETHER_FLOW ||
	    !is_zero_ether_addr(fsp->m_u.ether_spec.h_dest) ||
	    !is_zero_ether_addr(fsp->m_u.ether_spec.h_source) ||
	    fsp->m_u.ether_spec.h_proto != ETHER_TYPE_FULL_MASK) {
		netdev_dbg(netdev, "only ethernet protocol is supported\n");

		return -EOPNOTSUPP;
	}

	if (fsp->ring_cookie >
	    (TSNEP_RX_ASSIGN_QUEUE_MASK >> TSNEP_RX_ASSIGN_QUEUE_SHIFT)) {
		netdev_dbg(netdev, "invalid action\n");

		return -EINVAL;
	}

	if (fsp->location != RX_CLS_LOC_ANY &&
	    fsp->location >= adapter->rxnfc_max) {
		netdev_dbg(netdev, "invalid location\n");

		return -EINVAL;
	}

	rule = kzalloc(sizeof(*rule), GFP_KERNEL);
	if (!rule)
		return -ENOMEM;

	mutex_lock(&adapter->rxnfc_lock);

	if (fsp->location == RX_CLS_LOC_ANY) {
		retval = tsnep_rxnfc_find_location(adapter);
		if (retval < 0)
			goto failed;
		fsp->location = retval;
	}

	tsnep_rxnfc_init_rule(rule, fsp);

	retval = tsnep_rxnfc_check_rule(adapter, rule);
	if (retval)
		goto failed;

	old_rule = tsnep_get_rule(adapter, fsp->location);
	if (old_rule)
		tsnep_delete_rule(adapter, old_rule);

	tsnep_add_rule(adapter, rule);

	mutex_unlock(&adapter->rxnfc_lock);

	return 0;

failed:
	mutex_unlock(&adapter->rxnfc_lock);
	kfree(rule);
	return retval;
}

int tsnep_rxnfc_del_rule(struct tsnep_adapter *adapter,
			 struct ethtool_rxnfc *cmd)
{
	struct ethtool_rx_flow_spec *fsp =
		(struct ethtool_rx_flow_spec *)&cmd->fs;
	struct tsnep_rxnfc_rule *rule;

	mutex_lock(&adapter->rxnfc_lock);

	rule = tsnep_get_rule(adapter, fsp->location);
	if (!rule) {
		mutex_unlock(&adapter->rxnfc_lock);

		return -ENOENT;
	}

	tsnep_delete_rule(adapter, rule);

	mutex_unlock(&adapter->rxnfc_lock);

	return 0;
}

int tsnep_rxnfc_init(struct tsnep_adapter *adapter)
{
	int i;

	/* disable all rules */
	for (i = 0; i < adapter->rxnfc_max;
	     i += sizeof(u32) / TSNEP_RX_ASSIGN_OFFSET)
		iowrite32(0, adapter->addr + TSNEP_RX_ASSIGN + i);

	return 0;
}

void tsnep_rxnfc_cleanup(struct tsnep_adapter *adapter)
{
	tsnep_flush_rules(adapter);
}