/* webserver.c - Serving files through the libupnp built in web server
 *
 * 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 <sys/types.h>		/* POSIX */
#include <sys/stat.h>		/* ? */
#include <unistd.h>		/* POSIX */
#include <fcntl.h>		/* ? */
#include <inttypes.h>		/* POSIX */
#include <time.h>
#include "gettext.h"            /* Gnulib/gettext */
#define _(s) gettext(s)
#define N_(s) gettext_noop(s)
#include "xalloc.h"		/* Gnulib */
#include "quote.h"		/* Gnulib */
#include "quotearg.h"		/* Gnulib */
#include "minmax.h"		/* Gnulib */
#include "intutil.h"
#include "gmediaserver.h"
#include "hmap.h"
#include "schemas/ConnectionManager.h"
#include "schemas/ContentDirectory.h"

typedef struct {
    const char *fullpath;
    size_t pos;
    enum {
	FILE_LOCAL,
	FILE_LOCAL_SPECIAL,
	FILE_MEMORY,
	FILE_URL,
    } type;
    union {
	struct {
	    int fd;
	    Entry *entry;
	} local;
	struct {
	    const char *contents;
	    size_t len;
	} memory;
	struct {
	    Entry *entry;
	    HTTPResult *result;
	    size_t length;
	} url;
    } detail;
} WebServerFile;

static Entry *
get_entry_from_url(const char *filename)
{
    int32_t id;

    if (filename[0] != '/')
	return NULL;
    filename = strchr(filename+1, '/');
    if (filename == NULL)
	return NULL;
    filename++;
    if (filename[0] == '\0')
	return NULL;
    if (!parse_int32(filename, &id))
	return NULL;

    return get_entry_by_id(id);
}

static int
webserver_get_info(const char *filename, struct File_Info *info)
{
    Entry *entry;
    EntryDetail *detail;

    say(4, _("Request file information for %s\n"), quote(conv_filename(filename)));

    if (strcmp(filename, "/upnp/ContentDirectory.xml") == 0) {
	info->file_length = CONTENTDIRECTORY_DESC_LEN;
	info->last_modified = 0;
	info->is_directory = 0;
	info->is_readable = 1;
	info->content_type = ixmlCloneDOMString("text/xml");
	return 0;
    }
    if (strcmp(filename, "/upnp/ConnectionManager.xml") == 0) {
	info->file_length = CONNECTIONMANAGER_DESC_LEN;
	info->last_modified = 0;
	info->is_directory = 0;
	info->is_readable = 1;
	info->content_type = ixmlCloneDOMString("text/xml");
	return 0;
    }

    entry = get_entry_from_url(filename);
    if (entry == NULL)
	return -1;

    if (has_entry_detail(entry, DETAIL_FILE)) {
        detail = get_entry_detail(entry, DETAIL_FILE);
        info->is_readable = 1;
        info->is_directory = 0;
	if (S_ISREG(detail->data.file.mode))
	    info->file_length = detail->data.file.size;
	else
	    info->file_length = -1;
        info->last_modified = detail->data.file.mtime;
        info->content_type = ixmlCloneDOMString(detail->data.file.mime_type);
        say(4, _("  Readable: %s\n"), info->is_readable ? _("Yes") : _("No"));
        say(4, _("  Directory: %s\n"), info->is_readable ? _("Yes") : _("No"));
        say(4, _("  File Length: %" PRIi64 "\n"), (int64_t) info->file_length);
        /* say(4, _("  Last modified: %s\n"), asctime(localtime(info->last_modified))); */
        say(4, _("  Content-type: %s\n"), info->content_type);
        say(4, _("Returned info for file %s successfully\n"), quote(conv_filename(detail->data.file.filename)));
        return 0;
    }
    if (has_entry_detail(entry, DETAIL_URL)) {
        HTTPResult *result;
        char *value;
        uint32_t length;
        time_t modified;

        detail = get_entry_detail(entry, DETAIL_URL);
        result = http_query("HEAD", detail->data.url.url, false);
        if (result == NULL)
            return -1;

        if (result->result == -1) {
            info->is_readable = 1;
            info->is_directory = 0;
            info->file_length = -1;
            info->content_type = ixmlCloneDOMString("application/octet-stream");
            info->last_modified = 0;
            http_result_free(result);
            return 0;
        }

        info->is_readable = 1;
        info->is_directory = 0;
        value = hmap_get(result->headers, "Content-Length");
        if (value != NULL && parse_uint32(value, &length)) {
            info->file_length = length; /* XXX: truncated from uint32_t to int! */
        } else {
            info->file_length = -1; /* -1 means unknown - fetch as much as possible. */
        }
        value = hmap_get(result->headers, "Content-Type");
        if (value != NULL) {
            info->content_type = ixmlCloneDOMString(value);
        } else {
            info->content_type = ixmlCloneDOMString("application/octet-stream");
        }
        value = hmap_get(result->headers, "Last-Modified");
        if (value != NULL && parse_http_date(value, &modified)) {
            info->last_modified = modified;
        } else {
            info->last_modified = 0;
        }
        http_result_free(result);

        say(4, _("Returned info for URL %s successfully\n"), quotearg(detail->data.url.url));
        return 0;
    }

    return -1;
}

static UpnpWebFileHandle
webserver_open(const char *filename, enum UpnpOpenFileMode mode)
{
    Entry *entry;
    WebServerFile *file;

    say(4, _("Opening file %s\n"), quote(conv_filename(filename)));

    if (mode != UPNP_READ) {
	warn(_("%s: ignoring request to open file for writing\n"), quotearg(conv_filename(filename)));
	return NULL;
    }

    if (strcmp(filename, "/upnp/ContentDirectory.xml") == 0) {
	file = xmalloc(sizeof(WebServerFile));
	file->fullpath = xstrdup("ContentDirectory.xml in memory");
	file->pos = 0;
	file->type = FILE_MEMORY;
	file->detail.memory.contents = CONTENTDIRECTORY_DESC;
	file->detail.memory.len = CONTENTDIRECTORY_DESC_LEN;
	return file;
    }
    if (strcmp(filename, "/upnp/ConnectionManager.xml") == 0) {
	file = xmalloc(sizeof(WebServerFile));
	file->fullpath = xstrdup("ConnectionManager.xml in memory");
	file->pos = 0;
	file->type = FILE_MEMORY;
	file->detail.memory.contents = CONNECTIONMANAGER_DESC;
	file->detail.memory.len = CONNECTIONMANAGER_DESC_LEN;
	return file;
    }

    entry = get_entry_from_url(filename);
    if (entry == NULL)
	return NULL;

    if (has_entry_detail(entry, DETAIL_FILE)) {
        int fd;
        char *fullpath;
	EntryDetail *detail;
	
	detail = get_entry_detail(entry, DETAIL_FILE);
        fullpath = detail->data.file.filename;

        fd = open(fullpath, O_RDONLY);
        if (fd < 0) {
            warn(_("%s: cannot open for reading: %s\n"), quotearg(conv_filename(fullpath)), errstr);
            return NULL;
        }

        file = xmalloc(sizeof(WebServerFile));
        file->fullpath = fullpath;
        file->pos = 0;
        file->type = S_ISREG(detail->data.file.mode) ? FILE_LOCAL : FILE_LOCAL_SPECIAL;
        file->detail.local.entry = entry;
        file->detail.local.fd = fd;

        return (UpnpWebFileHandle) file;
    }
    if (has_entry_detail(entry, DETAIL_URL)) {
        char *url;
        HTTPResult *result;
        char *value;
        uint32_t length;

        url = get_entry_detail(entry, DETAIL_URL)->data.url.url;

        result = http_query("GET", url, true);
        if (result == NULL)
            return NULL;
        if (result->result == -1) {
            warn(_("%s: remote server did not support GET command\n"), quotearg(url));
            http_result_free(result);
            return NULL;
        }

        file = xmalloc(sizeof(WebServerFile));
        file->fullpath = url;
        file->pos = 0;
        file->type = FILE_URL;
        file->detail.url.entry = entry;
        file->detail.url.result = result;

        /* XXX: it is stupid to parse Content-Length here and in getinfo as well. */
        value = hmap_get(result->headers, "Content-Length");
        if (value != NULL && parse_uint32(value, &length)) {
            file->detail.url.length = length; /* XXX: truncated from uint32_t to int! */
        } else {
            file->detail.url.length = -1; /* -1 means unknown - fetch as much as possible. */
        }

        return (UpnpWebFileHandle) file;
    }

    return NULL;
}

static int
webserver_read(UpnpWebFileHandle fh, char *buf, size_t buflen)
{
    WebServerFile *file = (WebServerFile *) fh;
    ssize_t len = -1;

    say(4, _("Attempting to read %zu bytes at %zu from %s\n"), buflen, file->pos, quote(conv_filename(file->fullpath)));

    switch (file->type) {
    case FILE_LOCAL:
    case FILE_LOCAL_SPECIAL:
	len = read(file->detail.local.fd, buf, buflen);
	break;
    case FILE_MEMORY:
	len = MIN(buflen, file->detail.memory.len - file->pos);
	memcpy(buf, file->detail.memory.contents + file->pos, len);
	break;
    case FILE_URL:
	len = http_read(file->detail.url.result, buf, buflen);
	break;
    }

    if (len < 0)
	warn(_("%s: cannot read: %s\n"), quotearg(conv_filename(file->fullpath)), errstr);
    else
	file->pos += len;

    return len;
}

static int
webserver_write(UpnpWebFileHandle fh, char *buf, size_t buflen)
{
    WebServerFile *file = (WebServerFile *) fh;

    warn(_("%s: ignoring request to write to file\n"), quotearg(conv_filename(file->fullpath)));

    return -1;
}

static int
webserver_seek(UpnpWebFileHandle fh, off_t offset, int origin)
{
    WebServerFile *file = (WebServerFile *) fh;
    long newpos = -1;

    switch (origin) {
    case SEEK_SET:
	say(4, _("Attempting to seek to %" PRIi64 " (was at %zu) in %s\n"), (int64_t) offset, file->pos, quote(conv_filename(file->fullpath))); /* XXX: use quote or quotearg here? */
	newpos = offset;
	break;
    case SEEK_CUR:
	say(4, _("Attempting to seek by %" PRIi64 " from %zu in %s\n"), (int64_t) offset, file->pos, quote(conv_filename(file->fullpath))); /* XXX: use quote or quotearg here? */
	newpos = file->pos + offset;
	break;
    case SEEK_END:
	say(4, _("Attempting to seek by %" PRIi64 " from end (was at %zu) in %s\n"), (int64_t) offset, file->pos, quote(conv_filename(file->fullpath))); /* XXX: use quote or quotearg here? */
	if (file->type == FILE_LOCAL) {
	    struct stat sb;
	    if (stat(file->fullpath, &sb) < 0) {
		warn(_("%s: cannot stat: %s\n"), quotearg(conv_filename(file->fullpath)), errstr);
		return -1;
	    }
	    newpos = sb.st_size + offset;
	} else if (file->type == FILE_LOCAL_SPECIAL) {
	    warn(_("%s: cannot seek relative to end in special file: unknown length of data\n"), quotearg(conv_filename(file->fullpath)));
	    return -1;
	} else if (file->type == FILE_MEMORY) {
	    newpos = file->detail.memory.len + offset;
	} else if (file->type == FILE_URL) {
	    if (file->detail.url.length == -1) {
	        warn(_("%s: cannot seek relative to end in HTTP/ICY connection: unknown length of data\n"), quotearg(conv_filename(file->fullpath)));
	        return -1;
            }
            newpos = file->detail.url.length + offset;
	}
	break;
    }

    switch (file->type) {
    case FILE_LOCAL:
    case FILE_LOCAL_SPECIAL:
	/* Just make sure we cannot seek before start of file. */
	if (newpos < 0) {
	    warn(_("%s: cannot seek: %s\n"), quotearg(conv_filename(file->fullpath)), strerror(EINVAL));
	    return -1;
	}
	/* Don't seek with origin as specified above, as file may have
	 * changed in size since our last stat.
	 */
	if (lseek(file->detail.local.fd, newpos, SEEK_SET) == -1) {
	    warn(_("%s: cannot seek: %s\n"), quotearg(conv_filename(file->fullpath)), errstr);
	    return -1;
	}
	break;
    case FILE_MEMORY:
	if (newpos < 0 || newpos > file->detail.memory.len) {
	    warn(_("%s: cannot seek: %s\n"), quotearg(conv_filename(file->fullpath)), strerror(EINVAL));
	    return -1;
	}
	break;
    case FILE_URL:
        if (newpos < 0 || newpos < file->pos) {
	    warn(_("%s: cannot seek backwards in HTTP/ICY connections\n"), quotearg(conv_filename(file->fullpath)));
	    return -1;
        }
        if (newpos != file->pos) {
            if (http_skip(file->detail.url.result, newpos-file->pos) <= 0) {
                warn(_("%s: cannot seek: %s\n"), quotearg(conv_filename(file->fullpath)), errstr);
                return -1;
            }
        }
        break;
    }

    file->pos = newpos;
    return 0;
}

static int
webserver_close(UpnpWebFileHandle fh)
{
    WebServerFile *file = (WebServerFile *) fh;

    say(4, _("Closing file %s\n"), quote(conv_filename(file->fullpath)));

    switch (file->type) {
    case FILE_LOCAL:
    case FILE_LOCAL_SPECIAL:
	close(file->detail.local.fd); /* Ignore errors since only reading */
	break;
    case FILE_MEMORY:
	/* no operation */
	break;
    case FILE_URL:
        http_result_free(file->detail.url.result); /* Ignore errors since only reading */
        break;
    }
    free(file);

    return 0;
}

struct UpnpVirtualDirCallbacks virtual_dir_callbacks = {
    webserver_get_info,
    webserver_open,
    webserver_read,
    webserver_write,
    webserver_seek,
    webserver_close
};
