/* SPDX-License-Identifier: BSD-3-Clause */
/**
 * @file
 * @copyright Copyright 2013-2014 Sascha Kruse
 * @copyright Copyright 2014-2026 Dunst contributors
 * @license BSD-3-Clause
 */

#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <glib.h>
#include <pwd.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <wordexp.h>

#include "utils.h"
#include "log.h"
#include "settings_data.h"

char *string_replace_char(char needle, char replacement, char *haystack)
{
        ASSERT_OR_RET(haystack, NULL);

        char *current = haystack;
        while ((current = strchr(current, needle)))
                *current++ = replacement;
        return haystack;
}

char *string_replace_at(char *buf, int pos, int len, const char *repl)
{
        assert(buf);
        assert(repl);

        char *tmp;
        int size, buf_len, repl_len;

        buf_len = strlen(buf);
        repl_len = strlen(repl);
        size = (buf_len - len) + repl_len + 1;

        if (repl_len <= len) {
                tmp = buf;
        } else {
                tmp = g_malloc(size);
                memcpy(tmp, buf, pos);
        }

        memcpy(tmp + pos, repl, repl_len);
        memmove(tmp + pos + repl_len, buf + pos + len, buf_len - (pos + len) + 1);

        if (tmp != buf) {
                g_free(buf);
        }

        return tmp;
}

char *string_replace_all(const char *needle, const char *replacement, char *haystack)
{
        ASSERT_OR_RET(haystack, NULL);
        assert(needle);
        assert(replacement);

        char *start;
        int needle_pos;
        int needle_len, repl_len;

        needle_len = strlen(needle);
        if (needle_len == 0) {
                return haystack;
        }

        start = strstr(haystack, needle);
        repl_len = strlen(replacement);

        while (start) {
                needle_pos = start - haystack;
                haystack = string_replace_at(haystack, needle_pos, needle_len, replacement);
                start = strstr(haystack + needle_pos + repl_len, needle);
        }
        return haystack;
}

char *string_append(char *a, const char *b, const char *sep)
{
        if (STR_EMPTY(a)) {
                g_free(a);
                return g_strdup(b);
        }
        if (STR_EMPTY(b))
                return a;

        char *new;
        if (!sep)
                new = g_strconcat(a, b, NULL);
        else
                new = g_strconcat(a, sep, b, NULL);
        g_free(a);

        return new;
}

char *string_strip_quotes(const char *value)
{
        ASSERT_OR_RET(value, NULL);

        size_t len = strlen(value);
        char *s;

        if (value[0] == '"' && value[len-1] == '"')
                s = g_strndup(value + 1, len-2);
        else
                s = g_strdup(value);

        return s;
}

void string_strip_delimited(char *str, char a, char b)
{
        assert(str);

        int iread=-1, iwrite=0, copen=0;
        int cskip = 0;
        while (str[++iread] != 0) {
                if (str[iread] == a) {
                        ++copen;
                } else if (str[iread] == b && copen > 0) {
                        --copen;
                } else if (copen == 0) {
                        cskip = 0;
                        str[iwrite++] = str[iread];
                }
                if (copen > 0) {
                        cskip++;
                }
        }
        if (copen > 0) {
                iread -= cskip;
                for (int i = 0; i < cskip; i++) {
                        str[iwrite++] = str[iread++];
                }
        }
        str[iwrite] = 0;
}

char **string_to_array(const char *string, const char *delimiter)
{
        char **arr = NULL;
        if (string) {
                arr = g_strsplit(string, delimiter, 0);
                for (int i = 0; arr[i]; i++) {
                        g_strstrip(arr[i]);
                }
        }
        return arr;
}

int string_array_length(char **s)
{
        if (!s)
                return -1;

        int len = 0;
        while (s[len])
                len++;

        return len;
}

char *string_to_path(char *string)
{
        ASSERT_OR_RET(string, string);

        wordexp_t we;
        switch (wordexp(string, &we, WRDE_NOCMD | WRDE_UNDEF)) {
                case 0:
                        break;
                case WRDE_BADCHAR:
                        LOG_W("Expansion of \"%s\" failed. It contains invalid characters.", string);
                        return string;
                case WRDE_BADVAL:
                        LOG_W("Expansion of \"%s\" failed. It contains an undefined variable.", string);
                        return string;
                case WRDE_CMDSUB:
                        LOG_W("Expansion of \"%s\" failed. The requested command substitution is currently not supported.", string);
                        return string;
                case WRDE_NOSPACE:
                        LOG_W("Expansion of \"%s\" failed. We ran out of memory.", string);
                        return string;
                case WRDE_SYNTAX:
                        LOG_W("Expansion of \"%s\" failed. It contains invalid syntax.", string);
                        return string;
        }
        g_free(string);

        char *res = g_strjoinv(" ", we.we_wordv);
        wordfree(&we);

        return res;
}

bool safe_string_to_long_long(long long *in, const char *str) {
        errno = 0;
        char *endptr;
        long long val = g_ascii_strtoll(str, &endptr, 10);

        if (errno != 0) {
                LOG_W("'%s': %s.", str, strerror(errno));
                return false;
        } else if (str == endptr) {
                LOG_W("'%s': No digits found.", str);
                return false;
        } else if (*endptr != '\0') {
                LOG_W("'%s': String contains non-digits.", str);
                return false;
        }
        *in = val;
        return true;
}

bool safe_string_to_int(int *in, const char *str) {
        long long l;
        if (!safe_string_to_long_long(&l, str))
                return false;

        // Check if it's in int range
        if (l < INT_MIN || l > INT_MAX) {
                errno = ERANGE;
                LOG_W("'%s': %s.", str, strerror(errno));
                return false;
        }

        *in = (int) l;
        return true;
}

bool safe_string_to_double(double *in, const char *str) {
        errno = 0;
        char *endptr;
        double val = g_ascii_strtod(str, &endptr);
        if (errno != 0) {
                LOG_W("'%s': %s.", str, strerror(errno));
                return false;
        } else if (str == endptr) {
                LOG_W("'%s': No digits found.", str);
                return false;
        } else if (*endptr != '\0') {
                LOG_W("'%s': String contains non-digits.", str);
                return false;
        }
        *in = val;
        return true;
}

gint64 string_to_time(const char *string)
{
        assert(string);

        errno = 0;
        char *endptr;
        gint64 val = strtol(string, &endptr, 10);

        if (errno != 0) {
                LOG_W("Time: '%s': %s.", string, strerror(errno));
                return 0;
        } else if (string == endptr) {
                errno = EINVAL;
                LOG_W("Time: '%s': No digits found.", string);
                return 0;
        } else if (val < -1) {
                // most times should not be negative, but show_age_threshhold
                // can be -1
                LOG_W("Time: '%s': Time should be positive (-1 is allowed too sometimes)",
                                string);
                errno = EINVAL;
        }
        else if (errno == 0 && !*endptr) {
                return S2US(val);
        }
        // endptr may point to a separating space
        while (isspace(*endptr))
                endptr++;

        if (val < 0) {
                LOG_W("Time: '%s' signal value -1 should not have a suffix", string);
                errno = EINVAL;
                return 0;
        }

        if (STRN_EQ(endptr, "ms", 2))
                return val * 1000;
        else if (STRN_EQ(endptr, "s", 1))
                return S2US(val);
        else if (STRN_EQ(endptr, "m", 1))
                return S2US(val) * 60;
        else if (STRN_EQ(endptr, "h", 1))
                return S2US(val) * 60 * 60;
        else if (STRN_EQ(endptr, "d", 1))
                return S2US(val) * 60 * 60 * 24;
        else
        {
                errno = EINVAL;
                return 0;
        }
}

gint64 time_monotonic_now(void)
{
        struct timespec tv_now;

        /**
         * @note On Linux, BOOTTIME is the correct monotonic time,
         * as BOOTTIME counts onwards during sleep. For all other
         * POSIX compliant OSes, MONOTONIC should also count onwards
         * during system sleep.
         */
#ifdef __linux__
        clock_gettime(CLOCK_BOOTTIME, &tv_now);
#else
        clock_gettime(CLOCK_MONOTONIC, &tv_now);
#endif
        return S2US(tv_now.tv_sec) + tv_now.tv_nsec / 1000;
}

gint64 time_now(void)
{
        struct timespec tv_now;

        clock_gettime(CLOCK_REALTIME, &tv_now);
        return S2US(tv_now.tv_sec) + tv_now.tv_nsec / 1000;
}

gint64 modification_time(const char *path)
{
        struct stat statbuf;
        if (!path || stat(path, &statbuf) != 0)
                return -1;

        return S2US(statbuf.st_mtim.tv_sec) + statbuf.st_mtim.tv_nsec / 1000;
}

const char *user_get_home(void)
{
        static const char *home_directory = NULL;
        ASSERT_OR_RET(!home_directory, home_directory);

        // Check the HOME variable for the user's home
        home_directory = getenv("HOME");
        ASSERT_OR_RET(!home_directory, home_directory);

        // Check the /etc/passwd entry for the user's home
        home_directory = getpwuid(getuid())->pw_dir;

        return home_directory;
}

bool safe_setenv(const char* key, const char* value){
        if (!key)
                return false;

        if (!value)
                setenv(key, "", 1);
        else
                setenv(key, value, 1);

        return true;
}

// These sections are not interpreted as rules
static const char* special_sections[] = {
        "global",
        "frame",
        "experimental",
        "shortcuts",
        "urgency_low",
        "urgency_normal",
        "urgency_critical",
};

static const char* deprecated_sections[] = {
        "frame",
        "shortcuts",
};

static const char* deprecated_sections_message[] = {
        "The settings from the frame section have been moved to the global section.", // frame
        "Settings in the shortcuts sections have been moved to the global section.\nAlternatively you can bind shortcuts in your window manager to dunstctl commands. For that, see the manual for dunstctl.", // shortcuts
};

bool is_special_section(const char* s) {
        for (size_t i = 0; i < G_N_ELEMENTS(special_sections); i++) {
                if (STR_EQ(special_sections[i], s)) {
                        return true;
                }
        }
        return false;
}

bool is_deprecated_section(const char* s) {
        for (size_t i = 0; i < G_N_ELEMENTS(deprecated_sections); i++) {
                if (STR_EQ(deprecated_sections[i], s)) {
                        return true;
                }
        }
        return false;
}

const char *get_section_deprecation_message(const char *s) {
        for (size_t i = 0; i < G_N_ELEMENTS(deprecated_sections); i++) {
                if (STR_EQ(deprecated_sections[i], s)) {
                        return deprecated_sections_message[i];
                }
        }
        return "";
}

char *string_strip_brackets(const char* s) {
        if (!s)
                return NULL;

        size_t len = strlen(s);
        if (s[0] == '(' && s[len-1] == ')')
                return g_strndup(s + 1, len-2);
        else
                return NULL;

}

bool is_readable_file(const char * const path)
{
        struct stat statbuf;
        bool result = false;

        if (path && 0 == stat(path, &statbuf)) {
                /** See what intersting stuff can be done with FIFOs */
                if (!(statbuf.st_mode & (S_IFIFO | S_IFREG))) {
                        /** Sets errno if stat() was successful but @p path [in]
                         * does not point to a regular file or FIFO. This
                         * just in case someone queries errno which would
                         * otherwise indicate success. */
                        errno = EINVAL;
                } else if (0 == access(path, R_OK)) { /* must also be readable */
                        result = true;
                }
        }

        return result;
}

FILE *fopen_verbose(const char * const path)
{
        FILE *f = NULL;
        char *real_path = string_to_path(g_strdup(path));

        if (is_readable_file(real_path) && (f = fopen(real_path, "r")))
                LOG_I(MSG_FOPEN_SUCCESS(path, f));
        else
                LOG_W(MSG_FOPEN_FAILURE(path));

        g_free(real_path);
        return f;
}

void add_paths_from_env(GPtrArray *arr, char *env_name, char *subdir, char *alternative)
{
        const char *xdg_data_dirs = g_getenv(env_name);
        if (!xdg_data_dirs)
                xdg_data_dirs = alternative;

        char **xdg_data_dirs_arr = string_to_array(xdg_data_dirs, ":");
        if (!xdg_data_dirs_arr)
                return;

        for (int i = 0; xdg_data_dirs_arr[i] != NULL; i++) {
                char *loc = g_build_filename(xdg_data_dirs_arr[i], subdir, NULL);
                g_ptr_array_add(arr, loc);
        }
        g_strfreev(xdg_data_dirs_arr);
}

bool string_is_int(const char *str) {
        if (str != NULL) {
                while (isspace(*str)) str++;
                while (isdigit(*str)) str++;
                while (isspace(*str)) str++;
                return *str == '\0';
        }
        return true;
}

bool is_like_path(const char *string)
{
        return string[0] == '/' || string[0] == '~'
                || (string[0] == '.' && string[1] == '.' && string[2] == '/')
                || (string[0] == '.' && string[1] == '/');
}
