/*
 * The following code is a port of FFmpeg/avformat/librist.c for obs-studio.
 * Port by pkv@obsproject.com.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#pragma once
#include <obs-module.h>
#include "obs-ffmpeg-url.h"
#include <librist/librist.h>
#include <librist/version.h>

// RIST_MAX_PACKET_SIZE - 28 minimum protocol overhead
#define RIST_MAX_PAYLOAD_SIZE (10000 - 28)

#define FF_LIBRIST_MAKE_VERSION(major, minor, patch) \
	((patch) + ((minor)*0x100) + ((major)*0x10000))
#define FF_LIBRIST_VERSION                                 \
	FF_LIBRIST_MAKE_VERSION(LIBRIST_API_VERSION_MAJOR, \
				LIBRIST_API_VERSION_MINOR, \
				LIBRIST_API_VERSION_PATCH)
#define FF_LIBRIST_VERSION_41 FF_LIBRIST_MAKE_VERSION(4, 1, 0)
#define FF_LIBRIST_VERSION_42 FF_LIBRIST_MAKE_VERSION(4, 2, 0)

#define FF_LIBRIST_FIFO_DEFAULT_SHIFT 13

typedef struct RISTContext {
	int profile;
	int buffer_size;
	int packet_size;
	int log_level;
	int encryption;
	int fifo_shift;
	bool overrun_nonfatal;
	char *secret;
	char *username;
	char *password;
	struct rist_logging_settings logging_settings;
	struct rist_peer_config peer_config;

	struct rist_peer *peer;
	struct rist_ctx *ctx;
	int statsinterval;
	struct rist_stats_sender_peer *stats_list;
} RISTContext;

static int risterr2ret(int err)
{
	switch (err) {
	case RIST_ERR_MALLOC:
		return AVERROR(ENOMEM);
	default:
		return AVERROR_EXTERNAL;
	}
}

static int log_cb(void *arg, enum rist_log_level log_level, const char *msg)
{
	int level;

	switch (log_level) {
	case RIST_LOG_ERROR:
		level = AV_LOG_ERROR;
		break;
	case RIST_LOG_WARN:
		level = AV_LOG_WARNING;
		break;
	case RIST_LOG_NOTICE:
		level = AV_LOG_INFO;
		break;
	case RIST_LOG_INFO:
		level = AV_LOG_VERBOSE;
		break;
	case RIST_LOG_DEBUG:
		level = AV_LOG_DEBUG;
		break;
	case RIST_LOG_DISABLE:
		level = AV_LOG_QUIET;
		break;
	default:
		level = AV_LOG_WARNING;
	}

	av_log(arg, level, "%s", msg);

	return 0;
}

static int librist_close(URLContext *h)
{
	RISTContext *s = h->priv_data;
	int ret = 0;
	if (s->secret)
		bfree(s->secret);
	if (s->username)
		bfree(s->username);
	if (s->password)
		bfree(s->password);
	s->peer = NULL;

	if (s->ctx)
		ret = rist_destroy(s->ctx);
	if (ret < 0) {
		blog(LOG_ERROR,
		     "[obs-ffmpeg mpegts muxer / librist]: Failed to close properly %s",
		     h->url);
		return -1;
	}
	s->ctx = NULL;

	return 0;
}

static int cb_stats(void *arg, const struct rist_stats *stats_container)
{
	RISTContext *s = (RISTContext *)arg;
	rist_log(&s->logging_settings, RIST_LOG_INFO, "%s\n",
		 stats_container->stats_json);
	if (stats_container->stats_type == RIST_STATS_SENDER_PEER) {
		blog(LOG_INFO, "---------------------------------");
		blog(LOG_DEBUG,
		     "[obs-ffmpeg mpegts muxer / librist]: Session Summary\n"
		     "\tbandwidth [%.3f Mbps]\n"
		     "\tpackets sent [%lu]\n"
		     "\tpkts received [%lu]\n"
		     "\tpkts retransmitted [%lu]\n"
		     "\tquality (pkt sent over sent+retransmitted+skipped) [%.2f]\n"
		     "\trtt [%" PRIu32 " ms]\n",
		     (double)(stats_container->stats.sender_peer.bandwidth) /
			     1000000.0,
		     stats_container->stats.sender_peer.sent,
		     stats_container->stats.sender_peer.received,
		     stats_container->stats.sender_peer.retransmitted,
		     stats_container->stats.sender_peer.quality,
		     stats_container->stats.sender_peer.rtt);
	}
	rist_stats_free(stats_container);
	return 0;
}

static int librist_open(URLContext *h, const char *uri)
{
	RISTContext *s = h->priv_data;
	struct rist_logging_settings *logging_settings = &s->logging_settings;
	struct rist_peer_config *peer_config = &s->peer_config;
	int ret;
	s->buffer_size = 3000;
	s->profile = RIST_PROFILE_MAIN;
	s->packet_size = 1316;
	s->log_level = RIST_LOG_INFO;
	s->encryption = 0;
	s->overrun_nonfatal = 0;
	s->fifo_shift = FF_LIBRIST_FIFO_DEFAULT_SHIFT;
	s->logging_settings =
		(struct rist_logging_settings)LOGGING_SETTINGS_INITIALIZER;
	s->statsinterval = 60000; // log stats every 60 seconds

	ret = rist_logging_set(&logging_settings, s->log_level, log_cb, h, NULL,
			       NULL);
	if (ret < 0) {
		blog(LOG_ERROR,
		     "[obs-ffmpeg mpegts muxer / librist]: Failed to initialize logging settings");
		return OBS_OUTPUT_CONNECT_FAILED;
	}

	blog(LOG_INFO,
	     "[obs-ffmpeg mpegts muxer / librist]: libRIST version: %s, API: %s",
	     librist_version(), librist_api_version());

	h->max_packet_size = s->packet_size;
	ret = rist_sender_create(&s->ctx, s->profile, 0, logging_settings);

	if (ret < 0) {
		blog(LOG_ERROR,
		     "[obs-ffmpeg mpegts muxer / librist]: Failed to create a sender");
		goto err;
	}

	ret = rist_peer_config_defaults_set(peer_config);
	if (ret < 0) {
		blog(LOG_ERROR,
		     "[obs-ffmpeg mpegts muxer / librist]: Failed to set peer config defaults");
		goto err;
	}

#if FF_LIBRIST_VERSION < FF_LIBRIST_VERSION_41
	ret = rist_parse_address(
		uri, (const struct rist_peer_config **)&peer_config);
#else
	ret = rist_parse_address2(uri, &peer_config);
#endif
	if (ret < 0) {
		blog(LOG_ERROR,
		     "[obs-ffmpeg mpegts muxer / librist]: Failed to parse url: %s",
		     uri);
		librist_close(h);
		return OBS_OUTPUT_INVALID_STREAM;
	}

	if (((s->encryption == 128 || s->encryption == 256) && !s->secret) ||
	    ((peer_config->key_size == 128 || peer_config->key_size == 256) &&
	     !peer_config->secret[0])) {
		blog(LOG_ERROR,
		     "[obs-ffmpeg mpegts muxer / librist]: Secret is mandatory if encryption is enabled");
		librist_close(h);
		return OBS_OUTPUT_INVALID_STREAM;
	}

	if (s->secret && peer_config->secret[0] == 0)
		av_strlcpy(peer_config->secret, s->secret,
			   RIST_MAX_STRING_SHORT);

	if (s->secret && (s->encryption == 128 || s->encryption == 256))
		peer_config->key_size = s->encryption;

	if (s->buffer_size) {
		peer_config->recovery_length_min = s->buffer_size;
		peer_config->recovery_length_max = s->buffer_size;
	}
	if (s->username && peer_config->srp_username[0] == 0)
		av_strlcpy(peer_config->srp_username, s->username,
			   RIST_MAX_STRING_LONG);
	if (s->password && peer_config->srp_password[0] == 0)
		av_strlcpy(peer_config->srp_password, s->password,
			   RIST_MAX_STRING_LONG);

	ret = rist_peer_create(s->ctx, &s->peer, &s->peer_config);
	if (ret < 0) {
		blog(LOG_ERROR,
		     "[obs-ffmpeg mpegts muxer / librist]: Failed to create a peer.");
		goto err;
	}

	ret = rist_start(s->ctx);
	if (ret < 0) {
		blog(LOG_ERROR,
		     "[obs-ffmpeg mpegts muxer / librist]: RIST failed to start");
		goto err;
	}
	if (rist_stats_callback_set(s->ctx, s->statsinterval, cb_stats,
				    (void *)s) == -1)
		rist_log(&s->logging_settings, RIST_LOG_ERROR,
			 "Could not enable stats callback");
	return 0;

err:
	librist_close(h);

	return OBS_OUTPUT_CONNECT_FAILED;
}

static int librist_write(URLContext *h, const uint8_t *buf, int size)
{
	RISTContext *s = h->priv_data;
	struct rist_data_block data_block = {0};
	int ret;

	data_block.ts_ntp = 0;
	data_block.payload = buf;
	data_block.payload_len = size;

	ret = rist_sender_data_write(s->ctx, &data_block);
	if (ret < 0) {
		blog(LOG_WARNING,
		     "[obs-ffmpeg mpegts muxer / librist]: Failed to send %i bytes",
		     size);
		return risterr2ret(ret);
	}

	return ret;
}
