/* main.c - Main routines for the gmediaserver
 *
 * 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 <upnp/upnp.h>		/* libupnp */
#include <upnp/upnptools.h>	/* libupnp */
#include <unistd.h>		/* POSIX */
#include <fcntl.h>		/* ? */
#include <getopt.h>		/* Gnulib, GNU */
#include <errno.h>		/* ? */
#include <stdio.h>		/* C89 */
#include <string.h>		/* C89 */
#include <stdlib.h>		/* C89 */
#include <stdarg.h>		/* C89 */
#include <stdbool.h>		/* Gnulib, C99 */
#include <signal.h>		/* ? */
#include <locale.h>		/* ? */
#include <iconv.h>		/* Gnulib, POSIX */
#include "striconv.h"		/* Gnulib */
#ifdef HAVE_NL_LANGINFO
#include <langinfo.h>
#endif
#include "gettext.h"		/* Gnulib/gettext */
#define _(s) gettext(s)
#define N_(s) gettext_noop(s)
#include "xalloc.h"		/* Gnulib */
#include "version-etc.h"	/* Gnulib */
#include "progname.h"		/* Gnulib */
#include "sig2str.h"		/* Gnulib */
#include "quote.h"		/* Gnulib */
#include "quotearg.h"		/* Gnulib */
#include "configmake.h"		/* Gnulib */
#include "tempfailure.h"
#include "intutil.h"
#include "strutil.h"
#include "gmediaserver.h"

enum {
    OPT_DISABLE_TAGS = 256,
    OPT_FS_CHARSET,
    OPT_DEVICE_CHARSET,
    OPT_LOG_CHARSET,
    OPT_FRIENDLY_NAME,
    OPT_PIDFILE,
    OPT_PORT,
    OPT_NOTIMESTAMP,
    OPT_TIMESTAMP,
    OPT_EXPIRE_TIME,
    OPT_FILE_TYPES,
    OPT_PROFILE,
    OPT_HELP,
    OPT_VERSION,
};

static const char *short_options = "bv::i:o:p:";
static struct option long_options[] = {
    { "disable-tags", no_argument, NULL, OPT_DISABLE_TAGS },
    { "fs-charset", required_argument, NULL, OPT_FS_CHARSET },
    { "device-charset", required_argument, NULL, OPT_DEVICE_CHARSET },
    { "log-charset", required_argument, NULL, OPT_LOG_CHARSET },
    { "friendly-name", required_argument, NULL, OPT_FRIENDLY_NAME },
    { "pid-file", required_argument, NULL, OPT_PIDFILE },
    { "profile", required_argument, NULL, OPT_PROFILE, },
    { "port", required_argument, NULL, 'p' },
    { "output", required_argument, NULL, 'o' },
    { "background", no_argument, NULL, 'b' },
    { "no-timestamp", no_argument, NULL, OPT_NOTIMESTAMP },
    { "timestamp", optional_argument, NULL, OPT_TIMESTAMP },
    { "interface", required_argument, NULL, 'i' },
    { "verbose", optional_argument, NULL, 'v' },
    { "expire-time", required_argument, NULL, OPT_EXPIRE_TIME },
    { "file-types", required_argument, NULL, OPT_FILE_TYPES },
    { "help", no_argument, NULL, OPT_HELP },
    { "version", no_argument, NULL, OPT_VERSION },
    { NULL, 0, NULL, 0 }
};

static iconv_t utf8_to_device = (iconv_t) -1;
static iconv_t utf8_to_log = (iconv_t) -1;
static iconv_t fs_to_utf8 = (iconv_t) -1;
const char version_etc_copyright[] = "Copyright (C) 2005, 2006 Oskar Liljeblad.";

char *
convert_string_to_device(const char *str)
{
    char *out;
    if (utf8_to_device == (iconv_t) -1)
        return xstrdup(str);
    out = str_cd_iconv(str, utf8_to_device);
    if (out != NULL)
        return out;
    warn(_("%s: cannot convert to device character set: %s\n"), quotearg(str), errstr);
    return xstrdup(str);
}

char *
convert_string_to_log(const char *str)
{
    char *out;

    if (utf8_to_log == (iconv_t) -1)
        return xstrdup(str);
    out = str_cd_iconv(str, utf8_to_log);
    if (out != NULL)
        return out;
    /* Cannot warn here - would deadlock! */
    return xstrdup(str);
}

static char *cache_fs_str = NULL;

char *
conv_filename(const char *str)
{
    free(cache_fs_str);
    if (fs_to_utf8 == (iconv_t) -1) {
        cache_fs_str = xstrdup(str);
    } else {
        cache_fs_str = str_cd_iconv(str, fs_to_utf8);
        if (cache_fs_str == NULL) {
            warn(_("%s: cannot convert filename to %s: %s\n"), quotearg(str), "UTF-8", errstr);
            cache_fs_str = xstrdup(str);
        }
    }
    return cache_fs_str;
}

static void
dummy_signal_handler(int signal)
{
    /* No operation */
}

static void
write_pid_file(const char *pidfilename, pid_t pid, bool background)
{
    if (pidfilename != NULL) {
	FILE *pidfh;

	/* If we are going to background, we don't want to log this message
         * to the console. We only want to tell if the pid file failed to
         * write, which is pretty serious though.
         */
	if (!background)
	    say(2, _("Writing pid to pid file %s\n"), quote(conv_filename(pidfilename)));
	if ((pidfh = fopen(pidfilename, "w")) == NULL)
	    die(_("cannot open pid file %s for writing - %s\n"), quote(conv_filename(pidfilename)), errstr);
	fprintf(pidfh, "%d", pid);
	if (fclose(pidfh) != 0)
	    die(_("cannot close pid file %s - %s\n"), quote(conv_filename(pidfilename)), errstr);
    }
}

int
main(int argc, char **argv)
{
    struct sigaction signalaction;
    sigset_t signalset;
    char *pidfilename = NULL;
    char *listenip = NULL;
    uint16_t listenport = 0;
    bool background = false;
    bool timestamp_specified = false;
    char *logfilename = NULL;
    char *timestamp_format = NULL;
    uint32_t expire_time;
    char *fs_charset = NULL;
    char *device_charset = NULL;
    char *log_charset = NULL;

    set_program_name(argv[0]);
    set_quoting_style(0, escape_quoting_style);

    if (setlocale(LC_ALL, "") == NULL)
        warn(_("cannot set locale: %s\n"), errstr);
#ifdef ENABLE_NLS
    if (bindtextdomain(PACKAGE, LOCALEDIR) == NULL)
        warn(_("cannot bind message domain: %s\n"), errstr);
    if (textdomain(PACKAGE) == NULL)
        warn(_("cannot set message domain: %s\n"), errstr);
#endif

    for (;;) {
    	int c;

        c = getopt_long(argc, argv, short_options, long_options, NULL);
        if (c == -1)
            break;

        switch (c) {
        case 'i':
	    if ((listenip = get_ip_by_spec(optarg)) == NULL)
	    	die(_("invalid address or interface name %s\n"), quote(optarg));
            break;
        case 'o':
            logfilename = optarg;
            break;
	case OPT_DISABLE_TAGS:
	    tags_enabled = false;
	    break;
        case OPT_FS_CHARSET:
            fs_charset = optarg;
            break;
        case OPT_DEVICE_CHARSET:
            device_charset = optarg;
            break;
        case OPT_LOG_CHARSET:
            log_charset = optarg;
            break;
	case OPT_FRIENDLY_NAME:
	    if (optarg[0] == '\0')
		die(_("friendly name cannot be empty\n"));
	    friendly_name = xstrdup(optarg);
	    break;
	case OPT_FILE_TYPES:
	    file_types = optarg;
	    /* XXX: check that list is valid */
	    break;
	case OPT_PIDFILE:
	    pidfilename = optarg;
	    break;
	case 'p':
	    if (!parse_uint16(optarg, &listenport))
		die(_("invalid port number %s\n"), quote(optarg));
	    break;
        case OPT_EXPIRE_TIME:
            if (!parse_uint32(optarg, &expire_time) || expire_time > INT_MAX)
		die(_("invalid expire time %s\n"), quote(optarg));
            ssdp_expire_time = expire_time;
            /* XXX: parse better, allow hours etc? */
            break;
	case OPT_NOTIMESTAMP:
	    timestamp_format = NULL;
	    timestamp_specified = true;
	    break;
	case OPT_TIMESTAMP:
	    timestamp_format = (optarg != NULL ? optarg : DEFAULT_TIMESTAMP_FORMAT);
	    timestamp_specified = true;
	    break;
	case OPT_PROFILE:
	    if (strcmp(optarg, "generic") == 0) {
		/* no operation - no custom settings*/
	    } else if (strcmp(optarg, "mp101") == 0) {
	        file_types = "mp3,wma,m3u,pls";
	    } else if (strcmp(optarg, "dms1") == 0) {
	        file_types = "mp3,wma,m3u,pls";
		expire_time = 600;
	    } else if (strcmp(optarg, "ps3") == 0) {
	        file_types = "mp3,wma,wav,m4a,mpg,mp4,pls,m3u,bmp,gif,jpg,png,tiff";
	    } else {
		die(_("invalid profile name %s\n"), quote(optarg));
	    }
	    break;
        case 'b':
	    background = true;
	    break;
        case 'v':
	    if (optarg != NULL) {
		if (optarg[0] == 'v') {
		    for (verbosity = 0; optarg[verbosity] != '\0'; verbosity++) {
			if (optarg[verbosity] != 'v')
			    die(_("invalid verbosity %s\n"), quote(optarg));
		    }
		    verbosity++;
		} else {
		    if (!parse_uint32(optarg, &verbosity))
			die(_("invalid verbosity %s\n"), quote(optarg));
		}
		if (verbosity > MAX_VERBOSITY)
		    die(_("maximum verbosity is %d\n"), MAX_VERBOSITY);
	    } else {
		verbosity++;
	    }
            break;
        case OPT_HELP:
            printf(_("Usage: %s [OPTIONS]... DIRECTORIES...\n"), argv[0]);
            printf(_("Run the UPnP media server.\n\n"));
	    printf(_("      --friendly-name=NAME      set display name for media server\n"));
	    printf(_("      --disable-tags            do not scan files for tags\n"));
	    printf(_("      --fs-charset=CHARSET      character set used in file names\n"));
	    printf(_("      --device-charset=CHARSET  character set used in the player device\n"));
	    printf(_("      --log-charset=CHARSET     character set used in logs and display\n"));
	    printf(_("  -v, --verbose[=LEVEL]         set verbosity level (0-4)\n"));
            printf(_("      --pid-file=FILE           write pid to FILE when up and running\n"));
            printf(_("  -i, --interface=NAME          listen on a specific interface\n"));
            printf(_("  -p, --port=PORT               listen on a specific port\n"));
            printf(_("  -o, --output=LOGFILE          file for logging\n"));
            printf(_("  -b, --background              go to background (detach)\n"));
            printf(_("      --no-timestamp            do not prepend timestamp to log entries\n"));
            printf(_("      --timestamp[=FORMAT]      prepend timestamp with optional time format\n"));
	    printf(_("      --profile=NAME            specify profile (see below)\n"));
	    printf(_("      --file-types=TYPE[,..]    list of file types to serve (see docs)\n"));
            printf(_("      --expire-time=SECONDS     advertisement expire time (default 100)\n"));
            printf(_("      --help                    display this help and exit\n"));
            printf(_("      --version                 output version information and exit\n"));
	    printf(_("\nThe directories arguments specify where audio files are located.\n"
		     "See strftime(3) for description of the timestamp format.\n"));
	    printf(_("\nA profile specified with --profile enables settings for a certain device.\n"
		     "The following values are valid for --profile:\n\n"));
	    printf(_("  generic  no custom settings\n"
	             "  mp101    Netgear MP101\n"
		     "  dms1     Omnifi DMS1\n"
		     "  ps3      PlayStation 3\n"));
            printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
            exit(EXIT_SUCCESS);
        case OPT_VERSION:
            version_etc(stdout, NULL, PACKAGE, VERSION, "Oskar Liljeblad", NULL);
            exit(EXIT_SUCCESS);
    	case '?':
	    exit(EXIT_FAILURE);
        }
    }

    if (argc - optind <= 0) {
        warn(_("missing directory argument\n"));
        /* fprintf(stderr, _("Try `%s --help' for more information.\n"), argv[0]); */
        exit(EXIT_FAILURE);
    }

    if (background) {
        pid_t pid;

        /*say(2, "Moving process into background...\n");*/
    	if ((pid = fork()) < 0)
    	    die(_("cannot create new process - %s\n"), errstr);
        if (pid != 0) {
            /* Write pid before we return control to the shell. */
            write_pid_file(pidfilename, pid, true);
            exit(EXIT_SUCCESS);
        }

        fclose(stdin);	    	/* ignore errors */
	fclose(stdout);     	/* ignore errors */
	fclose(stderr);     	/* ignore errors */
        close(STDIN_FILENO);	/* ignore errors */
        close(STDOUT_FILENO);	/* ignore errors */
        close(STDERR_FILENO);	/* ignore errors */

        if (setsid() < 0)
	    die(_("cannot create process session - %s\n"), errstr);

	/* XXX: The following is necessary to work around a bug in
	 *      libupnp 1.2.1 - fd 0 must not be closed!
	 */
	if (open("/dev/null", O_RDONLY) < 0)
	    die(_("cannot open /dev/null - %s\n"), errstr);
    }


    if (!timestamp_specified && logfilename != NULL)
    	timestamp_format = DEFAULT_TIMESTAMP_FORMAT;

#ifdef HAVE_NL_LANGINFO
    if (fs_charset == NULL && getenv("G_BROKEN_FILENAMES") != NULL)
        fs_charset = nl_langinfo(CODESET);
#endif
    if (fs_charset != NULL) {
        fs_to_utf8 = iconv_open("UTF-8", fs_charset);
        if (fs_to_utf8 == (iconv_t) -1)
            die(_("cannot create character set convertor from %s to %s\n"), quotearg(fs_charset), "UTF-8");
    }
    say(4, _("Using local file system character set %s\n"), quote(fs_charset == NULL ? "UTF-8" : fs_charset));

    if (device_charset != NULL) {
        utf8_to_device = iconv_open(device_charset, "UTF-8");
        if (utf8_to_device == (iconv_t) -1)
            die(_("cannot create character set convertor from %s to %s\n"), "UTF-8", quotearg(device_charset));
    }
    say(4, _("Using device character set %s\n"), quote(device_charset == NULL ? "UTF-8" : device_charset));

#ifdef HAVE_NL_LANGINFO
    if (log_charset == NULL)
        log_charset = nl_langinfo(CODESET);
#endif
    if (log_charset != NULL) {
        utf8_to_log = iconv_open(log_charset, "UTF-8");
        if (utf8_to_log == (iconv_t) -1)
            die(_("cannot create character set convertor from %s to %s\n"), "UTF-8", quotearg(log_charset));
    }
    say(4, _("Using log character set %s\n"), quote(log_charset == NULL ? "UTF-8" : log_charset));

    init_logging(logfilename, timestamp_format);

    /* We could write pid before initiating logging too.
     */
    if (!background)
        write_pid_file(pidfilename, getpid(), false);

#ifndef MSG_NOSIGNAL
    signal(SIGPIPE, SIG_IGN);
#endif
    signalaction.sa_handler = dummy_signal_handler;
    if (sigemptyset(&signalaction.sa_mask) < 0)
    	die(_("cannot empty signal set - %s\n"), errstr);
    signalaction.sa_flags = SA_RESTART;
#ifdef HAVE_STRUCT_SIGACTION_SA_RESTORER
    signalaction.sa_restorer = NULL;
#endif
    if (sigaction(SIGINT, &signalaction, NULL) < 0)
    	die(_("cannot register signal handler - %s\n"), errstr);
    if (sigaction(SIGTERM, &signalaction, NULL) < 0)
    	die(_("cannot register signal handler - %s\n"), errstr);
    if (sigaction(SIGUSR1, &signalaction, NULL) < 0)
    	die(_("cannot register signal handler - %s\n"), errstr);

    if (sigemptyset(&signalset) < 0)
    	die(_("cannot empty signal set - %s\n"), errstr);
    if (sigaddset(&signalset, SIGTERM) < 0)
    	die(_("cannot add signal to set - %s\n"), errstr);
    if (sigaddset(&signalset, SIGINT) < 0)
    	die(_("cannot add signal to set - %s\n"), errstr);
    if (sigaddset(&signalset, SIGUSR1) < 0)
    	die(_("cannot add signal to set - %s\n"), errstr);
    if (sigprocmask(SIG_BLOCK, &signalset, NULL) < 0)
    	die(_("cannot block signals - %s\n"), errstr);

    if (!init_metadata()) {
        finish_logging(false);
        exit(EXIT_FAILURE);
    }

    if (!scan_entries(argv+optind, argc-optind, 0)) {
        finish_metadata();
        finish_logging(false);
	exit(EXIT_FAILURE);
    }

    init_contentdir();

    init_upnp(listenip, listenport);

    while (true) {
    	int sig;
    	char signame[SIG2STR_MAX];

      	if ((sig = TEMP_FAILURE_RETRY(sigwaitinfo(&signalset, NULL))) < 0)
    	    die(_("sigwaitinfo failed - %s\n"), errstr);
        if (sig2str(sig, signame) == 0)
      	    say(2, _("Received %s signal\n"), signame);
      	else
      	    say(2, _("Received signal %d\n"), sig);

      	if (sig == SIGUSR1) {
      	    lock_metadata();
      	    clear_entries();
      	    if (scan_entries(argv+optind, argc-optind, 0)) {
      	        say(2, _("Scanning of entries succeeded.\n"));
      	    } else {
      	        say(2, _("Scanning of entries failed.\n"));
      	    }
      	    bump_update_id();
      	    unlock_metadata();
      	} else if (sig == SIGTERM || sig == SIGINT) {
            break;
        }
    }
    
    finish_upnp();
    finish_contentdir();
    finish_metadata();

    if (pidfilename != NULL)
    	unlink(pidfilename); /* ignore errors */

    finish_logging(true);

    if (fs_to_utf8 != (iconv_t) -1)
        iconv_close(fs_to_utf8); /* ignore errors (only EINVAL) */
    if (utf8_to_device != (iconv_t) -1)
        iconv_close(utf8_to_device); /* ignore errors (only EINVAL) */
    if (utf8_to_log != (iconv_t) -1)
        iconv_close(utf8_to_log); /* ignore errors (only EINVAL) */

    free(cache_fs_str);

    exit(EXIT_SUCCESS);
}
