/* ====================================================================
*    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 <apr_strings.h>

#include "serf.h"
#include "serf_bucket_util.h"

#include "serf_private.h"


/* Use a linked list to store the config values, as we'll only store a couple
   of values per context. */
struct serf__config_hdr_t {
    struct config_entry_t *first;
};

typedef void (*config_free_cb_t)(serf_bucket_alloc_t *alloc, void *value);

typedef struct config_entry_t {
    apr_uint32_t key;
    void *value;
    struct config_entry_t *next;
    config_free_cb_t free_cb;
} config_entry_t;

static serf__config_hdr_t *create_config_hdr(serf_bucket_alloc_t *allocator)
{
    serf__config_hdr_t *hdr = serf_bucket_mem_calloc(allocator, sizeof(*hdr));
    return hdr;
}

static apr_status_t
add_or_replace_entry(serf_config_t *config,
                     serf__config_hdr_t *hdr, serf_config_key_t key, void *value,
                     config_free_cb_t free_cb)
{
    config_entry_t *iter = hdr->first;
    config_entry_t *last = iter;
    int found = FALSE;

    /* Find the entry with the matching key. If it exists, replace its value. */
    while (iter != NULL) {
        if (iter->key == key) {
            found = TRUE;
            break;
        }
        last = iter;
        iter = iter->next;
    }

    if (found) {
        iter->key = key;
        if (iter->free_cb)
            iter->free_cb(config->allocator, iter->value);
        iter->value = value;
        iter->free_cb = free_cb;
    } else {
        /* Not found, create a new entry and append it to the list. */
        config_entry_t *entry = serf_bucket_mem_alloc(config->allocator,
                                                      sizeof(*entry));

        entry->key = key;
        entry->value = value;
        entry->free_cb = free_cb;
        entry->next = NULL;

        if (last)
            last->next = entry;
        else
            hdr->first = entry;
    }

    return APR_SUCCESS;
}

static apr_status_t config_set_object(serf_config_t *config,
                                      serf_config_key_t key,
                                      void *value,
                                      config_free_cb_t free_cb)
{
    serf__config_hdr_t *target;

    /* Set the value in the hash table of the selected category */
    if (key & SERF_CONFIG_PER_CONTEXT) {
        target = config->per_context;
    }
    else if (key & SERF_CONFIG_PER_HOST) {
        target = config->per_host;
    }
    else {
        target = config->per_conn;
    }

    if (!target) {
        /* Config object doesn't manage keys in this category */
        return APR_EINVAL;
    }

    return add_or_replace_entry(config, target, key, value, free_cb);
}

static void cleanup_hdr(serf__config_store_t *store, serf__config_hdr_t *hdr)
{
    config_entry_t *e = hdr->first;

    serf_bucket_mem_free(store->allocator, hdr);

    while (e) {
        config_entry_t *next = e->next;

        if (e->free_cb)
            e->free_cb(store->allocator, e->value);

        serf_bucket_mem_free(store->allocator, e);
        e = next;
    }
}

static apr_status_t cleanup_store(void *baton)
{
    serf__config_store_t *store = baton;
    apr_hash_index_t *hi;

    for (hi = apr_hash_first(store->pool, store->global_per_host);
         hi;
         hi = apr_hash_next(hi))
      {
          const void *key;
          void *val;

          apr_hash_this(hi, &key, NULL, &val);

          serf_bucket_mem_free(store->allocator, (void *)key);

          cleanup_hdr(store, val);
      }

    for (hi = apr_hash_first(store->pool, store->global_per_conn);
         hi;
         hi = apr_hash_next(hi))
      {
          const void *key;
          void *val;

          apr_hash_this(hi, &key, NULL, &val);

          serf_bucket_mem_free(store->allocator, (void *)key);

          cleanup_hdr(store, val);
      }

    cleanup_hdr(store, store->global_per_context);

    return APR_SUCCESS;
}

/*** Config Store ***/
apr_status_t serf__config_store_init(serf_context_t *ctx)
{
    apr_pool_t *pool = ctx->pool;
    serf_bucket_alloc_t *alloc = serf_bucket_allocator_create(pool,
                                                              NULL, NULL);

    ctx->config_store.pool = pool;
    ctx->config_store.allocator = alloc;
    ctx->config_store.global_per_context = create_config_hdr(alloc);
    ctx->config_store.global_per_host = apr_hash_make(pool);
    ctx->config_store.global_per_conn = apr_hash_make(pool);

    apr_pool_cleanup_register(pool, &ctx->config_store, cleanup_store,
                              apr_pool_cleanup_null);

    return APR_SUCCESS;
}

/* Defines the key to use for per host settings */
static const char * host_key_for_conn(serf_connection_t *conn,
                                      apr_pool_t *pool)
{
    /* SCHEME://HOSTNAME:PORT, e.g. http://localhost:12345 */
    return conn->host_url;
}

/* Defines the key to use for per connection settings */
static const char * conn_key_for_conn(serf_connection_t *conn,
                                      apr_pool_t *pool)
{
    /* Key needs to be unique per connection, so stringify its pointer value */
    return apr_psprintf(pool, "%pp", conn);
}

/* Defines the key to use for per connection settings */
static const char * conn_key_for_client(serf_incoming_t *incoming,
                                        apr_pool_t *pool)
{
    /* Key needs to be unique per connection, so stringify its pointer value */
    return apr_psprintf(pool, "%pp", incoming);
}

/* Defines the key to use for per connection settings */
static const char * conn_key_for_listener(serf_listener_t *listener,
                                          apr_pool_t *pool)
{
    /* Key needs to be unique per connection, so stringify its pointer value */
    return apr_psprintf(pool, "%pp", listener);
}

apr_status_t serf__config_store_create_ctx_config(serf_context_t *ctx,
                                                  serf_config_t **config)
{
    serf__config_store_t *config_store = &ctx->config_store;

    serf_config_t *cfg = apr_pcalloc(ctx->pool, sizeof(serf_config_t));
    cfg->ctx_pool = config_store->pool;
    cfg->allocator = config_store->allocator;
    cfg->per_context = config_store->global_per_context;

    *config = cfg;
    return APR_SUCCESS;
}

apr_status_t serf__config_store_create_conn_config(serf_connection_t *conn,
                                                   serf_config_t **config)
{
    serf__config_store_t *config_store = &conn->ctx->config_store;
    const char *host_key, *conn_key;
    serf__config_hdr_t *per_conn, *per_host;
    apr_pool_t *tmp_pool;
    apr_status_t status;

    serf_config_t *cfg = apr_pcalloc(conn->pool, sizeof(serf_config_t));
    cfg->ctx_pool = config_store->pool;
    cfg->allocator = config_store->allocator;
    cfg->per_context = config_store->global_per_context;

    if ((status = apr_pool_create(&tmp_pool, cfg->ctx_pool)) != APR_SUCCESS)
        return status;

    /* Find the config values for this connection, create empty structure
        if needed */
    conn_key = conn_key_for_conn(conn, tmp_pool);
    per_conn = apr_hash_get(config_store->global_per_conn, conn_key,
                            APR_HASH_KEY_STRING);
    if (!per_conn) {
        per_conn = create_config_hdr(cfg->allocator);
        apr_hash_set(config_store->global_per_conn,
                     serf_bstrdup(cfg->allocator, conn_key),
                     APR_HASH_KEY_STRING, per_conn);
    }
    cfg->per_conn = per_conn;

    /* Find the config values for this host, create empty structure
        if needed */
    host_key = host_key_for_conn(conn, tmp_pool);
    per_host = apr_hash_get(config_store->global_per_host,
                            host_key,
                            APR_HASH_KEY_STRING);
    if (!per_host) {
        per_host = create_config_hdr(cfg->allocator);
        apr_hash_set(config_store->global_per_host,
                     serf_bstrdup(cfg->allocator, host_key),
                     APR_HASH_KEY_STRING, per_host);
    }
    cfg->per_host = per_host;

    apr_pool_destroy(tmp_pool);

    *config = cfg;

    return APR_SUCCESS;
}

apr_status_t serf__config_store_create_client_config(serf_incoming_t *client,
                                                     serf_config_t **config)
{
    serf__config_store_t *config_store = &client->ctx->config_store;
    const char *client_key;
    serf__config_hdr_t *per_conn;
    apr_pool_t *tmp_pool;
    apr_status_t status;

    serf_config_t *cfg = apr_pcalloc(client->pool, sizeof(serf_config_t));
    cfg->ctx_pool = config_store->pool;
    cfg->allocator = config_store->allocator;
    cfg->per_context = config_store->global_per_context;

    if ((status = apr_pool_create(&tmp_pool, client->pool)) != APR_SUCCESS)
        return status;

    /* Find the config values for this connection, create empty structure
    if needed */
    client_key = conn_key_for_client(client, tmp_pool);
    per_conn = apr_hash_get(config_store->global_per_conn, client_key,
                            APR_HASH_KEY_STRING);
    if (!per_conn) {
        per_conn = create_config_hdr(cfg->allocator);
        apr_hash_set(config_store->global_per_conn,
                     serf_bstrdup(cfg->allocator, client_key),
                     APR_HASH_KEY_STRING, per_conn);
    }
    cfg->per_conn = per_conn;
    cfg->per_host = NULL;

    apr_pool_destroy(tmp_pool);

    *config = cfg;

    return APR_SUCCESS;
}

apr_status_t serf__config_store_create_listener_config(serf_listener_t *listener,
                                                       serf_config_t **config)
{
    serf__config_store_t *config_store = &listener->ctx->config_store;
    const char *client_key;
    serf__config_hdr_t *per_conn;
    apr_pool_t *tmp_pool;
    apr_status_t status;

    serf_config_t *cfg = apr_pcalloc(listener->pool, sizeof(serf_config_t));
    cfg->ctx_pool = config_store->pool;
    cfg->allocator = config_store->allocator;
    cfg->per_context = config_store->global_per_context;

    if ((status = apr_pool_create(&tmp_pool, listener->pool)) != APR_SUCCESS)
        return status;

    /* Find the config values for this connection, create empty structure
    if needed */
    client_key = conn_key_for_listener(listener, tmp_pool);
    per_conn = apr_hash_get(config_store->global_per_conn, client_key,
                            APR_HASH_KEY_STRING);
    if (!per_conn) {
        per_conn = create_config_hdr(cfg->allocator);
        apr_hash_set(config_store->global_per_conn,
                     serf_bstrdup(cfg->allocator, client_key),
                     APR_HASH_KEY_STRING, per_conn);
    }
    cfg->per_conn = per_conn;
    cfg->per_host = NULL;

    apr_pool_destroy(tmp_pool);

    *config = cfg;

    return APR_SUCCESS;
}

apr_status_t
serf__config_store_remove_connection(serf__config_store_t config_store,
                                     serf_connection_t *conn)
{
    return APR_ENOTIMPL; /* Mem leak? */
}

apr_status_t
serf__config_store_remove_client(serf__config_store_t config_store,
                                 serf_incoming_t *client)
{
    return APR_ENOTIMPL; /* Mem leak? */
}


apr_status_t
serf__config_store_remove_host(serf__config_store_t config_store,
                               const char *hostname_port)
{
    return APR_ENOTIMPL; /* Mem leak? */
}

/*** Config ***/
apr_status_t serf_config_set_string(serf_config_t *config,
                                    serf_config_key_t key,
                                    const char *value)
{
    /* Cast away const is ok here, the callers should always use
       serf_config_get_string for this key. */
    return serf_config_set_object(config, key, (void *)value);
}

apr_status_t serf_config_set_stringc(serf_config_t *config,
                                     serf_config_key_t key,
                                     const char *value)
{
    char *cvalue;

    cvalue = serf_bstrdup(config->allocator, value);

    return config_set_object(config, key, cvalue,
                             serf_bucket_mem_free);
}

apr_status_t serf_config_set_stringf(serf_config_t *config,
                                     serf_config_key_t key,
                                     apr_pool_t *scratch_pool,
                                     const char *fmt, ...)
{
    va_list argp;
    char *cvalue;

    va_start(argp, fmt);
    cvalue = apr_pvsprintf(scratch_pool, fmt, argp);
    va_end(argp);

    return serf_config_set_stringc(config, key, cvalue);
}

apr_status_t serf_config_set_object(serf_config_t *config,
                                    serf_config_key_t key,
                                    void *value)
{
    return config_set_object(config, key, value, NULL);
}

apr_status_t serf_config_get_string(serf_config_t *config,
                                    serf_config_key_t key,
                                    const char **value)
{
    return serf_config_get_object(config, key, (void**)value);
}

apr_status_t serf_config_get_object(serf_config_t *config,
                                    serf_config_key_t key,
                                    void **value)
{
    serf__config_hdr_t *target;

    if (config == NULL) {
        *value = NULL;
        return APR_EINVAL;
    }
    if (key & SERF_CONFIG_PER_CONTEXT)
        target = config->per_context;
    else if (key & SERF_CONFIG_PER_HOST)
        target = config->per_host;
    else
        target = config->per_conn;

    *value = NULL;
    if (target) {
        config_entry_t *iter = target->first;
        /* Find the matching key and return its value */
        while (iter != NULL) {
            if (iter->key == key) {
                *value = iter->value;
                return APR_SUCCESS;
            }
            iter = iter->next;
        }
        return APR_SUCCESS;
    } else {
        /* Config object doesn't manage keys in this category */
        return APR_EINVAL;
    }
}

apr_status_t serf_config_remove_value(serf_config_t *config,
                                      serf_config_key_t key)
{
    return serf_config_set_object(config, key, NULL);
}

struct ssl_session_data
{
    unsigned char *session;
    apr_size_t session_len;
};

#define SERF_CONFIG__SSL_SESSION       (SERF_CONFIG_PER_HOST | 0xF00001)

apr_status_t
serf__config_store_set_ssl_session(serf_config_t *config,
                                   const unsigned char *session,
                                   apr_size_t session_len)
{
    struct ssl_session_data *ssld = serf_bucket_mem_alloc(config->allocator,
                                                          sizeof(*ssld)
                                                          + session_len);

    ssld->session = ((unsigned char *)ssld) + sizeof(*ssld);
    ssld->session_len = session_len;

    memcpy(ssld->session, session, session_len);

    return config_set_object(config, SERF_CONFIG__SSL_SESSION, ssld,
                             serf_bucket_mem_free);
}

apr_status_t
serf__config_store_get_ssl_session(serf_config_t *config,
                                   const unsigned char **session,
                                   apr_size_t *session_len)
{
    const struct ssl_session_data *ssld;
    apr_status_t status;
    void *value;

    status = serf_config_get_object(config, SERF_CONFIG__SSL_SESSION, &value);

    if (status)
        return status;
    else if (!value)
        return APR_ENOENT;

    ssld = value;

    *session = ssld->session;
    *session_len = ssld->session_len;
    return APR_SUCCESS;
}
