/*
 *   BSD LICENSE
 *
 *   Copyright(c) 2017 Cavium, Inc.. All rights reserved.
 *   All rights reserved.
 *
 *   Redistribution and use in source and binary forms, with or without
 *   modification, are permitted provided that the following conditions
 *   are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in
 *       the documentation and/or other materials provided with the
 *       distribution.
 *     * Neither the name of Cavium, Inc. nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 *
 *   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *   "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 *   A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 *   OWNER(S) OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 *   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 *   LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 *   DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 *   THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 *   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 *   OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "lio_bsd.h"
#include "lio_common.h"
#include "lio_droq.h"
#include "lio_iq.h"
#include "lio_response_manager.h"
#include "lio_device.h"
#include "lio_network.h"
#include "lio_ctrl.h"
#include "cn23xx_pf_device.h"
#include "lio_image.h"
#include "lio_ioctl.h"
#include "lio_main.h"
#include "lio_rxtx.h"

static int	lio_set_rx_csum(if_t ifp, uint32_t data);
static int	lio_set_tso4(if_t ifp);
static int	lio_set_tso6(if_t ifp);
static int	lio_set_lro(if_t ifp);
static int	lio_change_mtu(if_t ifp, int new_mtu);
static int	lio_set_mcast_list(if_t ifp);
static inline enum	lio_ifflags lio_get_new_flags(if_t ifp);

static inline bool
lio_is_valid_ether_addr(const uint8_t *addr)
{

	return (!(0x01 & addr[0]) && !((addr[0] + addr[1] + addr[2] + addr[3] +
					addr[4] + addr[5]) == 0x00));
}

static int
lio_change_dev_flags(if_t ifp)
{
	struct lio_ctrl_pkt	nctrl;
	struct lio		*lio = if_getsoftc(ifp);
	struct octeon_device	*oct = lio->oct_dev;
	int ret = 0;

	bzero(&nctrl, sizeof(struct lio_ctrl_pkt));

	/* Create a ctrl pkt command to be sent to core app. */
	nctrl.ncmd.cmd64 = 0;
	nctrl.ncmd.s.cmd = LIO_CMD_CHANGE_DEVFLAGS;
	nctrl.ncmd.s.param1 = lio_get_new_flags(ifp);
	nctrl.iq_no = lio->linfo.txpciq[0].s.q_no;
	nctrl.lio = lio;
	nctrl.cb_fn = lio_ctrl_cmd_completion;

	ret = lio_send_ctrl_pkt(oct, &nctrl);
	if (ret)
		lio_dev_err(oct, "Failed to change flags ret %d\n", ret);

	return (ret);
}

/*
 * lio_ioctl : User calls this routine for configuring
 * the interface.
 *
 * return 0 on success, positive on failure
 */
int
lio_ioctl(if_t ifp, u_long cmd, caddr_t data)
{
	struct lio	*lio = if_getsoftc(ifp);
	struct ifreq	*ifrequest = (struct ifreq *)data;
	int	error = 0;

	switch (cmd) {
	case SIOCSIFADDR:
		lio_dev_dbg(lio->oct_dev, "ioctl: SIOCSIFADDR\n");
		if_setflagbits(ifp, IFF_UP, 0);
		error = ether_ioctl(ifp, cmd, data);
		break;
	case SIOCSIFMTU:
		lio_dev_dbg(lio->oct_dev, "ioctl: SIOCSIFMTU\n");
		error = lio_change_mtu(ifp, ifrequest->ifr_mtu);
		break;
	case SIOCSIFFLAGS:
		lio_dev_dbg(lio->oct_dev, "ioctl: SIOCSIFFLAGS\n");
		if (if_getflags(ifp) & IFF_UP) {
			if (if_getdrvflags(ifp) & IFF_DRV_RUNNING) {
				if ((if_getflags(ifp) ^ lio->if_flags) &
				    (IFF_PROMISC | IFF_ALLMULTI))
					error = lio_change_dev_flags(ifp);
			} else {
				if (!(atomic_load_acq_int(&lio->ifstate) &
				      LIO_IFSTATE_DETACH))
					lio_open(lio);
			}
		} else {
			if (if_getdrvflags(ifp) & IFF_DRV_RUNNING)
				lio_stop(ifp);
		}
		lio->if_flags = if_getflags(ifp);
		break;
	case SIOCADDMULTI:
		lio_dev_dbg(lio->oct_dev, "ioctl: SIOCADDMULTI\n");
		if (if_getdrvflags(ifp) & IFF_DRV_RUNNING)
			error = lio_set_mcast_list(ifp);
		break;
	case SIOCDELMULTI:
		lio_dev_dbg(lio->oct_dev, "ioctl: SIOCSIFMULTI\n");
		break;
	case SIOCSIFMEDIA:
		lio_dev_dbg(lio->oct_dev, "ioctl: SIOCSIFMEDIA\n");
	case SIOCGIFMEDIA:
		lio_dev_dbg(lio->oct_dev, "ioctl: SIOCGIFMEDIA\n");
	case SIOCGIFXMEDIA:
		lio_dev_dbg(lio->oct_dev, "ioctl: SIOCGIFXMEDIA\n");
		error = ifmedia_ioctl(ifp, ifrequest, &lio->ifmedia, cmd);
		break;
	case SIOCSIFCAP:
		{
			int	features = ifrequest->ifr_reqcap ^
					if_getcapenable(ifp);

			lio_dev_dbg(lio->oct_dev, "ioctl: SIOCSIFCAP (Set Capabilities)\n");

			if (!features)
				break;

			if (features & IFCAP_TXCSUM) {
				if_togglecapenable(ifp, IFCAP_TXCSUM);
				if (if_getcapenable(ifp) & IFCAP_TXCSUM)
					if_sethwassistbits(ifp, (CSUM_TCP |
								 CSUM_UDP |
								 CSUM_IP), 0);
				else
					if_sethwassistbits(ifp, 0,
							(CSUM_TCP | CSUM_UDP |
							 CSUM_IP));
			}
			if (features & IFCAP_TXCSUM_IPV6) {
				if_togglecapenable(ifp, IFCAP_TXCSUM_IPV6);
				if (if_getcapenable(ifp) & IFCAP_TXCSUM_IPV6)
					if_sethwassistbits(ifp, (CSUM_UDP_IPV6 |
							   CSUM_TCP_IPV6), 0);
				else
					if_sethwassistbits(ifp, 0,
							   (CSUM_UDP_IPV6 |
							    CSUM_TCP_IPV6));
			}
			if (features & (IFCAP_RXCSUM | IFCAP_RXCSUM_IPV6))
				error |= lio_set_rx_csum(ifp, (features &
							       (IFCAP_RXCSUM |
							 IFCAP_RXCSUM_IPV6)));

			if (features & IFCAP_TSO4)
				error |= lio_set_tso4(ifp);

			if (features & IFCAP_TSO6)
				error |= lio_set_tso6(ifp);

			if (features & IFCAP_LRO)
				error |= lio_set_lro(ifp);

			if (features & IFCAP_VLAN_HWTAGGING)
				if_togglecapenable(ifp, IFCAP_VLAN_HWTAGGING);

			if (features & IFCAP_VLAN_HWFILTER)
				if_togglecapenable(ifp, IFCAP_VLAN_HWFILTER);

			if (features & IFCAP_VLAN_HWTSO)
				if_togglecapenable(ifp, IFCAP_VLAN_HWTSO);

			VLAN_CAPABILITIES(ifp);
			break;
		}
	default:
		lio_dev_dbg(lio->oct_dev, "ioctl: UNKNOWN (0x%X)\n", (int)cmd);
		error = ether_ioctl(ifp, cmd, data);
		break;
	}

	return (error);
}

static int
lio_set_tso4(if_t ifp)
{
	struct lio	*lio = if_getsoftc(ifp);

	if (if_getcapabilities(ifp) & IFCAP_TSO4) {
		if_togglecapenable(ifp, IFCAP_TSO4);
		if (if_getcapenable(ifp) & IFCAP_TSO4)
			if_sethwassistbits(ifp, CSUM_IP_TSO, 0);
		else
			if_sethwassistbits(ifp, 0, CSUM_IP_TSO);
	} else {
		lio_dev_info(lio->oct_dev, "TSO4 capability not supported\n");
		return (EINVAL);
	}

	return (0);
}

static int
lio_set_tso6(if_t ifp)
{
	struct lio	*lio = if_getsoftc(ifp);

	if (if_getcapabilities(ifp) & IFCAP_TSO6) {
		if_togglecapenable(ifp, IFCAP_TSO6);
		if (if_getcapenable(ifp) & IFCAP_TSO6)
			if_sethwassistbits(ifp, CSUM_IP6_TSO, 0);
		else
			if_sethwassistbits(ifp, 0, CSUM_IP6_TSO);
	} else {
		lio_dev_info(lio->oct_dev, "TSO6 capability not supported\n");
		return (EINVAL);
	}

	return (0);
}

static int
lio_set_rx_csum(if_t ifp, uint32_t data)
{
	struct lio	*lio = if_getsoftc(ifp);
	int	ret = 0;

	if (if_getcapabilities(ifp) & (IFCAP_RXCSUM | IFCAP_RXCSUM_IPV6)) {
		if_togglecapenable(ifp, (IFCAP_RXCSUM | IFCAP_RXCSUM_IPV6));

		if (data) {
			/* LRO requires RXCSUM */
			if ((if_getcapabilities(ifp) & IFCAP_LRO) &&
			    (if_getcapenable(ifp) & IFCAP_LRO)) {
				ret = lio_set_feature(ifp, LIO_CMD_LRO_DISABLE,
						      LIO_LROIPV4 |
						      LIO_LROIPV6);
				if_togglecapenable(ifp, IFCAP_LRO);
			}
		}
	} else {
		lio_dev_info(lio->oct_dev, "Rx checksum offload capability not supported\n");
		return (ENODEV);
	}

	return ((ret) ? EINVAL : 0);
}

static int
lio_set_lro(if_t ifp)
{
	struct lio	*lio = if_getsoftc(ifp);
	int	ret = 0;

	if (!(if_getcapabilities(ifp) & IFCAP_LRO)) {
		lio_dev_info(lio->oct_dev, "LRO capability not supported\n");
		return (ENODEV);
	}

	if ((!(if_getcapenable(ifp) & IFCAP_LRO)) &&
	    (if_getcapenable(ifp) & IFCAP_RXCSUM) &&
	    (if_getcapenable(ifp) & IFCAP_RXCSUM_IPV6)) {
		if_togglecapenable(ifp, IFCAP_LRO);

		if (lio_hwlro)
			ret = lio_set_feature(ifp, LIO_CMD_LRO_ENABLE, LIO_LROIPV4 |
					      LIO_LROIPV6);

	} else if (if_getcapenable(ifp) & IFCAP_LRO) {
		if_togglecapenable(ifp, IFCAP_LRO);

		if (lio_hwlro)
			ret = lio_set_feature(ifp, LIO_CMD_LRO_DISABLE, LIO_LROIPV4 |
					      LIO_LROIPV6);
	} else
		lio_dev_info(lio->oct_dev, "LRO requires RXCSUM");

	return ((ret) ? EINVAL : 0);
}

static void
lio_mtu_ctl_callback(struct octeon_device *oct, uint32_t status, void *buf)
{
	struct lio_soft_command	*sc = buf;
	volatile int		*mtu_sc_ctx;

	mtu_sc_ctx = sc->ctxptr;

	if (status) {
		lio_dev_err(oct, "MTU updation ctl instruction failed. Status: %llx\n",
			    LIO_CAST64(status));
		*mtu_sc_ctx = -1;
		/*
		 * This barrier is required to be sure that the
		 * response has been written fully.
		 */
		wmb();
		return;
	}

	*mtu_sc_ctx = 1;

	/*
	 * This barrier is required to be sure that the response has been
	 * written fully.
	 */
	wmb();
}

/* @param ifp is network device */
static int
lio_change_mtu(if_t ifp, int new_mtu)
{
	struct lio		*lio = if_getsoftc(ifp);
	struct octeon_device	*oct = lio->oct_dev;
	struct lio_soft_command	*sc;
	union octeon_cmd	*ncmd;
	volatile int		*mtu_sc_ctx;
	int	retval = 0;

	if (lio->mtu == new_mtu)
		return (0);

	/*
	 * Limit the MTU to make sure the ethernet packets are between
	 * LIO_MIN_MTU_SIZE bytes and LIO_MAX_MTU_SIZE bytes
	 */
	if ((new_mtu < LIO_MIN_MTU_SIZE) || (new_mtu > LIO_MAX_MTU_SIZE)) {
		lio_dev_err(oct, "Invalid MTU: %d\n", new_mtu);
		lio_dev_err(oct, "Valid range %d and %d\n",
			    LIO_MIN_MTU_SIZE, LIO_MAX_MTU_SIZE);
		return (EINVAL);
	}

	sc = lio_alloc_soft_command(oct, OCTEON_CMD_SIZE, 16,
				    sizeof(*mtu_sc_ctx));
	if (sc == NULL)
		return (ENOMEM);

	ncmd = (union octeon_cmd *)sc->virtdptr;
	mtu_sc_ctx = sc->ctxptr;

	*mtu_sc_ctx = 0;

	ncmd->cmd64 = 0;
	ncmd->s.cmd = LIO_CMD_CHANGE_MTU;
	ncmd->s.param1 = new_mtu;

	lio_swap_8B_data((uint64_t *)ncmd, (OCTEON_CMD_SIZE >> 3));

	sc->iq_no = lio->linfo.txpciq[0].s.q_no;

	lio_prepare_soft_command(oct, sc, LIO_OPCODE_NIC,
				 LIO_OPCODE_NIC_CMD, 0, 0, 0);

	sc->callback = lio_mtu_ctl_callback;
	sc->callback_arg = sc;
	sc->wait_time = 5000;

	retval = lio_send_soft_command(oct, sc);
	if (retval == LIO_IQ_SEND_FAILED) {
		lio_dev_info(oct,
			     "Failed to send MTU update Control message\n");
		retval = EBUSY;
		goto mtu_updation_failed;
	}

	/*
	 * Sleep on a wait queue till the cond flag indicates that the
	 * response arrived or timed-out.
	 */
	lio_sleep_cond(oct, mtu_sc_ctx);

	if (*mtu_sc_ctx < 0) {
		retval = EBUSY;
		goto mtu_updation_failed;
	}
	lio_dev_info(oct, "MTU Changed from %d to %d\n", if_getmtu(ifp),
		     new_mtu);
	if_setmtu(ifp, new_mtu);
	lio->mtu = new_mtu;
	retval = 0;			/*
				         * this updation is make sure that LIO_IQ_SEND_STOP case
				         * also success
				         */

mtu_updation_failed:
	lio_free_soft_command(oct, sc);

	return (retval);
}

/* @param ifp network device */
int
lio_set_mac(if_t ifp, uint8_t *p)
{
	struct lio_ctrl_pkt	nctrl;
	struct lio		*lio = if_getsoftc(ifp);
	struct octeon_device	*oct = lio->oct_dev;
	int	ret = 0;

	if (!lio_is_valid_ether_addr(p))
		return (EADDRNOTAVAIL);

	bzero(&nctrl, sizeof(struct lio_ctrl_pkt));

	nctrl.ncmd.cmd64 = 0;
	nctrl.ncmd.s.cmd = LIO_CMD_CHANGE_MACADDR;
	nctrl.ncmd.s.param1 = 0;
	nctrl.ncmd.s.more = 1;
	nctrl.iq_no = lio->linfo.txpciq[0].s.q_no;
	nctrl.lio = lio;
	nctrl.cb_fn = lio_ctrl_cmd_completion;
	nctrl.wait_time = 100;

	nctrl.udd[0] = 0;
	/* The MAC Address is presented in network byte order. */
	memcpy((uint8_t *)&nctrl.udd[0] + 2, p, ETHER_HDR_LEN);

	ret = lio_send_ctrl_pkt(lio->oct_dev, &nctrl);
	if (ret < 0) {
		lio_dev_err(oct, "MAC Address change failed\n");
		return (ENOMEM);
	}

	memcpy(((uint8_t *)&lio->linfo.hw_addr) + 2, p, ETHER_HDR_LEN);

	return (0);
}

/*
 * \brief Converts a mask based on ifp flags
 * @param ifp network device
 *
 * This routine generates a lio_ifflags mask from the ifp flags
 * received from the OS.
 */
static inline enum lio_ifflags
lio_get_new_flags(if_t ifp)
{
	enum lio_ifflags f = LIO_IFFLAG_UNICAST;

	if (if_getflags(ifp) & IFF_PROMISC)
		f |= LIO_IFFLAG_PROMISC;

	if (if_getflags(ifp) & IFF_ALLMULTI)
		f |= LIO_IFFLAG_ALLMULTI;

	if (if_getflags(ifp) & IFF_MULTICAST) {
		f |= LIO_IFFLAG_MULTICAST;

		/*
		 * Accept all multicast addresses if there are more than we
		 * can handle
		 */
		if (if_llmaddr_count(ifp) > LIO_MAX_MULTICAST_ADDR)
			f |= LIO_IFFLAG_ALLMULTI;
	}
	if (if_getflags(ifp) & IFF_BROADCAST)
		f |= LIO_IFFLAG_BROADCAST;

	return (f);
}

static u_int
lio_copy_maddr(void *arg, struct sockaddr_dl *sdl, u_int cnt)
{
	uint64_t *mc = arg;

	if (cnt == LIO_MAX_MULTICAST_ADDR)
		return (0);

	mc += cnt;
	*mc = 0;
	memcpy(((uint8_t *)mc) + 2, LLADDR(sdl), ETHER_ADDR_LEN);
	/* no need to swap bytes */

	return (1);
}

/* @param ifp network device */
static int
lio_set_mcast_list(if_t ifp)
{
	struct lio		*lio = if_getsoftc(ifp);
	struct octeon_device	*oct = lio->oct_dev;
	struct lio_ctrl_pkt	nctrl;
	int	mc_count;
	int	ret;

	bzero(&nctrl, sizeof(struct lio_ctrl_pkt));

	/* Create a ctrl pkt command to be sent to core app. */
	nctrl.ncmd.cmd64 = 0;
	nctrl.ncmd.s.cmd = LIO_CMD_SET_MULTI_LIST;
	nctrl.ncmd.s.param1 = lio_get_new_flags(ifp);
	nctrl.iq_no = lio->linfo.txpciq[0].s.q_no;
	nctrl.lio = lio;
	nctrl.cb_fn = lio_ctrl_cmd_completion;

	/* copy all the addresses into the udd */
	mc_count = if_foreach_llmaddr(ifp, lio_copy_maddr, &nctrl.udd[0]);

	/*
	 * Apparently, any activity in this call from the kernel has to
	 * be atomic. So we won't wait for response.
	 */
	nctrl.wait_time = 0;
	nctrl.ncmd.s.param2 = mc_count;
	nctrl.ncmd.s.more = mc_count;

	ret = lio_send_ctrl_pkt(lio->oct_dev, &nctrl);
	if (ret < 0) {
		lio_dev_err(oct, "DEVFLAGS change failed in core (ret: 0x%x)\n",
			    ret);
	}

	return ((ret) ? EINVAL : 0);
}