#include "FFPlatform_private.h"
#include "common/FFstrbuf.h"
#include "common/arrutil.h"
#include "common/strutil.h"
#include "common/io.h"
#include "common/path.h"

#include <unistd.h>
#include <pwd.h>
#include <limits.h>
#include <sys/utsname.h>
#include <paths.h>

#ifdef __APPLE__
    #include <mach-o/dyld.h>
    #include <sys/sysctl.h>
#elif defined(__FreeBSD__) || defined(__NetBSD__)
    #include <sys/sysctl.h>
#elif defined(__OpenBSD__)
    #include <sys/sysctl.h>
    #include <sys/stat.h>
    #include <kvm.h>
    #include "common/path.h"
#elif defined(__HAIKU__)
    #include <image.h>
    #include <OS.h>
#endif

static void getExePath(FFPlatform* platform) {
    char exePath[PATH_MAX];
#if defined(__linux__) || defined(__GNU__)
    ssize_t exePathLen = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
    if (exePathLen > 0) {
        exePath[exePathLen] = '\0';
    }
#elif defined(__APPLE__)
    uint32_t exePathLen = sizeof(exePath);
    if (_NSGetExecutablePath(exePath, &exePathLen) == 0) {
        exePathLen = (uint32_t) strlen(exePath);
    } else {
        exePathLen = 0;
    }
#elif defined(__FreeBSD__) || defined(__NetBSD__)
    size_t exePathLen = sizeof(exePath);
    if (sysctl(
            (int[]) { CTL_KERN,
    #ifdef __FreeBSD__
                KERN_PROC,
                KERN_PROC_PATHNAME,
                (pid_t) platform->pid
    #else
                KERN_PROC_ARGS,
                (pid_t) platform->pid,
                KERN_PROC_PATHNAME
    #endif
            },
            4,
            exePath,
            &exePathLen,
            nullptr,
            0) < 0)
        exePathLen = 0;
    else {
        exePathLen--; // remove terminating NUL
    }
#elif defined(__OpenBSD__)
    // OpenBSD doesn't have a reliable way to get the executable path.
    // Current implementation uses argv[0], which can be easily spoofed.
    // See #2195
    size_t exePathLen = 0;
    kvm_t* kd = kvm_openfiles(nullptr, nullptr, nullptr, KVM_NO_FILES, nullptr);
    if (kd) {
        int kpCount;
        struct kinfo_proc* kp = kvm_getprocs(kd, KERN_PROC_PID, (pid_t) platform->pid, sizeof(*kp), &kpCount);
        if (kp && kpCount == 1) {
            char** argv = kvm_getargv(kd, kp, 0);
            if (argv && argv[0]) {
                char* arg0 = argv[0];
                if (arg0[0]) {
                    if (strchr(arg0, '/') != nullptr) // likely a path (absolute or relative)
                    {
                        exePathLen = strlen(arg0);
                        if (exePathLen < ARRAY_SIZE(exePath)) {
                            memcpy(exePath, arg0, exePathLen);
                            exePath[exePathLen] = '\0';
                        } else {
                            exePathLen = 0;
                        }
                    } else {
                        FF_STRBUF_AUTO_DESTROY tmpPath = ffStrbufCreate();
                        if (ffFindExecutableInPath(arg0, &tmpPath) == nullptr && tmpPath.length < ARRAY_SIZE(exePath)) {
                            memcpy(exePath, tmpPath.chars, tmpPath.length + 1);
                            exePathLen = tmpPath.length;
                        }
                    }

                    if (exePathLen > 0) {
                        struct stat st;
                        if (stat(exePath, &st) == 0 && S_ISREG(st.st_mode)) {
                            int cntp;
                            struct kinfo_file* kf = kvm_getfiles(kd, KERN_FILE_BYPID, (pid_t) platform->pid, sizeof(*kf), &cntp);
                            if (kf) {
                                int i;
                                for (i = 0; i < cntp; i++) {
                                    if (kf[i].fd_fd == KERN_FILE_TEXT) {
                                        // KERN_FILE_TEXT is the executable file, not a shared library, and should be unique in the list.
                                        if (st.st_dev != (dev_t) kf[i].va_fsid || st.st_ino != (ino_t) kf[i].va_fileid) {
                                            i = -1;
                                        }
                                        break;
                                    }
                                }
                                if (i < 0) {
                                    exePathLen = 0;
                                }
                            } else {
                                // If we can't get the list of open files, we can't verify that the file is actually the executable
                                // Assume it is
                            }
                        } else {
                            exePathLen = 0;
                        }
                    }
                }
            }
        }
        kvm_close(kd);
    }
#elif defined(__sun)
    ssize_t exePathLen = readlink("/proc/self/path/a.out", exePath, sizeof(exePath) - 1);
    if (exePathLen > 0) {
        exePath[exePathLen] = '\0';
    }
#elif defined(__HAIKU__)
    size_t exePathLen = 0;
    image_info info;
    int32 cookie = 0;

    while (get_next_image_info(B_CURRENT_TEAM, &cookie, &info) == B_OK) {
        if (info.type == B_APP_IMAGE) {
            exePathLen = strlcpy(exePath, info.name, sizeof(exePath));
            break;
        }
    }
#endif
    if (exePathLen > 0) {
        ffStrbufEnsureFree(&platform->exePath, PATH_MAX);
        if (realpath(exePath, platform->exePath.chars)) {
            ffStrbufRecalculateLength(&platform->exePath);
        } else {
            ffStrbufSetNS(&platform->exePath, (uint32_t) exePathLen, exePath);
        }
    }
}

static void platformPathAddEnv(FFlist* dirs, const char* env) {
    const char* envValue = getenv(env);
    if (!ffStrSet(envValue)) {
        return;
    }

    FF_STRBUF_AUTO_DESTROY value = ffStrbufCreateA(64);
    ffStrbufAppendS(&value, envValue);

    uint32_t startIndex = 0;
    while (startIndex < value.length) {
        uint32_t colonIndex = ffStrbufNextIndexC(&value, startIndex, ':');
        value.chars[colonIndex] = '\0';

        if (!ffStrSet(value.chars + startIndex)) {
            startIndex = colonIndex + 1;
            continue;
        }

        ffPlatformPathAddAbsolute(dirs, value.chars + startIndex);

        startIndex = colonIndex + 1;
    }
}

static void getHomeDir(FFPlatform* platform, const struct passwd* pwd) {
    const char* home = pwd ? pwd->pw_dir : getenv("HOME");
    ffStrbufAppendS(&platform->homeDir, home);
    ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
}

static void getCacheDir(FFPlatform* platform) {
    const char* cache = getenv("XDG_CACHE_HOME");
    if (ffStrSet(cache)) {
        ffStrbufAppendS(&platform->cacheDir, cache);
        ffStrbufEnsureEndsWithC(&platform->cacheDir, '/');
    } else {
        ffStrbufAppend(&platform->cacheDir, &platform->homeDir);
        ffStrbufAppendS(&platform->cacheDir, ".cache/");
    }
}

static void getConfigDirs(FFPlatform* platform) {
    // Always make sure `${XDG_CONFIG_HOME:-$HOME/.config}` is the first entry
    platformPathAddEnv(&platform->configDirs, "XDG_CONFIG_HOME");
    ffPlatformPathAddHome(&platform->configDirs, platform, ".config/");

#if defined(__APPLE__)
    ffPlatformPathAddHome(&platform->configDirs, platform, "Library/Preferences/");
    ffPlatformPathAddHome(&platform->configDirs, platform, "Library/Application Support/");
#endif
#if defined(__HAIKU__)
    ffPlatformPathAddHome(&platform->configDirs, platform, "config/settings/");
#endif

    ffPlatformPathAddHome(&platform->configDirs, platform, "");
    platformPathAddEnv(&platform->configDirs, "XDG_CONFIG_DIRS");

#if !defined(__APPLE__)
    ffPlatformPathAddAbsolute(&platform->configDirs, FASTFETCH_TARGET_DIR_ETC "/xdg/");
#endif

    ffPlatformPathAddAbsolute(&platform->configDirs, FASTFETCH_TARGET_DIR_ETC "/");
    ffPlatformPathAddAbsolute(&platform->configDirs, FASTFETCH_TARGET_DIR_INSTALL_SYSCONF "/");
}

static void getDataDirs(FFPlatform* platform) {
    platformPathAddEnv(&platform->dataDirs, "XDG_DATA_HOME");
    ffPlatformPathAddHome(&platform->dataDirs, platform, ".local/share/");

    // Add ${currentExePath}/../share
    if (platform->exePath.length > 0) {
        FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateCopy(&platform->exePath);
        ffStrbufSubstrBeforeLastC(&path, '/');
        ffStrbufSubstrBeforeLastC(&path, '/');
        ffStrbufAppendS(&path, "/share");
        ffPlatformPathAddAbsolute(&platform->dataDirs, path.chars);
    }

#ifdef __APPLE__
    ffPlatformPathAddHome(&platform->dataDirs, platform, "Library/Application Support/");
#endif

    ffPlatformPathAddHome(&platform->dataDirs, platform, "");
    platformPathAddEnv(&platform->dataDirs, "XDG_DATA_DIRS");
#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
    ffPlatformPathAddAbsolute(&platform->dataDirs, FF_PATH_PKG_BASE "/share/");
#endif
    ffPlatformPathAddAbsolute(&platform->dataDirs, FASTFETCH_TARGET_DIR_USR "/local/share/");
    ffPlatformPathAddAbsolute(&platform->dataDirs, FASTFETCH_TARGET_DIR_USR "/share/");
}

static void getUserName(FFPlatform* platform, const struct passwd* pwd) {
    if (pwd) {
        ffStrbufSetS(&platform->userName, pwd->pw_name);
        ffStrbufSetS(&platform->fullUserName, pwd->pw_gecos);
        ffStrbufTrimSpace(&platform->fullUserName);
    } else {
        ffStrbufSetS(&platform->userName, getenv("USER"));
    }
}

static void getHostName(FFPlatform* platform, const struct utsname* uts) {
    ffStrbufAppendS(&platform->hostName, uts->nodename);
}

static void getUserShell(FFPlatform* platform, const struct passwd* pwd) {
    const char* shell = getenv("SHELL");
    if (!ffStrSet(shell) && pwd) {
        shell = pwd->pw_shell;
    }

    ffStrbufAppendS(&platform->userShell, shell);
}

static void getSysinfo(FFPlatformSysinfo* info, const struct utsname* uts) {
    ffStrbufAppendS(&info->name, uts->sysname);
    ffStrbufAppendS(&info->release, uts->release);
    ffStrbufAppendS(&info->version, uts->version);
#ifdef __HAIKU__
    /* historical reason */
    if (ffStrEquals(uts->machine, "BePC")) {
        ffStrbufSetStatic(&info->architecture, "i386");
    } else
#endif
        ffStrbufAppendS(&info->architecture, uts->machine);

#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__)
    size_t length = sizeof(info->pageSize);
    sysctl((int[]) { CTL_HW, HW_PAGESIZE }, 2, &info->pageSize, &length, nullptr, 0);
#else
    info->pageSize = (uint32_t) sysconf(_SC_PAGESIZE);
#endif
}

static void getCwd(FFPlatform* platform) {
    char cwd[PATH_MAX];
    if (getcwd(cwd, sizeof(cwd)) != nullptr) {
        ffStrbufSetS(&platform->cwd, cwd);
        ffStrbufEnsureEndsWithC(&platform->cwd, '/');
    }
}

void ffPlatformInitImpl(FFPlatform* platform) {
    platform->pid = (uint32_t) getpid();
    platform->uid = getuid();
    struct passwd* pwd = getpwuid(platform->uid);

    struct utsname uts;
    if (uname(&uts) < 0) {
        memset(&uts, 0, sizeof(uts));
    }

    getExePath(platform);
    getCwd(platform);
    getHomeDir(platform, pwd);
    getCacheDir(platform);
    getConfigDirs(platform);
    getDataDirs(platform);

    getUserName(platform, pwd);
    getHostName(platform, &uts);
    getUserShell(platform, pwd);

    getSysinfo(&platform->sysinfo, &uts);
}
