/* upnp.c - Routines dealing interfacing with the upnp library
 *
 * Copyright (C) 2005, 2006, 2007  Oskar Liljeblad
 *
 * 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 3 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 Library 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include <config.h>
#include <stdbool.h>		/* Gnulib, C99 */
#include <upnp/upnp.h>		/* libupnp */
#include <upnp/upnptools.h>	/* libupnp */
#include <assert.h>		/* C89 */
#include <netinet/in.h>		/* ?; inet_ntoa */
#include <arpa/inet.h>		/* ?; inet_ntoa */
#include <netdb.h>
#include <inttypes.h>		/* POSIX */
#include "gettext.h"            /* Gnulib/gettext */
#define _(s) gettext(s)
#define N_(s) gettext_noop(s)
#include "quotearg.h"		/* Gnulib */
#include "xvasprintf.h"		/* Gnulib */
#include "xgethostname.h"	/* Gnulib */
#include "xalloc.h"		/* Gnulib */
#include <uuid/uuid.h>
#include "intutil.h"
#include "gmediaserver.h"
#include "schemas/MediaServer.h"

#define GMEDIASERVER_SSDP_PAUSE 100

char *friendly_name = NULL;
int ssdp_expire_time = GMEDIASERVER_SSDP_PAUSE;
static UpnpDevice_Handle device;
static char device_udn[42]; /* uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + null-byte */

static Service services[] = {
  {
    CONTENTDIR_SERVICE_ID,
    "urn:schemas-upnp-org:service:ContentDirectory:1",
    contentdir_service_actions,
    contentdir_service_variables,
  },
  {
    CONNECTMGR_SERVICE_ID,
    "urn:schemas-upnp-org:service:ConnectionManager:1",
    connectmgr_service_actions,
    connectmgr_service_variables,
  },
  { 0, }
};

static void
say_ip_addr(int level, const char* header, struct sockaddr_storage* addr)
{
  char hbuf[NI_MAXHOST];
  int err;
  int slen;
#ifdef __sun
  switch (addr->ss_family) {
  case AF_INET:
    slen = sizeof (struct sockaddr_in);
    break;
  case AF_INET6:
    slen = sizeof (struct sockaddr_in6);
    break;
  default:
    slen = sizeof(*addr);
    break;
  }
#else
  slen = addr->ss_len;
#endif
  err = getnameinfo((struct sockaddr*)addr, slen, hbuf, NI_MAXHOST,
		    NULL, 0, NI_NUMERICHOST);
  say(level, header, err == 0 ? hbuf : "(unknown)");
}

static const char *
upnp_errmsg(int res)
{
    return UpnpGetErrorMessage(res);

/*  switch (res) {
    case UPNP_E_SUCCESS:
    	return "Success";   	    	    // strerror(ENOMEM);
    case UPNP_E_INVALID_HANDLE:
    	return "Invalid UPnP handle";
    case UPNP_E_INVALID_PARAM:
    	return "Invalid argument";  	    // strerror(EINVAL);
    case UPNP_E_OUTOF_HANDLE:
    	return "Out of handles";
    case UPNP_E_OUTOF_CONTEXT:
    case UPNP_E_BUFFER_TOO_SMALL:
    case UPNP_E_INVALID_SID:
    case UPNP_E_INIT_FAILED:
    	return UpnpGetErrorMessage(res);
    case UPNP_E_OUTOF_MEMORY:
    	return "Cannot allocate memory";    // strerror(ENOMEM);
    case UPNP_E_INIT:
    	return "UPnP already initialized";
    case UPNP_E_INVALID_DESC:
    	return "Invalid description document";
    case UPNP_E_INVALID_URL:
    	return "Invalid URL";
    case UPNP_E_INVALID_DEVICE:
    	return "Invalid device";
    case UPNP_E_INVALID_SERVICE:
    	return "Invalid service";
    case UPNP_E_BAD_RESPONSE:
    	return "Bad response";
    case UPNP_E_BAD_REQUEST:
    	return "Bad request";
    case UPNP_E_INVALID_ACTION:
    	return "Invalid action";
    case UPNP_E_FINISH:
    	return "UPnP not initialized";
    case UPNP_E_URL_TOO_BIG:
    	return "URL too long";
    case UPNP_E_BAD_HTTPMSG:
    	return "Invalid HTTP message";
    case UPNP_E_ALREADY_REGISTERED:
    	return "Client/device already registered";
    case UPNP_E_NETWORK_ERROR:
    	return "Network error";
    case UPNP_E_SOCKET_WRITE:
    case UPNP_E_SOCKET_READ:
    case UPNP_E_SOCKET_BIND:
    case UPNP_E_SOCKET_CONNECT:
    case UPNP_E_OUTOF_SOCKET:
    case UPNP_E_LISTEN:
    case UPNP_E_TIMEDOUT:
    case UPNP_E_SOCKET_ERROR:
    case UPNP_E_FILE_WRITE_ERROR:
    case UPNP_E_EVENT_PROTOCOL:
    case UPNP_E_SUBSCRIBE_UNACCEPTED:
    case UPNP_E_UNSUBSCRIBE_UNACCEPTED:
    case UPNP_E_NOTIFY_UNACCEPTED:
    case UPNP_E_INVALID_ARGUMENT:
    case UPNP_E_FILE_NOT_FOUND:
    case UPNP_E_FILE_READ_ERROR:
    case UPNP_E_EXT_NOT_XML:
    case UPNP_E_NO_WEB_SERVER:
    case UPNP_E_OUTOF_BOUNDS:
    case UPNP_E_NOT_FOUND:
    case UPNP_E_INTERNAL_ERROR:
    case UPNP_SOAP_E_INVALID_ACTION:
    case UPNP_SOAP_E_INVALID_ARGS:
    case UPNP_SOAP_E_OUT_OF_SYNC:
    case UPNP_SOAP_E_INVALID_VAR:
    case UPNP_SOAP_E_ACTION_FAILED:
    }
*/
}

static void
say_document(int level, const char *header, IXML_Document *document)
{
    if (verbosity >= level) {
        DOMString dump;
        char *line;
        char *line2;

        say(level, header);
        dump = ixmlPrintDocument(document);
        for (line = dump; (line2 = strstr(line, "\n")) != NULL; line = line2+1) {
            *line2 = '\0';
            say(level, "  %s\n", line);
        }
        if (*line)
            say(level, "  %s\n", line);
        ixmlFreeDOMString(dump);
    }
}

static Service *
find_service(const char *service_id)
{
    int c;
    for (c = 0; services[c].id != NULL; c++) {
        if (strcmp(services[c].id, service_id) == 0)
            return &services[c];
    }
    return NULL;
}

static ServiceAction *
find_service_action(Service *service, const char *action_name)
{
    int c;
    for (c = 0; service->actions[c].name != NULL; c++) {
        if (strcmp(service->actions[c].name, action_name) == 0)
            return &service->actions[c];
    }
    return NULL;
}

static ServiceVariable *
find_service_variable(Service *service, const char *variable_name)
{
    int c;
    for (c = 0; service->variables[c].name != NULL; c++) {
        if (strcmp(service->variables[c].name, variable_name) == 0)
            return &service->variables[c];
    }
    return NULL;
}

void
notify_change(const char *service_id, ServiceVariable *variable)
{
    const char *variable_names[1] = { variable->name };
    const char *variable_values[1] = { variable->value };
    int res;

    say(3, _("Variable %s has changed value to %s.\n"), variable->name, quotearg(variable->value));
    res = UpnpNotify(device, device_udn, service_id, variable_names, variable_values, 1);
    if (res != UPNP_E_SUCCESS)
        say(1, _("Cannot notify variable update - %s\n"), upnp_errmsg(res));
}

static void
handle_subscription_request(struct Upnp_Subscription_Request *request)
{
    Service *service;
    int c;
    int variable_count;
    int res;

    say(2, _("Event received: Subscription request\n"));
    say(3, _("Event device UDN: %s\n"), quotearg(request->UDN));
    say(3, _("Event service ID: %s\n"), quotearg(request->ServiceId));
    say(3, _("Event request SID: %s\n"), quotearg(request->Sid));

    if (strcmp(request->UDN, device_udn) != 0) {
        say(1, _("Discarding event - event device UDN (%s) not recognized\n"), quotearg(request->UDN));
        return;
    }
    if ((service = find_service(request->ServiceId)) == NULL) {
        say(1, _("Discarding event - service ID (%s) not recognized\n"), quotearg(request->ServiceId));
        return;
    }

    for (c = 0; service->variables[c].name != NULL; c++);
    variable_count = c;

    {
        const char *variable_names[variable_count];
        char *variable_values[variable_count];

        /* Make a copy of variable values since UpnpAcceptSubscription
         * may take some time to call.
         */
        lock_metadata();
        for (c = 0; c < variable_count; c++) {
            variable_names[c] = service->variables[c].name;
            variable_values[c] = xstrdup(service->variables[c].value);
        }
        unlock_metadata();

        res = UpnpAcceptSubscription(device, request->UDN, request->ServiceId,
                                     variable_names, (const char **) variable_values,
                                     variable_count, request->Sid);
        for (c = 0; c < variable_count; c++)
            free(variable_values[c]);
        if (res != UPNP_E_SUCCESS) {
            say(1, _("Cannot accept service subscription - %s\n"), upnp_errmsg(res));
            return;
        }
    }
}

static void
handle_get_var_request(struct Upnp_State_Var_Request *request)
{
    Service *service;
    ServiceVariable *variable;

    say(2, _("Event received: Get variable request\n"));
    say(3, _("Event device UDN: %s\n"), quotearg(request->DevUDN));
    say(3, _("Event service ID: %s\n"), quotearg(request->ServiceID));
    say(3, _("Event variable name: %s\n"), quotearg(request->StateVarName));
    say_ip_addr(3, _("Event source: %s\n"), &request->CtrlPtIPAddr);

    if (strcmp(request->DevUDN, device_udn) != 0) {
        say(1, _("Discarding event - event device UDN (%s) not recognized\n"), quotearg(request->DevUDN));
        strcpy(request->ErrStr, _("Unrecognized device"));
        request->CurrentVal = NULL;
        request->ErrCode = UPNP_SOAP_E_INVALID_VAR;
        return;
    }
    if ((service = find_service(request->ServiceID)) == NULL) {
	say(1, _("Discarding event - service ID (%s) not recognized\n"), quotearg(request->ServiceID));
	strcpy(request->ErrStr, _("Unrecognized service"));
        request->CurrentVal = NULL;
        request->ErrCode = UPNP_SOAP_E_INVALID_VAR;
	return;
    }
    if ((variable = find_service_variable(service, request->StateVarName)) == NULL) {
	say(1, _("Discarding event - variable name (%s) not recognized\n"), quotearg(request->StateVarName));
	strcpy(request->ErrStr, _("Unrecognized variable"));
        request->CurrentVal = NULL;
        request->ErrCode = UPNP_SOAP_E_INVALID_VAR;
	return;
    }

    lock_metadata(); /* XXX: this is not nice. */
    request->CurrentVal = ixmlCloneDOMString(variable->value);
    unlock_metadata(); /* XXX: this is not nice. */
    request->ErrCode = UPNP_E_SUCCESS;
}

static void
handle_action_request(struct Upnp_Action_Request *request)
{
    Service *service;
    ServiceAction *action;
    ActionEvent event;

    say(2, _("Event received: Action request\n"));
    say(3, _("Event device UDN: %s\n"), quotearg(request->DevUDN));
    say(3, _("Event service ID: %s\n"), quotearg(request->ServiceID));
    say(3, _("Event action name: %s\n"), quotearg(request->ActionName));
    say_ip_addr(3, _("Event source: %s\n"), &request->CtrlPtIPAddr);
    say_document(4, _("Event action request:\n"), request->ActionRequest);

    if (strcmp(request->DevUDN, device_udn) != 0) {
        say(1, _("Discarding event - event device UDN (%s) not recognized\n"), quotearg(request->DevUDN));
        strcpy(request->ErrStr, _("Unrecognized device"));
        request->ActionResult = NULL;
        request->ErrCode = UPNP_SOAP_E_INVALID_ACTION;
        return;
    }
    if ((service = find_service(request->ServiceID)) == NULL) {
	say(1, _("Discarding event - service ID (%s) not recognized\n"), quotearg(request->ServiceID));
	strcpy(request->ErrStr, _("Unrecognized service"));
        request->ActionResult = NULL;
        request->ErrCode = UPNP_SOAP_E_INVALID_ACTION;
	return;
    }
    if ((action = find_service_action(service, request->ActionName)) == NULL) {
	say(1, _("Discarding event - action name (%s) not recognized\n"), quotearg(request->ActionName));
	strcpy(request->ErrStr, _("Unrecognized action"));
	request->ActionResult = NULL;
	request->ErrCode = UPNP_SOAP_E_INVALID_ACTION;
	return;
    }

    event.request = request;
    event.status = true;
    event.service = service;
    if (action->function(&event) && event.status) {
        request->ErrCode = UPNP_E_SUCCESS;
        say(2, _("Action succeeded.\n"));
        say_document(4, _("Event action result:\n"), request->ActionResult);
    } else {
        say(2, _("Action failed.\n"));
        say(3, _("Error code: %d\n"), request->ErrCode);
        say(3, _("Error message: %s\n"), request->ErrStr);
    }
}

static int
device_callback_event_handler(Upnp_EventType eventtype, void *event, void *cookie)
{
    switch (eventtype) {
    case UPNP_EVENT_SUBSCRIPTION_REQUEST:
        handle_subscription_request((struct Upnp_Subscription_Request *) event);
        break;
    case UPNP_CONTROL_GET_VAR_REQUEST:
        handle_get_var_request((struct Upnp_State_Var_Request *) event);
        break;
    case UPNP_CONTROL_ACTION_REQUEST:
        handle_action_request((struct Upnp_Action_Request *) event);
        break;
    default:
        say(2, _("Received an unknown event (type 0x%x)\n"), eventtype);
        break;
    }

    return 0; /* return valid is ignored by libupnp */
}

void
upnp_set_error(ActionEvent *event, int error_code, const char *format, ...)
{
    va_list ap;
    
    va_start(ap, format);
    event->status = false;
    event->request->ActionResult = NULL;
    event->request->ErrCode = UPNP_SOAP_E_ACTION_FAILED;
    vsnprintf(event->request->ErrStr, sizeof(event->request->ErrStr), format, ap);
    va_end(ap);
}

int32_t
upnp_get_i4(ActionEvent *event, const char *key)
{
    char *value;
    int32_t out;

    value = upnp_get_string(event, key);
    if (value == NULL)
	return 0; /* upnp_get_string will have set error details */
    if (parse_int32(value, &out))
	return out;

    upnp_set_error(event, UPNP_SOAP_E_INVALID_ARGS,
        _("Invalid value for %s argument (%s) - expected 32-bit signed integer"), key, quotearg(value));
    return 0;
}

uint32_t
upnp_get_ui4(ActionEvent *event, const char *key)
{
    char *value;
    uint32_t out;

    value = upnp_get_string(event, key);
    if (value == NULL)
	return 0; /* upnp_get_string will have set error details */
    if (parse_uint32(value, &out))
	return out;

    upnp_set_error(event, UPNP_SOAP_E_INVALID_ARGS,
        _("Invalid value for %s argument (%s) - expected 32-bit unsigned integer"), key, quotearg(value));
    return 0;
}

char *
upnp_get_string(ActionEvent *event, const char *key)
{
    IXML_Node *node;
    
    node = (IXML_Node *) event->request->ActionRequest;
    if (node == NULL) {
	upnp_set_error(event, UPNP_SOAP_E_INVALID_ARGS,
	    _("Invalid action request document"));
	return NULL;
    }
    node = ixmlNode_getFirstChild(node);
    if (node == NULL) {
	upnp_set_error(event, UPNP_SOAP_E_INVALID_ARGS,
	    _("Invalid action request document"));
	return NULL;
    }
    node = ixmlNode_getFirstChild(node);

    for (; node != NULL; node = ixmlNode_getNextSibling(node)) {
	if (strcmp(ixmlNode_getNodeName(node), key) == 0) {
	    node = ixmlNode_getFirstChild(node);
	    if (node == NULL) {
		/* Are we sure empty arguments are reported like this? */
		return "";
	    }
	    return (char *) ixmlNode_getNodeValue(node); /* XXX: const? */
	}
    }

    upnp_set_error(event, UPNP_SOAP_E_INVALID_ARGS,
        _("Missing action request argument (%s)"), key);
    return NULL;
}

/* XXX: key is really const here, but UpnpAddToActionResponse doesn't take const key */
bool
upnp_add_response(ActionEvent *event, char *key, const char *value)
{
    char *val;
    int res;

    if (!event->status)
	return false;

    val = strdup(value);
    if (val == NULL) {
	/* report memory failure */
	event->status = false;
        event->request->ActionResult = NULL;
        event->request->ErrCode = UPNP_SOAP_E_ACTION_FAILED;
        strcpy(event->request->ErrStr, errstr);
        return false;
    }

    res = UpnpAddToActionResponse(&event->request->ActionResult, event->request->ActionName, event->service->type, key, val);
    if (res != UPNP_E_SUCCESS) {
	/* report custom error */
        free(val);
        event->request->ActionResult = NULL;
        event->request->ErrCode = UPNP_SOAP_E_ACTION_FAILED;
        strcpy(event->request->ErrStr, upnp_errmsg(res));
        return false;
    }

    return true;
}

void
init_upnp(const char *listenip, uint16_t listenport)
{
    int res;
    uuid_t device_uuid;
    char *mediaserver_desc;

    if (listenip != NULL)
        say(2, _("Using IP address %s.\n"), quotearg(listenip));

    say(3, _("Initializing UPnP subsystem...\n"));
    res = UpnpInit(listenip, listenport);
    if (res != UPNP_E_SUCCESS)
        die(_("cannot initialize UPnP subsystem - %s\n"), upnp_errmsg(res));

    say(1, _("UPnP MediaServer listening on %s:%d\n"), UpnpGetServerIpAddress(), UpnpGetServerPort());

    say(3, _("Enabling UPnP web server...\n"));
    res = UpnpEnableWebserver(TRUE);
    if (res != UPNP_E_SUCCESS)
        die(_("cannot enable UPnP web server - %s\n"), upnp_errmsg(res));
    res = UpnpSetVirtualDirCallbacks(&virtual_dir_callbacks);
    if (res != UPNP_E_SUCCESS)
        die(_("cannot set virtual directory callbacks - %s\n"), upnp_errmsg(res));
    res = UpnpAddVirtualDir("/files");
    if (res != UPNP_E_SUCCESS)
        die(_("cannot add virtual directory for web server - %s\n"), upnp_errmsg(res));
    res = UpnpAddVirtualDir("/upnp");
    if (res != UPNP_E_SUCCESS)
        die(_("cannot add virtual directory for web server - %s\n"), upnp_errmsg(res));

    say(3, _("Generating device UDN (UUID)...\n"));
    uuid_generate(device_uuid);
    sprintf(device_udn, "uuid:");
    uuid_unparse_lower(device_uuid, device_udn+strlen(device_udn));
    say(4, _("  UDN: %s\n"), device_udn);

    say(3, _("Registering UPnP root device...\n"));

    if (friendly_name == NULL)
	friendly_name = xasprintf(_("GMediaServer on %s"), xgethostname());
    mediaserver_desc = xasprintf(MEDIASERVER_DESC, device_udn, friendly_name);
    free(friendly_name);
    res = UpnpRegisterRootDevice2(UPNPREG_BUF_DESC, mediaserver_desc,
              strlen(mediaserver_desc), 1, device_callback_event_handler, NULL, &device);
    free(mediaserver_desc);
    if (res != UPNP_E_SUCCESS)
        die(_("cannot register root device - %s\n"), upnp_errmsg(res));

    say(1, _("Sending UPnP advertisement for device (expire time %d %s)...\n"),
            ssdp_expire_time, ngettext("second", "seconds", ssdp_expire_time));
    res = UpnpSendAdvertisement(device, ssdp_expire_time);
    if (res != UPNP_E_SUCCESS)
        die(_("cannot send device advertisement - %s\n"), upnp_errmsg(res));

    say(1, _("Listening for control point connections...\n"));
}

void
finish_upnp(void)
{
    int res;

    say(1, _("Shutting down UPnP MediaServer...\n"));
    res = UpnpUnRegisterRootDevice(device);
    if (res != UPNP_E_SUCCESS)
        die(_("cannot unregister root device - %s\n"), upnp_errmsg(res));

    say(3, _("Deinitializing UPnP subsystem...\n"));
    res = UpnpFinish();
    if (res != UPNP_E_SUCCESS)
        die(_("cannot deinitialize UPnP library - %s\n"), upnp_errmsg(res));
}
