/* This file is part of wmaloader.*
 * Copyright 2004 Andrew Wild */

/* wmaloader 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. */

/* wmaloader 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 wmaloader; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/*------------------------------------------------------------------------*/

/* system */
#include <unistd.h>
#include <stdio.h>
#include <syslog.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <semaphore.h>
#include <getopt.h>
#include <stdarg.h>

/* upnp library */
#include "upnp.h"
#include "upnptools.h"
#include "LinkedList.h"

/* wmaloader components */
#include "common.h"
#include "image_transfer.h"

/* Root device type for  the Linksys WMA11b */
#define WMA_DEVICE_TYPE "urn:schemas-upnp-org:device:AppLoaderClient:1"

/* ApplicationTransferService type */
#define ATS_TYPE "urn:schemas-upnp-org:service:ApplicationTransferService:1"

/* AdapterInfoService type */
#define AIS_TYPE "urn:schemas-upnp-org:service:AdapterInfoService:1"

/* URL for control of ApplicationTransferService */
#define ATS_CONTROL_URL "/ApplicationTransferService/control"

/* URL for control of AdapterInfoService */
#define AIS_CONTROL_URL "/AdapterInfoService/control"

#define SYSLOG_IDENT "wmaloader"
#define BANNER "Boot Daemon for Linksys WMA11b. Andrew Wild <acw43@cam.ac.uk>\n"

/* Handle to this upnp control point */
UpnpDevice_Handle ctrlhandle;

/* List of upnp discoveries */
LinkedList discoveries;

/* Syncronisation variables */
pthread_mutex_t list_mutex; /* Control access to discovery linked list */
sem_t is_work_to_do;        /* Indicate when list is not empty */

/* Command line options */
struct {
  char* interface;   // Interface to listen on if not all
  char* image;       // Path to image file (mandatory)
  int   daemon;      // True if program should become a daemon
  int   help;        // Show usage
} global_options;

/* Flag set when running in the background (messages send to syslog) */
int backgrounded = FALSE;

/*------------------------------------------------------------------------*/
char *urlcat(char *a, char *b){
  /* Join two URLs */
  static char output[512];

  if ((strlen(a) + strlen(b)) > 511) return "";
  UpnpResolveURL(a, b, output);
 
  return output;
}

/*------------------------------------------------------------------------*/
IXML_Document *SendAction(char* action_name, char *service_type, 
						  char* control_url, int count, ...)
{
  /* Send an action to the control url specified.
   * Returns the response as a IXML document or NULL for failure */

  int ret, n;
  va_list va;
  IXML_Document *action = NULL, *response = NULL;

  /* Create action */
  action = UpnpMakeAction(action_name, service_type, 0, NULL);

  /* Add argument to action */
  va_start(va, count);
  for (n = 0; n < count; n++){
	char *arg_name, *arg_val;
	arg_name = va_arg(va, char*);
	arg_val  = va_arg(va, char*);
	UpnpAddToAction(&action, action_name, service_type, arg_name, arg_val);
  }
  va_end(va);

  /* Send action */
  ret = UpnpSendAction(ctrlhandle, control_url, service_type, 
					   NULL, action, &response);

  /* Free action */
  if (action)   ixmlDocument_free(action);

  if (ret != UPNP_E_SUCCESS){
	message("Unable to send action: %d (%s)\n", ret, UpnpGetErrorMessage(ret));
	if (response) ixmlDocument_free(response);
	return NULL;
  }
  
  return response;
}

/*------------------------------------------------------------------------*/
int QueryDevice(struct Upnp_Discovery* device)
{
  /* Query the WMA device.
   * This function first invokes the GetTransferState of the
   * ApplicationTransferService service. If the response is "NOT_STARTED"
   * it proceeds to query firmware version then returns TRUE. Otherwise
   * FALSE is returned. */

  IXML_Document *response = NULL;
  
  message("Querying %s\n", device->DeviceId);
  
  /* Get device state. */
  response = SendAction("GetTransferState", ATS_TYPE,
						urlcat(device->Location, ATS_CONTROL_URL), 0);
  
  if (response == NULL){
	message("Failed response to GetTransferState()");
  }
  else{
	int do_boot = TRUE;
	
	char *state = ixmlNode_getNodeValue
	  (ixmlNode_getFirstChild
	   (ixmlNodeList_item
		(ixmlElement_getElementsByTagName
		 (ixmlDocument_getElementById
		  (response,	"TransferState"), "*" ), 0)));
	
	message("Device reports state: %s\n", state);
	
	/* Only boot if device reports it is NOT_STARTED */
	if (strcmp(state, "NOT_STARTED") != 0) do_boot = FALSE;
	
	ixmlDocument_free(response);
	
	if (! do_boot) return FALSE;
  }
  
  /* Get firmware version. For the curious only. */
  response = SendAction("GetExtDeviceDescription", AIS_TYPE,
						urlcat(device->Location, AIS_CONTROL_URL), 0);
  
  if (response == NULL){
	message("Failed response to GetExtDeviceDescription()");
  }
  else{
	message("Device reports firmware version: %s\n", ixmlNode_getNodeValue
			(ixmlNode_getFirstChild
			 (ixmlNodeList_item
			  (ixmlElement_getElementsByTagName
			   (ixmlDocument_getElementById
				(response,	"version"), "*" ), 0))));
	
	
	ixmlDocument_free(response);
  }
  return TRUE;
}

/*------------------------------------------------------------------------*/
void BootDevice(struct Upnp_Discovery* device)
{
  /* Invoke the SetApplicationPackageURI action */
  /* Device should respond by fetching image for the URI sent */

  char image_uri[128];
  char image_size[10];
  int ret = 0;
  IXML_Document *action = NULL, *response = NULL;

  snprintf(image_size, 10, "%d", it_get_image_size());
  snprintf(image_uri, 128, "uri://%s:%d", UpnpGetServerIpAddress(), it_get_port());

  action = UpnpMakeAction("SetApplicationPackageURI", ATS_TYPE, 2,
						  "ApplicationURI", image_uri, 
						  "ImageLength", image_size);

  if (action == NULL){
	message("Unable to create action IXML document\n");
	return;
  }

  message("Booting %s\n", device->DeviceId);

  ret = UpnpSendAction(ctrlhandle, urlcat(device->Location, ATS_CONTROL_URL),
					   ATS_TYPE, NULL, action, &response);

  /* "ret" is not checked because the WMA11b does not give normally
   * a correct response. This was confirmed with network sniffing.
   * Uncommenting this code results in a UPNP_E_BAD_RESPONSE message */
  //if (ret != UPNP_E_SUCCESS){
  //	message("Unable to invoke SetApplicationPackageURI action: %d (%s)\n",
  //		ret, UpnpGetErrorMessage(ret));
  //}

  if (action)   ixmlDocument_free(action);
  if (response) ixmlDocument_free(response);
}

/*------------------------------------------------------------------------*/
/* Handle Upnp library callbacks */
int LoaderCallbackFunc(Upnp_EventType EventType, void *Event, void *Cookie)
{
  struct Upnp_Discovery *device;

  switch (EventType){
  case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
  case UPNP_DISCOVERY_SEARCH_RESULT:
	device = (struct Upnp_Discovery*)Event;	

	/* Add to linked list if this is an AppLoader device */
	if (strncmp(device->DeviceType,
				WMA_DEVICE_TYPE,
				sizeof(WMA_DEVICE_TYPE)) == 0){

	  /* Take a copy of the Upnp_Discovery structure */
	  device = malloc(sizeof(struct Upnp_Discovery));
	  if (device == NULL) break;
	  memcpy(device, Event, sizeof(struct Upnp_Discovery));
	  
	  pthread_mutex_lock(&list_mutex);
	  
	  if (! ListFind(&discoveries, NULL, &device)){
		ListAddTail(&discoveries, device);
		sem_post(&is_work_to_do);
	  }
	  
	  pthread_mutex_unlock(&list_mutex);
	}
	break;

	/* Unused events */
  case UPNP_EVENT_RECEIVED:
  case UPNP_CONTROL_ACTION_REQUEST:
  case UPNP_CONTROL_ACTION_COMPLETE:
  case UPNP_CONTROL_GET_VAR_REQUEST:
  case UPNP_CONTROL_GET_VAR_COMPLETE:
  case UPNP_DISCOVERY_SEARCH_TIMEOUT:
  case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
  case UPNP_EVENT_SUBSCRIPTION_REQUEST:
  case UPNP_EVENT_RENEWAL_COMPLETE:
  case UPNP_EVENT_SUBSCRIBE_COMPLETE:
  case UPNP_EVENT_UNSUBSCRIBE_COMPLETE:
  case UPNP_EVENT_AUTORENEWAL_FAILED:
  case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
	break;
  }
  return 0;
}

/*------------------------------------------------------------------------*/
int initialise_upnp_library(){

  /* Initialise upnp library */
  int ret = UpnpInit(global_options.interface, 0);
  if (ret != UPNP_E_SUCCESS){
	message("upnp library initialisation failed: %d (%s)\n",
			ret, UpnpGetErrorMessage(ret));
	UpnpFinish();
	return FALSE;
  }

  message("UPnP Library initialised. Using %s:%d\n",
		 UpnpGetServerIpAddress(), UpnpGetServerPort());
  
  /* Register callback function */
  ret = UpnpRegisterClient(LoaderCallbackFunc, NULL, &ctrlhandle);
  if (ret != UPNP_E_SUCCESS){
	message("callback function failed to register: %d (%s)\n",
			ret, UpnpGetErrorMessage(ret));
	UpnpFinish();
	return FALSE;
  }
  
  return TRUE;
}

/*------------------------------------------------------------------------*/
int initialise_image_transfer()
{
  /* Start the image transfer thread and perform some basic sanity
   * checking. When this function completes the software is ready
   * to accept connections for image transfer. See image_transfer.c */

  pthread_t th_transfer;
  int ret;

  /* Initialise image transfer code */
  if (! it_init(global_options.interface,
				global_options.image)) return FALSE;

  /* Check image file can be read OK */
  if (it_get_image_size() == 0){
	message("Unable to read image %s\n", global_options.image);
	return FALSE;
  }

  /* Start image transfer thread. */
  ret = pthread_create(&th_transfer, NULL, &it_thread_entry, NULL);
  if (ret != 0){
	perror("initialise_image_transfer() pthread_create");
	return FALSE;
  } 
  
  message("Listening for image transfer on port %d\n", it_get_port());
  return TRUE;
}

/*------------------------------------------------------------------------*/
int initialise_sync_variables(){
  /* Set thread syncronisation variables to their initial state */
  if (pthread_mutex_init(&list_mutex, NULL) != 0) return FALSE;
  if (sem_init(&is_work_to_do, 0, 0) != 0) return FALSE;
  return TRUE;
}

/*------------------------------------------------------------------------*/
int list_compare_func(void *a, void *b){
  /* Compare function for LinkedList */
  struct Upnp_Discovery *_a = (struct Upnp_Discovery*)a;
  struct Upnp_Discovery *_b = (struct Upnp_Discovery*)b;
  return (_a->DeviceId == _b->DeviceId);
}

/*------------------------------------------------------------------------*/
void list_free_func(void *item){
  /* Free function for LinkedList */
  free(item);
}

/*------------------------------------------------------------------------*/
void DoWork()
{
  struct Upnp_Discovery* device = NULL;

  /* Fetch a discovery from list */	
  pthread_mutex_lock(&list_mutex);
  if (ListHead(&discoveries)) device = ListHead(&discoveries)->item;
  pthread_mutex_unlock(&list_mutex);
  
  if (device){

	/* Boot device if it checks out */
	if (QueryDevice(device)) BootDevice(device);
	
	/* Remove device from list */
	pthread_mutex_lock(&list_mutex);
	ListDelNode(&discoveries, ListHead(&discoveries), 1 /* Free item */);
	pthread_mutex_unlock(&list_mutex);
  }
}

/*------------------------------------------------------------------------*/
int parse_command_line(int argc, char **argv){
	char x = 0;
    int idx = 0;

	static struct option opts[] =
	{
		{"interface", 1, 0,                      0 }, // 0
	    {"image",     1, 0,                      0 }, // 1
	    {"daemon",    0, &global_options.daemon, 1 }, // 2
		{"help",      0, &global_options.help,   1 }, // 3
		{0, 0, 0, 0}
	};

	/* Set default options */
	global_options.interface = NULL;
	global_options.image     = NULL;
	global_options.daemon    = FALSE;
	global_options.help      = FALSE;

    while ( x != -1 ){
		x = getopt_long(argc, argv, "dhv", opts, &idx);
		if (x == '?') return 0;
		if (x == ':') return 0;
		if (x == 0){
			//A long option
			switch (idx){
			case 0: global_options.interface = optarg; break;
			case 1:	global_options.image     = optarg; break;
		    }
		}
		else if (x == 'd') global_options.daemon = 1;
		else if (x == 'v' || x == 'h') global_options.help = 1;
	}

	// Check for missing required arguments
	if (! global_options.image){
	  message("Missing required argument --image\n");
	  return 0;
	}
	
	return 1;
}

/*------------------------------------------------------------------------*/
void print_usage(){
  printf("\n%s\n", BANNER);
  printf("Usage: wmaloader --image <file> [options]\n");
  printf("\n");
  printf("Options:\n");
  printf("  --image <file>  Path to boot image. Required.\n");
  printf("  --daemon or -d  Detach from terminal and run as daemon.\n");
  printf("  --interface     IP address to bind to in dotted decimal \n");
  printf("                   format. If not supplied the uPnP library will\n");
  printf("                   choose the first available address.\n");
}

/*------------------------------------------------------------------------*/
int daemonise(){

  /* Open syslog ready for writing messages and print banner */
  openlog(SYSLOG_IDENT, 0, LOG_DAEMON);
  syslog(LOG_INFO, BANNER);

  /* Fork and leave only the child running */
  if (daemon(TRUE, FALSE) == -1){
	perror("daemonise() daemon");
	return FALSE;
  }

  backgrounded = TRUE;

  return TRUE;
}

/*------------------------------------------------------------------------*/
int main(int argc, char** argv)
{
  /* Read command line options into structure global_options */
  if (! parse_command_line(argc, argv)){
	print_usage();
	return EXIT_FAILURE;
  }

  /* Fork and daemonise if required */
  if (global_options.daemon && ! daemonise()) return EXIT_FAILURE;

  /* Initialisation */
  if (! initialise_image_transfer()) return EXIT_FAILURE;
  if (! initialise_upnp_library())   return EXIT_FAILURE;
  if (! initialise_sync_variables()) return EXIT_FAILURE;

  if (ListInit(&discoveries, &list_compare_func, &list_free_func) != 0)
	return EXIT_FAILURE;

 /* Start search for WMA device */
  if (UpnpSearchAsync(ctrlhandle, 5, WMA_DEVICE_TYPE, 0) != UPNP_E_SUCCESS){
	message("Failed to start search");
	UpnpFinish();
	return 0;
  }

  /* Main Loop */
  while(1){
	/* Wait for something to do */
	sem_wait(&is_work_to_do);

	/* Do something */
	DoWork();
  }
  
  UpnpFinish();

  return 0;
}

