/* ====================================================================
 *    Licensed to the Apache Software Foundation (ASF) under one
 *    or more contributor license agreements.  See the NOTICE file
 *    distributed with this work for additional information
 *    regarding copyright ownership.  The ASF licenses this file
 *    to you under the Apache License, Version 2.0 (the
 *    "License"); you may not use this file except in compliance
 *    with the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing,
 *    software distributed under the License is distributed on an
 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *    KIND, either express or implied.  See the License for the
 *    specific language governing permissions and limitations
 *    under the License.
 * ====================================================================
 */

#include <stdlib.h>

#include <apr.h>
#include <apr_uri.h>
#include <apr_signal.h>
#include <apr_strings.h>
#include <apr_atomic.h>
#include <apr_base64.h>
#include <apr_getopt.h>
#include <apr_version.h>

#include "serf.h"

typedef struct app_ctx_t {
    int foo;
    apr_pool_t *pool;
} app_ctx_t;

typedef struct listener_ctx_t {
    app_ctx_t *app;
    const char *proto;

} listener_ctx_t;

typedef struct client_ctx_t {
    listener_ctx_t *listener;
    apr_pool_t *pool;
    serf_bucket_alloc_t *alloc;
} client_ctx_t;

typedef struct request_ctx_t {
    client_ctx_t *client;
    apr_pool_t *pool;
    serf_bucket_t *headers;
    const char *method;
    const char *path;
    int http_version;
} request_ctx_t;

static int pool_abort_func(int retcode)
{
    fprintf(stderr, "Out of memory\n");
    abort();
    return 0;
}

/* A flag to see if we've been cancelled by the client or not. */
static volatile sig_atomic_t cancelled = FALSE;

/* A signal handler to support cancellation. */
static void
signal_handler(int signum)
{
    apr_signal(signum, SIG_IGN);
    cancelled = TRUE;
}


static apr_status_t client_setup(apr_socket_t *skt,
                                 serf_bucket_t **read_bkt,
                                 serf_bucket_t **write_bkt,
                                 void *setup_baton,
                                 apr_pool_t *pool)
{
    client_ctx_t *cctx = setup_baton;
    if (cctx->alloc == NULL)
        cctx->alloc = serf_bucket_allocator_create(cctx->pool, NULL, NULL);

    *read_bkt = serf_bucket_socket_create(skt, cctx->alloc);
    return APR_SUCCESS;
}

static apr_status_t client_closed(serf_incoming_t *incoming,
                                  void *closed_baton,
                                  apr_status_t why,
                                  apr_pool_t *pool)
{
    client_ctx_t *cctx = closed_baton;
    apr_pool_destroy(cctx->pool);
    return APR_SUCCESS;
}

static apr_status_t request_handler(serf_incoming_request_t *req,
                                    serf_bucket_t *request,
                                    void *handler_baton,
                                    apr_pool_t *pool)
{
    request_ctx_t *rctx = handler_baton;
    apr_status_t status;

    if (!rctx->method) {
        status = serf_bucket_incoming_request_read(&rctx->headers,
                                                   &rctx->method,
                                                   &rctx->path,
                                                   &rctx->http_version,
                                                   request);
        if (status) {
            rctx->method = NULL;
            return status;
        }
    }

    status = serf_bucket_incoming_request_wait_for_headers(request);
    if (status)
        return status;

    status = serf_incoming_response_create(req);
    if (status)
        return status;

    do
    {
        const char *data;
        apr_size_t len;

        status = serf_bucket_read(request, SERF_READ_ALL_AVAIL, &data, &len);
    } while (status == APR_SUCCESS);

    return status;
}

static apr_status_t request_generate_response(serf_bucket_t **resp_bkt,
                                              serf_incoming_request_t *req,
                                              void *setup_baton,
                                              serf_bucket_alloc_t *alloc,
                                              apr_pool_t *pool)
{
    request_ctx_t *rctx = setup_baton;
    serf_bucket_t *agg = serf_bucket_aggregate_create(alloc);
    serf_bucket_t *body;
    serf_bucket_t *tmp;
#define CRLF "\r\n"

    tmp = SERF_BUCKET_SIMPLE_STRING("HTTP/1.1 200 BOE" CRLF, alloc);
    serf_bucket_aggregate_append(agg, tmp);

    tmp = SERF_BUCKET_SIMPLE_STRING("Content-Type: text/plain" CRLF, alloc);
    serf_bucket_aggregate_append(agg, tmp);

    tmp = SERF_BUCKET_SIMPLE_STRING(CRLF, alloc);
    serf_bucket_aggregate_append(agg, tmp);

    body = serf_bucket_aggregate_create(alloc);

    if (rctx->method)
    {
        tmp = SERF_BUCKET_SIMPLE_STRING("Method: ", alloc);
        serf_bucket_aggregate_append(body, tmp);

        tmp = serf_bucket_simple_copy_create(rctx->method, strlen(rctx->method), alloc);
        serf_bucket_aggregate_append(body, tmp);

        tmp = SERF_BUCKET_SIMPLE_STRING(CRLF "Path: ", alloc);
        serf_bucket_aggregate_append(body, tmp);

        tmp = serf_bucket_simple_copy_create(rctx->path, strlen(rctx->path), alloc);
        serf_bucket_aggregate_append(body, tmp);

        tmp = SERF_BUCKET_SIMPLE_STRING(CRLF, alloc);
        serf_bucket_aggregate_append(body, tmp);
    }

    tmp = SERF_BUCKET_SIMPLE_STRING(CRLF, alloc);
    serf_bucket_aggregate_append(body, tmp);

    tmp = body;
    serf_bucket_aggregate_append(agg, tmp);

    tmp = SERF_BUCKET_SIMPLE_STRING(CRLF, alloc);
    serf_bucket_aggregate_append(agg, tmp);

    *resp_bkt = agg;
    return APR_SUCCESS;
}

static apr_status_t client_request_acceptor(serf_bucket_t **req_bkt,
                                            serf_bucket_t *stream,
                                            serf_incoming_request_t *req,
                                            void *request_baton,
                                            serf_incoming_request_handler_t *handler,
                                            void **handler_baton,
                                            serf_incoming_response_setup_t *response_setup,
                                            void **response_setup_baton,
                                            apr_pool_t *pool)
{
    client_ctx_t *cctx = request_baton;
    apr_pool_t *request_pool;
    request_ctx_t *rctx;

    *req_bkt = serf_bucket_incoming_request_create(stream, stream->allocator);

    apr_pool_create(&request_pool, cctx->pool);

    rctx = apr_pcalloc(request_pool, sizeof(*rctx));
    rctx->client = cctx;

    *handler = request_handler;
    *handler_baton = rctx;

    *response_setup = request_generate_response;
    *response_setup_baton = rctx;

    return APR_SUCCESS;
}


static apr_status_t client_accept(serf_context_t *ctx,
                                  serf_listener_t *l,
                                  void *accept_baton,
                                  apr_socket_t *insock,
                                  apr_pool_t *pool)
{
    serf_incoming_t *client;
    listener_ctx_t *lctx = accept_baton;
    apr_pool_t *cctx_pool;
    client_ctx_t *cctx;
    apr_status_t status;

    apr_pool_create(&cctx_pool, pool);
    cctx = apr_pcalloc(cctx_pool, sizeof(*cctx));
    cctx->pool = cctx_pool;
    cctx->listener = lctx;

    status = serf_incoming_create2(&client, ctx, insock,
                                   client_setup, cctx,
                                   client_closed, cctx,
                                   client_request_acceptor, cctx,
                                   pool);
    if (status)
        return status;

    if (lctx->proto && !strcmp(lctx->proto, "fcgi")) {
        serf_incoming_set_framing_type(client,
                                       SERF_CONNECTION_FRAMING_TYPE_FCGI);
    }
    else if (lctx->proto && !strcmp(lctx->proto, "h2c")) {
        serf_incoming_set_framing_type(client,
                                       SERF_CONNECTION_FRAMING_TYPE_HTTP2);
    }
    else if (lctx->proto && !strcmp(lctx->proto, "http1")) {
        serf_incoming_set_framing_type(client,
                                       SERF_CONNECTION_FRAMING_TYPE_HTTP1);
    }

    return status;
}


/* Value for 'no short code' should be > 255 */
#define CERTFILE 256
#define CERTPWD  257
#define HTTP2FLAG 258
#define H2DIRECT 259

static const apr_getopt_option_t options[] =
{

    { "help",    'h', 0, "Display this help" },
    { "version", 'V', 0, "Display version" },
    { "listen",  'l', 1, "<[protocol,][host:]port> Configure listener"},
    { "verbose", 'v', 0, "Enable debug logging" },
/*    { NULL,      'H', 0, "Print response headers" },
    { NULL,      'n', 1, "<count> Fetch URL <count> times" },
    { NULL,      'c', 1, "<count> Use <count> concurrent connections" },
    { NULL,   'x', 1, "<count> Number of maximum outstanding requests inflight" },
    { "user",    'U', 1, "<user> Username for Basic/Digest authentication" },
    { "pwd",     'P', 1, "<password> Password for Basic/Digest authentication" },
    { NULL,      'm', 1, "<method> Use the <method> HTTP Method" },
    { NULL,      'f', 1, "<file> Use the <file> as the request body" },
    { NULL,      'p', 1, "<hostname:port> Use the <host:port> as proxy server" },
    { "cert",    CERTFILE, 1, "<file> Use SSL client certificate <file>" },
    { "certpwd", CERTPWD, 1, "<password> Password for the SSL client certificate" },
    { NULL,      'r', 1, "<header:value> Use <header:value> as request header" },
    { "http2",   HTTP2FLAG, 0, "Enable http2 (https only) (Experimental)" },
    { "h2direct",H2DIRECT, 0, "Enable h2direct (Experimental)" },*/

    { NULL, 0 }
};

static void print_usage(apr_pool_t *pool)
{
    int i = 0;

    puts("serf_httpd [options] directory\n");
    puts("Options:");

    while (options[i].optch > 0) {
        const apr_getopt_option_t* o = &options[i];

        if (o->optch <= 255) {
            printf(" -%c", o->optch);
            if (o->name)
                printf(", ");
        }
        else {
            printf("     ");
        }

        printf("%s%s\t%s\n",
               o->name ? "--" : "\t",
               o->name ? o->name : "",
               o->description);

        i++;
    }
}

static void configure_listener(serf_context_t *ctx,
                               app_ctx_t *app,
                               const char *arg,
                               apr_pool_t *pool)
{
    const char *listen_spec;
    char *addr = NULL;
    char *scope_id = NULL;
    apr_port_t port;
    apr_status_t status;
    listener_ctx_t *lctx;
    serf_listener_t *listener;

    char *comma = strchr(arg, ',');

    listen_spec = comma ? comma + 1 : arg;

    status = apr_parse_addr_port(&addr, &scope_id, &port, listen_spec, pool);
    if (status) {
        printf("Error parsing listen address: %s\n", listen_spec);
        exit(1);
    }

    if (!addr) {
        addr = "0.0.0.0";
    }

    if (port == 0) {
        port = 8080;
    }

    lctx = apr_pcalloc(pool, sizeof(*lctx));
    lctx->app = app;

    if (comma)
        lctx->proto = apr_pstrmemdup(pool, arg, comma - arg);

    status = serf_listener_create(&listener, ctx, addr, port, lctx,
                                  client_accept, pool);
    if (status) {
        printf("Error creating listener '%s': %d", listen_spec, status);
        exit(1);
    }
}

int main(int argc, const char **argv)
{
    apr_status_t status;
    apr_pool_t *app_pool;
    apr_pool_t *scratch_pool;
    serf_context_t *context;
    app_ctx_t app_ctx;
    apr_getopt_t *opt;
    apr_allocator_t *allocator;
    int opt_c;
    int verbose = 0;
    const char *opt_arg;
    const char *root_dir;
    apr_finfo_t root_info;

    apr_initialize();
    atexit(apr_terminate);

    apr_pool_create(&app_pool, NULL);

    apr_pool_abort_set(pool_abort_func, app_pool);

    /* Keep a maximum of 16 MB unused memory inside APR. */
    allocator = apr_pool_allocator_get(app_pool);
    if (allocator != NULL)
        apr_allocator_max_free_set(allocator, 16384 * 1024);
    /* else: APR pool debugging... leave this to apr */

    apr_signal(SIGINT, signal_handler);
#ifdef SIGBREAK
    /* SIGBREAK is a Win32 specific signal generated by ctrl-break. */
    apr_signal(SIGBREAK, signal_handler);
#endif
#ifdef SIGHUP
    apr_signal(SIGHUP, signal_handler);
#endif
#ifdef SIGTERM
    apr_signal(SIGTERM, signal_handler);
#endif

#ifdef SIGPIPE
    /* Disable SIGPIPE generation for the platforms that have it. */
    apr_signal(SIGPIPE, SIG_IGN);
#endif


    apr_pool_create(&scratch_pool, app_pool);

    apr_getopt_init(&opt, scratch_pool, argc, argv);
    context = serf_context_create(app_pool);

    apr_getopt_init(&opt, scratch_pool, argc, argv);
    while ((status = apr_getopt_long(opt, options, &opt_c, &opt_arg)) ==
           APR_SUCCESS) {
        switch (opt_c) {
            case 'h':
                print_usage(scratch_pool);
                exit(0);
                break;
            case 'V':
                puts("Serf version: " SERF_VERSION_STRING);
                exit(0);
            case 'l':
                configure_listener(context, &app_ctx, opt_arg, app_pool);
                break;
            case 'v':
                verbose++;
                break;

            default:
                break;
        }
    }

    if (opt->ind != opt->argc - 1) {
        print_usage(scratch_pool);
        exit(-1);
    }

    /* FIXME: root_dir is never used. */
    root_dir = argv[opt->ind];
    status = apr_stat(&root_info, root_dir, APR_FINFO_TYPE, scratch_pool);
    printf("Ignoring root directory %s%s", root_dir,
           (status == APR_SUCCESS
            ? (root_info.filetype == APR_DIR ? "\n"
               : " (which isn't a directory)\n")
            : " (which doesn't even exist)\n"));

    /* Setup debug logging */
    if (verbose) {
        serf_log_output_t *output;
        apr_uint32_t level;

        level = SERF_LOG_WARNING;
        if (verbose >= 3)
            level = SERF_LOG_DEBUG;
        else if (verbose >= 2)
            level = SERF_LOG_INFO;

        status = serf_logging_create_stream_output(
            &output,
            context,
            level,
            SERF_LOGCOMP_ALL_MSG & ~(SERF_LOGCOMP_RAWMSG | SERF_LOGCOMP_SSLMSG),
            SERF_LOG_DEFAULT_LAYOUT,
            stderr,
            app_pool);

        if (!status)
            serf_logging_add_output(context, output);
    }

    while (1) {
        apr_pool_clear(scratch_pool);
        status = serf_context_run(context, apr_time_from_msec(50),
                                  scratch_pool);
        if (APR_STATUS_IS_TIMEUP(status)) {
            if (cancelled)
                break;
            else
                continue;
        }
        if (status) {
            char buf[200];
            const char *err_string;
            err_string = serf_error_string(status);
            if (!err_string) {
                err_string = apr_strerror(status, buf, sizeof(buf));
            }

            printf("Error running context: (%d) %s\n", status, err_string);
            apr_pool_destroy(app_pool);
            exit(1);
        }
    }

    apr_pool_destroy(app_pool);
    return 0;
}
