/* -*- tab-width: 4; indent-tabs-mode: nil -*- */
/* vim: set ts=4 sts=4 sw=4 expandtab number : */
/*
 * http.c : GeeXboX uShare Web Server handler.
 * Originally developped for the GeeXboX project.
 * Parts of the code are originated from GMediaServer from Oskar Liljeblad.
 * Copyright (C) 2005-2007 Benjamin Zores <ben@geexbox.org>
 *
 * 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 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 <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#include <upnp/upnp.h>
#include <upnp/upnptools.h>

#include "services.h"
#include "cds.h"
#include "cms.h"
#include "msr.h"
#include "metadata.h"
#include "http.h"
#include "minmax.h"
#include "trace.h"
#include "presentation.h"
#include "osdep.h"
#include "mime.h"
#include "recpt1.h"
#include "tssplitter_lite.h"

#define PROTOCOL_TYPE_PRE_SZ  11   /* for the str length of "http-get:*:" */
#define PROTOCOL_TYPE_SUFF_SZ 2    /* for the str length of ":*" */

extern thread_data tdata;

struct web_file_t {
    char *fullpath;
    off_t pos;
    enum {
        FILE_LOCAL,
        FILE_MEMORY,
        FILE_STREAM
    } type;
    union {
        struct {
            int fd;
            struct upnp_entry_t *entry;
        } local;
        struct {
            char *contents;
            off_t len;
        } memory;
        struct {
            int id;
            STREAM_QUEUE_T *p_queue;
            ARIB_STD_B25_BUFFER *qbuf;
            off_t len;
        } stream;
    } detail;
};

static off_t get_streaming_length (void);

    static inline void
set_info_file (struct File_Info *info, const size_t length,
        const char *content_type)
{
    info->file_length = length;
    info->last_modified = 0;
    info->is_directory = 0;
    info->is_readable = 1;
    info->content_type = ixmlCloneDOMString (content_type);
}

//#define STREAM_LOCATION "/web/stream.ts"
    static int
http_get_info (const char *filename, struct File_Info *info)
{
    extern struct ushare_t *ut;
    struct upnp_entry_t *entry = NULL;
    struct stat st;
    int upnp_id = 0;
    char *content_type = NULL;
    char *protocol = NULL;
    extern thread_data *gp_tdata;
    thread_data *tdata = gp_tdata;

    if (!filename || !info)
        return -1;

    log_verbose ("http_get_info, filename : %s\n", filename);

    upnp_id = atoi (strrchr (filename, '/') + 1);
    entry = upnp_get_entry (ut, upnp_id);

#if 0
    if ( (!strcmp (filename, STREAM_LOCATION)) ||
          ( (entry->fullpath != NULL) &&
            ( !strcmp(entry->fullpath, STREAM_LOCATION))) ) {
#endif
    if (!strcmp (filename, STREAM_LOCATION)) {
        log_verbose ("http_get_info, stream location found.\n");
        info->is_readable = 1;
        info->file_length = get_streaming_length();
        info->last_modified = time(NULL);
        info->is_directory = 0;
        info->content_type = ixmlCloneDOMString ("video/mpeg");
        return 0;
    }
    if (!strcmp (filename, CDS_LOCATION))
    {
        set_info_file (info, CDS_DESCRIPTION_LEN, SERVICE_CONTENT_TYPE);
        return 0;
    }

    if (!strcmp (filename, CMS_LOCATION))
    {
        set_info_file (info, CMS_DESCRIPTION_LEN, SERVICE_CONTENT_TYPE);
        return 0;
    }

    if (!strcmp (filename, MSR_LOCATION))
    {
        set_info_file (info, MSR_DESCRIPTION_LEN, SERVICE_CONTENT_TYPE);
        return 0;
    }

    if (ut->use_presentation && !strcmp (filename, USHARE_PRESENTATION_PAGE))
    {
        if (build_presentation_page (ut) < 0)
            return -1;

        set_info_file (info, ut->presentation->len, PRESENTATION_PAGE_CONTENT_TYPE);
        return 0;
    }

    if (ut->use_presentation && !strncmp (filename, USHARE_CGI, strlen (USHARE_CGI)))
    {
        if (process_cgi (ut, (char *) (filename + strlen (USHARE_CGI) + 1)) < 0)
            return -1;

        set_info_file (info, ut->presentation->len, PRESENTATION_PAGE_CONTENT_TYPE);
        return 0;
    }

    if (!entry)
        return -1;
    log_verbose ("http_get_info, entry found.\n");

    if (!entry->fullpath)
        return -1;

#if 0
    if (stat (entry->fullpath, &st) < 0)
        return -1;

    if (access (entry->fullpath, R_OK) < 0)
    {
        if (errno != EACCES) {
            log_verbose ("http_get_info, access() error.\n");
            return -1;
        }
        info->is_readable = 0;
    }
    else
        info->is_readable = 1;

    /* file exist and can be read */
    info->file_length = st.st_size;
    info->last_modified = st.st_mtime;
    info->is_directory = S_ISDIR (st.st_mode);
#endif

    info->is_readable = 1;
    info->file_length = get_streaming_length();
    info->last_modified = time(NULL);
    info->is_directory = 0;

    protocol = 
#ifdef HAVE_DLNA
        entry->dlna_profile ?
        dlna_write_protocol_info (DLNA_PROTOCOL_INFO_TYPE_HTTP,
                DLNA_ORG_PLAY_SPEED_NORMAL,
                DLNA_ORG_CONVERSION_NONE,
                DLNA_ORG_OPERATION_RANGE,
                ut->dlna_flags, entry->dlna_profile) :
#endif /* HAVE_DLNA */
        mime_get_protocol (entry->mime_type);

    content_type =
        strndup ((protocol + PROTOCOL_TYPE_PRE_SZ),
                strlen (protocol + PROTOCOL_TYPE_PRE_SZ)
                - PROTOCOL_TYPE_SUFF_SZ);
    free (protocol);

    if (content_type)
    {
        info->content_type = ixmlCloneDOMString (content_type);
        free (content_type);
    }
    else
        info->content_type = ixmlCloneDOMString ("");

    return 0;
}

    static UpnpWebFileHandle
get_file_memory (const char *fullpath, const char *description,
        const size_t length)
{
    struct web_file_t *file;

//    log_verbose ("get_file_memory() description[%s]\n",
//            description);
    file = malloc (sizeof (struct web_file_t));
    file->fullpath = strdup (fullpath);
    file->pos = 0;
    file->type = FILE_MEMORY;
    file->detail.memory.contents = strdup (description);
    if ( file->detail.memory.contents  == NULL ) {
        log_verbose ("get_file_memory() null\n");
    }
    file->detail.memory.len = length;
//    log_verbose ("get_file_memory() path[%s] contents[%s]\n",
//            file->fullpath, file->detail.memory.contents);

    return ((UpnpWebFileHandle) file);
}

/*
 * 2. get_file_stream() では、open()時の操作(ファイル情報の付加)の他、ファイル情報と queue の紐付けを実施
 * ※ get_file_memory() からの改造点に着眼して記載
 *   2.1 tdata->streamer->mutex を lock してから以下のloop処理を行う(loop最大数は16で決め打ち)
 *     2.1.1 tdata->streamer->stream_session[i] が NULL であるか確認
 *       2.1.1.2 NULLである場合
 *         2.1.1.2.1 create_queue を実施
 *     2.1.1.3 NULL ではない場合
 *       2.1.1.2.1 得にやること無し
 *   2.2 tdata->streamer->mutex を unlock
 */
    static UpnpWebFileHandle
get_file_stream (const char *fullpath, thread_data *tdata)
{
#define STREAM_QUEUE (8192)
    struct web_file_t *file;
    int i = 0;
    file = malloc (sizeof (struct web_file_t));

    // 2.1 tdata->streamer->mutex を lock してから以下のloop処理を行う(loop最大数は16で決め打ち)
    pthread_mutex_lock(&tdata->streamer->mutex);
    for( i=0; i < STREAM_MAX; i++ ) {
        if ( tdata->streamer->stream_session[i] == NULL ) {
            // 2.1.1 tdata->streamer->stream_session[i] が NULL である場合
            // 2.1.1.1 新規ストリーム用のキューを作成
            file->detail.stream.id = tdata->streamer->stream_nr;
            tdata->streamer->stream_session[i] = malloc(sizeof(session));
            if ( tdata->streamer->stream_session[i] == NULL ) {
                return NULL;
            }
            tdata->streamer->stream_session[i]->is_valid = true;
            tdata->streamer->stream_session[i]->p_queue = create_queue(STREAM_QUEUE);
            if ( tdata->streamer->stream_session[i]->p_queue  == NULL ) {
                log_error ("get_file_stream(): tdata->streamer->stream_session[%d].p_queue alloc failed.\n", i);
                return NULL;
            }
            pthread_mutex_init(&tdata->streamer->stream_session[i]->p_queue->mutex, NULL);
            pthread_cond_init(&tdata->streamer->stream_session[i]->p_queue->cond_avail, NULL);
            pthread_cond_init(&tdata->streamer->stream_session[i]->p_queue->cond_used, NULL);
            tdata->streamer->stream_nr++;
            break;
        } else {
            // 2.1.2 tdata->streamer->stream_session[i] が NULL ではない
            if ( ! tdata->streamer->stream_session[i]->is_valid ) {
                // 2.1.2.1 tdata->streamer->stream_session[i] が未使用状態である場合
                file->detail.stream.id = i;
                //tdata->streamer->stream_nr++;
                tdata->streamer->stream_session[i]->is_valid = true;
                tdata->streamer->stream_session[i]->p_queue = create_queue(STREAM_QUEUE);
                if ( tdata->streamer->stream_session[i]->p_queue  != NULL ) {
                    pthread_mutex_init(&tdata->streamer->stream_session[i]->p_queue->mutex, NULL);
                    pthread_cond_init(&tdata->streamer->stream_session[i]->p_queue->cond_avail, NULL);
                    pthread_cond_init(&tdata->streamer->stream_session[i]->p_queue->cond_used, NULL);
                } else {
                    log_error ("get_file_stream(): tdata->streamer->stream_session[%d].p_queue alloc failed.\n", i);
                    return NULL;
                }
                break;
            }
        }
    }
    pthread_mutex_unlock(&tdata->streamer->mutex);
    if ( i == STREAM_MAX ) {
        log_verbose ("get_file_stream(): cannot get new file_stream.\n");
    }

    file->detail.stream.p_queue = tdata->streamer->stream_session[i]->p_queue;
    file->fullpath = strdup (fullpath);
    file->pos = 0;
    file->type = FILE_STREAM;
    file->detail.stream.len = get_streaming_length(); //ファイルサイズ(残録画時間xビットレート)
    file->detail.stream.qbuf = stream_dequeue(file->detail.stream.p_queue);
    if ( file->detail.stream.qbuf == NULL ) {
        log_error ("get_file_stream(): stream_dequeue error.\n");
        return NULL;
    }
    log_verbose ("get_file_stream(): finish.\n");

    return ((UpnpWebFileHandle) file);
}

    static UpnpWebFileHandle
http_open (const char *filename, enum UpnpOpenFileMode mode)
{
    extern struct ushare_t *ut;
    struct upnp_entry_t *entry = NULL;
    struct web_file_t *file;
    int fd, upnp_id = 0;
    extern thread_data *gp_tdata;
    thread_data *tdata = gp_tdata;

    if (!filename)
        return NULL;

    if (mode != UPNP_READ)
        return NULL;

    if (!strcmp (filename, CDS_LOCATION))
        return get_file_memory (CDS_LOCATION, CDS_DESCRIPTION, CDS_DESCRIPTION_LEN);

    if (!strcmp (filename, CMS_LOCATION))
        return get_file_memory (CMS_LOCATION, CMS_DESCRIPTION, CMS_DESCRIPTION_LEN);

    if (!strcmp (filename, MSR_LOCATION))
        return get_file_memory (MSR_LOCATION, MSR_DESCRIPTION, MSR_DESCRIPTION_LEN);

    if (ut->use_presentation && ( !strcmp (filename, USHARE_PRESENTATION_PAGE)
                || !strncmp (filename, USHARE_CGI, strlen (USHARE_CGI))))
        return get_file_memory (USHARE_PRESENTATION_PAGE, ut->presentation->buf,
                ut->presentation->len);

    upnp_id = atoi (strrchr (filename, '/') + 1);
    entry = upnp_get_entry (ut, upnp_id);
    if (!entry)
        return NULL;

    if (!entry->fullpath)
        return NULL;

    /*
     * 1. http_open() では entry がストリーム再生用のものである場合に、
     * get_file_stream()を呼び出しハンドラを返却する
     */
    log_verbose ("Fullpath : %s\n", entry->fullpath);
    if (!strcmp (entry->fullpath, STREAM_LOCATION))
        return get_file_stream (STREAM_LOCATION, tdata);

    fd = open (entry->fullpath, O_RDONLY | O_NONBLOCK | O_SYNC | O_NDELAY);
    if (fd < 0)
        return NULL;

    file = malloc (sizeof (struct web_file_t));
    file->fullpath = strdup (entry->fullpath);
    file->pos = 0;
    file->type = FILE_LOCAL;
    file->detail.local.entry = entry;
    file->detail.local.fd = fd;

    return ((UpnpWebFileHandle) file);
}

    static int
http_read (UpnpWebFileHandle fh, char *buf, size_t buflen)
{
    struct web_file_t *file = (struct web_file_t *) fh;
    ssize_t len = -1;
    extern thread_data *gp_tdata;
    thread_data *tdata = gp_tdata;

    //log_verbose ("http_read file:[%s]\n", file->fullpath);

    if (!file)
        return -1;

    switch (file->type)
    {
        case FILE_LOCAL:
            log_verbose ("Read local file.\n");
            len = read (file->detail.local.fd, buf, buflen);
            break;
        case FILE_MEMORY:
            log_verbose ("Read file from memory.\n");
            len = (size_t) MIN (buflen, file->detail.memory.len - file->pos);
            memcpy (buf, file->detail.memory.contents + file->pos, (size_t) len);
            break;
        case FILE_STREAM:
            //log_verbose ("Read file from stream.\n");
            if ( file->detail.stream.qbuf->size <= file->pos ) {
                free(file->detail.stream.qbuf->data);
                file->detail.stream.qbuf->data = NULL;
                free(file->detail.stream.qbuf);
                file->detail.stream.qbuf = NULL;
                file->detail.stream.qbuf = stream_dequeue(file->detail.stream.p_queue);
                file->pos = 0;
            }
            if ( file->detail.stream.qbuf == NULL ) {
                log_verbose ("http_read stream_dequeue error NULL\n");
                return 0;
            }
            len = (size_t) MIN (buflen, file->detail.stream.qbuf->size - file->pos);
            memcpy (buf, file->detail.stream.qbuf->data + file->pos, (size_t) len);
            break;
        default:
            log_verbose ("Unknown file type.\n");
            break;
    }

    if (len >= 0)
        file->pos += len;

    //log_verbose ("Read %zd bytes.\n", len);

    return len;
}

    static int
http_write (UpnpWebFileHandle fh __attribute__((unused)),
        char *buf __attribute__((unused)),
        size_t buflen __attribute__((unused)))
{
    log_verbose ("http write\n");

    return 0;
}

    static int
http_seek (UpnpWebFileHandle fh, off_t offset, int origin)
{
    struct web_file_t *file = (struct web_file_t *) fh;
    off_t newpos = -1;

    log_verbose ("http_seek\n");

    if (!file)
        return -1;

    switch (origin)
    {
        case SEEK_SET:
            log_verbose ("Attempting to seek to %lld (was at %lld) in %s\n",
                    offset, file->pos, file->fullpath);
            newpos = offset;
            break;
        case SEEK_CUR:
            log_verbose ("Attempting to seek by %lld from %lld in %s\n",
                    offset, file->pos, file->fullpath);
            newpos = file->pos + offset;
            break;
        case SEEK_END:
            log_verbose ("Attempting to seek by %lld from end (was at %lld) in %s\n",
                    offset, file->pos, file->fullpath);

            if (file->type == FILE_LOCAL)
            {
                struct stat sb;
                if (stat (file->fullpath, &sb) < 0)
                {
                    log_verbose ("%s: cannot stat: %s\n",
                            file->fullpath, strerror (errno));
                    return -1;
                }
                newpos = sb.st_size + offset;
            }
            else if (file->type == FILE_MEMORY)
                newpos = file->detail.memory.len + offset;
            break;
    }

    switch (file->type)
    {
        case FILE_LOCAL:
            /* Just make sure we cannot seek before start of file. */
            if (newpos < 0)
            {
                log_verbose ("%s: cannot seek: %s\n", 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)
            {
                log_verbose ("%s: cannot seek: %s\n", file->fullpath, strerror (errno));
                return -1;
            }
            break;
        case FILE_MEMORY:
            if (newpos < 0 || newpos > file->detail.memory.len)
            {
                log_verbose ("%s: cannot seek: %s\n", file->fullpath, strerror (EINVAL));
                return -1;
            }
            break;
        case FILE_STREAM:
            log_verbose ("%s: cannot seek: %s\n", file->fullpath, "STREAM");
            newpos = file->pos;
            break;
    }

    file->pos = newpos;

    return 0;
}

    static int
http_close (UpnpWebFileHandle fh)
{
    struct web_file_t *file = (struct web_file_t *) fh;
    extern thread_data *gp_tdata;
    thread_data *tdata = gp_tdata;
    STREAM_QUEUE_T *p_queue;
    int i;
    int id = 0;
    int j;

    if (!file)
        return -1;

    switch (file->type)
    {
        case FILE_LOCAL:
            close (file->detail.local.fd);
            break;
        case FILE_MEMORY:
            /* no close operation */
            if (file->detail.memory.contents)
                free (file->detail.memory.contents);
            break;
        case FILE_STREAM:
            p_queue = file->detail.stream.p_queue;
            if ( p_queue != NULL) {
                id = file->detail.stream.id;
                pthread_mutex_lock(&tdata->streamer->mutex);
                tdata->streamer->stream_session[id]->is_valid = false;
                pthread_mutex_unlock(&tdata->streamer->mutex);
                pthread_mutex_lock(&p_queue->mutex);
                while ( 0 < p_queue->num_used ) {
                    free(p_queue->buffer[p_queue->out]->data);
                    p_queue->buffer[p_queue->out]->data = NULL;
                    free(p_queue->buffer[p_queue->out]);
                    p_queue->buffer[p_queue->out] = NULL;

                    p_queue->out++;
                    p_queue->out %= p_queue->size;
                    p_queue->num_avail++;
                    p_queue->num_used--;
                }
                pthread_mutex_unlock(&p_queue->mutex);
                destroy_stream_queue(p_queue);
                tdata->streamer->stream_session[id]->p_queue = NULL;
            }
            break;
        default:
            log_verbose ("Unknown file type.\n");
            break;
    }

    if (file->fullpath)
        free (file->fullpath);
    free (file);

    return 0;
}

struct UpnpVirtualDirCallbacks virtual_dir_callbacks =
{
    http_get_info,
    http_open,
    http_read,
    http_write,
    http_seek,
    http_close
};

// TS_BITRATEはやっつけ仕事過ぎる気がします
#define TS_BITRATE (10*1000*1000/8)
#define MAX_STREAMING ((off_t)100*1000*1000*1000)
static off_t
get_streaming_length (void)
{
    off_t length = 0;
    extern thread_data *gp_tdata;
    thread_data *tdata = gp_tdata;
    time_t cur_time;
    struct timespec cur;
    struct timespec diff;
    clock_gettime(CLOCK_REALTIME, &cur);
    off_t bitrate = 0;

    if ( tdata->indefinite ) {
        return MAX_STREAMING;
    }
    diff.tv_nsec = cur.tv_nsec - tdata->streamer->start.tv_nsec;
    diff.tv_sec = cur.tv_sec - tdata->streamer->start.tv_sec;

    if ( diff.tv_sec < 1 ) {
        bitrate = TS_BITRATE;
    } else {
        bitrate = (((off_t)tdata->streamer->total_byte)*1e9) /
            ( (((off_t)diff.tv_sec)*1e9) + diff.tv_nsec);
    }

    time(&cur_time);
    length = (tdata->start_time +tdata->recsec -cur_time) * bitrate;
    if ( length < 0 ) {
        length = 0;
    }
    length = length - (length % LENGTH_PACKET);
    return length;
}
