/*
 * %CopyrightBegin%
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright Ericsson AB 2017-2025. All Rights Reserved.
 *
 * Licensed 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.
 *
 * %CopyrightEnd%
 */

#include "erl_osenv.h"

#include "global.h"
#include "erl_alloc.h"
#include "erl_process.h"

#define STACKBUF_SIZE (512)

typedef struct __env_rbtnode_t {
    struct __env_rbtnode_t *parent;
    struct __env_rbtnode_t *left;
    struct __env_rbtnode_t *right;

    int is_red;

    erts_osenv_data_t key;
    erts_osenv_data_t value;
} env_rbtnode_t;

#define ERTS_RBT_PREFIX env
#define ERTS_RBT_T env_rbtnode_t
#define ERTS_RBT_KEY_T erts_osenv_data_t
#define ERTS_RBT_FLAGS_T int
#define ERTS_RBT_INIT_EMPTY_TNODE(T) \
    do { \
        (T)->parent = NULL; \
        (T)->left = NULL; \
        (T)->right = NULL; \
        (T)->is_red = 0; \
    } while(0)
#define ERTS_RBT_IS_RED(T) ((T)->is_red)
#define ERTS_RBT_SET_RED(T) ((T)->is_red = 1)
#define ERTS_RBT_IS_BLACK(T) (!ERTS_RBT_IS_RED(T))
#define ERTS_RBT_SET_BLACK(T) ((T)->is_red = 0)
#define ERTS_RBT_GET_FLAGS(T) ((T)->is_red)
#define ERTS_RBT_SET_FLAGS(T, F) ((T)->is_red = F)
#define ERTS_RBT_GET_PARENT(T) ((T)->parent)
#define ERTS_RBT_SET_PARENT(T, P) ((T)->parent = P)
#define ERTS_RBT_GET_RIGHT(T) ((T)->right)
#define ERTS_RBT_SET_RIGHT(T, R) ((T)->right = (R))
#define ERTS_RBT_GET_LEFT(T) ((T)->left)
#define ERTS_RBT_SET_LEFT(T, L) ((T)->left = (L))
#define ERTS_RBT_GET_KEY(T) ((T)->key)
#define ERTS_RBT_IS_LT(KX, KY) (compare_env_keys(KX, KY) < 0)
#define ERTS_RBT_IS_EQ(KX, KY) (compare_env_keys(KX, KY) == 0)
#define ERTS_RBT_WANT_FOREACH_DESTROY
#define ERTS_RBT_WANT_FOREACH
#define ERTS_RBT_WANT_REPLACE
#define ERTS_RBT_WANT_DELETE
#define ERTS_RBT_WANT_INSERT
#define ERTS_RBT_WANT_LOOKUP

static int compare_env_keys(const erts_osenv_data_t a, const erts_osenv_data_t b);

#include "erl_rbtree.h"

static int compare_env_keys(const erts_osenv_data_t a, const erts_osenv_data_t b) {
    int relation;

#ifdef __WIN32__
    /* Environment variables are case-insensitive on Windows. */
    relation = _wcsnicmp((const WCHAR*)a.data, (const WCHAR*)b.data,
                         MIN(a.length, b.length) / sizeof(WCHAR));
#else
    relation = sys_memcmp(a.data, b.data, MIN(a.length, b.length));
#endif

    if(relation != 0) {
        return relation;
    }

    if(a.length < b.length) {
        return -1;
    } else if(a.length == b.length) {
        return 0;
    } else {
        return 1;
    }
}

static void *convert_value_to_native(Eterm term, char *stackbuf,
        int stackbuf_size, Sint *length) {
    int encoding;
    void *result;

    if(is_atom(term)) {
        return NULL;
    }

    encoding = erts_get_native_filename_encoding();
    *length = erts_native_filename_need(term, encoding);

    if(*length < 0) {
        return NULL;
    } else if(*length >= stackbuf_size) {
        result = erts_alloc(ERTS_ALC_T_TMP, *length);
    } else {
        result = stackbuf;
    }

    erts_native_filename_put(term, encoding, (byte*)result);

    return result;
}

static void *convert_key_to_native(Eterm term, char *stackbuf,
        int stackbuf_size, Sint *length) {
    byte *name_iterator, *name_end;
    void *result;
    int encoding;

    result = convert_value_to_native(term, stackbuf, stackbuf_size, length);

    if(result == NULL || length == 0) {
        return NULL;
    }

    encoding = erts_get_native_filename_encoding();

    name_iterator = (byte*)result;
    name_end = &name_iterator[*length];

#ifdef __WIN32__
    /* Windows stores per-drive working directories as variables starting with
     * '=', so we skip the first character to tolerate that. */
    name_iterator = erts_raw_env_next_char(name_iterator, encoding);
#endif

    while(name_iterator < name_end) {
        if(erts_raw_env_char_is_7bit_ascii_char('=', name_iterator, encoding)) {
            if(result != stackbuf) {
                erts_free(ERTS_ALC_T_TMP, result);
            }

            return NULL;
        }

        name_iterator = erts_raw_env_next_char(name_iterator, encoding);
    }

    return result;
}

void erts_osenv_init(erts_osenv_t *env) {
    env->variable_count = 0;
    env->content_size = 0;
    env->tree = NULL;
}

static int destroy_foreach(env_rbtnode_t *node, void *_state, Sint reds) {
    erts_free(ERTS_ALC_T_ENVIRONMENT, node);
    (void)_state;
    return 1;
}

void erts_osenv_clear(erts_osenv_t *env) {
    env_rbt_foreach_destroy(&env->tree, &destroy_foreach, NULL);
    erts_osenv_init(env);
}

struct __env_merge {
    int overwrite_existing;
    erts_osenv_t *env;
};

static int merge_foreach(env_rbtnode_t *node, void *_state, Sint reds) {
    struct __env_merge *state = (struct __env_merge*)(_state);
    env_rbtnode_t *existing_node;

    existing_node = env_rbt_lookup(state->env->tree, node->key);

    if(existing_node == NULL || state->overwrite_existing) {
        erts_osenv_put_native(state->env, &node->key, &node->value);
    }
    return 1;
}

void erts_osenv_merge(erts_osenv_t *env, const erts_osenv_t *with, int overwrite) {
    struct __env_merge merge_state;

    merge_state.overwrite_existing = overwrite;
    merge_state.env = env;

    env_rbt_foreach(with->tree, merge_foreach, &merge_state);
}

struct __env_foreach_term {
    erts_osenv_foreach_term_cb_t user_callback;
    struct process *process;
    void *user_state;
};

static int foreach_term_wrapper(env_rbtnode_t *node, void *_state, Sint reds) {
    struct __env_foreach_term *state = (struct __env_foreach_term*)_state;
    Eterm key, value;

    key = erts_convert_native_to_filename(state->process,
        node->key.length, (byte*)node->key.data);
    value = erts_convert_native_to_filename(state->process,
        node->value.length, (byte*)node->value.data);

    state->user_callback(state->process, state->user_state, key, value);
    return 1;
}

void erts_osenv_foreach_term(const erts_osenv_t *env, struct process *process,
        void *state, erts_osenv_foreach_term_cb_t callback) {
    struct __env_foreach_term wrapper_state;

    wrapper_state.user_callback = callback;
    wrapper_state.user_state = state;
    wrapper_state.process = process;

    env_rbt_foreach(env->tree, foreach_term_wrapper, &wrapper_state);
}

int erts_osenv_get_term(const erts_osenv_t *env, Process *process,
        Eterm key_term, Eterm *out_term) {
    char key_stackbuf[STACKBUF_SIZE];
    erts_osenv_data_t key;
    int result;

    key.data = convert_key_to_native(key_term, key_stackbuf,
        STACKBUF_SIZE, &key.length);
    result = -1;

    if(key.data != NULL) {
        env_rbtnode_t *node;

        node = env_rbt_lookup(env->tree, key);
        result = 0;

        if(node != NULL) {
            (*out_term) = erts_convert_native_to_filename(process,
                node->value.length, (byte*)node->value.data);
            result = 1;
        }

        if(key.data != key_stackbuf) {
            erts_free(ERTS_ALC_T_TMP, key.data);
        }
    }

    return result;
}

int erts_osenv_put_term(erts_osenv_t *env, Eterm key_term, Eterm value_term) {
    char key_stackbuf[STACKBUF_SIZE], value_stackbuf[STACKBUF_SIZE];
    erts_osenv_data_t key, value;
    int result;

    key.data = convert_key_to_native(key_term, key_stackbuf,
        STACKBUF_SIZE, &key.length);
    value.data = convert_value_to_native(value_term, value_stackbuf,
        STACKBUF_SIZE, &value.length);
    result = -1;

    if(value.data != NULL && key.data != NULL) {
        result = erts_osenv_put_native(env, &key, &value);
    }

    if(value.data != NULL && value.data != value_stackbuf) {
        erts_free(ERTS_ALC_T_TMP, value.data);
    }

    if(key.data != NULL && key.data != key_stackbuf) {
        erts_free(ERTS_ALC_T_TMP, key.data);
    }

    return result;
}

int erts_osenv_unset_term(erts_osenv_t *env, Eterm key_term) {
    char key_stackbuf[STACKBUF_SIZE];
    erts_osenv_data_t key;
    int result;

    key.data = convert_key_to_native(key_term, key_stackbuf,
        STACKBUF_SIZE, &key.length);
    result = -1;

    if(key.data != NULL) {
        result = erts_osenv_unset_native(env, &key);

        if(key.data != key_stackbuf) {
            erts_free(ERTS_ALC_T_TMP, key.data);
        }
    }

    return result;
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

struct __env_foreach_native {
    erts_osenv_foreach_native_cb_t user_callback;
    void *user_state;
};

static int foreach_native_wrapper(env_rbtnode_t *node, void *_state, Sint reds) {
    struct __env_foreach_native *state = (struct __env_foreach_native*)_state;

    state->user_callback(state->user_state, &node->key, &node->value);
    return 1;
}

void erts_osenv_foreach_native(const erts_osenv_t *env, void *state,
        erts_osenv_foreach_native_cb_t callback) {
    struct __env_foreach_native wrapper_state;

    wrapper_state.user_callback = callback;
    wrapper_state.user_state = state;

    env_rbt_foreach(env->tree, foreach_native_wrapper, &wrapper_state);
}

int erts_osenv_get_native(const erts_osenv_t *env,
        const erts_osenv_data_t *key,
        erts_osenv_data_t *value) {
    env_rbtnode_t *node = env_rbt_lookup(env->tree, *key);

    if(node != NULL) {
        if(value != NULL) {
            if(node->value.length > value->length) {
                return -1;
            }

            sys_memcpy(value->data, node->value.data, node->value.length);
            value->length = node->value.length;
        }

        return 1;
    }

    return 0;
}

int erts_osenv_put_native(erts_osenv_t *env, const erts_osenv_data_t *key,
        const erts_osenv_data_t *value) {
    env_rbtnode_t *old_node, *new_node;

    new_node = erts_alloc(ERTS_ALC_T_ENVIRONMENT, sizeof(env_rbtnode_t) +
        key->length + value->length);

    new_node->key.data = (char*)(&new_node[1]);
    new_node->key.length = key->length;
    new_node->value.data = &((char*)new_node->key.data)[key->length];
    new_node->value.length = value->length;

    sys_memcpy(new_node->key.data, key->data, key->length);
    sys_memcpy(new_node->value.data, value->data, value->length);

    old_node = env_rbt_lookup(env->tree, *key);

    if(old_node != NULL) {
        env->content_size -= old_node->value.length;
        env->content_size -= old_node->key.length;
        env_rbt_replace(&env->tree, old_node, new_node);
    } else {
        env_rbt_insert(&env->tree, new_node);
        env->variable_count++;
    }

    env->content_size += new_node->value.length;
    env->content_size += new_node->key.length;

    if(old_node != NULL) {
        erts_free(ERTS_ALC_T_ENVIRONMENT, old_node);
    }

    return 1;
}

int erts_osenv_unset_native(erts_osenv_t *env, const erts_osenv_data_t *key) {
    env_rbtnode_t *old_node = env_rbt_lookup(env->tree, *key);

    if(old_node != NULL) {
        env->content_size -= old_node->value.length;
        env->content_size -= old_node->key.length;
        env->variable_count -= 1;

        env_rbt_delete(&env->tree, old_node);
        erts_free(ERTS_ALC_T_ENVIRONMENT, old_node);
        return 1;
    }

    return 0;
}
