/* Copyright (c) 2022, Qualcomm Innovation Center, Inc. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 and
 * only version 2 as published by the Free Software Foundation.
 *
 * 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.
 */
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/socket.h>
#include <linux/udp.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <net/net_namespace.h>
#include <net/udp.h>
#include <net/udp_tunnel.h>
#include <linux/net.h>
#include <linux/inet.h>

#include "main.h"
#include "debug.h"
#include "pci.h"
#include "bus.h"

extern struct cnss_plat_data *cnss_get_plat_priv_by_instance_id(
			int instance_id);
struct udp_port_cfg udp_conf;
#define INT_MODULE_PARM(n, v) static int n = v; \
				module_param(n, int, 0444)
#define STRING_MODULE_PARM(s, v) static char *s = v; \
				module_param(s, charp, 0000)

#define Q6STREAM_SIZE   (1024 * 1024)
#define TOTAL_PAGES_PER_DATA    (Q6STREAM_SIZE / PAGE_SIZE)
#define PAGES_PER_DATA  8
#define COMP_PAGES_PER_DATA (TOTAL_PAGES_PER_DATA - PAGES_PER_DATA)


/* insmod ipq_cnss2_stream.ko stream_dst_port=5004
 * stream_dst_addr="X.X.X.X" stream_src_port=5006
 * stream_src_addr="X.X.X.X instance_id=0x3a
 * stream_board_id="epryLd_IWCiRu-h9Aq9Yqg"
 */
INT_MODULE_PARM(stream_src_port, 5005);
MODULE_PARM_DESC(stream_src_port, "UDP src port");
INT_MODULE_PARM(stream_dst_port, 5004);
MODULE_PARM_DESC(stream_dst_port, "UDP dst port");
STRING_MODULE_PARM(stream_src_addr, "127.0.0.1");
MODULE_PARM_DESC(stream_src_addr, "IPv4 src address");
STRING_MODULE_PARM(stream_dst_addr, "127.0.0.1");
MODULE_PARM_DESC(stream_dst_addr, "IPv4 src address");
static char stream_board_id[32];
module_param_string(stream_board_id, stream_board_id,
		 sizeof(stream_board_id), 0);
MODULE_PARM_DESC(stream_board_id, "Boardid");
static unsigned int instance_id = 0x3a;
module_param(instance_id, uint, 0600);
MODULE_PARM_DESC(instance_id, "wlfw service instance id");

unsigned int udp_packet_no;
#define QLD_STREAM_PORT 5004
#define SEQ_NO_SIZE 4

static DEFINE_SPINLOCK(qdss_work_lock);
void qld_stream_work_hdlr(struct work_struct *work)
{
	struct qdss_stream_data *qdss_stream  = container_of(work,
							struct qdss_stream_data,
							qld_stream_work);
	struct cnss_plat_data *plat_priv = NULL;
	int ret = 0;
	int i = 0;
	struct page *transfer_page;
	void *vaddr, *next_page;
	unsigned int need_to_enable;
	unsigned long flags;
	int wrapped_around_seq_no;

	plat_priv = cnss_get_plat_priv_by_instance_id(instance_id);
	need_to_enable = 0;
	spin_lock_irqsave(&qdss_work_lock, flags);
	if (atomic_read(&qdss_stream->seq_no) >= COMP_PAGES_PER_DATA) {
		wrapped_around_seq_no = (atomic_read(&qdss_stream->seq_no) %
					TOTAL_PAGES_PER_DATA);
		need_to_enable = 1;
	}
	spin_unlock_irqrestore(&qdss_work_lock, flags);

	for (i = atomic_read(&qdss_stream->completed_seq_no);
		(i < atomic_read(&qdss_stream->seq_no))
		&& (i < TOTAL_PAGES_PER_DATA) ; i++) {

		vaddr = phys_to_virt(CNSS_ETR_SG_ENT_TO_BLK(((uint32_t *)
					qdss_stream->qdss_vaddr)[i]));
		next_page = vaddr + PAGE_SIZE;
		udp_packet_no++;
		*(unsigned int *)next_page = udp_packet_no;
		memcpy(next_page + SEQ_NO_SIZE, stream_board_id, sizeof(stream_board_id));
		dmac_flush_range((void *)next_page, (void *)next_page
					+ SEQ_NO_SIZE + sizeof(stream_board_id));
		transfer_page = virt_to_page(vaddr);
		ret = kernel_sendpage(qdss_stream->qld_stream_sock,
				transfer_page,
				0, PAGE_SIZE + SEQ_NO_SIZE + sizeof(stream_board_id),
				MSG_DONTWAIT);
		if (ret < PAGE_SIZE + SEQ_NO_SIZE)
			cnss_pr_err("ERROR: Can't send 4096 bytes %d\n", ret);
		atomic_inc(&qdss_stream->completed_seq_no);
	}

	if (need_to_enable) {
		spin_lock_irqsave(&qdss_work_lock, flags);
		if (atomic_read(&qdss_stream->completed_seq_no)
				== TOTAL_PAGES_PER_DATA) {
			atomic_set(&qdss_stream->completed_seq_no, 0);
			atomic_set(&qdss_stream->seq_no, wrapped_around_seq_no);
		}
		spin_unlock_irqrestore(&qdss_work_lock, flags);
	}
}
EXPORT_SYMBOL(qld_stream_work_hdlr);

int setup_kernel_qld_socket(struct cnss_plat_data *plat_priv)
{
	struct socket *sock;
	struct qdss_stream_data *qdss_stream;
	int err = 0;

	memset(&udp_conf, 0, sizeof(udp_conf));
	udp_conf.family = AF_INET;
	udp_conf.local_ip.s_addr = in_aton(stream_src_addr);
	udp_conf.local_udp_port = htons(stream_src_port);
	udp_conf.peer_udp_port = htons(stream_dst_port);
	udp_conf.use_udp_checksums = 1;
	udp_conf.peer_ip.s_addr = in_aton(stream_dst_addr);

	/* Open QLD stream socket */
	err = udp_sock_create(&init_net, &udp_conf, &sock);
	if (err < 0) {
		cnss_pr_err("ERROR: Could not create QLD STREAM socket\n");
		return -err;
	}
	qdss_stream = &plat_priv->qdss_stream;
	qdss_stream->qld_stream_sock = sock;
	INIT_WORK(&qdss_stream->qld_stream_work, qld_stream_work_hdlr);

	return 0;
}

static void __exit cnss_stream_fini(void)
{
	struct cnss_plat_data *plat_priv = NULL;
	struct qdss_stream_data *qdss_stream;

	plat_priv = cnss_get_plat_priv_by_instance_id(instance_id);
	if (!plat_priv) {
		cnss_pr_err("plat_priv is NULL\n");
		return;
	}
	qdss_stream = &plat_priv->qdss_stream;
	flush_work(&qdss_stream->qld_stream_work);
	kernel_sock_shutdown(qdss_stream->qld_stream_sock, SHUT_RDWR);
	sock_release(qdss_stream->qld_stream_sock);
	return;
}

static int __init cnss_stream_init(void)
{
	struct cnss_plat_data *plat_priv = NULL;

	plat_priv = cnss_get_plat_priv_by_instance_id(instance_id);

	if (!plat_priv) {
		cnss_pr_err("plat_priv is NULL\n");
		return 0;
	}
	setup_kernel_qld_socket(plat_priv);
	return 0;
}
module_init(cnss_stream_init);
module_exit(cnss_stream_fini);

MODULE_LICENSE("GPL v2");