/*
 * e1oip.c - Actual TDM/E1oIP handling within OCTOI
 *
 * (C) 2022 by Harald Welte <laforge@osmocom.org>
 *
 * All Rights Reserved
 *
 * SPDX-License-Identifier: GPL-2.0+
 *
 * 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.
 */

#include <time.h>
#include <errno.h>
#include <unistd.h>
#include <stdint.h>
#include <inttypes.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>

#include <talloc.h>

#include <osmocom/core/isdnhdlc.h>
#include <osmocom/core/utils.h>
#include <osmocom/core/stats.h>
#include <osmocom/core/rate_ctr.h>
#include <osmocom/core/msgb.h>
#include <osmocom/core/socket.h>
#include <osmocom/core/sockaddr_str.h>

#include <osmocom/octoi/e1oip_proto.h>

#include "octoi_sock.h"
#include "frame_fifo.h"
#include "e1oip.h"

static const struct rate_ctr_desc iline_ctr_description[] = {
	[LINE_CTR_E1oIP_UNDERRUN] = { "e1oip:underrun", "Frames underrun / slipped in IP->E1 direction"},
	[LINE_CTR_E1oIP_SUBSTITUTED] = { "e1oip:substituted", "Frames substituted in E1->IP direction"},
	[LINE_CTR_E1oIP_E1T_OVERFLOW] = { "e1oip:e1t_overflow", "Frames overflowing the RIFO in IP->E1 direction"},
	[LINE_CTR_E1oIP_E1O_OVERFLOW] = { "e1oip:e1o_overflow", "Frames overflowed in E1->IP direction"},
	[LINE_CTR_E1oIP_RX_OUT_OF_ORDER] = { "e1oip:rx:pkt_out_of_order", "Packets out-of-order in IP->E1 direction"},
	[LINE_CTR_E1oIP_RX_OUT_OF_WIN] = { "e1oip:rx:pkt_out_of_win", "Packets out-of-rx-window in IP->E1 direction"},
	[LINE_CTR_E1oIP_CONNECT_ACCEPT] = { "e1oip:connect_accepted", "OCTOI connections entering accepted state" },
	[LINE_CTR_E1oIP_RX_BYTES] = { "e1oip:rx:bytes", "Number of bytes received including UDP+IP header" },
	[LINE_CTR_E1oIP_RX_PACKETS] = { "e1oip:rx:packets", "Number of UDP packets received" },
	[LINE_CTR_E1oIP_TX_BYTES] = { "e1oip:tx:bytes", "Number of bytes transmitted including UDP+IP header" },
	[LINE_CTR_E1oIP_TX_PACKETS] = { "e1oip:tx:packets", "Number of UDP packets transmitted" },
};

static const struct rate_ctr_group_desc iline_ctrg_desc = {
	.group_name_prefix = "e1oip_line",
	.group_description = "Counters for E1oIP line",
	.class_id = OSMO_STATS_CLASS_GLOBAL,
	.num_ctr = ARRAY_SIZE(iline_ctr_description),
	.ctr_desc = iline_ctr_description,
};

static const struct osmo_stat_item_desc iline_stat_description[] = {
	[LINE_STAT_E1oIP_RTT] = { "e1oip:rtt", "Round Trip Time (in us)" },
	[LINE_STAT_E1oIP_E1O_FIFO] = { "e1oip:e1o_fifo_level", "E1 originated FIFO level" },
	[LINE_STAT_E1oIP_E1T_FIFO] = { "e1oip:e1t_fifo_level", "E1 terminated FIFO level" },
	[LINE_STAT_E1oIP_E1O_TS] = { "e1oip:e1o_ts_active", "E1 timeslots active in E1->IP direction" },
	[LINE_STAT_E1oIP_E1T_TS] = { "e1oip:e1t_ts_active", "E1 timeslots active in IP->E1 direction" },
};

static const struct osmo_stat_item_group_desc iline_stats_desc = {
	.group_name_prefix = "e1oip_line",
	.group_description = "Stat items for E1oIP line",
	.class_id = OSMO_STATS_CLASS_GLOBAL,
	.num_items = ARRAY_SIZE(iline_stat_description),
	.item_desc = iline_stat_description,
};


/* E1 -> IP FIFO threshold reached: send one packet */
static void fifo_threshold_cb(struct frame_fifo *fifo, unsigned int frames, void *priv)
{
	struct e1oip_line *iline = priv;
	struct octoi_peer *peer = iline->peer;
	struct msgb *msg;
	struct e1oip_tdm_hdr *eith;
	unsigned int n_frames = fifo->threshold;
	uint8_t buf[n_frames][BYTES_PER_FRAME];
	const uint8_t *ref_frame;
	uint32_t ts_mask = 0;
	unsigned int num_ts = 0;
	unsigned int i;
	uint8_t *cur;
	int rc;

	msg = msgb_alloc_c(iline, 2048, "E1oIP UDP Tx");
	OSMO_ASSERT(msg);
	eith = (struct e1oip_tdm_hdr *) msgb_put(msg, sizeof(*eith));

	eith->frame_nr = htons(iline->e1o.next_seq);

	//printf("%s: n_frames=%u\n", __func__, n_frames);

	/* first pull all the frames to a local buffer */
	for (i = 0; i < n_frames; i++) {
		rc = frame_fifo_out(&iline->e1o.fifo, buf[i]);
		if (rc < 0) {
			/* this situation cannot really happen: The FIFO called us that
			 * a certain threshold is reached, but now it cannot provide
			 * frames? */
			LOGPEER(peer, LOGL_ERROR,
				"frame_fifo_out failure for frame %u/%u\n", iline->e1o.next_seq + i, i);
		}
	}
	iline_stat_set(iline, LINE_STAT_E1oIP_E1O_FIFO, frame_fifo_frames(&iline->e1o.fifo));

	/* then compute the ts_mask */
	for (i = 0, ref_frame = iline->e1o.last_frame; i < n_frames; i++, ref_frame = buf[i-1]) {
		/* FIXME: what to do about TS0? */
		for (unsigned int j = 1; j < BYTES_PER_FRAME; j++) {
			if (buf[i][j] != ref_frame[j])
				ts_mask |= (1U << j);
		}
	}
	eith->ts_mask = htonl(ts_mask);

	for (i = 0; i < BYTES_PER_FRAME; i++) {
		if (ts_mask & (1U << i))
			num_ts++;
	}
	iline_stat_set(iline, LINE_STAT_E1oIP_E1O_TS, num_ts);

	/* finally, encode the payload */
	if (num_ts == 0) {
		/* explicitly encode the number of frames, as the receiver can not determine it
		 * if we don't include any data */
		msgb_put_u8(msg, n_frames);
	} else {
		cur = msgb_put(msg, num_ts * n_frames);
		for (i = 0; i < n_frames; i++) {
			for (unsigned int j = 0; j < BYTES_PER_FRAME; j++) {
				if (ts_mask & (1U << j))
					*cur++ = buf[i][j];
			}
		}
	}

	/* send the packet to the peer */
	octoi_tx(peer, E1OIP_MSGT_TDM_DATA, 0, msgb_data(msg), msgb_length(msg));
	iline_ctr_add(iline, LINE_CTR_E1oIP_TX_PACKETS, 1);
	iline_ctr_add(iline, LINE_CTR_E1oIP_TX_BYTES, peer->sock->iph_udph_size + msgb_length(msg));
	msgb_free(msg);

	/* update the local state */
	iline->e1o.next_seq += n_frames;
	if (n_frames)
		memcpy(iline->e1o.last_frame, buf[n_frames-1], BYTES_PER_FRAME);
}

/* build a table indexed by offset inside the EoIP TDM frame resulting in TS number */
static unsigned int ts_mask2idx(uint8_t *out, uint32_t ts_mask)
{
	unsigned int i;
	uint8_t *cur = out;

	memset(out, 0xff, BYTES_PER_FRAME);

	for (i = 0; i < BYTES_PER_FRAME; i++) {
		if (ts_mask & (1U << i))
			*cur++ = i;
	}

	return (cur - out);
}

/* An E1OIP_MSGT_TDM_DATA message was received from a remote IP peer */
int e1oip_rcvmsg_tdm_data(struct e1oip_line *iline, struct msgb *msg)
{
	const int WIN = 8000;
	struct octoi_peer *peer = iline->peer;
	const struct e1oip_tdm_hdr *e1th;
	uint16_t frame_nr;
	uint32_t fn32;
	bool update_next;
	uint32_t ts_mask;
	uint8_t idx2ts[BYTES_PER_FRAME];
	unsigned int n_frames;
	uint8_t frame_buf[BYTES_PER_FRAME];
	unsigned int num_ts;
	uint16_t exp_next_seq = iline->e1t.next_fn32 & 0xffff;
	struct timespec ts;

	/* update the timestamp at which we last received data from this peer */
	clock_gettime(CLOCK_MONOTONIC, &ts);
	peer->last_rx_tdm = ts.tv_sec;

	if (!peer->tdm_permitted)
		return -EPERM;

	if (msgb_l2len(msg) < sizeof(*e1th))
		return -EINVAL;

	/* read header */
	e1th = (const struct e1oip_tdm_hdr *) msgb_l2(msg);
	msg->l3h = msgb_l2(msg) + sizeof(*e1th);
	frame_nr = ntohs(e1th->frame_nr);
	ts_mask = ntohl(e1th->ts_mask);

	if (frame_nr != exp_next_seq) {
		uint16_t frame_nr_ofs;

		LOGPEER(peer, LOGL_NOTICE, "RxIP: frame_nr=%u, but expected %u\n",
			frame_nr, exp_next_seq);

		frame_nr_ofs = frame_nr - (exp_next_seq - WIN);
		if (frame_nr_ofs > (2 * WIN)) {
			/* Outside window, throw packet away */
			LOGPEER(peer, LOGL_NOTICE, "RxIP: frame_nr=%u at exp_next_fn32=%u; "
				"received frame outside +/- 1s window of expected frame\n",
				frame_nr, iline->e1t.next_fn32);
			iline_ctr_add(iline, LINE_CTR_E1oIP_RX_OUT_OF_WIN, 1);
			return -EINVAL;
		}

		iline_ctr_add(iline, LINE_CTR_E1oIP_RX_OUT_OF_ORDER, 1);

		fn32 = iline->e1t.next_fn32 + frame_nr_ofs - WIN;
		update_next = frame_nr_ofs >= WIN;
	} else {
		fn32 = iline->e1t.next_fn32;
		update_next = true;
	}

	/* compute E1oIP idx to timeslot table */
	num_ts = ts_mask2idx(idx2ts, ts_mask);
	iline_stat_set(iline, LINE_STAT_E1oIP_E1T_TS, num_ts);
	if (num_ts > 0) {
		n_frames = msgb_l3len(msg) / num_ts;
		if (msgb_l3len(msg) % num_ts) {
			LOGPEER(peer, LOGL_NOTICE,
				"RxIP: %u extraneous bytes (len=%u, num_ts=%u, n_frames=%u)\n",
				msgb_length(msg) % num_ts, msgb_length(msg), num_ts, n_frames);
		}
		LOGPEER(peer, LOGL_INFO, "RxIP: frame=%05u ts_mask=0x%08x num_ts=%02u, n_frames=%u\n",
			frame_nr, ts_mask, num_ts, n_frames);
	} else {
		if (msgb_l3len(msg) < 1) {
			LOGPEER(peer, LOGL_ERROR, "RxIP: num_ts==0 but no n_frames octet!\n");
			n_frames = BYTES_PER_FRAME; /* hackish assumption */
		} else
			n_frames = msg->l3h[0];
	}

	memcpy(frame_buf, iline->e1t.last_frame, BYTES_PER_FRAME);
	for (unsigned int i = 0; i < n_frames; i++) {
		int rc;
		for (unsigned int j = 0; j < num_ts; j++) {
			uint8_t ts_nr = idx2ts[j];
			frame_buf[ts_nr] = e1th->data[i*num_ts + j];
		}
		/* FIXME: what to do about TS0? */
		rc = frame_rifo_in(&iline->e1t.rifo, frame_buf, fn32+i);
		if (rc < 0)
			iline_ctr_add(iline, LINE_CTR_E1oIP_E1T_OVERFLOW, 1);
	}
	/* update local state */
	memcpy(iline->e1t.last_frame, frame_buf, BYTES_PER_FRAME);
	if (update_next)
		iline->e1t.next_fn32 = fn32 + n_frames;

	iline_stat_set(iline, LINE_STAT_E1oIP_E1T_FIFO, frame_rifo_depth(&iline->e1t.rifo));

	return 0;
}

static int g_ctr_idx = 0;

void e1oip_line_set_name(struct e1oip_line *iline, const char *name)
{
	rate_ctr_group_set_name(iline->ctrs, name);
	osmo_stat_item_group_set_name(iline->stats, name);
}

struct e1oip_line *e1oip_line_alloc(struct octoi_peer *peer)
{
	struct e1oip_line *iline;
	int ctr_idx = g_ctr_idx++;

	if (peer->iline)
		return NULL;

	iline = talloc_zero(peer, struct e1oip_line);
	if (!iline)
		return NULL;

	iline->ctrs = rate_ctr_group_alloc(iline, &iline_ctrg_desc, ctr_idx);
	iline->stats = osmo_stat_item_group_alloc(iline, &iline_stats_desc, ctr_idx);
	e1oip_line_set_name(iline, peer->name);

	iline->cfg.batching_factor = DEFAULT_BATCHING_FACTOR;
	iline->cfg.prefill_frame_count = DEFAULT_PREFILL_FRAME_COUNT;

	e1oip_line_reset(iline);

	iline->peer = peer;
	peer->iline = iline;

	return iline;
}

void e1oip_line_configure(struct e1oip_line *iline, uint8_t batching_factor,
			  uint32_t prefill_frame_count)
{
	iline->cfg.batching_factor = batching_factor;
	iline->cfg.prefill_frame_count = prefill_frame_count;
}

void e1oip_line_reset(struct e1oip_line *iline)
{
	frame_fifo_init(&iline->e1o.fifo, iline->cfg.batching_factor, fifo_threshold_cb, iline);
	memset(&iline->e1o.last_frame, 0xff, sizeof(iline->e1o.last_frame));
	iline->e1o.next_seq = 0;

	frame_rifo_init(&iline->e1t.rifo);
	memset(&iline->e1t.last_frame, 0xff, sizeof(iline->e1t.last_frame));
	iline->e1t.next_fn32 = 0;
	iline->e1t.primed_rx_tdm = false;
}

void e1oip_line_destroy(struct e1oip_line *iline)
{
	if (!iline)
		return;

	rate_ctr_group_free(iline->ctrs);
	osmo_stat_item_group_free(iline->stats);
	if (iline->peer)
		iline->peer->iline = NULL;
	iline->peer = NULL;
	talloc_free(iline);
}
