/** @file daap_xform.c
 *  XMMS2 transform for accessing DAAP music shares.
 *
 *  Copyright (C) 2006-2023 XMMS2 Team
 *
 *  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.
 */

/* XXX as of the current implementation, there is no method of logging out
 * of the servers.
 */

#include <xmms/xmms_xformplugin.h>
#include <xmms/xmms_log.h>

#include "daap_cmd.h"
#include "daap_util.h"
#include "daap_mdns_browse.h"
#include "daap_conn.h"

#include <stdlib.h>
#include <glib.h>
#include <glib/gprintf.h>

#define DEFAULT_DAAP_PORT 3689

/*
 * Type definitions
 */

typedef struct {
	gchar *host;
	guint port;

	xmms_daap_conn_t *conn;

	xmms_error_t status;
} xmms_daap_data_t;

typedef struct {
	gboolean logged_in;

	guint session_id;
	guint revision_id;
	guint request_id;
} xmms_daap_login_data_t;

static GHashTable *login_sessions = NULL;

/*
 * Function prototypes
 */

static gboolean
xmms_daap_plugin_setup (xmms_xform_plugin_t *xform_plugin);
static gboolean
xmms_daap_init (xmms_xform_t *xform);
static void
xmms_daap_destroy (xmms_xform_t *xform);
static gint
xmms_daap_read (xmms_xform_t *xform, void *buffer,
                gint len, xmms_error_t *error);
static gboolean
xmms_daap_browse (xmms_xform_t *xform, const gchar *url, xmms_error_t *error);

/*
 * Plugin header
 */
XMMS_XFORM_PLUGIN_DEFINE ("daap",
                          "DAAP access plugin",
                          "SoC",
                          "Accesses iTunes (DAAP) music shares",
                          xmms_daap_plugin_setup);


/**
 * Extract hostname, port and command from an url.
 * daap://hostname:port/command
 */
static gboolean
get_data_from_url (const gchar *url, gchar **host, guint *port, gchar **cmd, xmms_error_t *err)
{
	const gchar *port_ptr, *cmd_ptr, *end_ptr, *stripped;

	stripped = url + sizeof (gchar) * strlen ("daap://");

	end_ptr = stripped + sizeof (gchar) * strlen (stripped);

	if (stripped == end_ptr) {
		xmms_error_set (err, XMMS_ERROR_INVAL, "Empty URL");
		return FALSE;
	}

	port_ptr = strstr (stripped, ":");
	if (port && port_ptr && (port_ptr + 1) != end_ptr) {
		*port = strtol (port_ptr + 1, (gchar **) NULL, 10);
		if (*port == 0) {
			*port = DEFAULT_DAAP_PORT;
		}
	} else if (port) {
		*port = DEFAULT_DAAP_PORT;
	}

	cmd_ptr = strstr (stripped, "/");
	if (cmd && cmd_ptr && (cmd_ptr + 1) != end_ptr) {
		*cmd = g_strdup (cmd_ptr);
	} else if (cmd) {
		/* cmd wanted but not found */
		xmms_error_set (err, XMMS_ERROR_INVAL, "No file requested");
	} else if (!cmd && cmd_ptr && (cmd_ptr + 1) != end_ptr) {
		/* cmd not wanted but found */
		xmms_error_set (err, XMMS_ERROR_NOENT, "No such directory");
		return FALSE;
	}

	if (host) {
		if (port_ptr) {
			*host = g_strndup (stripped, port_ptr - stripped);
		} else if (cmd_ptr) {
			*host = g_strndup (stripped, cmd_ptr - stripped);
		} else {
			*host = g_strdup (stripped);
		}
	}

	return TRUE;
}


static gboolean
xmms_daap_plugin_setup (xmms_xform_plugin_t *xform_plugin)
{
	xmms_xform_methods_t methods;

	XMMS_XFORM_METHODS_INIT (methods);
	methods.init = xmms_daap_init;
	methods.destroy = xmms_daap_destroy;
	methods.read = xmms_daap_read;
	methods.browse = xmms_daap_browse;

	xmms_xform_plugin_methods_set (xform_plugin, &methods);

	xmms_xform_plugin_indata_add (xform_plugin,
	                              XMMS_STREAM_TYPE_MIMETYPE,
	                              "application/x-url",
	                              XMMS_STREAM_TYPE_URL,
	                              "daap://*",
	                              XMMS_STREAM_TYPE_END);

	if (!daap_mdns_setup ()) {
		return FALSE;
	}

	if (!login_sessions) {
		login_sessions = g_hash_table_new (g_str_hash, g_str_equal);
	}

	return TRUE;
}


/**
 * Add a song to the browsing list.
 */
static void
daap_add_song_to_list (xmms_xform_t *xform, cc_item_record_t *song)
{
	gchar *songurl;

	songurl = g_strdup_printf ("%u.%s", song->dbid, song->song_format);
	xmms_xform_browse_add_entry (xform, songurl, 0);
	g_free (songurl);

	if (song->iname) {
		xmms_xform_browse_add_entry_property_str (xform, "title",
		                                          song->iname);
	}

	if (song->song_data_artist) {
		xmms_xform_browse_add_entry_property_str (xform, "artist",
		                                          song->song_data_artist);
	}

	if (song->song_data_album) {
		xmms_xform_browse_add_entry_property_str (xform, "album",
		                                          song->song_data_album);
	}

	xmms_xform_browse_add_entry_property_int (xform, "tracknr",
	                                          song->song_track_no);
}


/**
 * Scan a daap server for songs.
 */
static gboolean
daap_get_urls_from_server (xmms_xform_t *xform, gchar *host, guint port,
                           xmms_error_t *err)
{
	GSList *dbid_list = NULL;
	GSList *song_list = NULL, *song_el;
	cc_item_record_t *db_data;
	xmms_daap_login_data_t *login_data;
	gchar *hash;

	hash = g_strdup_printf ("%s:%u", host, port);

	login_data = g_hash_table_lookup (login_sessions, hash);

	if (!login_data) {
		login_data = g_new0 (xmms_daap_login_data_t, 1);

		login_data->session_id = daap_command_login (host, port, 0, err);
		if (xmms_error_iserror (err)) {
			g_free (login_data);
			return FALSE;
		}

		login_data->revision_id = daap_command_update (host, port,
		                                               login_data->session_id,
		                                               0);

		login_data->request_id = 1;
		login_data->logged_in = TRUE;

		g_hash_table_insert (login_sessions, hash, login_data);
	} else {
		login_data->revision_id = daap_command_update (host, port,
		                                               login_data->session_id,
		                                               0);
	}

	dbid_list = daap_command_db_list (host, port, login_data->session_id,
	                                  login_data->revision_id, 0);
	if (!dbid_list) {
		return FALSE;
	}

	/* XXX i've never seen more than one db per server out in the wild,
	 *     let's hope that never changes *wink*
	 *     just use the first db in the list */
	db_data = (cc_item_record_t *) dbid_list->data;
	song_list = daap_command_song_list (host, port, login_data->session_id,
	                                    login_data->revision_id,
	                                    0, db_data->dbid);

	g_slist_foreach (dbid_list, (GFunc) cc_item_record_free, NULL);
	g_slist_free (dbid_list);

	if (!song_list) {
		return FALSE;
	}

	for (song_el = song_list; song_el; song_el = g_slist_next (song_el)) {
		daap_add_song_to_list (xform, song_el->data);
	}

	g_slist_foreach (song_list, (GFunc) cc_item_record_free, NULL);
	g_slist_free (song_list);

	return TRUE;
}


/*
 * Member functions
 */

static gboolean
xmms_daap_init (xmms_xform_t *xform)
{
	gint dbid;
	GSList *dbid_list = NULL;
	xmms_daap_data_t *data;
	xmms_daap_login_data_t *login_data;
	xmms_error_t err;
	const gchar *url;
	const gchar *metakey;
	gchar *command, *hash;
	guint filesize;

	g_return_val_if_fail (xform, FALSE);

	url = xmms_xform_indata_get_str (xform, XMMS_STREAM_TYPE_URL);

	g_return_val_if_fail (url, FALSE);

	data = g_new0 (xmms_daap_data_t, 1);

	xmms_error_reset (&err);

	if (!get_data_from_url (url, &(data->host), &(data->port), &command, &err)) {
		goto init_error;
	}

	hash = g_strdup_printf ("%s:%u", data->host, data->port);

	login_data = g_hash_table_lookup (login_sessions, hash);
	if (!login_data) {
		XMMS_DBG ("creating login data for %s", hash);
		login_data = g_new0 (xmms_daap_login_data_t, 1);

		login_data->request_id = 1;
		login_data->logged_in = TRUE;

		login_data->session_id = daap_command_login (data->host, data->port,
		                                             login_data->request_id,
		                                             &err);
		if (xmms_error_iserror (&err)) {
			g_free (login_data);
			goto init_error;
		}

		g_hash_table_insert (login_sessions, hash, login_data);
	}

	login_data->revision_id = daap_command_update (data->host, data->port,
	                                               login_data->session_id,
	                                               login_data->request_id);
	dbid_list = daap_command_db_list (data->host, data->port,
	                                  login_data->session_id,
	                                  login_data->revision_id,
	                                  login_data->request_id);
	if (!dbid_list) {
		goto init_error;
	}

	/* XXX: see XXX in the browse function above */
	dbid = ((cc_item_record_t *) dbid_list->data)->dbid;
	/* want to request a stream, but don't read the data yet */
	data->conn = daap_command_init_stream (data->host, data->port,
	                                       login_data->session_id,
	                                       login_data->revision_id,
	                                       login_data->request_id, dbid,
	                                       command, &filesize);

	if (! data->conn) {
		goto init_error;
	}
	login_data->request_id++;

	metakey = XMMS_MEDIALIB_ENTRY_PROPERTY_SIZE;
	xmms_xform_metadata_set_int (xform, metakey, filesize);

	xmms_xform_private_data_set (xform, data);

	xmms_xform_outdata_type_add (xform,
	                             XMMS_STREAM_TYPE_MIMETYPE,
	                             "application/octet-stream",
	                             XMMS_STREAM_TYPE_END);

	g_slist_foreach (dbid_list, (GFunc) cc_item_record_free, NULL);
	g_slist_free (dbid_list);
	g_free (command);

	return TRUE;

init_error:
	if (data) {
		if (data->host)
			g_free (data->host);
		g_free (data);
	}
	return FALSE;
}

static void
xmms_daap_destroy (xmms_xform_t *xform)
{
	xmms_daap_data_t *data;

	data = xmms_xform_private_data_get (xform);

	daap_conn_free (data->conn);

	g_free (data->host);
	g_free (data);
}

static gint
xmms_daap_read (xmms_xform_t *xform, void *buffer, gint len, xmms_error_t *error)
{
	xmms_daap_data_t *data;
	gsize read_bytes = 0;
	GIOStatus status;

	data = xmms_xform_private_data_get (xform);

	/* request is performed, header is stripped. now read the data. */
	while (read_bytes == 0) {
		status = g_io_channel_read_chars (data->conn->chan, buffer, len,
		                                  &read_bytes, NULL);
		if (status == G_IO_STATUS_EOF || status == G_IO_STATUS_ERROR) {
			break;
		}
	}

	return read_bytes;
}


static gboolean
xmms_daap_browse (xmms_xform_t *xform, const gchar *url, xmms_error_t *error)
{
	gboolean ret = FALSE;

	if (g_ascii_strcasecmp (url, "daap://") == 0) {

		GSList *sl = daap_mdns_get_server_list ();

		for (; sl; sl = g_slist_next (sl)) {
			daap_mdns_server_t *mdns_serv;
			gchar *str;

			mdns_serv = sl->data;

			str = g_strdup_printf ("%s:%d", mdns_serv->address,
			                       mdns_serv->port);
			xmms_xform_browse_add_entry (xform, str, XMMS_XFORM_BROWSE_FLAG_DIR);
			g_free (str);

			xmms_xform_browse_add_entry_property_str (xform, "servername",
			                                          mdns_serv->server_name);

			xmms_xform_browse_add_entry_property_str (xform, "ip",
			                                          mdns_serv->address);

			xmms_xform_browse_add_entry_property_str (xform, "name",
			                                          mdns_serv->mdns_hostname);

			xmms_xform_browse_add_entry_property_int (xform, "port",
			                                          mdns_serv->port);

			/* TODO implement the machinery to allow for this */
			// val = xmmsv_new_int (mdns_serv->need_auth);
			// xmms_xform_browse_add_entry_property (xform, "passworded", val);
			// val = xmmsv_new_int (mdns_serv->version);
			// xmms_xform_browse_add_entry_property (xform, "version", val);
		}

		ret = TRUE;

		g_slist_free (sl);
	} else {
		gchar *host;
		guint port;

		if (get_data_from_url (url, &host, &port, NULL, error)) {
			ret = daap_get_urls_from_server (xform, host, port, error);
			g_free (host);
		}
	}

	return ret;
}
