#ifdef WL_EXT_GENL
#include <bcmendian.h>
#include <wl_android.h>
#include <dhd_config.h>
#include <net/genetlink.h>

#define AGENL_ERROR(name, arg1, args...) \
	do { \
		if (android_msg_level & ANDROID_ERROR_LEVEL) { \
			printf("[%s] AGENL-ERROR) %s : " arg1, name, __func__, ## args); \
		} \
	} while (0)
#define AGENL_TRACE(name, arg1, args...) \
	do { \
		if (android_msg_level & ANDROID_TRACE_LEVEL) { \
			printf("[%s] AGENL-TRACE) %s : " arg1, name, __func__, ## args); \
		} \
	} while (0)
#define AGENL_INFO(name, arg1, args...) \
	do { \
		if (android_msg_level & ANDROID_INFO_LEVEL) { \
			printf("[%s] AGENL-INFO) %s : " arg1, name, __func__, ## args); \
		} \
	} while (0)

#define htod32(i) i
#define htod16(i) i
#define dtoh32(i) i
#define dtoh16(i) i

#ifdef SENDPROB
#define MGMT_PROBE_REQ 0x40
#define MGMT_PROBE_RES 0x50
#endif

enum {
	__GENL_CUSTOM_ATTR_INVALID,
	GENL_CUSTOM_ATTR_MSG,	/* message */
	__GENL_CUSTOM_ATTR_MAX,
};

enum {
	__GENLL_CUSTOM_COMMAND_INVALID,
	GENL_CUSTOM_COMMAND_BIND,	/* bind */
	GENL_CUSTOM_COMMAND_SEND,	/* user -> kernel */
	GENL_CUSTOM_COMMAND_RECV,	/* kernel -> user */
	__GENL_CUSTOM_COMMAND_MAX,
};

#if defined(ALIBABA_ZEROCONFIG)
#define GENL_FAMILY_NAME	"WIFI_NL_CUSTOM"
#define PROBE_RSP_DST_MAC_OFFSET	4
#define PROBE_RSP_VNDR_ID_OFFSET	55
#else
#define GENL_FAMILY_NAME	"WLAN_NL_CUSTOM"
#define PROBE_RSP_DST_MAC_OFFSET	4
#define PROBE_RSP_VNDR_ID_OFFSET	DOT11_MGMT_HDR_LEN
#endif
#define PROBE_RSP_VNDR_LEN_OFFSET	(PROBE_RSP_VNDR_ID_OFFSET+1)
#define PROBE_RSP_VNDR_OUI_OFFSET	(PROBE_RSP_VNDR_ID_OFFSET+2)
#define MAX_CUSTOM_PKT_LENGTH	2048
#define GENL_CUSTOM_ATTR_MAX	(__GENL_CUSTOM_ATTR_MAX - 1)
#define GENLMSG_UNICAST_RETRY_LIMIT 5

typedef struct genl_params {
	struct net_device *dev;
	bool bind;
	int pm;
	int bind_pid;
	int send_retry_cnt;
} genl_params_t;

struct genl_params *g_zconf = NULL;

static int wl_ext_genl_bind(struct sk_buff *skb, struct genl_info *info);
static int wl_ext_genl_recv(struct sk_buff *skb, struct genl_info *info);
static int wl_ext_genl_send(struct genl_params *zconf, struct net_device *dev,
	char* buf, int buf_len);

static struct nla_policy wl_ext_genl_policy[GENL_CUSTOM_ATTR_MAX + 1] = {
	[GENL_CUSTOM_ATTR_MSG] = {.type = NLA_NUL_STRING},
};

static struct genl_ops wl_ext_genl_ops[] = {
	{
		.cmd = GENL_CUSTOM_COMMAND_BIND,
		.flags = 0,
		.policy = wl_ext_genl_policy,
		.doit = wl_ext_genl_bind,
		.dumpit = NULL,
	},
	{
		.cmd = GENL_CUSTOM_COMMAND_SEND,
		.flags = 0,
		.policy = wl_ext_genl_policy,
		.doit = wl_ext_genl_recv,
		.dumpit = NULL,
	},
};

static struct genl_family wl_ext_genl_family = {
	.id = GENL_ID_GENERATE,
	.hdrsize = 0,
	.name = GENL_FAMILY_NAME,
	.version = 1,
	.maxattr = GENL_CUSTOM_ATTR_MAX,
};

#ifdef SENDPROB
static int
wl_ext_add_del_ie_hex(struct net_device *dev, uint pktflag,
	char *ie_data, int ie_len, const char* add_del_cmd)
{
	vndr_ie_setbuf_t *vndr_ie = NULL;
	char iovar_buf[WLC_IOCTL_SMLEN]="\0";
	int tot_len = 0, iecount;
	int err = -1;

	if (!ie_len) {
		AGENL_ERROR(dev->name, "wrong ie_len %d\n", ie_len);
		goto exit;
	}

	tot_len = (int)(sizeof(vndr_ie_setbuf_t) + (ie_len));
	vndr_ie = (vndr_ie_setbuf_t *) kzalloc(tot_len, GFP_KERNEL);
	if (!vndr_ie) {
		AGENL_ERROR(dev->name, "IE memory alloc failed\n");
		err = -ENOMEM;
		goto exit;
	}

	/* Copy the vndr_ie SET command ("add"/"del") to the buffer */
	strncpy(vndr_ie->cmd, add_del_cmd, VNDR_IE_CMD_LEN - 1);
	vndr_ie->cmd[VNDR_IE_CMD_LEN - 1] = '\0';

	/* Set the IE count - the buffer contains only 1 IE */
	iecount = htod32(1);
	memcpy((void *)&vndr_ie->vndr_ie_buffer.iecount, &iecount, sizeof(s32));

	/* Set packet flag to indicate that BEACON's will contain this IE */
	pktflag = htod32(pktflag);
	memcpy((void *)&vndr_ie->vndr_ie_buffer.vndr_ie_list[0].pktflag, &pktflag,
		sizeof(u32));

	/* Set the IE ID */
	vndr_ie->vndr_ie_buffer.vndr_ie_list[0].vndr_ie_data.id = (uchar)DOT11_MNG_VS_ID;

	/* Set the IE LEN */
	vndr_ie->vndr_ie_buffer.vndr_ie_list[0].vndr_ie_data.len = ie_len;

	/* Set the IE OUI and DATA */
	memcpy((char *)vndr_ie->vndr_ie_buffer.vndr_ie_list[0].vndr_ie_data.oui, ie_data, ie_len);

	err = wldev_iovar_setbuf(dev, "vndr_ie", vndr_ie, tot_len,
		iovar_buf, sizeof(iovar_buf), NULL);
	if (err != 0)
		AGENL_ERROR(dev->name, "vndr_ie, ret=%d\n", err);

exit:
	if (vndr_ie) {
		kfree(vndr_ie);
	}
	return err;
}

static int
wl_ext_send_probersp(struct net_device *dev, char* buf, int buf_len)
{
	char addr[ETHER_ADDR_LEN], *pVndrOUI;
	char iovar_buf[WLC_IOCTL_SMLEN]="\0";
	int err = -1, ie_len;

	if (buf == NULL || buf_len <= 0){
		AGENL_ERROR(dev->name, "buf is NULL or buf_len <= 0\n");
		return -1;
	}

	AGENL_TRACE(dev->name, "Enter\n");

	memcpy(addr, (buf+PROBE_RSP_DST_MAC_OFFSET), ETHER_ADDR_LEN);
	pVndrOUI = (buf+PROBE_RSP_VNDR_OUI_OFFSET);
	ie_len = *(buf+PROBE_RSP_VNDR_LEN_OFFSET);

	if (ie_len > (buf_len-PROBE_RSP_VNDR_OUI_OFFSET)) {
		AGENL_ERROR(dev->name, "wrong vendor ie len %d\n", ie_len);
		return -1;
	}

	err = wl_ext_add_del_ie_hex(dev, VNDR_IE_PRBRSP_FLAG, pVndrOUI, ie_len, "add");
	if (err)
		goto exit;

	err = wldev_iovar_setbuf(dev, "send_probresp", addr, ETHER_ADDR_LEN,
		iovar_buf, sizeof(iovar_buf), NULL);
	if (err != 0)
		AGENL_ERROR(dev->name, "vndr_ie, ret=%d\n", err);

	OSL_SLEEP(100);
	wl_ext_add_del_ie_hex(dev, VNDR_IE_PRBRSP_FLAG, pVndrOUI, ie_len, "del");

exit:
	return err;
}

static int
wl_ext_set_probreq(struct net_device *dev, bool set)
{
	int bytes_written = 0;
	char recv_probreq[32];

	AGENL_TRACE(dev->name, "Enter\n");

	if (set) {
		sprintf(recv_probreq, "wl recv_probreq 1");
		wl_android_ext_priv_cmd(dev, recv_probreq, 0, &bytes_written);
	} else {
		sprintf(recv_probreq, "wl recv_probreq 0");
		wl_android_ext_priv_cmd(dev, recv_probreq, 0, &bytes_written);
	}

	return 0;
}

void
wl_ext_probreq_event(struct net_device *dev, void *argu,
	const wl_event_msg_t *e, void *data)
{
	struct genl_params *zconf = (struct genl_params *)argu;
	int i, ret = 0, num_ie = 0, totlen;
	uint32 event_len = 0;
	char *buf, *pbuf;
	uint rem_len, buflen = MAX_CUSTOM_PKT_LENGTH;
	uint32 event_id[] = {DOT11_MNG_VS_ID};
	uint32 datalen = ntoh32(e->datalen);
	bcm_tlv_t *ie;

	AGENL_TRACE(dev->name, "Enter\n");

	rem_len = buflen;
	buf = kzalloc(MAX_CUSTOM_PKT_LENGTH, GFP_KERNEL);
	if (unlikely(!buf)) {
		AGENL_ERROR(dev->name, "Could not allocate buf\n");
		return;
	}

	// copy mgmt header
	pbuf = buf;
	memcpy(pbuf, data, DOT11_MGMT_HDR_LEN);
	rem_len -= (DOT11_MGMT_HDR_LEN+1);
	datalen -= DOT11_MGMT_HDR_LEN;
	data += DOT11_MGMT_HDR_LEN;

	// copy IEs
	pbuf = buf + DOT11_MGMT_HDR_LEN;
#if 1 // non-sort by id
	ie = (bcm_tlv_t*)data;
	totlen = datalen;
	while (ie && totlen >= TLV_HDR_LEN) {
		int ie_id = -1;
		int ie_len = ie->len + TLV_HDR_LEN;
		for (i=0; i<sizeof(event_id)/sizeof(event_id[0]); i++) {
			if (ie->id == event_id[i]) {
				ie_id = ie->id;
				break;
			}
		}
		if ((ie->id == ie_id) && (totlen >= ie_len) && (rem_len >= ie_len)) {
			memcpy(pbuf, ie, ie_len);
			pbuf += ie_len;
			rem_len -= ie_len;
			num_ie++;
		}
		ie = (bcm_tlv_t*)((uint8*)ie + ie_len);
		totlen -= ie_len;
	}
#else // sort by id
	for (i = 0; i < sizeof(event_id)/sizeof(event_id[0]); i++) {
		void *pdata = data;
		int data_len = datalen;
		while (rem_len > 0) {
			ie = bcm_parse_tlvs(pdata, data_len, event_id[i]);
			if (!ie)
				break;
			if (rem_len < (ie->len+TLV_HDR_LEN)) {
				ANDROID_TRACE(("%s: buffer is not enough\n", __FUNCTION__));
				break;
			}
			memcpy(pbuf, ie, min(ie->len+TLV_HDR_LEN, rem_len));
			pbuf += (ie->len+TLV_HDR_LEN);
			rem_len -= (ie->len+TLV_HDR_LEN);
			data_len -= (((void *)ie-pdata) + (ie->len+TLV_HDR_LEN));
			pdata = (char *)ie + (ie->len+TLV_HDR_LEN);
			num_ie++;
		}
	}
#endif
	if (num_ie) {
		event_len = buflen - rem_len;
		AGENL_INFO(dev->name, "num_ie=%d\n", num_ie);
		if (android_msg_level & ANDROID_INFO_LEVEL)
			prhex("buf", buf, event_len);
		ret = wl_ext_genl_send(zconf, dev, buf, event_len);
	}

	if(buf)
		kfree(buf);
	return;
}
#endif

static int
wl_ext_genl_recv(struct sk_buff *skb, struct genl_info *info)
{
	struct genl_params *zconf = g_zconf;
	struct net_device *dev;
	struct nlattr *na;
	char* pData = NULL;
	int DataLen = 0;

	if (info == NULL) {
		AGENL_ERROR(dev->name, "genl_info is NULL\n");
		return -1;
	}

	if (zconf == NULL) {
		AGENL_ERROR("wlan", "g_zconf is NULL\n");
		return -1;
	}
	dev = zconf->dev;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 7, 0)
	AGENL_TRACE(dev->name, "Enter snd_portid=%d\n", info->snd_portid);
#else
	AGENL_TRACE(dev->name, "Enter\n");
#endif
	na = info->attrs[GENL_CUSTOM_ATTR_MSG];

	if (na) {
		pData = (char*) nla_data(na);
		DataLen = nla_len(na);
		AGENL_INFO(dev->name, "nla_len(na) : %d\n", DataLen);
		if (android_msg_level & ANDROID_INFO_LEVEL)
			prhex("nla_data(na)", pData, DataLen);
	}

#ifdef SENDPROB
	if(*pData == MGMT_PROBE_RES) {
		wl_ext_send_probersp(dev, pData, DataLen);
	} else if(*pData == MGMT_PROBE_REQ) {
		AGENL_ERROR(dev->name, "probe req\n");
	} else {
		AGENL_ERROR(dev->name, "Unexpected pkt %d\n", *pData);
		if (android_msg_level & ANDROID_INFO_LEVEL)
			prhex("nla_data(na)", pData, DataLen);
	}
#endif

	return 0;
}

static int
wl_ext_genl_send(struct genl_params *zconf, struct net_device *dev,
	char* buf, int buf_len)
{
	struct sk_buff *skb = NULL;
	char* msg_head = NULL;
	int ret = -1;
	int bytes_written = 0;
	char recv_probreq[32];

	if (zconf->bind_pid == -1) {
		AGENL_ERROR(dev->name, "There is no binded process\n");
		return -1;
	}

	if(buf == NULL || buf_len <= 0) {
		AGENL_ERROR(dev->name, "buf is NULL or buf_len : %d\n", buf_len);
		return -1;
	}

	skb = genlmsg_new(MAX_CUSTOM_PKT_LENGTH, GFP_KERNEL);

	if (skb) {
		msg_head = genlmsg_put(skb, 0, 0, &wl_ext_genl_family, 0, GENL_CUSTOM_COMMAND_RECV);
		if (msg_head == NULL) {
			nlmsg_free(skb);
			AGENL_ERROR(dev->name, "genlmsg_put fail\n");
			return -1;
		}

		ret = nla_put(skb, GENL_CUSTOM_ATTR_MSG, buf_len, buf);
		if (ret != 0) {
			nlmsg_free(skb);
			AGENL_ERROR(dev->name, "nla_put fail : %d\n", ret);
			return ret;
		}

		genlmsg_end(skb, msg_head);

		/* sending message */
		AGENL_TRACE(dev->name, "send to process %d\n", zconf->bind_pid);
		ret = genlmsg_unicast(&init_net, skb, zconf->bind_pid);
		if (ret != 0) {
			AGENL_ERROR(dev->name, "genlmsg_unicast fail : %d\n", ret);
			zconf->send_retry_cnt++;
			if(zconf->send_retry_cnt >= GENLMSG_UNICAST_RETRY_LIMIT) {
				AGENL_ERROR(dev->name, "Exceeding retry cnt %d, Unbind pid : %d\n",
					zconf->send_retry_cnt, zconf->bind_pid);
				zconf->bind_pid = -1;
				sprintf(recv_probreq, "wl recv_probreq 0");
				wl_android_ext_priv_cmd(dev, recv_probreq, 0, &bytes_written);
			}
			return ret;
		}
	} else {
		AGENL_ERROR(dev->name, "genlmsg_new fail\n");
		return -1;
	}

	zconf->send_retry_cnt = 0;

	return 0;
}

static int
wl_ext_genl_bind(struct sk_buff *skb, struct genl_info *info)
{
	struct genl_params *zconf = g_zconf;
	struct net_device *dev;
	struct dhd_pub *dhd;
	struct nlattr *na;
	bool bind;
	char* pData = NULL;
	int DataLen = 0;

	if (info == NULL) {
		AGENL_ERROR("wlan", "genl_info is NULL\n");
		return -1;
	}

	if (zconf == NULL) {
		AGENL_ERROR("wlan", "zconf is NULL\n");
		return -1;
	}
	dev = zconf->dev;
	dhd = dhd_get_pub(dev);

	AGENL_TRACE(dev->name, "Enter\n");

	na = info->attrs[GENL_CUSTOM_ATTR_MSG];
	if (na) {
		pData = (char*) nla_data(na);
		DataLen = nla_len(na);
		AGENL_INFO(dev->name, "nla_len(na) : %d\n", DataLen);
		if (android_msg_level & ANDROID_INFO_LEVEL)
			prhex("nla_data(na)", pData, DataLen);
	}

	if (strcmp(pData, "BIND") == 0) {
		bind = TRUE;
	} else if (strcmp(pData, "UNBIND") == 0) {
		bind = FALSE;
	} else {
		AGENL_ERROR(dev->name, "Unknown cmd %s\n", pData);
		return -1;
	}

	if (bind == zconf->bind) {
		AGENL_TRACE(dev->name, "Already %s\n", bind?"BIND":"UNBIND");
		return 0;
	}

	if (bind) {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 7, 0)
		zconf->bind_pid = info->snd_portid;
#endif
		AGENL_TRACE(dev->name, "BIND pid = %d\n", zconf->bind_pid);
#ifdef SENDPROB
		wl_ext_set_probreq(dev, TRUE);
#endif
		zconf->bind = TRUE;
		zconf->pm = dhd->conf->pm;
		dhd->conf->pm = PM_OFF;
	} else {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 7, 0)
		AGENL_TRACE(dev->name, "UNBIND snd_portid = %d\n", info->snd_portid);
#else
		AGENL_TRACE(dev->name, "UNBIND pid = %d\n", zconf->bind_pid);
#endif
		zconf->bind_pid = -1;
#ifdef SENDPROB
		wl_ext_set_probreq(dev, FALSE);
#endif
		dhd->conf->pm = zconf->pm;
		zconf->bind = FALSE;
	}

	return 0;
}

int
wl_ext_genl_init(struct net_device *net)
{
	struct dhd_pub *dhd = dhd_get_pub(net);
	struct genl_params *zconf = dhd->zconf;
	int ret = 0;

	AGENL_TRACE(net->name, "Enter falimy name: \"%s\"\n", wl_ext_genl_family.name);

	zconf = kzalloc(sizeof(struct genl_params), GFP_KERNEL);
	if (unlikely(!zconf)) {
		AGENL_ERROR(net->name, "Could not allocate zconf\n");
		return -ENOMEM;
	}
	dhd->zconf = (void *)zconf;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 10, 0)
	ret = genl_register_family(&wl_ext_genl_family);
	//fix me: how to attach wl_ext_genl_ops
	ret = -1;
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(3, 13, 0)
	ret = genl_register_family_with_ops(&wl_ext_genl_family, wl_ext_genl_ops);
#else
	ret = genl_register_family_with_ops(&wl_ext_genl_family, wl_ext_genl_ops,
		ARRAY_SIZE(wl_ext_genl_ops));
#endif
	if (ret != 0) {
		AGENL_ERROR(net->name, "GE_NELINK family registration fail\n");
		goto err;
	}
	zconf->bind_pid = -1;
#ifdef SENDPROB
	ret = wl_ext_event_register(net, dhd, WLC_E_PROBREQ_MSG, wl_ext_probreq_event,
		zconf, PRIO_EVENT_IAPSTA);
	if (ret)
		goto err;
#endif
	zconf->dev = net;
	g_zconf = zconf;

	return ret;
err:
	if(zconf)
		kfree(zconf);
	return ret;
}

void
wl_ext_genl_deinit(struct net_device *net)
{
	struct dhd_pub *dhd = dhd_get_pub(net);
	struct genl_params *zconf = dhd->zconf;

	AGENL_TRACE(net->name, "Enter\n");

#ifdef SENDPROB
	wl_ext_event_deregister(net, dhd, WLC_E_PROBREQ_MSG, wl_ext_probreq_event);
#endif

	genl_unregister_family(&wl_ext_genl_family);
	if(zconf != NULL) {
		kfree(dhd->zconf);
		dhd->zconf = NULL;
	}
	g_zconf = NULL;

}
#endif
