/*
 * %CopyrightBegin%
 *
 * Copyright Ericsson AB 2002-2012. All Rights Reserved.
 *
 * The contents of this file are subject to the Erlang Public License,
 * Version 1.1, (the "License"); you may not use this file except in
 * compliance with the License. You should have received a copy of the
 * Erlang Public License along with this software. If not, it can be
 * retrieved online at http://www.erlang.org/.
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 *
 * %CopyrightEnd%
 */

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include "sys.h"
#include "erl_sys_driver.h"
#include "erl_alloc.h"

static WCHAR *merge_environment(WCHAR *current, WCHAR *add);
static WCHAR *arg_to_env(WCHAR **arg);
static WCHAR **env_to_arg(WCHAR *env);
static WCHAR **find_arg(WCHAR **arg, WCHAR *str);
static int compare(const void *a, const void *b);

static erts_smp_rwmtx_t environ_rwmtx;

void
erts_sys_env_init(void)
{
    erts_smp_rwmtx_init(&environ_rwmtx, "environ");
}

int
erts_sys_putenv_raw(char *key, char *value)
{
    int res;
    erts_smp_rwmtx_rwlock(&environ_rwmtx);
    res = (SetEnvironmentVariable((LPCTSTR) key,
				  (LPCTSTR) value) ? 0 : 1);
    erts_smp_rwmtx_rwunlock(&environ_rwmtx);
    return res;
}

int
erts_sys_putenv(char *key, char *value)
{
    int res;
    WCHAR *wkey = (WCHAR *) key;
    WCHAR *wvalue = (WCHAR *) value;
    erts_smp_rwmtx_rwlock(&environ_rwmtx);
    res = (SetEnvironmentVariableW(wkey,
				   wvalue) ? 0 : 1);
    erts_smp_rwmtx_rwunlock(&environ_rwmtx);
    return res;
}

int
erts_sys_getenv(char *key, char *value, size_t *size)
{
    size_t req_size = 0;
    int res = 0;
    DWORD new_size;
    WCHAR *wkey = (WCHAR *) key;
    WCHAR *wvalue = (WCHAR *) value;
    DWORD wsize = *size / (sizeof(WCHAR) / sizeof(char));

    SetLastError(0);
    erts_smp_rwmtx_rlock(&environ_rwmtx);
    new_size = GetEnvironmentVariableW(wkey,
				       wvalue,
				      (DWORD) wsize);
    res = !new_size && GetLastError() == ERROR_ENVVAR_NOT_FOUND ? -1 : 0;
    erts_smp_rwmtx_runlock(&environ_rwmtx);
    if (res < 0)
	return res;
    res = new_size > wsize ? 1 : 0;
    *size = new_size * (sizeof(WCHAR) / sizeof(char));
    return res;
}
int
erts_sys_getenv__(char *key, char *value, size_t *size)
{
    size_t req_size = 0;
    int res = 0;
    DWORD new_size;

    SetLastError(0);
    new_size = GetEnvironmentVariable((LPCTSTR) key,
				      (LPTSTR) value,
				      (DWORD) *size);
    res = !new_size && GetLastError() == ERROR_ENVVAR_NOT_FOUND ? -1 : 0;
    if (res < 0)
	return res;
    res = new_size > *size ? 1 : 0;
    *size = new_size;
    return res;
}

int
erts_sys_getenv_raw(char *key, char *value, size_t *size)
{
    int res;
    erts_smp_rwmtx_rlock(&environ_rwmtx);
    res = erts_sys_getenv__(key, value, size);
    erts_smp_rwmtx_runlock(&environ_rwmtx);
    return res;
}

void init_getenv_state(GETENV_STATE *state)
{
    erts_smp_rwmtx_rlock(&environ_rwmtx);
    state->environment_strings = GetEnvironmentStringsW();
    state->next_string = state->environment_strings;
}

char *getenv_string(GETENV_STATE *state)
{
    ERTS_SMP_LC_ASSERT(erts_smp_lc_rwmtx_is_rlocked(&environ_rwmtx));
    if (state->next_string[0] == L'\0') {
	return NULL;
    } else {
	WCHAR *res = state->next_string;
	state->next_string += wcslen(res) + 1;
	return (char *) res;
    }
}

void fini_getenv_state(GETENV_STATE *state)
{
    FreeEnvironmentStringsW(state->environment_strings);
    state->environment_strings = state->next_string = NULL;
    erts_smp_rwmtx_runlock(&environ_rwmtx);
}

int erts_sys_unsetenv(char *key)
{
    int res = 0;
    WCHAR *wkey = (WCHAR *) key;

    SetLastError(0);
    erts_smp_rwmtx_rlock(&environ_rwmtx);
    GetEnvironmentVariableW(wkey,
                            NULL,
                            0);
    if (GetLastError() != ERROR_ENVVAR_NOT_FOUND) {
        res = (SetEnvironmentVariableW(wkey,
                                       NULL) ? 0 : 1);
    }
    erts_smp_rwmtx_runlock(&environ_rwmtx);
    return res;
}

char*
win_build_environment(char* new_env)
{
    if (new_env == NULL) {
	return NULL;
    } else {
	WCHAR *tmp, *merged, *tmp_new;

	tmp_new = (WCHAR *) new_env;
	
	erts_smp_rwmtx_rlock(&environ_rwmtx);
	tmp = GetEnvironmentStringsW();
	merged = merge_environment(tmp, tmp_new);

	FreeEnvironmentStringsW(tmp);
	erts_smp_rwmtx_runlock(&environ_rwmtx);
	return (char *) merged;
    }
}

static WCHAR *
merge_environment(WCHAR *old, WCHAR *add)
{
    WCHAR **a_arg = env_to_arg(add);
    WCHAR **c_arg = env_to_arg(old);
    WCHAR *ret;
    int i, j;
    
    for(i = 0; c_arg[i] != NULL; ++i)
	;

    for(j = 0; a_arg[j] != NULL; ++j)
	;

    c_arg = erts_realloc(ERTS_ALC_T_TMP,
			 c_arg, (i+j+1) * sizeof(WCHAR *));

    for(j = 0; a_arg[j] != NULL; ++j){
	WCHAR **tmp;
	WCHAR *current = a_arg[j];
	WCHAR *eq_p = wcschr(current,L'=');
	int unset = (eq_p!=NULL && eq_p[1]==L'\0');

	if ((tmp = find_arg(c_arg, current)) != NULL) {
	    if (!unset) {
		*tmp = current;
	    } else {
		*tmp = c_arg[--i];
		c_arg[i] = NULL;
	    }
	} else if (!unset) {
	    c_arg[i++] = current;
	    c_arg[i] = NULL;
	}
    }
    ret = arg_to_env(c_arg);
    erts_free(ERTS_ALC_T_TMP, c_arg);
    erts_free(ERTS_ALC_T_TMP, a_arg);
    return ret;
}

static WCHAR**
find_arg(WCHAR **arg, WCHAR *str)
{
    WCHAR *tmp;
    int len;

    if ((tmp = wcschr(str, L'=')) != NULL) {
	tmp++;
	len = tmp - str;
	while (*arg != NULL){
	    if (_wcsnicmp(*arg, str, len) == 0){
		return arg;
	    }
	    ++arg;
	}
    }
    return NULL;
}

static int
compare(const void *a, const void *b)
{
    WCHAR *s1 = *((WCHAR **) a);
    WCHAR *s2 = *((WCHAR **) b);
    WCHAR *e1 = wcschr(s1,L'=');
    WCHAR *e2 = wcschr(s2,L'=');
    int ret;
    int len;
  
    if(!e1)
	e1 = s1 + wcslen(s1);
    if(!e2)
	e2 = s2 + wcslen(s2);
  
    if((e1 - s1) > (e2 - s2))
	len = (e2 - s2);
    else
	len = (e1 - s1);
  
    ret = _wcsnicmp(s1,s2,len);
    if (ret == 0)
	return ((e1 - s1) - (e2 - s2));
    else
	return ret;
}

static WCHAR**
env_to_arg(WCHAR *env)
{
    WCHAR **ret;
    WCHAR *tmp;
    int i;
    int num_strings = 0;

    for(tmp = env; *tmp != '\0'; tmp += wcslen(tmp)+1) {
	++num_strings;
    }
    ret = erts_alloc(ERTS_ALC_T_TMP, sizeof(WCHAR *) * (num_strings + 1));
    i = 0;
    for(tmp = env; *tmp != '\0'; tmp += wcslen(tmp)+1){
	ret[i++] = tmp;
    }
    ret[i] = NULL;
    return ret;
}

static WCHAR *
arg_to_env(WCHAR **arg)
{
    WCHAR *block;
    WCHAR *ptr;
    int i;
    int totlen = 1;		/* extra '\0' */

    for(i = 0; arg[i] != NULL; ++i) {
	totlen += wcslen(arg[i])+1;
    }

    /* sort the environment vector */
    qsort(arg, i, sizeof(WCHAR *), &compare);

    if (totlen == 1){
	block = erts_alloc(ERTS_ALC_T_ENVIRONMENT, 2 * sizeof(WCHAR));
	block[0] = block[1] = '\0'; 
    } else {
	block = erts_alloc(ERTS_ALC_T_ENVIRONMENT, totlen * sizeof(WCHAR));
	ptr = block;
	for(i=0; arg[i] != NULL; ++i){
	    wcscpy(ptr, arg[i]);
	    ptr += wcslen(ptr)+1;
	}
	*ptr = '\0';
    }
    return block;
}
