// This file is part of BOINC.
// http://boinc.berkeley.edu
// Copyright (C) 2018 University of California
//
// BOINC is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License
// as published by the Free Software Foundation,
// either version 3 of the License, or (at your option) any later version.
//
// BOINC is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with BOINC.  If not, see <http://www.gnu.org/licenses/>.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <string>
#include <vector>
#include <sstream>

using std::string;

// normally configure would find this out
// only activate one of the three to test with
#define HAVE_SYS_UTSNAME_H 1
#define HAVE_SYS_SYSCTL_H 0
#define HAVE_SYS_SYSTEMINFO_H 0

#if HAVE_SYS_SYSCTL_H
#include <sys/sysctl.h>
#endif
#if HAVE_SYS_SYSTEMINFO_H
#include <sys/systeminfo.h>
#endif
#if HAVE_SYS_UTSNAME_H
#include <sys/utsname.h>
#endif

#define false 0
#define true 1
#define bool int
#define strlcpy strncpy
#define strlcat strncat
#define safe_strcpy(x, y) strlcpy(x, y, sizeof(x))
#define safe_strcat(x, y) strlcat(x, y, sizeof(x))
#define LINUX_LIKE_SYSTEM (defined(__linux__) || defined(__GNU__) || defined(__GLIBC__)) && !defined(__HAIKU__)

enum LINUX_OS_INFO_PARSER {
    lsbrelease,
    osrelease,
    redhatrelease
};

const char command_lsbrelease[] = "/usr/bin/lsb_release -a 2>&1";
const char file_osrelease[] = "/etc/os-release";
const char file_redhatrelease[] = "/etc/redhat-release";

// remove whitespace from start and end of a string
//
void strip_whitespace(string& str) {
    while (1) {
        if (str.length() == 0) break;
        if (!isascii(str[0])) break;
        if (!isspace(str[0])) break;
        str.erase(0, 1);
    }

    int n = (int) str.length();
    while (n>0) {
        if (!isascii(str[n-1])) break;
        if (!isspace(str[n-1])) break;
        n--;
    }
    str.erase(n, str.length()-n);
}

void strip_whitespace(char *str) {
    string s = str;
    strip_whitespace(s);
    safe_strcpy(str, s.c_str());
}

// remove whitespace and quotes from start and end of a string
//
void strip_quotes(string& str) {
    while (1) {
        if (str.length() == 0) break;
        if (str[0] == '"' || str[0] == '\'') {
            str.erase(0, 1);
            continue;
        }
        if (!isascii(str[0])) break;
        if (!isspace(str[0])) break;
        str.erase(0, 1);
    }

    int n = (int) str.length();
    while (n>0) {
        if (str[n-1] == '"' || str[n-1] == '\'') {
            if (str[n-2] != '\\') {
                n--;
                continue;
            }
        }
        if (!isascii(str[n-1])) break;
        if (!isspace(str[n-1])) break;
        n--;
    }
    str.erase(n, str.length()-n);
}

void strip_quotes(char *str) {
    string s = str;
    strip_quotes(s);
    safe_strcpy(str, s.c_str());
}

void unescape_os_release(char* buf) {
    char* out = buf;
    char* in = buf;
    while (*in) {
        if (*in != '\\') {
            *out++ = *in++;
        } else if (*(in+1) == '$') {
            *out++ = '$';
            in += 2;
        } else if (*(in+1) == '\'') {
            *out++ = '\'';
            in += 2;
        } else if (*(in+1) == '"') {
            *out++ = '"';
            in += 2;
        } else if (*(in+1) == '\\') {
            *out++ = '\\';
            in += 2;
        } else if (*(in+1) == '`') {
            *out++ = '`';
            in += 2;
        } else {
            *out++ = *in++;
        }
    }
    *out = 0;
}

int get_libc_version(string& version, string& extra_info) {
    char buf[1024] = "";
    string strbuf;
    FILE* f = popen("PATH=/usr/bin:/bin:/usr/local/bin ldd --version 2>&1", "r");
    if (f) {
        char* retval = fgets(buf, sizeof(buf), f);
        strbuf = (string)buf;
        while (fgets(buf, sizeof(buf), f)) {
            // consume output to allow command to exit gracefully
        }
        int status = pclose(f);
        if (!retval || status == -1 || !WIFEXITED(status) || WEXITSTATUS(status)) {
            return 1;
        }
        strip_whitespace(strbuf);
        string::size_type parens1 = strbuf.find('(');
        string::size_type parens2 = strbuf.rfind(')');
        string::size_type blank = strbuf.rfind(' ');

        if (blank != string::npos) {
            // extract version number
            version = strbuf.substr(blank+1);
        } else {
            return 1;
        }
        if (parens1 != string::npos && parens2 != string::npos && parens1 < parens2) {
            // extract extra information without parenthesis
            extra_info = strbuf.substr(parens1+1, parens2-parens1-1);
        }
    } else {
        return 1;
    }
    return 0;
}

std::vector<string> split(std::string s, char delim) {
    std::vector<string> result;
    std::stringstream ss(s);
    std::string item;
    while (getline(ss, item, delim)) {
        result.push_back(item);
    }
    return result;
}

bool parse_linux_os_info(const std::vector<std::string>& lines, const LINUX_OS_INFO_PARSER parser,
    char* os_name, const int os_name_size, char* os_version, const int os_version_size) {
    if (lines.empty()) {
        return false;
    }

    bool found_something = false;
    char buf[256], buf2[256];
    char dist_pretty[256], dist_name[256], dist_version[256], dist_codename[256];
    //string os_version_extra("");
    safe_strcpy(dist_pretty, "");
    safe_strcpy(dist_name, "");
    safe_strcpy(dist_version, "");
    safe_strcpy(dist_codename, "");

    switch (parser) {
    case lsbrelease: {
        for (unsigned int i = 0; i < lines.size(); ++i) {
            safe_strcpy(buf, lines[i].c_str());
            strip_whitespace(buf);
            if (strstr(buf, "Description:")) {
                found_something = true;
                safe_strcpy(dist_pretty, strchr(buf, ':') + 1);
                strip_whitespace(dist_pretty);
            }
            if (strstr(buf, "Distributor ID:")) {
                found_something = true;
                safe_strcpy(dist_name, strchr(buf, ':') + 1);
                strip_whitespace(dist_name);
            }
            if (strstr(buf, "Release:")) {
                found_something = true;
                safe_strcpy(dist_version, strchr(buf, ':') + 1);
                strip_whitespace(dist_version);
            }
            if (strstr(buf, "Codename:")) {
                found_something = true;
                safe_strcpy(dist_codename, strchr(buf, ':') + 1);
                strip_whitespace(dist_codename);
            }
        }
        break;
    }
    case osrelease: {
        for (unsigned int i = 0; i < lines.size(); ++i) {
            safe_strcpy(buf, lines[i].c_str());
            strip_whitespace(buf);
            // check if substr is at the beginning of the line
            if (strstr(buf, "PRETTY_NAME=") == buf) {
                found_something = true;
                safe_strcpy(buf2, strchr(buf, '=') + 1);
                strip_quotes(buf2);
                unescape_os_release(buf2);
                safe_strcpy(dist_pretty, buf2);
                continue;
            }
            if (strstr(buf, "NAME=") == buf) {
                found_something = true;
                safe_strcpy(buf2, strchr(buf, '=') + 1);
                strip_quotes(buf2);
                unescape_os_release(buf2);
                safe_strcpy(dist_name, buf2);
                continue;
            }
            if (strstr(buf, "VERSION=") == buf) {
                found_something = true;
                safe_strcpy(buf2, strchr(buf, '=') + 1);
                strip_quotes(buf2);
                unescape_os_release(buf2);
                safe_strcpy(dist_version, buf2);
                continue;
            }
            // could also be "UBUNTU_CODENAME="
            if (strstr(buf, "CODENAME=")) {
                found_something = true;
                safe_strcpy(buf2, strchr(buf, '=') + 1);
                strip_quotes(buf2);
                unescape_os_release(buf2);
                safe_strcpy(dist_codename, buf2);
                continue;
            }
        }
        break;
    }
    case redhatrelease: {
        safe_strcpy(buf, lines.front().c_str());
        found_something = true;
        strip_whitespace(buf);
        safe_strcpy(dist_pretty, buf);
        break;
    }
    default: {
        return false;
    }
    }

    if (found_something) {
        safe_strcpy(buf2, "");
        if (strlen(dist_pretty)) {
            safe_strcat(buf2, dist_pretty);
        }
        else {
            if (strlen(dist_name)) {
                safe_strcat(buf2, dist_name);
                safe_strcat(buf2, " ");
            }
            if (strlen(dist_version)) {
                safe_strcat(buf2, dist_version);
                safe_strcat(buf2, " ");
            }
            if (strlen(dist_codename)) {
                safe_strcat(buf2, dist_codename);
                safe_strcat(buf2, " ");
            }
            strip_whitespace(buf2);
        }
        strlcpy(os_version, buf2, os_version_size);
        if (strlen(dist_name)) {
            strlcpy(os_name, dist_name, os_name_size);
        }

        return true;
    }

    return false;
}

bool parse_linux_os_info(FILE* file, const LINUX_OS_INFO_PARSER parser,
    char* os_name, const int os_name_size, char* os_version, const int os_version_size) {

    if (!file) {
        return false;
    }

    char buf[256];
    std::vector<std::string> lines;

    while (fgets(buf, 256, file)) {
        lines.push_back(buf);
    }

    return parse_linux_os_info(lines, parser, os_name, os_name_size, os_version, os_version_size);
}

bool parse_linux_os_info(const std::string& line, const LINUX_OS_INFO_PARSER parser,
    char* os_name, const int os_name_size, char* os_version, const int os_version_size) {
    if (line.empty()) {
        return false;
    }

    const char delim = '\n';

    return parse_linux_os_info(split(line, delim), parser, os_name, os_name_size, os_version, os_version_size);
}

int main(void) {
    char buf[256], features[1024], model_buf[1024];
    bool vendor_found=false, model_found=false;
    bool cache_found=false, features_found=false;
    bool icache_found=false,dcache_found=false;
    bool model_hack=false, vendor_hack=false;
    int n;
#if !defined(__aarch64__) && !defined(__arm__)
    int family=-1, model=-1, stepping=-1;
#else
    char implementer[32] = {0}, architecture[32] = {0}, variant[32] = {0}, cpu_part[32] = {0}, revision[32] = {0};
    bool model_info_found=false;
#endif
    char  p_vendor[256], p_model[256], product_name[256];
    char  os_name[256], os_version[256];
    string os_version_extra("");
    char buf2[256];
    int m_cache=-1;

    // parse /proc/cpuinfo as in parse_cpuinfo_linux() from hostinfo_unix.cpp
    FILE* f = fopen("/proc/cpuinfo", "r");
    if (!f) return (EXIT_FAILURE);

    safe_strcpy(p_model, "");

#ifdef __mips__
    safe_strcpy(p_model, "MIPS ");
    model_hack = true;
#elif __alpha__
    safe_strcpy(p_vendor, "HP (DEC) ");
    vendor_hack = true;
#elif __hppa__
    safe_strcpy(p_vendor, "HP ");
    vendor_hack = true;
#elif __ia64__
    safe_strcpy(p_model, "IA-64 ");
    model_hack = true;
#elif defined(__arm__) || defined(__aarch64__)
    safe_strcpy(p_vendor, "ARM ");
    vendor_hack = vendor_found = true;
#endif

    safe_strcpy(features, "");
    safe_strcpy(product_name, "");
    while (fgets(buf, 256, f)) {
        strip_whitespace(buf);
        if (
                /* there might be conflicts if we dont #ifdef */
#ifdef __ia64__
            strstr(buf, "vendor     : ")
#elif __hppa__
            strstr(buf, "cpu\t\t: ")
#elif __powerpc__
            strstr(buf, "machine\t\t: ")
#elif __sparc__
            strstr(buf, "type\t\t: ")
#elif __alpha__
            strstr(buf, "cpu\t\t\t: ")
#else
            strstr(buf, "vendor_id\t: ") || strstr(buf, "system type\t\t: ")
#endif
        ) {
            if (!vendor_hack && !vendor_found) {
                vendor_found = true;
                strlcpy(p_vendor, strchr(buf, ':') + 2, sizeof(p_vendor));
            } else if (!vendor_found) {
                vendor_found = true;
                strlcpy(buf2, strchr(buf, ':') + 2, sizeof(p_vendor) - strlen(p_vendor) - 1);
                safe_strcat(p_vendor, buf2);
            }
        }

#if defined(__aarch64__) || defined(__arm__)
        if (
            // Hardware is specifying the board this CPU is on, store it in product_name while we parse /proc/cpuinfo
            strstr(buf, "Hardware\t: ")
        ) {
            // this makes sure we only ever copy as much bytes as we can still store in host.product_name
            int t = sizeof(product_name) - strlen(product_name) - 2;
            strlcpy(buf2, strchr(buf, ':') + 2, ((t<sizeof(buf2))?t:sizeof(buf2)));
            strip_whitespace(buf2);
            if (strlen(product_name)) {
                safe_strcat(product_name, " ");
            }
            safe_strcat(product_name, buf2);
        }
#endif
        if (
#ifdef __ia64__
            strstr(buf, "family     : ") || strstr(buf, "model name : ")
#elif __powerpc__ || __sparc__
            strstr(buf, "cpu\t\t: ")
//#elif __arm__
//            strstr(buf, "Processor\t: ") || strstr(buf, "model name")
#elif defined(__aarch64__) || defined(__arm__)
            // Hardware is a fallback specifying the board this CPU is on (not ideal but better than nothing)
            strstr(buf, "model name") || strstr(buf, "Processor") || strstr(buf, "Hardware")
#else
            strstr(buf, "model name\t: ") || strstr(buf, "cpu model\t\t: ")
#endif
        ) {
            if (!model_hack && !model_found) {
                model_found = true;
#ifdef __powerpc__
                char *coma = NULL;
                if ((coma = strrchr(buf, ','))) {   /* we have ", altivec supported" */
                    *coma = '\0';    /* strip the unwanted line */
                    safe_strcpy(features, "altivec");
                    features_found = true;
                }
#endif
                strlcpy(p_model, strchr(buf, ':') + 2, sizeof(p_model));
            } else if (!model_found) {
#ifdef __ia64__
                /* depending on kernel version, family can be either
                a number or a string. If number, we have a model name,
                else we don't */
                char *testc = NULL;
                testc = strrchr(buf, ':')+2;
                if (isdigit(*testc)) {
                    family = atoi(testc);
                    continue;    /* skip this line */
                }
#endif
                model_found = true;
                strlcpy(buf2, strchr(buf, ':') + 2, sizeof(p_model) - strlen(p_model) - 1);
                strip_whitespace(buf2);
                safe_strcat(p_model, buf2);
            }
        }

#if  !defined(__hppa__) && !defined(__aarch64__) && !defined(__arm__)
    /* XXX hppa: "cpu family\t: PA-RISC 2.0" */
        if (strstr(buf, "cpu family\t: ") && family<0) {
            family = atoi(buf+strlen("cpu family\t: "));
        }
        /* XXX hppa: "model\t\t: 9000/785/J6000" */
    /* XXX alpha: "cpu model\t\t: EV6" -> ==buf necessary */
        if ((strstr(buf, "model\t\t: ") == buf) && model<0) {
            model = atoi(buf+strlen("model\t\t: "));
        }
        /* ia64 */
        if (strstr(buf, "model      : ") && model<0) {
            model = atoi(buf+strlen("model     : "));
        }
#endif
#if !defined(__aarch64__) && !defined(__arm__)
        if (strstr(buf, "stepping\t: ") && stepping<0) {
            stepping = atoi(buf+strlen("stepping\t: "));
        }
#else
        if (strstr(buf, "CPU implementer") && strlen(implementer) == 0) {
            strlcpy(implementer, strchr(buf, ':') + 2, sizeof(implementer));
            model_info_found = true;
        }
        if (strstr(buf, "CPU architecture") && strlen(architecture) == 0) {
            strlcpy(architecture, strchr(buf, ':') + 2, sizeof(architecture));
            model_info_found = true;
        }
        if (strstr(buf, "CPU variant") && strlen(variant) == 0) {
            strlcpy(variant, strchr(buf, ':') + 2, sizeof(variant));
            model_info_found = true;
        }
        if (strstr(buf, "CPU part") && strlen(cpu_part) == 0) {
            strlcpy(cpu_part, strchr(buf, ':') + 2, sizeof(cpu_part));
            model_info_found = true;
        }
        if (strstr(buf, "CPU revision") && strlen(revision) == 0) {
            strlcpy(revision, strchr(buf, ':') + 2, sizeof(revision));
            model_info_found = true;
        }
#endif

#ifdef __hppa__
        bool icache_found=false,dcache_found=false;
        if (!icache_found && strstr(buf, "I-cache\t\t: ")) {
            icache_found = true;
            sscanf(buf, "I-cache\t\t: %d", &n);
            m_cache += n*1024;
        }
        if (!dcache_found && strstr(buf, "D-cache\t\t: ")) {
            dcache_found = true;
            sscanf(buf, "D-cache\t\t: %d", &n);
            m_cache += n*1024;
        }
#elif __powerpc__
        if (!cache_found && strstr(buf, "L2 cache\t: ")) {
            cache_found = true;
            sscanf(buf, "L2 cache\t: %d", &n);
            m_cache = n*1024;
        }
#else
        if (!cache_found && (strstr(buf, "cache size\t: ") == buf)) {
            cache_found = true;
            sscanf(buf, "cache size\t: %d", &n);
            m_cache = n*1024;
        }
#endif
        if (!features_found) {
            // Some versions of the linux kernel call them flags,
            // others call them features, so look for both.
            //
            if ((strstr(buf, "flags\t\t: ") == buf)) {
                strlcpy(features, strchr(buf, ':') + 2, sizeof(features));
            } else if ((strstr(buf, "features\t\t: ") == buf)) {
                strlcpy(features, strchr(buf, ':') + 2, sizeof(features));
            } else if ((strstr(buf, "features   : ") == buf)) { /* ia64 */
                strlcpy(features, strchr(buf, ':') + 2, sizeof(features));
            } else if ((strstr(buf, "Features\t: ") == buf)) { /* arm */
                strlcpy(features, strchr(buf, ':') + 2, sizeof(features));
            }
            if (strlen(features)) {
                features_found = true;
            }
        }
    }
    strlcpy(model_buf, p_model, sizeof(model_buf));
#if !defined(__aarch64__) && !defined(__arm__)
    if (family>=0 || model>=0 || stepping>0) {
        safe_strcat(model_buf, " [");
        if (family>=0) {
            sprintf(buf, "Family %d ", family);
            safe_strcat(model_buf, buf);
        }
        if (model>=0) {
            sprintf(buf, "Model %d ", model);
            safe_strcat(model_buf, buf);
        }
        if (stepping>=0) {
            sprintf(buf, "Stepping %d", stepping);
            safe_strcat(model_buf, buf);
        }
        safe_strcat(model_buf, "]");
    }
#else
    if (model_info_found) {
        safe_strcat(model_buf, " [");
        if (strlen(implementer)>0) {
            sprintf(buf, "Impl %s ", implementer);
            safe_strcat(model_buf, buf);
        }
        if (strlen(architecture)>0) {
            sprintf(buf, "Arch %s ", architecture);
            safe_strcat(model_buf, buf);
        }
        if (strlen(variant)>0) {
            sprintf(buf, "Variant %s ", variant);
            safe_strcat(model_buf, buf);
        }
        if (strlen(cpu_part)>0) {
            sprintf(buf, "Part %s ", cpu_part);
            safe_strcat(model_buf, buf);
        }
        if (strlen(revision)>0) {
            sprintf(buf, "Rev %s", revision);
            safe_strcat(model_buf, buf);
        }
        safe_strcat(model_buf, "]");
    }
#endif
    if (strlen(features)) {
        safe_strcat(model_buf, "[");
        safe_strcat(model_buf, features);
        safe_strcat(model_buf, "]");
    }


    printf("p_vendor: %s\nm_cache: %d\nmodel_buf: %s\n",
        p_vendor, m_cache, model_buf
    );
    fclose(f);

    // detect OS name as in HOST_INFO::get_os_info()
    safe_strcpy(os_name, "");
    safe_strcpy(os_version, "");

#if HAVE_SYS_UTSNAME_H
    struct utsname u;
    uname(&u);
#ifdef ANDROID
    safe_strcpy(os_name, "Android");
#else
    safe_strcpy(os_name, u.sysname);
#endif //ANDROID
#if defined(__EMX__) // OS2: version is in u.version
    safe_strcpy(os_version, u.version);
#elif defined(__HAIKU__)
    snprintf(os_version, sizeof(os_version), "%s, %s", u.release, u.version);
#else
    safe_strcpy(os_version, u.release);
#endif
#ifdef _HPUX_SOURCE
    safe_strcpy(p_model, u.machine);
    safe_strcpy(p_vendor, "Hewlett-Packard");
#endif
#elif HAVE_SYS_SYSCTL_H && defined(CTL_KERN) && defined(KERN_OSTYPE) && defined(KERN_OSRELEASE)
    mib[0] = CTL_KERN;
    mib[1] = KERN_OSTYPE;
    len = sizeof(os_name);
    sysctl(mib, 2, &os_name, &len, NULL, 0);

    mib[0] = CTL_KERN;
    mib[1] = KERN_OSRELEASE;
    len = sizeof(os_version);
    sysctl(mib, 2, &os_version, &len, NULL, 0);
#elif HAVE_SYS_SYSTEMINFO_H
    sysinfo(SI_SYSNAME, os_name, sizeof(os_name));
    sysinfo(SI_RELEASE, os_version, sizeof(os_version));
#else
#error Need to specify a method to obtain OS name/version
#endif

#if LINUX_LIKE_SYSTEM
    bool found_something = false;
    char dist_pretty[256], dist_name[256], dist_version[256], dist_codename[256];
    safe_strcpy(dist_pretty, "");
    safe_strcpy(dist_name, "");
    safe_strcpy(dist_version, "");
    safe_strcpy(dist_codename, "");

    // see: http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/lsbrelease.html
    // although the output is not clearly specified it seems to be constant
    f = popen(command_lsbrelease, "r");
    if (f) {
        found_something = parse_linux_os_info(f, lsbrelease, dist_name, sizeof(dist_name),
            dist_version, sizeof(dist_version));
        pclose(f);
    }
    if (!found_something) {
        // see: https://www.freedesktop.org/software/systemd/man/os-release.html
        f = fopen(file_osrelease, "r");
        if (f) {
            found_something = parse_linux_os_info(f, osrelease, dist_name, sizeof(dist_name),
                dist_version, sizeof(dist_version));
            fclose(f);
        }
    }

    if (!found_something) {
        // last ditch effort for older redhat releases
        f = fopen(file_redhatrelease, "r");
        if (f) {
            found_something = parse_linux_os_info(f, redhatrelease, dist_name, sizeof(dist_name),
                dist_version, sizeof(dist_version));
            fclose(f);
        }
    }

    if (found_something) {
        os_version_extra = (string)os_version;
        safe_strcpy(os_version, dist_version);
        if (strlen(dist_name)) {
            safe_strcat(os_name, " ");
            safe_strcat(os_name, dist_name);
        }
    }

    string libc_version(""), libc_extra_info("");
    if (!get_libc_version(libc_version, libc_extra_info)) {
        // add info to os_version_extra
        if (!os_version_extra.empty()) {
            os_version_extra += "|";
        }
        os_version_extra += "libc " + libc_version;
        if (!libc_extra_info.empty()) {
            os_version_extra += " (" + libc_extra_info + ")";
        }
    }

    if (!os_version_extra.empty()) {
        safe_strcat(os_version, " [");
        safe_strcat(os_version, os_version_extra.c_str());
        safe_strcat(os_version, "]");
    }
#endif //LINUX_LIKE_SYSTEM
    printf("os_name: %s\nos_version: %s\nproduct_name: %s\n",
        os_name, os_version, product_name
    );
}
