/*
   forward-ethernet-fd2110.c - Ethernet driver for FD2110 board

   Copyright (C) 2017 - 2024 SoftLab-NSK <forward@softlab.tv>

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; either version 2
   of the License, or (at your option) any later version.

   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 Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

#include "forward-ethernet.h"
#include "forward-sdma.h"
#include "forward-pll.h"
#include "fd2110_reg.h"
#include "forward-fd2110.h"

#include <linux/etherdevice.h>
#include <linux/ethtool.h>
#include <linux/delay.h>
#include <net/route.h>
#include <net/ip6_route.h>
#include <net/ip.h>
#include <linux/inetdevice.h>

#define FD2110_MIXER_ARP_NUM_RETRIES 5
#define FD2110_MIXER_ARP_POLL_PERIOD 1

static bool fd2110_mixer_get_dst(void *private, bool ipv6, const u8 *dst_ip, u8 *dst_mac,
				 u8 *dst_ttl)
{
	struct forward_ethernet *eth = (struct forward_ethernet *)private;
	struct rtable *rt = NULL;
	struct neighbour *nbr = NULL;
	const void *daddr;
	struct dst_entry *dst = NULL;
	bool result = false;
	int cnt;

	if (ipv6) {
		struct flowi6 fl6;

		daddr = dst_ip;

		if (ipv6_addr_is_multicast((struct in6_addr *)daddr)) {
			ipv6_eth_mc_map((struct in6_addr *)daddr, dst_mac);
			*dst_ttl = READ_ONCE(dev_net(eth->net)->ipv4.sysctl_ip_default_ttl);
			result = true;
			goto end;
		}

		memset(&fl6, 0, sizeof(fl6));
		memcpy(&fl6.daddr, daddr, sizeof(fl6.daddr));

		dst = ip6_route_output(dev_net(eth->net), NULL, &fl6);
		if (IS_ERR(dst))
			goto end;
	} else {
		daddr = &dst_ip[12];

		if (ipv4_is_multicast(*((u32 *)daddr))) {
			ip_eth_mc_map(*((u32 *)daddr), dst_mac);
			*dst_ttl = READ_ONCE(dev_net(eth->net)->ipv4.sysctl_ip_default_ttl);
			result = true;
			goto end;
		}

#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 10, 0)) || \
	(RHEL_RELEASE_CODE >= RHEL_RELEASE_VERSION(9, 6))
		rt = ip_route_output(dev_net(eth->net), *((u32 *)daddr), 0, 0, 0,
				     RT_SCOPE_UNIVERSE);
#else
		rt = ip_route_output(dev_net(eth->net), *((u32 *)daddr), 0, 0, 0);
#endif
		if (IS_ERR(dst))
			goto end;
		dst = &rt->dst;
	}

	*dst_ttl = dst_metric_raw(dst, RTAX_HOPLIMIT);
	if (!*dst_ttl)
		*dst_ttl = READ_ONCE(dev_net(eth->net)->ipv4.sysctl_ip_default_ttl);

	nbr = dst_neigh_lookup(dst, daddr);
	if (IS_ERR(nbr))
		goto end;

	for (cnt = FD2110_MIXER_ARP_NUM_RETRIES * 100; !(nbr->nud_state & NUD_VALID) && (cnt > 0);
	     cnt--) {
		if (cnt % 100 == 99)
			neigh_event_send(nbr, NULL);
		msleep(FD2110_MIXER_ARP_POLL_PERIOD);
	}

	if (!(nbr->nud_state & NUD_VALID))
		goto end;

	neigh_ha_snapshot(dst_mac, nbr, eth->net);

	result = true;

end:
	if (nbr)
		neigh_release(nbr);
	if (rt) {
		ip_rt_put(rt);
		dst = NULL;
	}
	if (dst)
		dst_release(dst);

	return result;
}

static bool fd2110_mixer_get_src_ip6(struct forward_ethernet *eth, u8 *src_ip)
{
	struct inet6_dev *in6_dev;
	struct inet6_ifaddr *ifa;
	bool result = false;

	rcu_read_lock();
	in6_dev = __in6_dev_get(eth->net);

	if (!in6_dev) {
		rcu_read_unlock();
		return result;
	}

	memset(src_ip, 0, 16);
	read_lock_bh(&in6_dev->lock);
	list_for_each_entry (ifa, &in6_dev->addr_list, if_list) {
		if ((ifa->scope != IFA_LINK) && (ifa->scope != IFA_HOST)) {
			memcpy(src_ip, &ifa->addr, 16);
			result = true;
			break;
		}
	}
	read_unlock_bh(&in6_dev->lock);
	rcu_read_unlock();

	return result;
}

static bool fd2110_mixer_get_src_ip4(struct forward_ethernet *eth, u8 *src_ip)
{
	const struct in_ifaddr *ifa;
	struct in_device *indev;
	bool result = false;

	indev = in_dev_get(eth->net);
	if (!indev)
		return result;

	in_dev_for_each_ifa_rtnl(ifa, indev)
	{
		if (ifa->ifa_local) {
			*((u64 *)&src_ip[0]) = 0x0;
			*((u64 *)&src_ip[8]) = 0xFFFF0000ULL | ((u64)ifa->ifa_local << 32);
			result = true;
		}
	}
	in_dev_put(indev);

	return result;
}

static bool fd2110_mixer_get_src(void *private, bool ipv6, u8 *src_ip)
{
	struct forward_ethernet *eth = (struct forward_ethernet *)private;
	bool result;

	if (ipv6)
		result = fd2110_mixer_get_src_ip6(eth, src_ip);
	else
		result = fd2110_mixer_get_src_ip4(eth, src_ip);

	return result;
}

static struct forward_fd2110_mixer_ops fd2110_mixer_ops = {
	.get_dst = fd2110_mixer_get_dst,
	.get_src = fd2110_mixer_get_src,
};

static int fd2110_init(struct forward_ethernet *eth)
{
	struct forward_dev *fdev = eth->board->dev;
	u32 mac_l, mac_h;
	u8 mac[6];

	mac_l = ioread32(&fdev->csr[FD2110_EthernetMACL_A(eth->index)]);
	mac_h = ioread32(&fdev->csr[FD2110_EthernetMACH_A(eth->index)]);

	mac[0] = (mac_l >> 0) & 0xFF;
	mac[1] = (mac_l >> 8) & 0xFF;
	mac[2] = (mac_l >> 16) & 0xFF;
	mac[3] = (mac_l >> 24) & 0xFF;
	mac[4] = (mac_h >> 0) & 0xFF;
	mac[5] = (mac_h >> 8) & 0xFF;

#if (LINUX_VERSION_CODE >= KERNEL_VERSION(5, 12, 0))
	eth_hw_addr_set(eth->net, mac);
#else
	ether_addr_copy(eth->net->dev_addr, mac);
#endif

	forward_info(fdev, "Hardware MAC address: %pm", mac);

	eth->rx = &eth->dev->sdma->rings[eth->index];
	eth->tx = &eth->dev->sdma->rings[2 + eth->index];

	forward_fd2110_out_mixer_set_ops(fdev, eth->index, &fd2110_mixer_ops, eth);

	return 0;
}

static void fd2110_fini(struct forward_ethernet *eth)
{
	forward_fd2110_out_mixer_set_ops(eth->board->dev, eth->index, NULL, NULL);
}

static forward_irq_t fd2110_irq_mask(struct forward_ethernet *eth)
{
	forward_irq_t result = { 0 };
	forward_irq_flags_set_one(result, eth->index ? 21 : 20);
	forward_irq_flags_set_one(result, eth->index ? 23 : 22);
	return result;
}

static void fd2110_disable_irq(struct forward_ethernet *eth, bool rx, bool tx)
{
	forward_irq_t mask = { 0 };
	struct forward_dev *fdev = eth->board->dev;
	if (rx)
		forward_irq_flags_set_one(mask, eth->index ? 21 : 20);
	if (tx)
		forward_irq_flags_set_one(mask, eth->index ? 23 : 22);
	fdev->cfg.disable_interrupts(fdev, &mask);
}

static void fd2110_enable_irq(struct forward_ethernet *eth, bool rx, bool tx)
{
	forward_irq_t mask = { 0 };
	struct forward_dev *fdev = eth->board->dev;
	if (rx)
		forward_irq_flags_set_one(mask, eth->index ? 21 : 20);
	if (tx)
		forward_irq_flags_set_one(mask, eth->index ? 23 : 22);
	fdev->cfg.enable_interrupts(fdev, &mask);
}

static void fd2110_irq_info(struct forward_ethernet *eth, const forward_irq_t *irq, bool *rx,
			    bool *tx, struct timespec64 *ts)
{
	struct forward_dev *fdev = eth->board->dev;

	ts->tv_nsec = ioread32(&fdev->csr[FD2110_TimeCounterNanoseconds_A]);
	ts->tv_sec = (u64)ioread32(&fdev->csr[FD2110_TimeCounterSecondsH_A]) << 32;
	ts->tv_sec |= ioread32(&fdev->csr[FD2110_TimeCounterSecondsL_A]);

	*rx = forward_irq_has_irq(*irq, eth->index ? 21 : 20);
	*tx = forward_irq_has_irq(*irq, eth->index ? 23 : 22);
}

static int fd2110_toggle_link(struct forward_ethernet *eth, bool up)
{
	struct forward_dev *fdev = eth->board->dev;
	FD2110_EthernetControl ctl = FD2110_EthernetControl_R(fdev->csr, eth->index);
	ctl.reset = up ? 0 : 1;
	FD2110_EthernetControl_W(fdev->csr, eth->index, ctl);

	return 0;
}

static int fd2110_toggle_mcast(struct forward_ethernet *eth, bool mcast, bool promisc)
{
	struct forward_dev *fdev = eth->board->dev;
	FD2110_EthernetControl ctl = FD2110_EthernetControl_R(fdev->csr, eth->index);
	ctl.rxNoMCast = mcast ? 0 : 1;
	ctl.rxPromisc = promisc ? 1 : 0;
	FD2110_EthernetControl_W(fdev->csr, eth->index, ctl);

	return 0;
}

static int fd2110_set_mac(struct forward_ethernet *eth, u8 *addr)
{
	struct forward_dev *fdev = eth->board->dev;
	u32 mac_l = 0, mac_h = 0;
	mac_l |= (s32)addr[0] << 0;
	mac_l |= (s32)addr[1] << 8;
	mac_l |= (s32)addr[2] << 16;
	mac_l |= (s32)addr[3] << 24;
	mac_h |= (s32)addr[4] << 0;
	mac_h |= (s32)addr[5] << 8;

	iowrite32(mac_l, &fdev->csr[FD2110_EthernetMACL_A(eth->index)]);
	iowrite32(mac_h, &fdev->csr[FD2110_EthernetMACH_A(eth->index)]);

	return 0;
}

static bool fd2110_get_link_status(struct forward_ethernet *eth)
{
	struct forward_dev *fdev = eth->board->dev;
	FD2110_EthernetControl ctl = FD2110_EthernetControl_R(fdev->csr, eth->index);
	return (!ctl.reset && ctl.locked) ? true : false;
}

static int fd2110_ptp_adjfine(struct forward_ethernet_board *brd, s64 ppm16)
{
	struct forward_pll *pll = brd->dev->pll;

	if (!pll)
		return -EOPNOTSUPP;

	if (!pll->ops->tune)
		return -EOPNOTSUPP;

	pll->ops->tune(pll, ppm16 * 1000000 / 65536);

	return 0;
}

static int fd2110_ptp_adjtime(struct forward_ethernet_board *brd, s64 delta)
{
	struct forward_dev *dev = brd->dev;
	s64 ds = delta / 1000000000;
	s32 dns = delta - (ds * 1000000000LL);

	iowrite32((ds >> 32) & 0xFFFFFFFF, &dev->csr[FD2110_TimeCounterSecondsH_A]);
	iowrite32(ds & 0xFFFFFFFF, &dev->csr[FD2110_TimeCounterSecondsL_A]);
	iowrite32(dns & 0xFFFFFFFF, &dev->csr[FD2110_TimeCounterNanoseconds_A]);

	return 0;
}

static int fd2110_ptp_gettimex(struct forward_ethernet_board *brd, struct timespec64 *ts,
			       struct ptp_system_timestamp *sts)
{
	struct forward_dev *dev = brd->dev;

	ptp_read_system_prets(sts);
	ts->tv_nsec = ioread32(&dev->csr[FD2110_TimeCounterNanoseconds_A]);
	ptp_read_system_postts(sts);

	ts->tv_sec = (u64)ioread32(&dev->csr[FD2110_TimeCounterSecondsL_A]);
	ts->tv_sec |= ((u64)ioread32(&dev->csr[FD2110_TimeCounterSecondsH_A]) << 32);

	return 0;
}

static int fd2110_ptp_settime(struct forward_ethernet_board *brd, const struct timespec64 *ts)
{
	struct forward_dev *dev = brd->dev;
	struct timespec64 cur;
	s64 ds;
	s32 dns;

	cur.tv_nsec = ioread32(&dev->csr[FD2110_TimeCounterNanoseconds_A]);
	cur.tv_sec = (u64)ioread32(&dev->csr[FD2110_TimeCounterSecondsL_A]);
	cur.tv_sec |= ((u64)ioread32(&dev->csr[FD2110_TimeCounterSecondsH_A]) << 32);

	ds = ts->tv_sec - cur.tv_sec;
	dns = ts->tv_nsec - cur.tv_nsec;

	if (dns > 500000000) {
		ds += 1;
		dns -= 1000000000;
	}
	if (dns < -500000000) {
		ds -= 1;
		dns += 1000000000;
	}

	iowrite32((ds >> 32) & 0xFFFFFFFF, &dev->csr[FD2110_TimeCounterSecondsH_A]);
	iowrite32(ds & 0xFFFFFFFF, &dev->csr[FD2110_TimeCounterSecondsL_A]);
	iowrite32(dns & 0xFFFFFFFF, &dev->csr[FD2110_TimeCounterNanoseconds_A]);

	return 0;
}

static void fd2110_get_link_ksettings(struct forward_ethernet *eth,
				      struct ethtool_link_ksettings *ks)
{
	FD2110_EthernetControl ctl = FD2110_EthernetControl_R(eth->dev->csr, eth->index);

	ethtool_link_ksettings_zero_link_mode(ks, supported);
	ethtool_link_ksettings_zero_link_mode(ks, advertising);
	ethtool_link_ksettings_zero_link_mode(ks, lp_advertising);

	ethtool_link_ksettings_add_link_mode(ks, supported, 10000baseCR_Full);
	ethtool_link_ksettings_add_link_mode(ks, supported, 25000baseCR_Full);
	ethtool_link_ksettings_add_link_mode(ks, supported, FIBRE);
	ethtool_link_ksettings_add_link_mode(ks, supported, FEC_NONE);

	if (ctl.locked && !ctl.reset) {
		switch (ctl.mode) {
		case 3:
			ks->base.speed = SPEED_10000;
			break;
		case 4:
			ks->base.speed = SPEED_25000;
			break;
		default:
			ks->base.speed = SPEED_UNKNOWN;
			break;
		}
		ks->base.duplex = DUPLEX_FULL;
	} else {
		ks->base.speed = SPEED_UNKNOWN;
		ks->base.duplex = DUPLEX_FULL;
	}
	ks->base.autoneg = AUTONEG_DISABLE;
	ks->base.port = PORT_DA;
}

static int fd2110_set_link_ksettings(struct forward_ethernet *eth,
				     const struct ethtool_link_ksettings *ks)
{
	FD2110_EthernetControl ctl = FD2110_EthernetControl_R(eth->dev->csr, eth->index);
	int req_mode = ctl.mode;

	switch (ks->base.speed) {
	case 10000:
		req_mode = 3;
		break;
	case 25000:
		req_mode = 4;
		break;
	default:
		return -EINVAL;
	}

	if (req_mode != ctl.mode) {
		netif_carrier_off(eth->net);
		netif_tx_stop_all_queues(eth->net);

		ctl.reset = 1;
		ctl.mode = req_mode;

		FD2110_EthernetControl_W(eth->dev->csr, eth->index, ctl);
		msleep(100);

		ctl.reset = 0;
		FD2110_EthernetControl_W(eth->dev->csr, eth->index, ctl);

		netif_carrier_on(eth->net);
		netif_tx_start_all_queues(eth->net);
	}
	return 0;
}

static void fd2110_update_stats(struct forward_ethernet *eth)
{
	FD2110_SDMAStats rx_sts;
	FD2110_EthernetStats mac_sts;
	unsigned long sflags;

	rx_sts = FD2110_SDMAStats_R(eth->dev->csr, eth->index);
	FD2110_SDMAStats_W(eth->dev->csr, eth->index, rx_sts);
	mac_sts = FD2110_EthernetStats_R(eth->dev->csr, eth->index);
	FD2110_EthernetStats_W(eth->dev->csr, eth->index, mac_sts);

	spin_lock_irqsave(&eth->stats_lock, sflags);
	eth->stats.rx_mac_errors += mac_sts.dropped;
	eth->stats.rx_fifo_errors += rx_sts.drops;
	spin_unlock_irqrestore(&eth->stats_lock, sflags);
}

static int fd2110_sfp_read_reg(struct forward_ethernet *eth, u16 address, u8 *data)
{
	return eth->board->dev->i2c_xfer(eth->board->dev, eth->index, true, 0x50 + address / 256,
					 address % 256, data) ?
		       0 :
		       -EIO;
}

static void fd2110_sfp_status(struct forward_ethernet *eth, bool *absense, bool *rx_los,
			      bool *tx_fault)
{
	FD2110_EthernetControl reth = FD2110_EthernetControl_R(eth->board->dev->csr, eth->index);

	if (absense)
		*absense = reth.sfpModAbs ? true : false;

	if (rx_los)
		*rx_los = reth.sfpRxLOS ? true : false;

	if (tx_fault)
		*tx_fault = reth.sfpTxFault ? true : false;
}

const struct forward_ethernet_ops fd2110_ops = {
	.num_eth = 2,

	.init = fd2110_init,
	.fini = fd2110_fini,

	.irq_mask = fd2110_irq_mask,
	.disable_irq = fd2110_disable_irq,
	.enable_irq = fd2110_enable_irq,
	.irq_info = fd2110_irq_info,

	.toggle_link = fd2110_toggle_link,
	.toggle_mcast = fd2110_toggle_mcast,
	.set_mac = fd2110_set_mac,
	.get_link_status = fd2110_get_link_status,

	.get_link_ksettings = fd2110_get_link_ksettings,
	.set_link_ksettings = fd2110_set_link_ksettings,

	.update_stats = fd2110_update_stats,

	.ptp_adjfine = fd2110_ptp_adjfine,
	.ptp_adjtime = fd2110_ptp_adjtime,
	.ptp_gettimex = fd2110_ptp_gettimex,
	.ptp_settime = fd2110_ptp_settime,

	.sfp_read_reg = fd2110_sfp_read_reg,
	.sfp_status = fd2110_sfp_status,
};
