/* -*- coding: utf-8 -*-
 * ----------------------------------------------------------------------
 * Copyright © 2013-2014, RedJack, LLC.
 * All rights reserved.
 *
 * Please see the COPYING file in this distribution for license details.
 * ----------------------------------------------------------------------
 */

#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "libcork/core.h"
#include "libcork/ds.h"
#include "libcork/os/subprocess.h"
#include "libcork/helpers/errors.h"

#if defined(__APPLE__)
/* Apple doesn't provide access to the "environ" variable from a shared library.
 * There's a workaround function to grab the environ pointer described at [1].
 *
 * [1] http://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man7/environ.7.html
 */
#include <crt_externs.h>
#define environ  (*_NSGetEnviron())

#elif !defined(__MINGW32__)
/* On all other POSIX platforms, we assume that environ is available in shared
 * libraries. */
extern char  **environ;

#endif


#ifdef __MINGW32__

int setenv(const char *name, const char *value, int replace)
{
    int out;
    size_t namelen, valuelen;
    char *envstr;

    if (!name || !value) return -1;
    if (!replace) {
        char *oldval = NULL;
        oldval = getenv(name);
        if (oldval) return 0;
    }

    namelen = strlen(name);
    valuelen = strlen(value);
    envstr = malloc((namelen + valuelen + 2));
    if (!envstr) return -1;

    memcpy(envstr, name, namelen);
    envstr[namelen] = '=';
    memcpy(envstr + namelen + 1, value, valuelen);
    envstr[namelen + valuelen + 1] = 0;

    out = putenv(envstr);
    /* putenv(3) makes the argument string part of the environment,
     * and changing that string modifies the environment --- which
     * means we do not own that storage anymore.  Do not free
     * envstr.
     */

    return out;
}

int unsetenv(const char *env)
{
    char *name;
    int ret;

    name = malloc(strlen(env)+2);
    strcat(strcpy(name, env), "=");
    ret = putenv(name);
    free(name);

    return ret;
}

int clearenv(void)
{
    char **env = environ;
    if (!env)
        return 0;
    while (*env) {
        free(*env);
        env++;
    }
    free(env);
    environ = NULL;
    return 0;
}

#endif


struct cork_env_var {
    const char  *name;
    const char  *value;
};

static struct cork_env_var *
cork_env_var_new(const char *name, const char *value)
{
    struct cork_env_var  *var = cork_new(struct cork_env_var);
    var->name = cork_strdup(name);
    var->value = cork_strdup(value);
    return var;
}

static void
cork_env_var_free(void *vvar)
{
    struct cork_env_var  *var = vvar;
    cork_strfree(var->name);
    cork_strfree(var->value);
    cork_delete(struct cork_env_var, var);
}


struct cork_env {
    struct cork_hash_table  *variables;
    struct cork_buffer  buffer;
};

struct cork_env *
cork_env_new(void)
{
    struct cork_env  *env = cork_new(struct cork_env);
    env->variables = cork_string_hash_table_new(0, 0);
    cork_hash_table_set_free_value(env->variables, cork_env_var_free);
    cork_buffer_init(&env->buffer);
    return env;
}

static void
cork_env_add_internal(struct cork_env *env, const char *name, const char *value)
{
    if (env == NULL) {
        setenv(name, value, true);
    } else {
        struct cork_env_var  *var = cork_env_var_new(name, value);
        void  *old_var;

        cork_hash_table_put
            (env->variables, (void *) var->name, var, NULL, NULL, &old_var);

        if (old_var != NULL) {
            cork_env_var_free(old_var);
        }
    }
}

struct cork_env *
cork_env_clone_current(void)
{
    char  **curr;
    struct cork_env  *env = cork_env_new();

    for (curr = environ; *curr != NULL; curr++) {
        const char  *entry = *curr;
        const char  *equal;

        equal = strchr(entry, '=');
        if (CORK_UNLIKELY(equal == NULL)) {
            /* This environment entry is malformed; skip it. */
            continue;
        }

        /* Make a copy of the name so that it's NUL-terminated rather than
         * equal-terminated. */
        cork_buffer_set(&env->buffer, entry, equal - entry);
        cork_env_add_internal(env, env->buffer.buf, equal + 1);
    }

    return env;
}


void
cork_env_free(struct cork_env *env)
{
    cork_hash_table_free(env->variables);
    cork_buffer_done(&env->buffer);
    cork_delete(struct cork_env, env);
}

const char *
cork_env_get(struct cork_env *env, const char *name)
{
    if (env == NULL) {
        return getenv(name);
    } else {
        struct cork_env_var  *var =
            cork_hash_table_get(env->variables, (void *) name);
        return (var == NULL)? NULL: var->value;
    }
}

void
cork_env_add(struct cork_env *env, const char *name, const char *value)
{
    cork_env_add_internal(env, name, value);
}

void
cork_env_add_vprintf(struct cork_env *env, const char *name,
                     const char *format, va_list args)
{
    cork_buffer_vprintf(&env->buffer, format, args);
    cork_env_add_internal(env, name, env->buffer.buf);
}

void
cork_env_add_printf(struct cork_env *env, const char *name,
                    const char *format, ...)
{
    va_list  args;
    va_start(args, format);
    cork_env_add_vprintf(env, name, format, args);
    va_end(args);
}

void
cork_env_remove(struct cork_env *env, const char *name)
{
    if (env == NULL) {
        unsetenv(name);
    } else {
        void  *old_var;
        cork_hash_table_delete(env->variables, (void *) name, NULL, &old_var);
        if (old_var != NULL) {
            cork_env_var_free(old_var);
        }
    }
}

static enum cork_hash_table_map_result
cork_env_set_vars(void *user_data, struct cork_hash_table_entry *entry)
{
    struct cork_env_var  *var = entry->value;
    setenv(var->name, var->value, false);
    return CORK_HASH_TABLE_MAP_CONTINUE;
}

#if ((defined(__APPLE__) || (defined(BSD) && (BSD >= 199103))) && !defined(__GNU__)) || defined (__CYGWIN__)
/* A handful of platforms [1] don't provide clearenv(), so we must implement our
 * own version that clears the environ array directly.
 *
 * [1] http://www.gnu.org/software/gnulib/manual/html_node/clearenv.html
 */
static void
clearenv(void)
{
    *environ = NULL;
}

#else
/* Otherwise assume that we have clearenv available. */
#endif

void
cork_env_replace_current(struct cork_env *env)
{
    clearenv();
    cork_hash_table_map(env->variables, NULL, cork_env_set_vars);
}
