// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (c) 2014-2019, The Linux Foundation. All rights reserved.
 */

#include <linux/debugfs.h>
#include <linux/export.h>
#include <linux/fs.h>
#include <linux/if_ether.h>
#include <linux/ioctl.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/msm_ipa.h>
#include <linux/mutex.h>
#include <linux/skbuff.h>
#include <linux/types.h>
#include <linux/ipv6.h>
#include <net/addrconf.h>
#include <linux/ipa.h>
#include <linux/cdev.h>
#include <linux/ipa_odu_bridge.h>
#include "../ipa_common_i.h"
#include "../ipa_v3/ipa_pm.h"

#define ODU_BRIDGE_DRV_NAME "odu_ipa_bridge"

#define ODU_BRIDGE_DBG(fmt, args...) \
	do { \
		pr_debug(ODU_BRIDGE_DRV_NAME " %s:%d " fmt, \
			__func__, __LINE__, ## args); \
		IPA_IPC_LOGGING(ipa_get_ipc_logbuf(), \
			ODU_BRIDGE_DRV_NAME " %s:%d " fmt, ## args); \
		IPA_IPC_LOGGING(ipa_get_ipc_logbuf_low(), \
			ODU_BRIDGE_DRV_NAME " %s:%d " fmt, ## args); \
	} while (0)
#define ODU_BRIDGE_DBG_LOW(fmt, args...) \
	do { \
		pr_debug(ODU_BRIDGE_DRV_NAME " %s:%d " fmt, \
			__func__, __LINE__, ## args); \
		IPA_IPC_LOGGING(ipa_get_ipc_logbuf_low(), \
			ODU_BRIDGE_DRV_NAME " %s:%d " fmt, ## args); \
	} while (0)
#define ODU_BRIDGE_ERR(fmt, args...) \
	do { \
		pr_err(ODU_BRIDGE_DRV_NAME " %s:%d " fmt, \
			__func__, __LINE__, ## args); \
		IPA_IPC_LOGGING(ipa_get_ipc_logbuf(), \
			ODU_BRIDGE_DRV_NAME " %s:%d " fmt, ## args); \
		IPA_IPC_LOGGING(ipa_get_ipc_logbuf_low(), \
			ODU_BRIDGE_DRV_NAME " %s:%d " fmt, ## args); \
	} while (0)

#define ODU_BRIDGE_FUNC_ENTRY() \
	ODU_BRIDGE_DBG_LOW("ENTRY\n")
#define ODU_BRIDGE_FUNC_EXIT() \
	ODU_BRIDGE_DBG_LOW("EXIT\n")


#define ODU_BRIDGE_IS_QMI_ADDR(daddr) \
	(memcmp(&(daddr), &odu_bridge_ctx->llv6_addr, sizeof((daddr))) \
		== 0)

#define ODU_BRIDGE_IPV4_HDR_NAME "odu_br_ipv4"
#define ODU_BRIDGE_IPV6_HDR_NAME "odu_br_ipv6"

#define IPA_ODU_SYS_DESC_FIFO_SZ 0x800

#ifdef CONFIG_COMPAT
#define ODU_BRIDGE_IOC_SET_LLV6_ADDR32 _IOW(ODU_BRIDGE_IOC_MAGIC, \
				ODU_BRIDGE_IOCTL_SET_LLV6_ADDR, \
				compat_uptr_t)
#endif

#define IPA_ODU_VER_CHECK() \
	do { \
		ret = 0;\
		if (ipa_get_hw_type() == IPA_HW_None) { \
			pr_err("IPA HW is unknown\n"); \
			ret = -EFAULT; \
		} \
		else if (ipa_get_hw_type() < IPA_HW_v3_0) \
			ret = 1; \
	} while (0)

/**
 * struct stats - driver statistics, viewable using debugfs
 * @num_ul_packets: number of packets bridged in uplink direction
 * @num_dl_packets: number of packets bridged in downink direction
 * bridge
 * @num_lan_packets: number of packets bridged to APPS on bridge mode
 */
struct stats {
	u64 num_ul_packets;
	u64 num_dl_packets;
	u64 num_lan_packets;
};

/**
 * struct odu_bridge_ctx - ODU bridge driver context information
 * @class: kernel class pointer
 * @dev_num: kernel device number
 * @dev: kernel device struct pointer
 * @cdev: kernel character device struct
 * @netdev_name: network interface name
 * @device_ethaddr: network interface ethernet address
 * @priv: client's private data. to be used in client's callbacks
 * @tx_dp_notify: client callback for handling IPA ODU_PROD callback
 * @send_dl_skb: client callback for sending skb in downlink direction
 * @stats: statistics, how many packets were transmitted using the SW bridge
 * @is_conencted: is bridge connected ?
 * @is_suspended: is bridge suspended ?
 * @mode: ODU mode (router/bridge)
 * @lock: for the initialization, connect and disconnect synchronization
 * @llv6_addr: link local IPv6 address of ODU network interface
 * @odu_br_ipv4_hdr_hdl: handle for partial ipv4 ethernet header
 * @odu_br_ipv6_hdr_hdl: handle for partial ipv6 ethernet header
 * @odu_prod_hdl: handle for IPA_CLIENT_ODU_PROD pipe
 * @odu_emb_cons_hdl: handle for IPA_CLIENT_ODU_EMB_CONS pipe
 * @odu_teth_cons_hdl: handle for IPA_CLIENT_ODU_TETH_CONS pipe
 * @wakeup_request: client callback to wakeup
 */
struct odu_bridge_ctx {
	struct class *class;
	dev_t dev_num;
	struct device *dev;
	struct cdev cdev;
	char netdev_name[IPA_RESOURCE_NAME_MAX];
	u8 device_ethaddr[ETH_ALEN];
	void *priv;
	ipa_notify_cb tx_dp_notify;
	int (*send_dl_skb)(void *priv, struct sk_buff *skb);
	struct stats stats;
	bool is_connected;
	bool is_suspended;
	enum odu_bridge_mode mode;
	struct mutex lock;
	struct in6_addr llv6_addr;
	uint32_t odu_br_ipv4_hdr_hdl;
	uint32_t odu_br_ipv6_hdr_hdl;
	u32 odu_prod_hdl;
	u32 odu_emb_cons_hdl;
	u32 odu_teth_cons_hdl;
	u32 ipa_sys_desc_size;
	void *logbuf;
	void *logbuf_low;
	void (*wakeup_request)(void *cl_priv);
	u32 pm_hdl;
};
static struct odu_bridge_ctx *odu_bridge_ctx;

#ifdef CONFIG_DEBUG_FS
#define ODU_MAX_MSG_LEN 512
static char dbg_buff[ODU_MAX_MSG_LEN];
#endif

static void odu_bridge_emb_cons_cb(void *priv, enum ipa_dp_evt_type evt,
	unsigned long data)
{
	ODU_BRIDGE_FUNC_ENTRY();
	if (evt != IPA_RECEIVE) {
		ODU_BRIDGE_ERR("unexpected event\n");
		WARN_ON(1);
		return;
	}
	odu_bridge_ctx->send_dl_skb(priv, (struct sk_buff *)data);
	odu_bridge_ctx->stats.num_dl_packets++;
	ODU_BRIDGE_FUNC_EXIT();
}

static void odu_bridge_teth_cons_cb(void *priv, enum ipa_dp_evt_type evt,
	unsigned long data)
{
	struct ipv6hdr *ipv6hdr;
	struct sk_buff *skb = (struct sk_buff *)data;
	struct sk_buff *skb_copied;

	ODU_BRIDGE_FUNC_ENTRY();
	if (evt != IPA_RECEIVE) {
		ODU_BRIDGE_ERR("unexpected event\n");
		WARN_ON(1);
		return;
	}

	ipv6hdr = (struct ipv6hdr *)(skb->data + ETH_HLEN);
	if (ipv6hdr->version == 6 &&
	    ipv6_addr_is_multicast(&ipv6hdr->daddr)) {
		ODU_BRIDGE_DBG_LOW("Multicast pkt, send to APPS and adapter\n");
		skb_copied = skb_clone(skb, GFP_KERNEL);
		if (skb_copied) {
			odu_bridge_ctx->tx_dp_notify(odu_bridge_ctx->priv,
						IPA_RECEIVE,
						(unsigned long) skb_copied);
			odu_bridge_ctx->stats.num_lan_packets++;
		} else {
			ODU_BRIDGE_ERR("No memory\n");
		}
	}

	odu_bridge_ctx->send_dl_skb(priv, skb);
	odu_bridge_ctx->stats.num_dl_packets++;
	ODU_BRIDGE_FUNC_EXIT();
}

static int odu_bridge_connect_router(void)
{
	struct ipa_sys_connect_params odu_prod_params;
	struct ipa_sys_connect_params odu_emb_cons_params;
	int res;

	ODU_BRIDGE_FUNC_ENTRY();

	memset(&odu_prod_params, 0, sizeof(odu_prod_params));
	memset(&odu_emb_cons_params, 0, sizeof(odu_emb_cons_params));

	/* configure RX (ODU->IPA) EP */
	odu_prod_params.client = IPA_CLIENT_ODU_PROD;
	odu_prod_params.ipa_ep_cfg.hdr.hdr_len = ETH_HLEN;
	odu_prod_params.ipa_ep_cfg.nat.nat_en = IPA_SRC_NAT;
	odu_prod_params.desc_fifo_sz = odu_bridge_ctx->ipa_sys_desc_size;
	odu_prod_params.priv = odu_bridge_ctx->priv;
	odu_prod_params.notify = odu_bridge_ctx->tx_dp_notify;
	res = ipa_setup_sys_pipe(&odu_prod_params,
		&odu_bridge_ctx->odu_prod_hdl);
	if (res) {
		ODU_BRIDGE_ERR("fail to setup sys pipe ODU_PROD %d\n", res);
		goto fail_odu_prod;
	}

	/* configure TX (IPA->ODU) EP */
	odu_emb_cons_params.client = IPA_CLIENT_ODU_EMB_CONS;
	odu_emb_cons_params.ipa_ep_cfg.hdr.hdr_len = ETH_HLEN;
	odu_emb_cons_params.ipa_ep_cfg.nat.nat_en = IPA_BYPASS_NAT;
	odu_emb_cons_params.desc_fifo_sz = odu_bridge_ctx->ipa_sys_desc_size;
	odu_emb_cons_params.priv = odu_bridge_ctx->priv;
	odu_emb_cons_params.notify = odu_bridge_emb_cons_cb;
	res = ipa_setup_sys_pipe(&odu_emb_cons_params,
		&odu_bridge_ctx->odu_emb_cons_hdl);
	if (res) {
		ODU_BRIDGE_ERR("fail to setup sys pipe ODU_EMB_CONS %d\n", res);
		goto fail_odu_emb_cons;
	}

	ODU_BRIDGE_DBG("odu_prod_hdl = %d, odu_emb_cons_hdl = %d\n",
		odu_bridge_ctx->odu_prod_hdl, odu_bridge_ctx->odu_emb_cons_hdl);

	ODU_BRIDGE_FUNC_EXIT();

	return 0;

fail_odu_emb_cons:
	ipa_teardown_sys_pipe(odu_bridge_ctx->odu_prod_hdl);
	odu_bridge_ctx->odu_prod_hdl = 0;
fail_odu_prod:
	return res;
}

static int odu_bridge_connect_bridge(void)
{
	struct ipa_sys_connect_params odu_prod_params;
	struct ipa_sys_connect_params odu_emb_cons_params;
	struct ipa_sys_connect_params odu_teth_cons_params;
	int res;

	ODU_BRIDGE_FUNC_ENTRY();

	memset(&odu_prod_params, 0, sizeof(odu_prod_params));
	memset(&odu_emb_cons_params, 0, sizeof(odu_emb_cons_params));

	/* configure RX (ODU->IPA) EP */
	odu_prod_params.client = IPA_CLIENT_ODU_PROD;
	odu_prod_params.desc_fifo_sz = IPA_ODU_SYS_DESC_FIFO_SZ;
	odu_prod_params.priv = odu_bridge_ctx->priv;
	odu_prod_params.notify = odu_bridge_ctx->tx_dp_notify;
	odu_prod_params.skip_ep_cfg = true;
	res = ipa_setup_sys_pipe(&odu_prod_params,
		&odu_bridge_ctx->odu_prod_hdl);
	if (res) {
		ODU_BRIDGE_ERR("fail to setup sys pipe ODU_PROD %d\n", res);
		goto fail_odu_prod;
	}

	/* configure TX tethered (IPA->ODU) EP */
	odu_teth_cons_params.client = IPA_CLIENT_ODU_TETH_CONS;
	odu_teth_cons_params.desc_fifo_sz = IPA_ODU_SYS_DESC_FIFO_SZ;
	odu_teth_cons_params.priv = odu_bridge_ctx->priv;
	odu_teth_cons_params.notify = odu_bridge_teth_cons_cb;
	odu_teth_cons_params.skip_ep_cfg = true;
	res = ipa_setup_sys_pipe(&odu_teth_cons_params,
		&odu_bridge_ctx->odu_teth_cons_hdl);
	if (res) {
		ODU_BRIDGE_ERR("fail to setup sys pipe ODU_TETH_CONS %d\n",
				res);
		goto fail_odu_teth_cons;
	}

	/* configure TX embedded(IPA->ODU) EP */
	odu_emb_cons_params.client = IPA_CLIENT_ODU_EMB_CONS;
	odu_emb_cons_params.ipa_ep_cfg.hdr.hdr_len = ETH_HLEN;
	odu_emb_cons_params.ipa_ep_cfg.nat.nat_en = IPA_BYPASS_NAT;
	odu_emb_cons_params.desc_fifo_sz = IPA_ODU_SYS_DESC_FIFO_SZ;
	odu_emb_cons_params.priv = odu_bridge_ctx->priv;
	odu_emb_cons_params.notify = odu_bridge_emb_cons_cb;
	res = ipa_setup_sys_pipe(&odu_emb_cons_params,
		&odu_bridge_ctx->odu_emb_cons_hdl);
	if (res) {
		ODU_BRIDGE_ERR("fail to setup sys pipe ODU_EMB_CONS %d\n", res);
		goto fail_odu_emb_cons;
	}

	ODU_BRIDGE_DBG_LOW("odu_prod_hdl = %d, odu_emb_cons_hdl = %d\n",
		odu_bridge_ctx->odu_prod_hdl, odu_bridge_ctx->odu_emb_cons_hdl);
	ODU_BRIDGE_DBG_LOW("odu_teth_cons_hdl = %d\n",
		odu_bridge_ctx->odu_teth_cons_hdl);

	ODU_BRIDGE_FUNC_EXIT();

	return 0;

fail_odu_emb_cons:
	ipa_teardown_sys_pipe(odu_bridge_ctx->odu_teth_cons_hdl);
	odu_bridge_ctx->odu_teth_cons_hdl = 0;
fail_odu_teth_cons:
	ipa_teardown_sys_pipe(odu_bridge_ctx->odu_prod_hdl);
	odu_bridge_ctx->odu_prod_hdl = 0;
fail_odu_prod:
	return res;
}

static int odu_bridge_disconnect_router(void)
{
	int res;

	ODU_BRIDGE_FUNC_ENTRY();

	res = ipa_teardown_sys_pipe(odu_bridge_ctx->odu_prod_hdl);
	if (res)
		ODU_BRIDGE_ERR("teardown ODU PROD failed\n");
	odu_bridge_ctx->odu_prod_hdl = 0;

	res = ipa_teardown_sys_pipe(odu_bridge_ctx->odu_emb_cons_hdl);
	if (res)
		ODU_BRIDGE_ERR("teardown ODU EMB CONS failed\n");
	odu_bridge_ctx->odu_emb_cons_hdl = 0;

	ODU_BRIDGE_FUNC_EXIT();

	return 0;
}

static int odu_bridge_disconnect_bridge(void)
{
	int res;

	ODU_BRIDGE_FUNC_ENTRY();

	res = ipa_teardown_sys_pipe(odu_bridge_ctx->odu_prod_hdl);
	if (res)
		ODU_BRIDGE_ERR("teardown ODU PROD failed\n");
	odu_bridge_ctx->odu_prod_hdl = 0;

	res = ipa_teardown_sys_pipe(odu_bridge_ctx->odu_teth_cons_hdl);
	if (res)
		ODU_BRIDGE_ERR("teardown ODU TETH CONS failed\n");
	odu_bridge_ctx->odu_teth_cons_hdl = 0;

	res = ipa_teardown_sys_pipe(odu_bridge_ctx->odu_emb_cons_hdl);
	if (res)
		ODU_BRIDGE_ERR("teardown ODU EMB CONS failed\n");
	odu_bridge_ctx->odu_emb_cons_hdl = 0;

	return 0;
}

/**
 * odu_bridge_disconnect() - Disconnect odu bridge
 *
 * Disconnect all pipes
 *
 * Return codes: 0- success, error otherwise
 */
int odu_bridge_disconnect(void)
{
	int res;

	ODU_BRIDGE_FUNC_ENTRY();

	if (!odu_bridge_ctx) {
		ODU_BRIDGE_ERR("Not initialized\n");
		return -EFAULT;
	}

	if (!odu_bridge_ctx->is_connected) {
		ODU_BRIDGE_ERR("Not connected\n");
		return -EFAULT;
	}

	mutex_lock(&odu_bridge_ctx->lock);
	if (odu_bridge_ctx->mode == ODU_BRIDGE_MODE_ROUTER) {
		res = odu_bridge_disconnect_router();
		if (res) {
			ODU_BRIDGE_ERR("disconnect_router failed %d\n", res);
			goto out;
		}
	} else {
		res = odu_bridge_disconnect_bridge();
		if (res) {
			ODU_BRIDGE_ERR("disconnect_bridge failed %d\n", res);
			goto out;
		}
	}

	odu_bridge_ctx->is_connected = false;
	res = 0;
out:
	mutex_unlock(&odu_bridge_ctx->lock);
	ODU_BRIDGE_FUNC_EXIT();
	return res;
}
EXPORT_SYMBOL(odu_bridge_disconnect);

/**
 * odu_bridge_connect() - Connect odu bridge.
 *
 * Call to the mode-specific connect function for connection IPA pipes
 * Return codes: 0: success
 *		-EINVAL: invalid parameters
 *		-EPERM: Operation not permitted as the bridge is already
 *		connected
 */
int odu_bridge_connect(void)
{
	int res;

	ODU_BRIDGE_FUNC_ENTRY();

	if (!odu_bridge_ctx) {
		ODU_BRIDGE_ERR("Not initialized\n");
		return -EFAULT;
	}

	if (odu_bridge_ctx->is_connected) {
		ODU_BRIDGE_ERR("already connected\n");
		return -EFAULT;
	}

	mutex_lock(&odu_bridge_ctx->lock);
	if (odu_bridge_ctx->mode == ODU_BRIDGE_MODE_ROUTER) {
		res = odu_bridge_connect_router();
		if (res) {
			ODU_BRIDGE_ERR("connect_router failed\n");
			goto bail;
		}
	} else {
		res = odu_bridge_connect_bridge();
		if (res) {
			ODU_BRIDGE_ERR("connect_bridge failed\n");
			goto bail;
		}
	}

	odu_bridge_ctx->is_connected = true;
	res = 0;
bail:
	mutex_unlock(&odu_bridge_ctx->lock);
	ODU_BRIDGE_FUNC_EXIT();
	return res;
}
EXPORT_SYMBOL(odu_bridge_connect);

/**
 * odu_bridge_set_mode() - Set bridge mode to Router/Bridge
 * @mode: mode to be set
 */
static int odu_bridge_set_mode(enum odu_bridge_mode mode)
{
	int res;

	ODU_BRIDGE_FUNC_ENTRY();

	if (mode < 0 || mode >= ODU_BRIDGE_MODE_MAX) {
		ODU_BRIDGE_ERR("Unsupported mode: %d\n", mode);
		return -EFAULT;
	}

	ODU_BRIDGE_DBG_LOW("setting mode: %d\n", mode);
	mutex_lock(&odu_bridge_ctx->lock);

	if (odu_bridge_ctx->mode == mode) {
		ODU_BRIDGE_DBG_LOW("same mode\n");
		res = 0;
		goto bail;
	}

	if (odu_bridge_ctx->is_connected) {
		/* first disconnect the old configuration */
		if (odu_bridge_ctx->mode == ODU_BRIDGE_MODE_ROUTER) {
			res = odu_bridge_disconnect_router();
			if (res) {
				ODU_BRIDGE_ERR("disconnect_router failed\n");
				goto bail;
			}
		} else {
			res = odu_bridge_disconnect_bridge();
			if (res) {
				ODU_BRIDGE_ERR("disconnect_bridge failed\n");
				goto bail;
			}
		}

		/* connect the new configuration */
		if (mode == ODU_BRIDGE_MODE_ROUTER) {
			res = odu_bridge_connect_router();
			if (res) {
				ODU_BRIDGE_ERR("connect_router failed\n");
				goto bail;
			}
		} else {
			res = odu_bridge_connect_bridge();
			if (res) {
				ODU_BRIDGE_ERR("connect_bridge failed\n");
				goto bail;
			}
		}
	}
	odu_bridge_ctx->mode = mode;
	res = 0;
bail:
	mutex_unlock(&odu_bridge_ctx->lock);
	ODU_BRIDGE_FUNC_EXIT();
	return res;
};

/**
 * odu_bridge_set_llv6_addr() - Set link local ipv6 address
 * @llv6_addr: odu network interface link local address
 *
 * This function sets the link local ipv6 address provided by IOCTL
 */
static int odu_bridge_set_llv6_addr(struct in6_addr *llv6_addr)
{
	struct in6_addr llv6_addr_host;

	ODU_BRIDGE_FUNC_ENTRY();

	llv6_addr_host.s6_addr32[0] = ntohl(llv6_addr->s6_addr32[0]);
	llv6_addr_host.s6_addr32[1] = ntohl(llv6_addr->s6_addr32[1]);
	llv6_addr_host.s6_addr32[2] = ntohl(llv6_addr->s6_addr32[2]);
	llv6_addr_host.s6_addr32[3] = ntohl(llv6_addr->s6_addr32[3]);

	memcpy(&odu_bridge_ctx->llv6_addr, &llv6_addr_host,
				sizeof(odu_bridge_ctx->llv6_addr));
	ODU_BRIDGE_DBG_LOW("LLV6 addr: %pI6c\n", &odu_bridge_ctx->llv6_addr);

	ODU_BRIDGE_FUNC_EXIT();

	return 0;
};

static long odu_bridge_ioctl(struct file *filp,
			      unsigned int cmd,
			      unsigned long arg)
{
	int res = 0;
	struct in6_addr llv6_addr;

	ODU_BRIDGE_DBG("cmd=%x nr=%d\n", cmd, _IOC_NR(cmd));

	if ((_IOC_TYPE(cmd) != ODU_BRIDGE_IOC_MAGIC) ||
	    (_IOC_NR(cmd) >= ODU_BRIDGE_IOCTL_MAX)) {
		ODU_BRIDGE_ERR("Invalid ioctl\n");
		return -ENOIOCTLCMD;
	}

	switch (cmd) {
	case ODU_BRIDGE_IOC_SET_MODE:
		ODU_BRIDGE_DBG("ODU_BRIDGE_IOC_SET_MODE ioctl called\n");
		res = odu_bridge_set_mode(arg);
		if (res) {
			ODU_BRIDGE_ERR("Error, res = %d\n", res);
			break;
		}
		break;

	case ODU_BRIDGE_IOC_SET_LLV6_ADDR:
		ODU_BRIDGE_DBG("ODU_BRIDGE_IOC_SET_LLV6_ADDR ioctl called\n");
		res = copy_from_user(&llv6_addr,
			(struct in6_addr *)arg,
			sizeof(llv6_addr));
		if (res) {
			ODU_BRIDGE_ERR("Error, res = %d\n", res);
			res = -EFAULT;
			break;
		}

		res = odu_bridge_set_llv6_addr(&llv6_addr);
		if (res) {
			ODU_BRIDGE_ERR("Error, res = %d\n", res);
			break;
		}
		break;

	default:
		ODU_BRIDGE_ERR("Unknown ioctl: %d\n", cmd);
		WARN_ON(1);
	}

	return res;
}

#ifdef CONFIG_COMPAT
static long compat_odu_bridge_ioctl(struct file *file,
	unsigned int cmd, unsigned long arg)
{
	switch (cmd) {
	case ODU_BRIDGE_IOC_SET_LLV6_ADDR32:
		cmd = ODU_BRIDGE_IOC_SET_LLV6_ADDR;
		break;
	case ODU_BRIDGE_IOC_SET_MODE:
		break;
	default:
		return -ENOIOCTLCMD;
	}
	return odu_bridge_ioctl(file, cmd, (unsigned long)compat_ptr(arg));
}
#endif

#ifdef CONFIG_DEBUG_FS
static struct dentry *dent;
static struct dentry *dfile_stats;
static struct dentry *dfile_mode;

static ssize_t odu_debugfs_stats(struct file *file,
				  char __user *ubuf,
				  size_t count,
				  loff_t *ppos)
{
	int nbytes = 0;

	nbytes += scnprintf(&dbg_buff[nbytes],
			    ODU_MAX_MSG_LEN - nbytes,
			   "UL packets: %lld\n",
			    odu_bridge_ctx->stats.num_ul_packets);
	nbytes += scnprintf(&dbg_buff[nbytes],
			    ODU_MAX_MSG_LEN - nbytes,
			   "DL packets: %lld\n",
			    odu_bridge_ctx->stats.num_dl_packets);
	nbytes += scnprintf(&dbg_buff[nbytes],
			    ODU_MAX_MSG_LEN - nbytes,
			    "LAN packets: %lld\n",
			    odu_bridge_ctx->stats.num_lan_packets);
	return simple_read_from_buffer(ubuf, count, ppos, dbg_buff, nbytes);
}

static ssize_t odu_debugfs_hw_bridge_mode_write(struct file *file,
					const char __user *ubuf,
					size_t count,
					loff_t *ppos)
{
	unsigned long missing;
	enum odu_bridge_mode mode;

	if (count >= sizeof(dbg_buff))
		return -EFAULT;

	missing = copy_from_user(dbg_buff, ubuf, count);
	if (missing)
		return -EFAULT;

	if (count > 0)
		dbg_buff[count-1] = '\0';

	if (strcmp(dbg_buff, "router") == 0) {
		mode = ODU_BRIDGE_MODE_ROUTER;
	} else if (strcmp(dbg_buff, "bridge") == 0) {
		mode = ODU_BRIDGE_MODE_BRIDGE;
	} else {
		ODU_BRIDGE_ERR("Bad mode, got %s,\n"
			 "Use <router> or <bridge>.\n", dbg_buff);
		return count;
	}

	odu_bridge_set_mode(mode);
	return count;
}

static ssize_t odu_debugfs_hw_bridge_mode_read(struct file *file,
					     char __user *ubuf,
					     size_t count,
					     loff_t *ppos)
{
	int nbytes = 0;

	switch (odu_bridge_ctx->mode) {
	case ODU_BRIDGE_MODE_ROUTER:
		nbytes += scnprintf(&dbg_buff[nbytes],
			ODU_MAX_MSG_LEN - nbytes,
			"router\n");
		break;
	case ODU_BRIDGE_MODE_BRIDGE:
		nbytes += scnprintf(&dbg_buff[nbytes],
			ODU_MAX_MSG_LEN - nbytes,
			"bridge\n");
		break;
	default:
		nbytes += scnprintf(&dbg_buff[nbytes],
			ODU_MAX_MSG_LEN - nbytes,
			"mode error\n");
		break;

	}

	return simple_read_from_buffer(ubuf, count, ppos, dbg_buff, nbytes);
}

const struct file_operations odu_stats_ops = {
	.read = odu_debugfs_stats,
};

const struct file_operations odu_hw_bridge_mode_ops = {
	.read = odu_debugfs_hw_bridge_mode_read,
	.write = odu_debugfs_hw_bridge_mode_write,
};

static void odu_debugfs_init(void)
{
	const mode_t read_only_mode = 0444;
	const mode_t read_write_mode = 0666;

	dent = debugfs_create_dir("odu_ipa_bridge", 0);
	if (IS_ERR(dent)) {
		ODU_BRIDGE_ERR("fail to create folder odu_ipa_bridge\n");
		return;
	}

	dfile_stats =
		debugfs_create_file("stats", read_only_mode, dent,
				    0, &odu_stats_ops);
	if (!dfile_stats || IS_ERR(dfile_stats)) {
		ODU_BRIDGE_ERR("fail to create file stats\n");
		goto fail;
	}

	dfile_mode =
		debugfs_create_file("mode", read_write_mode,
				    dent, 0, &odu_hw_bridge_mode_ops);
	if (!dfile_mode ||
	    IS_ERR(dfile_mode)) {
		ODU_BRIDGE_ERR("fail to create file dfile_mode\n");
		goto fail;
	}

	return;
fail:
	debugfs_remove_recursive(dent);
}

static void odu_debugfs_destroy(void)
{
	debugfs_remove_recursive(dent);
}

#else
static void odu_debugfs_init(void) {}
static void odu_debugfs_destroy(void) {}
#endif /* CONFIG_DEBUG_FS */


static const struct file_operations odu_bridge_drv_fops = {
	.owner = THIS_MODULE,
	.unlocked_ioctl = odu_bridge_ioctl,
#ifdef CONFIG_COMPAT
	.compat_ioctl = compat_odu_bridge_ioctl,
#endif
};

/**
 * odu_bridge_tx_dp() - Send skb to ODU bridge
 * @skb: skb to send
 * @metadata: metadata on packet
 *
 * This function handles uplink packet.
 * In Router Mode:
 *	packet is sent directly to IPA.
 * In Router Mode:
 *	packet is classified if it should arrive to network stack.
 *	QMI IP packet should arrive to APPS network stack
 *	IPv6 Multicast packet should arrive to APPS network stack and Q6
 *
 * Return codes: 0- success, error otherwise
 */
int odu_bridge_tx_dp(struct sk_buff *skb, struct ipa_tx_meta *metadata)
{
	struct sk_buff *skb_copied = NULL;
	struct ipv6hdr *ipv6hdr;
	int res;

	ODU_BRIDGE_FUNC_ENTRY();

	switch (odu_bridge_ctx->mode) {
	case ODU_BRIDGE_MODE_ROUTER:
		/* Router mode - pass skb to IPA */
		res = ipa_tx_dp(IPA_CLIENT_ODU_PROD, skb, metadata);
		if (unlikely(res)) {
			ODU_BRIDGE_DBG("tx dp failed %d\n", res);
			goto out;
		}
		odu_bridge_ctx->stats.num_ul_packets++;
		goto out;

	case ODU_BRIDGE_MODE_BRIDGE:
		ipv6hdr = (struct ipv6hdr *)(skb->data + ETH_HLEN);
		if (ipv6hdr->version == 6 &&
		    ODU_BRIDGE_IS_QMI_ADDR(ipv6hdr->daddr)) {
			ODU_BRIDGE_DBG_LOW("QMI packet\n");
			skb_copied = skb_clone(skb, GFP_KERNEL);
			if (unlikely(!skb_copied)) {
				ODU_BRIDGE_ERR("No memory\n");
				return -ENOMEM;
			}
			odu_bridge_ctx->tx_dp_notify(odu_bridge_ctx->priv,
						     IPA_RECEIVE,
						     (unsigned long)skb_copied);
			odu_bridge_ctx->tx_dp_notify(odu_bridge_ctx->priv,
						     IPA_WRITE_DONE,
						     (unsigned long)skb);
			odu_bridge_ctx->stats.num_ul_packets++;
			odu_bridge_ctx->stats.num_lan_packets++;
			res = 0;
			goto out;
		}

		if (ipv6hdr->version == 6 &&
		    ipv6_addr_is_multicast(&ipv6hdr->daddr)) {
			ODU_BRIDGE_DBG_LOW(
				"Multicast pkt, send to APPS and IPA\n");
			skb_copied = skb_clone(skb, GFP_KERNEL);
			if (unlikely(!skb_copied)) {
				ODU_BRIDGE_ERR("No memory\n");
				return -ENOMEM;
			}

			res = ipa_tx_dp(IPA_CLIENT_ODU_PROD, skb, metadata);
			if (unlikely(res)) {
				ODU_BRIDGE_DBG("tx dp failed %d\n", res);
				dev_kfree_skb(skb_copied);
				goto out;
			}

			odu_bridge_ctx->tx_dp_notify(odu_bridge_ctx->priv,
						     IPA_RECEIVE,
						     (unsigned long)skb_copied);
			odu_bridge_ctx->stats.num_ul_packets++;
			odu_bridge_ctx->stats.num_lan_packets++;
			goto out;
		}

		res = ipa_tx_dp(IPA_CLIENT_ODU_PROD, skb, metadata);
		if (unlikely(res)) {
			ODU_BRIDGE_DBG("tx dp failed %d\n", res);
			goto out;
		}
		odu_bridge_ctx->stats.num_ul_packets++;
		goto out;

	default:
		ODU_BRIDGE_ERR("Unsupported mode: %d\n", odu_bridge_ctx->mode);
		WARN_ON(1);
		res = -EFAULT;

	}
out:
	ODU_BRIDGE_FUNC_EXIT();
	return res;
}
EXPORT_SYMBOL(odu_bridge_tx_dp);

static int odu_bridge_add_hdrs(void)
{
	struct ipa_ioc_add_hdr *hdrs;
	struct ipa_hdr_add *ipv4_hdr;
	struct ipa_hdr_add *ipv6_hdr;
	struct ethhdr *eth_ipv4;
	struct ethhdr *eth_ipv6;
	int res;

	ODU_BRIDGE_FUNC_ENTRY();
	hdrs = kzalloc(sizeof(*hdrs) + sizeof(*ipv4_hdr) + sizeof(*ipv6_hdr),
			GFP_KERNEL);
	if (!hdrs) {
		ODU_BRIDGE_ERR("no mem\n");
		res = -ENOMEM;
		goto out;
	}
	ipv4_hdr = &hdrs->hdr[0];
	eth_ipv4 = (struct ethhdr *)(ipv4_hdr->hdr);
	ipv6_hdr = &hdrs->hdr[1];
	eth_ipv6 = (struct ethhdr *)(ipv6_hdr->hdr);
	strlcpy(ipv4_hdr->name, ODU_BRIDGE_IPV4_HDR_NAME,
		IPA_RESOURCE_NAME_MAX);
	memcpy(eth_ipv4->h_source, odu_bridge_ctx->device_ethaddr, ETH_ALEN);
	eth_ipv4->h_proto = htons(ETH_P_IP);
	ipv4_hdr->hdr_len = ETH_HLEN;
	ipv4_hdr->is_partial = 1;
	ipv4_hdr->is_eth2_ofst_valid = 1;
	ipv4_hdr->eth2_ofst = 0;
	strlcpy(ipv6_hdr->name, ODU_BRIDGE_IPV6_HDR_NAME,
		IPA_RESOURCE_NAME_MAX);
	memcpy(eth_ipv6->h_source, odu_bridge_ctx->device_ethaddr, ETH_ALEN);
	eth_ipv6->h_proto = htons(ETH_P_IPV6);
	ipv6_hdr->hdr_len = ETH_HLEN;
	ipv6_hdr->is_partial = 1;
	ipv6_hdr->is_eth2_ofst_valid = 1;
	ipv6_hdr->eth2_ofst = 0;
	hdrs->commit = 1;
	hdrs->num_hdrs = 2;
	res = ipa_add_hdr(hdrs);
	if (res) {
		ODU_BRIDGE_ERR("Fail on Header-Insertion(%d)\n", res);
		goto out_free_mem;
	}
	if (ipv4_hdr->status) {
		ODU_BRIDGE_ERR("Fail on Header-Insertion ipv4(%d)\n",
				ipv4_hdr->status);
		res = ipv4_hdr->status;
		goto out_free_mem;
	}
	if (ipv6_hdr->status) {
		ODU_BRIDGE_ERR("Fail on Header-Insertion ipv6(%d)\n",
				ipv6_hdr->status);
		res = ipv6_hdr->status;
		goto out_free_mem;
	}
	odu_bridge_ctx->odu_br_ipv4_hdr_hdl = ipv4_hdr->hdr_hdl;
	odu_bridge_ctx->odu_br_ipv6_hdr_hdl = ipv6_hdr->hdr_hdl;

	res = 0;
out_free_mem:
	kfree(hdrs);
out:
	ODU_BRIDGE_FUNC_EXIT();
	return res;
}

static void odu_bridge_del_hdrs(void)
{
	struct ipa_ioc_del_hdr *del_hdr;
	struct ipa_hdr_del *ipv4;
	struct ipa_hdr_del *ipv6;
	int result;

	del_hdr = kzalloc(sizeof(*del_hdr) + sizeof(*ipv4) +
			sizeof(*ipv6), GFP_KERNEL);
	if (!del_hdr)
		return;
	del_hdr->commit = 1;
	del_hdr->num_hdls = 2;
	ipv4 = &del_hdr->hdl[0];
	ipv4->hdl = odu_bridge_ctx->odu_br_ipv4_hdr_hdl;
	ipv6 = &del_hdr->hdl[1];
	ipv6->hdl = odu_bridge_ctx->odu_br_ipv6_hdr_hdl;
	result = ipa_del_hdr(del_hdr);
	if (result || ipv4->status || ipv6->status)
		ODU_BRIDGE_ERR("ipa_del_hdr failed");
	kfree(del_hdr);
}

/**
 * odu_bridge_register_properties() - set Tx/Rx properties for ipacm
 *
 * Register the network interface interface with Tx and Rx properties
 * Tx properties are for data flowing from IPA to adapter, they
 * have Header-Insertion properties both for Ipv4 and Ipv6 Ethernet framing.
 * Rx properties are for data flowing from adapter to IPA, they have
 * simple rule which always "hit".
 *
 */
static int odu_bridge_register_properties(void)
{
	struct ipa_tx_intf tx_properties = {0};
	struct ipa_ioc_tx_intf_prop properties[2] = { {0}, {0} };
	struct ipa_ioc_tx_intf_prop *ipv4_property;
	struct ipa_ioc_tx_intf_prop *ipv6_property;
	struct ipa_ioc_rx_intf_prop rx_ioc_properties[2] = { {0}, {0} };
	struct ipa_rx_intf rx_properties = {0};
	struct ipa_ioc_rx_intf_prop *rx_ipv4_property;
	struct ipa_ioc_rx_intf_prop *rx_ipv6_property;
	int res = 0;

	ODU_BRIDGE_FUNC_ENTRY();

	tx_properties.prop = properties;
	ipv4_property = &tx_properties.prop[0];
	ipv4_property->ip = IPA_IP_v4;
	ipv4_property->dst_pipe = IPA_CLIENT_ODU_EMB_CONS;
	ipv4_property->hdr_l2_type = IPA_HDR_L2_ETHERNET_II;
	strlcpy(ipv4_property->hdr_name, ODU_BRIDGE_IPV4_HDR_NAME,
			IPA_RESOURCE_NAME_MAX);
	ipv6_property = &tx_properties.prop[1];
	ipv6_property->ip = IPA_IP_v6;
	ipv6_property->dst_pipe = IPA_CLIENT_ODU_EMB_CONS;
	ipv6_property->hdr_l2_type = IPA_HDR_L2_ETHERNET_II;
	strlcpy(ipv6_property->hdr_name, ODU_BRIDGE_IPV6_HDR_NAME,
			IPA_RESOURCE_NAME_MAX);
	tx_properties.num_props = 2;

	rx_properties.prop = rx_ioc_properties;
	rx_ipv4_property = &rx_properties.prop[0];
	rx_ipv4_property->ip = IPA_IP_v4;
	rx_ipv4_property->attrib.attrib_mask = 0;
	rx_ipv4_property->src_pipe = IPA_CLIENT_ODU_PROD;
	rx_ipv4_property->hdr_l2_type = IPA_HDR_L2_ETHERNET_II;
	rx_ipv6_property = &rx_properties.prop[1];
	rx_ipv6_property->ip = IPA_IP_v6;
	rx_ipv6_property->attrib.attrib_mask = 0;
	rx_ipv6_property->src_pipe = IPA_CLIENT_ODU_PROD;
	rx_ipv6_property->hdr_l2_type = IPA_HDR_L2_ETHERNET_II;
	rx_properties.num_props = 2;

	res = ipa_register_intf(odu_bridge_ctx->netdev_name, &tx_properties,
		&rx_properties);
	if (res) {
		ODU_BRIDGE_ERR("fail on Tx/Rx properties registration %d\n",
									res);
	}

	ODU_BRIDGE_FUNC_EXIT();

	return res;
}

static void odu_bridge_deregister_properties(void)
{
	int res;

	ODU_BRIDGE_FUNC_ENTRY();
	res = ipa_deregister_intf(odu_bridge_ctx->netdev_name);
	if (res)
		ODU_BRIDGE_ERR("Fail on Tx prop deregister %d\n", res);
	ODU_BRIDGE_FUNC_EXIT();
}

/**
 * odu_bridge_init() - Initialize the ODU bridge driver
 * @params: initialization parameters
 *
 * This function initialize all bridge internal data and register odu bridge to
 * kernel for IOCTL and debugfs.
 * Header addition and properties are registered to IPA driver.
 *
 * Return codes: 0: success,
 *		-EINVAL - Bad parameter
 *		Other negative value - Failure
 */
int odu_bridge_init(struct odu_bridge_params *params)
{
	int res;

	ODU_BRIDGE_FUNC_ENTRY();

	if (!params) {
		ODU_BRIDGE_ERR("null pointer params\n");
		return -EINVAL;
	}
	if (!params->netdev_name) {
		ODU_BRIDGE_ERR("null pointer params->netdev_name\n");
		return -EINVAL;
	}
	if (!params->tx_dp_notify) {
		ODU_BRIDGE_ERR("null pointer params->tx_dp_notify\n");
		return -EINVAL;
	}
	if (!params->send_dl_skb) {
		ODU_BRIDGE_ERR("null pointer params->send_dl_skb\n");
		return -EINVAL;
	}
	if (odu_bridge_ctx) {
		ODU_BRIDGE_ERR("Already initialized\n");
		return -EFAULT;
	}
	if (!ipa_is_ready()) {
		ODU_BRIDGE_ERR("IPA is not ready\n");
		return -EFAULT;
	}

	ODU_BRIDGE_DBG("device_ethaddr=%pM\n", params->device_ethaddr);

	odu_bridge_ctx = kzalloc(sizeof(*odu_bridge_ctx), GFP_KERNEL);
	if (!odu_bridge_ctx) {
		ODU_BRIDGE_ERR("kzalloc err.\n");
		return -ENOMEM;
	}

	odu_bridge_ctx->class = class_create(THIS_MODULE, ODU_BRIDGE_DRV_NAME);
	if (!odu_bridge_ctx->class) {
		ODU_BRIDGE_ERR("Class_create err.\n");
		res = -ENODEV;
		goto fail_class_create;
	}

	res = alloc_chrdev_region(&odu_bridge_ctx->dev_num, 0, 1,
				  ODU_BRIDGE_DRV_NAME);
	if (res) {
		ODU_BRIDGE_ERR("alloc_chrdev_region err.\n");
		res = -ENODEV;
		goto fail_alloc_chrdev_region;
	}

	odu_bridge_ctx->dev = device_create(odu_bridge_ctx->class, NULL,
		odu_bridge_ctx->dev_num, odu_bridge_ctx, ODU_BRIDGE_DRV_NAME);
	if (IS_ERR(odu_bridge_ctx->dev)) {
		ODU_BRIDGE_ERR(":device_create err.\n");
		res = -ENODEV;
		goto fail_device_create;
	}

	cdev_init(&odu_bridge_ctx->cdev, &odu_bridge_drv_fops);
	odu_bridge_ctx->cdev.owner = THIS_MODULE;
	odu_bridge_ctx->cdev.ops = &odu_bridge_drv_fops;

	res = cdev_add(&odu_bridge_ctx->cdev, odu_bridge_ctx->dev_num, 1);
	if (res) {
		ODU_BRIDGE_ERR(":cdev_add err=%d\n", -res);
		res = -ENODEV;
		goto fail_cdev_add;
	}

	odu_debugfs_init();

	strlcpy(odu_bridge_ctx->netdev_name, params->netdev_name,
		IPA_RESOURCE_NAME_MAX);
	odu_bridge_ctx->priv = params->priv;
	odu_bridge_ctx->tx_dp_notify = params->tx_dp_notify;
	odu_bridge_ctx->send_dl_skb = params->send_dl_skb;
	memcpy(odu_bridge_ctx->device_ethaddr, params->device_ethaddr,
		ETH_ALEN);
	odu_bridge_ctx->ipa_sys_desc_size = params->ipa_desc_size;
	odu_bridge_ctx->mode = ODU_BRIDGE_MODE_ROUTER;

	mutex_init(&odu_bridge_ctx->lock);

	res = odu_bridge_add_hdrs();
	if (res) {
		ODU_BRIDGE_ERR("fail on odu_bridge_add_hdr %d\n", res);
		goto fail_add_hdrs;
	}

	res = odu_bridge_register_properties();
	if (res) {
		ODU_BRIDGE_ERR("fail on register properties %d\n", res);
		goto fail_register_properties;
	}

	ODU_BRIDGE_FUNC_EXIT();
	return 0;

fail_register_properties:
	odu_bridge_del_hdrs();
fail_add_hdrs:
	odu_debugfs_destroy();
fail_cdev_add:
	device_destroy(odu_bridge_ctx->class, odu_bridge_ctx->dev_num);
fail_device_create:
	unregister_chrdev_region(odu_bridge_ctx->dev_num, 1);
fail_alloc_chrdev_region:
	class_destroy(odu_bridge_ctx->class);
fail_class_create:
	kfree(odu_bridge_ctx);
	odu_bridge_ctx = NULL;
	return res;
}
EXPORT_SYMBOL(odu_bridge_init);

/**
 * odu_bridge_cleanup() - De-Initialize the ODU bridge driver
 *
 * Return codes: 0: success,
 *		-EINVAL - Bad parameter
 *		Other negative value - Failure
 */
int odu_bridge_cleanup(void)
{
	ODU_BRIDGE_FUNC_ENTRY();

	if (!odu_bridge_ctx) {
		ODU_BRIDGE_ERR("Not initialized\n");
		return -EFAULT;
	}

	if (odu_bridge_ctx->is_connected) {
		ODU_BRIDGE_ERR("cannot deinit while bridge is conncetd\n");
		return -EFAULT;
	}

	odu_bridge_deregister_properties();
	odu_bridge_del_hdrs();
	odu_debugfs_destroy();
	cdev_del(&odu_bridge_ctx->cdev);
	device_destroy(odu_bridge_ctx->class, odu_bridge_ctx->dev_num);
	unregister_chrdev_region(odu_bridge_ctx->dev_num, 1);
	class_destroy(odu_bridge_ctx->class);
	ipc_log_context_destroy(odu_bridge_ctx->logbuf);
	ipc_log_context_destroy(odu_bridge_ctx->logbuf_low);
	kfree(odu_bridge_ctx);
	odu_bridge_ctx = NULL;

	ODU_BRIDGE_FUNC_EXIT();
	return 0;
}
EXPORT_SYMBOL(odu_bridge_cleanup);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("ODU bridge driver");
