/**
 * @file sipe-appshare.c
 *
 * pidgin-sipe
 *
 * Copyright (C) 2014-2017 SIPE Project <http://sipe.sourceforge.net/>
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <glib.h>
#include <string.h>

#include <gio/gio.h>

#include "sipmsg.h"
#include "sipe-appshare.h"
#include "sipe-appshare-client.h"
#include "sipe-backend.h"
#include "sipe-buddy.h"
#include "sipe-chat.h"
#include "sipe-common.h"
#include "sipe-conf.h"
#include "sipe-core.h"
#include "sipe-core-private.h"
#include "sipe-media.h"
#include "sipe-nls.h"
#include "sipe-schedule.h"
#include "sipe-user.h"
#include "sipe-utils.h"
#include "sdpmsg.h"

struct sipe_appshare {
	struct sipe_media_stream *stream;
	GSocket *socket;
	GIOChannel *channel;
	guint rdp_channel_readable_watch_id;
	guint rdp_channel_writable_watch_id;
	struct sipe_user_ask_ctx *ask_ctx;

	gchar rdp_channel_buffer[0x800];
	gchar *rdp_channel_buffer_pos;
	gsize rdp_channel_buffer_len;

	struct sipe_rdp_client client;
};

static void
sipe_appshare_free(struct sipe_appshare *appshare)
{
	if (appshare->rdp_channel_readable_watch_id != 0) {
		g_source_destroy(g_main_context_find_source_by_id(NULL,
				appshare->rdp_channel_readable_watch_id));
	}

	if (appshare->rdp_channel_writable_watch_id != 0) {
		g_source_destroy(g_main_context_find_source_by_id(NULL,
				appshare->rdp_channel_writable_watch_id));
	}

	if (appshare->channel) {
		GError *error = NULL;

		g_io_channel_shutdown(appshare->channel, TRUE, &error);
		if (error) {
			SIPE_DEBUG_ERROR("Error shutting down RDP channel: %s",
					 error->message);
			g_error_free(error);
		}
		g_io_channel_unref(appshare->channel);
	}

	if (appshare->socket) {
		g_object_unref(appshare->socket);
	}

	if (appshare->ask_ctx) {
		sipe_user_close_ask(appshare->ask_ctx);
	}

	g_free(appshare->client.cmdline);
	if (appshare->client.free_cb) {
		appshare->client.free_cb(&appshare->client);
	}

	g_free(appshare);
}

static gboolean
rdp_channel_readable_cb(GIOChannel *channel,
			GIOCondition condition,
			gpointer data)
{
	struct sipe_appshare *appshare = data;
	GError *error = NULL;
	gchar *buffer;
	gsize bytes_read;

	if (condition & G_IO_HUP) {
		struct sipe_media_call *call = appshare->stream->call;

		sipe_backend_media_hangup(call->backend_private, TRUE);
		return FALSE;
	}

	buffer = g_malloc(2048);
	while (sipe_media_stream_is_writable(appshare->stream)) {
		GIOStatus status;

		status = g_io_channel_read_chars(channel,
						 buffer, 2048,
						 &bytes_read, &error);
		if (error) {
			struct sipe_media_call *call = appshare->stream->call;

			SIPE_DEBUG_ERROR("Error reading from RDP channel: %s",
					 error->message);
			g_error_free(error);
			sipe_backend_media_hangup(call->backend_private, TRUE);
			g_free(buffer);
			return FALSE;
		}

		if (status == G_IO_STATUS_EOF) {
			struct sipe_media_call *call = appshare->stream->call;

			sipe_backend_media_hangup(call->backend_private, TRUE);
			g_free(buffer);
			return FALSE;
		}

		if (bytes_read == 0) {
			break;
		}

		sipe_media_stream_write(appshare->stream, (guint8 *)buffer,
					bytes_read);
		SIPE_DEBUG_INFO("Written: %" G_GSIZE_FORMAT "\n", bytes_read);
	}
	g_free(buffer);

	return TRUE;
}

static gboolean
socket_connect_cb(SIPE_UNUSED_PARAMETER GIOChannel *channel,
		  SIPE_UNUSED_PARAMETER GIOCondition condition,
		  gpointer data)
{
	struct sipe_appshare *appshare = data;
	GError *error = NULL;
	GSocket *data_socket;
	int fd;

	data_socket = g_socket_accept(appshare->socket, NULL, &error);
	if (error) {
		struct sipe_media_call *call = appshare->stream->call;

		SIPE_DEBUG_ERROR("Error accepting RDP client connection: %s",
				 error->message);
		g_error_free(error);
		sipe_backend_media_hangup(call->backend_private, TRUE);
		return FALSE;
	}

	g_io_channel_shutdown(appshare->channel, TRUE, &error);
	if (error) {
		struct sipe_media_call *call = appshare->stream->call;

		SIPE_DEBUG_ERROR("Error shutting down RDP channel: %s",
				 error->message);
		g_error_free(error);
		g_object_unref(data_socket);
		sipe_backend_media_hangup(call->backend_private, TRUE);
		return FALSE;
	}
	g_io_channel_unref(appshare->channel);

	g_object_unref(appshare->socket);
	appshare->socket = data_socket;

	fd = g_socket_get_fd(appshare->socket);
	if (fd < 0) {
		struct sipe_media_call *call = appshare->stream->call;

		SIPE_DEBUG_ERROR_NOFORMAT("Invalid file descriptor for RDP client connection socket");
		sipe_backend_media_hangup(call->backend_private, TRUE);
		return FALSE;
	}
	appshare->channel = g_io_channel_unix_new(fd);

	// No encoding for binary data
	g_io_channel_set_encoding(appshare->channel, NULL, &error);
	if (error) {
		struct sipe_media_call *call = appshare->stream->call;

		SIPE_DEBUG_ERROR("Error setting RDP channel encoding: %s",
				 error->message);
		g_error_free(error);
		sipe_backend_media_hangup(call->backend_private, TRUE);
		return FALSE;
	}

	appshare->rdp_channel_readable_watch_id =
			g_io_add_watch(appshare->channel, G_IO_IN | G_IO_HUP,
				       rdp_channel_readable_cb, appshare);

	return FALSE;
}

static void
launch_rdp_client(struct sipe_appshare *appshare)
{
	struct sipe_rdp_client *client = &appshare->client;
	struct sipe_media_call *call = appshare->stream->call;
	GSocketAddress *address;
	GError *error = NULL;
	int fd;

	address = client->get_listen_address_cb(client);
	if (!address) {
		sipe_backend_media_hangup(call->backend_private, TRUE);
		return;
	}

	appshare->socket = g_socket_new(g_socket_address_get_family(address),
					G_SOCKET_TYPE_STREAM,
					G_SOCKET_PROTOCOL_DEFAULT,
					&error);
	if (error) {
		SIPE_DEBUG_ERROR("Can't create RDP client listen socket: %s",
				 error->message);
		g_error_free(error);
		g_object_unref(address);
		sipe_backend_media_hangup(call->backend_private, TRUE);
		return;
	}

	g_socket_set_blocking(appshare->socket, FALSE);

	g_socket_bind(appshare->socket, address, TRUE, &error);
	g_object_unref(address);
	if (error) {
		SIPE_DEBUG_ERROR("Can't bind to RDP client socket: %s",
				 error->message);
		g_error_free(error);
		sipe_backend_media_hangup(call->backend_private, TRUE);
		return;
	}

	g_socket_listen(appshare->socket, &error);
	if (error) {
		SIPE_DEBUG_ERROR("Can't listen on RDP client socket: %s",
				 error->message);
		g_error_free(error);
		sipe_backend_media_hangup(call->backend_private, TRUE);
		return;
	}

	fd = g_socket_get_fd(appshare->socket);
	if (fd < 0) {
		SIPE_DEBUG_ERROR_NOFORMAT("Invalid file descriptor for RDP client listen socket");
		sipe_backend_media_hangup(call->backend_private, TRUE);
		return;
	}
	appshare->channel = g_io_channel_unix_new(fd);

	appshare->rdp_channel_readable_watch_id =
			g_io_add_watch(appshare->channel, G_IO_IN,
				       socket_connect_cb, appshare);

	address = g_socket_get_local_address(appshare->socket, &error);
	if (error) {
		SIPE_DEBUG_ERROR("Couldn't get appshare socket address: %s",
				 error->message);
		g_error_free(error);
		sipe_backend_media_hangup(call->backend_private, TRUE);
		return;
	}

	if (!client->launch_cb(client, address, appshare->stream)) {
		sipe_backend_media_hangup(call->backend_private, TRUE);
	}

	g_object_unref(address);
}

static gssize
rdp_client_channel_write(struct sipe_appshare *appshare)
{
	gsize bytes_written;
	GError *error = NULL;

	g_io_channel_write_chars(appshare->channel,
				 appshare->rdp_channel_buffer_pos,
				 appshare->rdp_channel_buffer_len,
				 &bytes_written, &error);
	if (error) {
		SIPE_DEBUG_ERROR("Couldn't write data to RDP client: %s",
				 error->message);
		g_error_free(error);
		return -1;
	}

	g_io_channel_flush(appshare->channel, &error);
	if (error) {
		if (g_error_matches(error, G_IO_CHANNEL_ERROR,
				    G_IO_CHANNEL_ERROR_PIPE)) {
			/* Ignore broken pipe here and wait for the call to be
			 * hung up upon G_IO_HUP in client_channel_cb(). */
			g_error_free(error);
			return 0;
		}

		SIPE_DEBUG_ERROR("Couldn't flush RDP channel: %s",
				 error->message);
		g_error_free(error);
		return -1;
	}

	appshare->rdp_channel_buffer_pos += bytes_written;
	appshare->rdp_channel_buffer_len -= bytes_written;

	return bytes_written;
}

static void
delayed_hangup_cb(SIPE_UNUSED_PARAMETER struct sipe_core_private *sipe_private,
		  gpointer data)
{
	struct sipe_media_call *call = data;

	sipe_backend_media_hangup(call->backend_private, TRUE);
}

static gboolean
rdp_channel_writable_cb(SIPE_UNUSED_PARAMETER GIOChannel *channel,
			SIPE_UNUSED_PARAMETER GIOCondition condition,
			gpointer data)
{
	struct sipe_appshare *appshare = data;
	struct sipe_media_call *call = appshare->stream->call;

	if (rdp_client_channel_write(appshare) < 0) {
		sipe_backend_media_hangup(call->backend_private, TRUE);
		return FALSE;
	}

	if (appshare->rdp_channel_buffer_len == 0) {
		// Writing done, disconnect writable watch.
		appshare->rdp_channel_writable_watch_id = 0;
		return FALSE;
	}

	return TRUE;
}

static void
read_cb(struct sipe_media_stream *stream)
{
	struct sipe_appshare *appshare = sipe_media_stream_get_data(stream);
	gint bytes_read = 0;
	gssize bytes_written = 0;

	if (appshare->rdp_channel_writable_watch_id != 0) {
		// Data still in the buffer. Let the client read it first.
		return;
	}

	while (bytes_read == (gint)bytes_written) {
		bytes_read = sipe_backend_media_stream_read(stream,
				(guint8 *)appshare->rdp_channel_buffer,
				sizeof (appshare->rdp_channel_buffer));
		if (bytes_read == 0) {
			return;
		}

		appshare->rdp_channel_buffer_pos = appshare->rdp_channel_buffer;
		appshare->rdp_channel_buffer_len = bytes_read;

		bytes_written = rdp_client_channel_write(appshare);

		if (bytes_written < 0) {
			/* Don't deallocate stream while in its read callback.
			 * Schedule call hangup to be executed after we're back
			 * in the message loop. */
			sipe_schedule_seconds(sipe_media_get_sipe_core_private(stream->call),
					      "appshare delayed hangup",
					      stream->call->backend_private,
					      0,
					      delayed_hangup_cb,
					      NULL);
			return;
		}
	}

	if (bytes_read != (gint)bytes_written) {
		/* Schedule writing of the buffer's remainder to when
		 * RDP channel becomes writable again. */
		appshare->rdp_channel_writable_watch_id =
				g_io_add_watch(appshare->channel, G_IO_OUT,
					       rdp_channel_writable_cb,
					       appshare);
	}
}

static void
writable_cb(struct sipe_media_stream *stream)
{
	struct sipe_appshare *appshare = sipe_media_stream_get_data(stream);

	if (!appshare->socket) {
		launch_rdp_client(appshare);
	}
}

static void
accept_cb(SIPE_UNUSED_PARAMETER struct sipe_core_private *sipe_private,
	  gpointer data)
{
	struct sipe_appshare *appshare = data;
	appshare->ask_ctx = NULL;

	sipe_backend_media_accept(appshare->stream->call->backend_private, TRUE);
}

static void
decline_cb(SIPE_UNUSED_PARAMETER struct sipe_core_private *sipe_private,
	  gpointer data)
{
	struct sipe_appshare *appshare = data;
	appshare->ask_ctx = NULL;

	sipe_backend_media_hangup(appshare->stream->call->backend_private, TRUE);
}

static struct sipe_user_ask_ctx *
ask_accept_applicationsharing(struct sipe_core_private *sipe_private,
			      const gchar *from,
			      SipeUserAskCb accept_cb,
			      SipeUserAskCb decline_cb,
			      gpointer user_data)
{
	struct sipe_user_ask_ctx *ctx;
	gchar *alias = sipe_buddy_get_alias(sipe_private, from);
	gchar *ask_msg = g_strdup_printf(_("%s wants to start presenting"),
					 alias ? alias : from);

	ctx = sipe_user_ask(sipe_private, ask_msg,
			     _("Accept"), accept_cb,
			     _("Decline"), decline_cb,
			     user_data);

	g_free(ask_msg);
	g_free(alias);

	return ctx;
}

static struct sipe_appshare *
initialize_appshare(struct sipe_media_stream *stream)
{
	struct sipe_appshare *appshare;
	struct sipe_media_call *call;
	struct sipe_core_private *sipe_private;
	const gchar *cmdline;

	call = stream->call;
	sipe_private = sipe_media_get_sipe_core_private(call);

	appshare = g_new0(struct sipe_appshare, 1);
	appshare->stream = stream;

	sipe_media_stream_set_data(stream, appshare,
				   (GDestroyNotify)sipe_appshare_free);

	cmdline = sipe_backend_setting(SIPE_CORE_PUBLIC,
				       SIPE_SETTING_RDP_CLIENT);
	if (is_empty(cmdline))
		cmdline = "remmina";
	appshare->client.cmdline = g_strdup(cmdline);

	if (strstr(cmdline, "xfreerdp")) {
		sipe_appshare_xfreerdp_init(&appshare->client);
	} else if (strstr(cmdline, "remmina")) {
		sipe_appshare_remmina_init(&appshare->client);
	} else {
		sipe_backend_notify_error(SIPE_CORE_PUBLIC,
					  _("Application sharing error"),
					  _("Unknown remote desktop client configured."));
		sipe_backend_media_hangup(call->backend_private, TRUE);
		return NULL;
	}

	sipe_media_stream_add_extra_attribute(stream,
			"x-applicationsharing-session-id", "1");
	sipe_media_stream_add_extra_attribute(stream,
			"x-applicationsharing-role", "viewer");
	sipe_media_stream_add_extra_attribute(stream,
			"x-applicationsharing-media-type", "rdp");

	stream->read_cb = read_cb;
	stream->writable_cb = writable_cb;

	return appshare;
}

void
process_incoming_invite_appshare(struct sipe_core_private *sipe_private,
				 struct sipmsg *msg)
{
	struct sipe_media_call *call;
	struct sipe_media_stream *stream;
	struct sipe_appshare *appshare;
	struct sdpmsg *sdpmsg;
	GSList *i;

	sdpmsg = sdpmsg_parse_msg(msg->body);

	/* Skype for Business compatibility - ignore desktop video. */
	i = sdpmsg->media;
	while (i) {
		struct sdpmedia *media = i->data;
		const gchar *label;

		i = i->next;

		label = sipe_utils_nameval_find(media->attributes, "label");

		if (sipe_strequal(media->name, "video") &&
		    sipe_strequal(label, "applicationsharing-video")) {
			sdpmsg->media = g_slist_remove(sdpmsg->media, media);
			sdpmedia_free(media);
		}
	}

	call = process_incoming_invite_call_parsed_sdp(sipe_private,
						       msg,
						       sdpmsg);
	if (!call) {
		return;
	}

	stream = sipe_core_media_get_stream_by_id(call, "applicationsharing");
	if (!stream) {
		sipe_backend_media_hangup(call->backend_private, TRUE);
		return;
	}

	appshare = initialize_appshare(stream);

	if (appshare) {
		gchar *from;

		from = parse_from(sipmsg_find_header(msg, "From"));
		appshare->ask_ctx = ask_accept_applicationsharing(sipe_private, from,
								  accept_cb,
								  decline_cb,
								  appshare);
		g_free(from);
	}
}

static void
connect_conference(struct sipe_core_private *sipe_private,
		   struct sipe_chat_session *chat_session)
{
	struct sipe_media_call *call;
	struct sipe_media_stream *stream;
	gchar * uri;

	chat_session->appshare_ask_ctx = NULL;

	uri = sipe_conf_build_uri(chat_session->id, "applicationsharing");

	call = sipe_media_call_new(sipe_private, uri, NULL,
				   SIPE_ICE_RFC_5245,
				   SIPE_MEDIA_CALL_NO_UI);

	g_free(uri);

	stream = sipe_media_stream_add(call, "applicationsharing",
				       SIPE_MEDIA_APPLICATION,
				       SIPE_ICE_RFC_5245, TRUE, 0);
	if (!stream) {
		sipe_backend_notify_error(SIPE_CORE_PUBLIC,
					  _("Application sharing error"),
					  _("Couldn't connect application sharing"));
		sipe_backend_media_hangup(call->backend_private, FALSE);
	}

	sipe_media_stream_add_extra_attribute(stream, "connection", "new");
	sipe_media_stream_add_extra_attribute(stream, "setup", "active");

	initialize_appshare(stream);
}

void
sipe_core_appshare_connect_conference(struct sipe_core_public *sipe_public,
				      struct sipe_chat_session *chat_session,
				      gboolean user_must_accept)
{
	if (user_must_accept) {
		const gchar *from;

		if (chat_session->appshare_ask_ctx) {
			// Accept dialog already opened.
			return;
		}

		if (chat_session->title) {
			from = chat_session->title;
		} else if (chat_session->organizer) {
			from = chat_session->organizer;
		} else {
			from = chat_session->id;
		}

		chat_session->appshare_ask_ctx =
				ask_accept_applicationsharing(SIPE_CORE_PRIVATE,
							      from,
							      (SipeUserAskCb)connect_conference,
							      NULL,
							      chat_session);
	} else {
		connect_conference(SIPE_CORE_PRIVATE, chat_session);
	}
}

/*
  Local Variables:
  mode: c
  c-file-style: "bsd"
  indent-tabs-mode: t
  tab-width: 8
  End:
*/
