/*
 * SPDX-License-Identifier: BSD-3-Clause
 * Copyright © 2015-2018 Intel
 * Copyright © 2015-2020 Inria.  All rights reserved.
 * See COPYING in top-level directory.
 */

#include "private/autogen/config.h"

#include <stdio.h>
#include <stdint.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>

#define KERNEL_SMBIOS_SYSFS "/sys/firmware/dmi/entries"

/* official strings, found at least in Intel S7200AP boards (also used by Cray) and some SuperMicro boards */
#define KNL_INTEL_GROUP_STRING "Group: Knights Landing Information"
#define KNM_INTEL_GROUP_STRING "Group: Knights Mill Information"
/* unexpected strings, found at least in Dell C6320p BIOS <=1.4.1 */
#define KNL_DELL_GROUP_STRING "Knights Landing Association"

static const char *allowed_group_strings[] =
{
    KNL_INTEL_GROUP_STRING,
    KNM_INTEL_GROUP_STRING,
    KNL_DELL_GROUP_STRING
};

/* Header is common part of all SMBIOS entries */
struct smbios_header
{
    uint8_t type;
    uint8_t length;
    uint16_t handle;
};

struct smbios_group
{
    uint8_t group_name;
};

/* This structrures are padded by compiler
 * So we hardcode size of the struct and use it
 * instead of sizeof() */
#define GROUP_ENTRY_SIZE 3
struct smbios_group_entry
{
    uint8_t type;
    uint16_t handle;
};

/* KNL header is similar as SMBIOS header
 * decided to add it for readability */
#define SMBIOS_KNL_HEADER_SIZE 7
struct knl_smbios_header
{
    uint8_t type;
    uint8_t length;
    uint16_t handle;
    uint16_t member_id;
    uint8_t member_name;
};

/* general info data */
#define SMBIOS_KNL_GENERAL_INFO_SIZE 5
struct knl_general_info
{
    uint8_t supp_cluster_mode;
    uint8_t cluster_mode;
    uint8_t supp_memory_mode;
    uint8_t memory_mode;
    uint8_t cache_info;
};

/* memory info */
#define SMBIOS_KNL_EDC_INFO_SIZE 9
struct knl_edc_info
{
    uint8_t mcdram_present;
    uint8_t mcdram_enabled;
    uint8_t allowed_channels;
    uint8_t reserved[4];
    uint8_t mcdram_info_size;
    uint8_t mcdram_info_count;
};

/* mcdram controller structure */
struct knl_mcdram_info {
    uint32_t status;
    uint8_t controller;
    uint8_t channel;
    uint16_t size64MB;
    uint8_t product_revision;
    uint8_t fwmajor_revision;
    uint8_t fwminor_revision;
    uint8_t fwpatch_revision;
};

/* internal data */
struct parser_data
{
    uint64_t mcdram_regular;
    uint64_t mcdram_cache;
    int cluster_mode;
    int memory_mode;
    int cache_info;
    int type_count;
    int knl_types[64];
};

enum cluster_mode
{
    QUADRANT = 1,
    HEMISPHERE = 2,
    SNC4 = 4,
    SNC2 = 8,
    ALL2ALL = 16
};

enum memory_mode
{
    CACHE = 1,
    FLAT = 2,
    HYBRID = 4
};

enum hybrid_cache
{
    H25 = 1,
    H50 = 2,
    H100 = 4 /* Incorrect but possible value */
};

static int get_file_buffer(const char *file, char *buffer, int size)
{
    FILE *f;

    printf("  File = %s\n", file);

    if (!buffer) {
        fprintf(stderr, "Unable to allocate buffer\n");
        return 0;
    }

    f = fopen(file, "rb");
    if (!f) {
        fprintf(stderr, "Unable to open %s (%s)\n", file, strerror(errno));
        return 0;
    }

    size = fread(buffer, 1, size, f);
    if (size == 0) {
        fprintf(stderr, "Unable to read file\n");
        fclose(f);
        return 0;
    }
    printf("    Read %d bytes\n", size);

    fclose(f);
    return size;
}

static int check_entry(struct smbios_header *h, const char *end, const char *query)
{
    char *group_strings = (char*)h + h->length;
    do {
        int len = strlen(group_strings);
        /* SMBIOS string entries end with "\0\0"
         * if length is 0 break and return
         * */
        if (len == 0)
            break;

        printf("  Looking for \"%s\" in group string \"%s\"\n", query, group_strings);
        if (!strncmp(group_strings, query, len))
            return 1;

        group_strings += len;
    } while(group_strings < end);

    return 0;
}

static int is_phi_group(struct smbios_header *h, const char *end)
{
    unsigned i;
    if (h->type != 14) {
        fprintf(stderr, "SMBIOS table is not group table\n");
        return 0;
    }

    for (i = 0; i < sizeof(allowed_group_strings)/sizeof(char*); i++) {
        if (check_entry(h, end, allowed_group_strings[i]))
            return 1;
    }

    return 0;
}

#define KNL_MEMBER_ID_GENERAL 0x1
#define KNL_MEMBER_ID_EDC 0x2

#define PATH_SIZE 512
#define SMBIOS_FILE_BUF_SIZE 4096
#define KNL_MCDRAM_SIZE (16ULL*1024*1024*1024)

static int process_smbios_group(const char *input_fsroot, char *dir_name, struct parser_data *data)
{
    char path[PATH_SIZE];
    char file_buf[SMBIOS_FILE_BUF_SIZE];
    struct smbios_header *h;
    char *p;
    char *end;
    int size;
    int i;

    snprintf(path, PATH_SIZE-1, "%s/" KERNEL_SMBIOS_SYSFS "/%s/raw", input_fsroot, dir_name);
    path[PATH_SIZE-1] = 0;

    size = get_file_buffer(path, file_buf, SMBIOS_FILE_BUF_SIZE);
    if (!size) {
        fprintf(stderr, "Unable to read raw table file\n");
        return -1;
    }

    h = (struct smbios_header*)file_buf;
    end = file_buf+size;
    if (!is_phi_group(h, end)) {
        printf("  Failed to find Phi group\n");
        fprintf(stderr, "SMBIOS table does not contain Xeon Phi entries\n");
        return -1;
    }
    printf("  Found Phi group\n");

    p = file_buf + sizeof(struct smbios_header) + sizeof(struct smbios_group);
    if ((char*)p >= end) {
        fprintf(stderr, "SMBIOS table does not have entries\n");
        return -1;
    }

    end = file_buf+h->length;

    i = 0;
    for (; p < end; i++, p+=3) {
        struct smbios_group_entry *e = (struct smbios_group_entry*)p;
        data->knl_types[i] = e->type;
        printf("    Found Xeon Phi type = %d\n", e->type);
    }

    data->type_count = i;
    return 0;
}

static int process_knl_entry(const char *input_fsroot, char *dir_name, struct parser_data *data)
{
    char path[PATH_SIZE];
    char file_buf[SMBIOS_FILE_BUF_SIZE];
    char *end;
    int size;
    struct knl_smbios_header *h;

    snprintf(path, PATH_SIZE-1, "%s/" KERNEL_SMBIOS_SYSFS "/%s/raw", input_fsroot, dir_name);
    path[PATH_SIZE-1] = 0;

    size = get_file_buffer(path, file_buf, SMBIOS_FILE_BUF_SIZE);
    if (!size) {
        fprintf(stderr, "Unable to read raw table file\n");
        return -1;
    }

    end = file_buf+size;
    h = (struct knl_smbios_header*)file_buf;
    if (h->member_id & KNL_MEMBER_ID_GENERAL) {
        struct knl_general_info *info =
            (struct knl_general_info*) (file_buf+SMBIOS_KNL_HEADER_SIZE);
        printf("  Getting general Xeon Phi info\n");
        data->cluster_mode = info->cluster_mode;
        data->memory_mode = info->memory_mode;
        data->cache_info = info->cache_info;
    } else if (h->member_id & KNL_MEMBER_ID_EDC) {
        struct knl_edc_info *info = (struct knl_edc_info*)(file_buf+SMBIOS_KNL_HEADER_SIZE);
        if (info->mcdram_present && info->mcdram_enabled) {
            struct knl_mcdram_info *mi = (struct knl_mcdram_info*)(info + 1);
            /* we use always smbios size not struct size
             * as it can change in future.*/
            int struct_size = info->mcdram_info_size;
            int i = 0;

            if (0 == struct_size) {
                printf("  MCDRAM info size is set to 0, falling back to known size\n");
                struct_size = sizeof(*mi);
            }
            printf("  Getting Xeon Phi MCDRAM info. Count=%d struct size=%d\n",
                   (int)info->mcdram_info_count, struct_size);
            for ( ; i < info->mcdram_info_count; i++) {
                if ((char*)mi >= end) {
                    fprintf(stderr, "SMBIOS Xeon Phi entry is too small\n");
                    return -1;
                }
                printf("  MCDRAM controller %d\n", mi->controller);
                if (mi->status & 0x1) {
                    printf("  Controller fused\n");
                } else {
                    data->mcdram_regular += mi->size64MB;
                    printf("  Size = %d MB\n", (int)mi->size64MB*64);
                }
                mi = (struct knl_mcdram_info*)(((char*)mi)+struct_size);
            }
            /* convert to bytes  */
            printf("  Total MCDRAM %llu MB\n", (long long unsigned int)data->mcdram_regular*64);
            data->mcdram_regular *= 64*1024*1024;
            /*
             * BIOS can expose some MCRAM controllers as fused
             * When this happens we hardcode MCDRAM size to 16 GB
             */
            if (data->mcdram_regular != KNL_MCDRAM_SIZE) {
                fprintf(stderr, "Not all MCDRAM is exposed in DMI. Please contact BIOS vendor\n");
                data->mcdram_regular = KNL_MCDRAM_SIZE;
            }

        } else {
            data->mcdram_regular = 0;
            data->mcdram_cache = 0;
        }

    } else {
        /* We skip unknown table */
        fprintf(stderr, "Ignoring unknown SMBIOS entry type=%x\n", h->member_id);
    }

    return 0;
}
static const char* get_memory_mode_str(int memory_mode, int hybrid_cache_size)
{
    switch (memory_mode) {
        case CACHE: return "Cache";
        case FLAT: return "Flat";
        case HYBRID:
            if (hybrid_cache_size == H25) {
                return "Hybrid25";
            } else if (hybrid_cache_size == H50) {
                return "Hybrid50";
            }
            return "Unknown";
        default:
            return "Unknown";
    }
}

static const char* get_cluster_mode_str(int cluster_mode)
{
    switch (cluster_mode) {
        case QUADRANT: return "Quadrant";
        case HEMISPHERE: return "Hemisphere";
        case ALL2ALL: return "All2All";
        case SNC2: return "SNC2";
        case SNC4: return "SNC4";
        default:
            return "Unknown";
    }
}

static int print_result(struct parser_data *data, const char *out_file)
{
    int node_count = 0;
    int fd;
    FILE *f;

    switch (data->cluster_mode) {
        case QUADRANT:
            node_count = 1;
            break;
        case HEMISPHERE:
            node_count = 1;
            break;
        case ALL2ALL:
            node_count = 1;
            break;
        case SNC2:
            node_count = 2;
            break;
        case SNC4:
            node_count = 4;
            break;
        default:
            fprintf(stderr, "Incorrect cluster mode %d\n", data->cluster_mode);
            return -1;
    }

    switch (data->memory_mode) {
        case CACHE:
            data->mcdram_cache = data->mcdram_regular;
            data->mcdram_regular = 0;
            break;
        case FLAT:
            data->mcdram_cache = 0;
            break;
        case HYBRID:
            if (data->cache_info == H25) {
                data->mcdram_cache = data->mcdram_regular/4;
            } else if (data->cache_info == H50) {
                data->mcdram_cache = data->mcdram_regular/2;
            } else if (data->cache_info == H100) {
                data->mcdram_cache = data->mcdram_regular;
            } else {
                fprintf(stderr, "SMBIOS reserved cache info value %d\n", data->cache_info);
                return -1;
            }
            data->mcdram_regular -= data->mcdram_cache;
            break;
        default:
            fprintf(stderr, "Incorrect memory mode %d\n", data->memory_mode);
            return -1;
    }

    printf("  Cluster Mode: %s Memory Mode: %s\n",
            get_cluster_mode_str(data->cluster_mode),
            get_memory_mode_str(data->memory_mode, data->cache_info));
    printf("  MCDRAM total = %llu bytes, cache = %llu bytes\n",
           (long long unsigned int)data->mcdram_regular,
           (long long unsigned int)data->mcdram_cache);
    data->mcdram_regular /= node_count;
    data->mcdram_cache /= node_count;
    printf("  MCDRAM total = %llu bytes, cache = %llu bytes per node\n",
           (long long unsigned int)data->mcdram_regular,
           (long long unsigned int)data->mcdram_cache);

    /* Now we can start printing stuff */
    /* use open+fdopen so that we can specify the file creation mode */
    fd = open(out_file, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
    if (fd < 0) {
        fprintf(stderr, "Unable to open file `%s' (%s).\n", out_file, strerror(errno));
        return -1;
    }
    f = fdopen(fd, "w");
    if (!f) {
        fprintf(stderr, "Unable to fdopen file `%s' (%s).\n", out_file, strerror(errno));
        close(fd);
        return -1;
    }

    fprintf(f, "version: 2\n");
    /* We cache is equal for node */
    fprintf(f, "cache_size: %llu\n",
                (long long unsigned int)data->mcdram_cache);
    fprintf(f, "associativity: 1\n");// direct-mapped cache
    fprintf(f, "inclusiveness: 1\n");// inclusive cache
    fprintf(f, "line_size: 64\n");
    fprintf(f, "cluster_mode: %s\n", get_cluster_mode_str(data->cluster_mode));
    fprintf(f, "memory_mode: %s\n", get_memory_mode_str(data->memory_mode, data->cache_info));
    fflush(f);
    fclose(f);
    close(fd);
    return 0;
}

/**
* Seeks SMBIOS sysfs for entry with type
*/
int hwloc_dump_hwdata_knl_smbios(const char *input_fsroot, const char *outfile);

int hwloc_dump_hwdata_knl_smbios(const char *input_fsroot, const char *outfile)
{
    DIR *d;
    int i;
    struct dirent *dir;
    struct parser_data data;
    char path[PATH_SIZE];
    int err;

    memset(&data, 0, sizeof(data));

    printf("Dumping Xeon Phi SMBIOS Memory-Side Cache information:\n");

    snprintf(path, PATH_SIZE-1, "%s/" KERNEL_SMBIOS_SYSFS, input_fsroot);
    path[PATH_SIZE-1] = 0;

    d = opendir(path);
    if (!d) {
        fprintf(stderr, "Unable to open dmi-sysfs dir: %s", path);
        return -1;
    }

    /* process Xeon Phi entries
     * start with group (type 14, dash os to omit 140 types) then find SMBIOS types for
     * Knights Landing mcdram indofrmation
     */
    while ((dir = readdir(d))) {
        if (strncmp("14-", dir->d_name, 3) == 0) {
            err = process_smbios_group(input_fsroot, dir->d_name, &data);
            if (err < 0) {
                closedir(d);
                return err;
            }
        }
    }

    if (!data.type_count) {
      fprintf (stderr, "  Couldn't find any Xeon Phi information.\n");
      closedir(d);
      return -1;
    }

    /* We probably have Xeon Phi type identifiers here */
    for (i = 0; i < data.type_count; i++) {
        char tab[16] = {0};
        int l = snprintf(tab, sizeof(tab)-1, "%d-", data.knl_types[i]);
        printf("\n");
        printf ("  Seeking dir ̀`%s' %d\n", tab, l);
        rewinddir(d);
        while ((dir = readdir(d))) {
            if (strncmp(dir->d_name, tab, l) == 0) {
                err = process_knl_entry(input_fsroot, dir->d_name, &data);
                if (err < 0) {
                    closedir(d);
                    return err;
                }
            }
        }
    }

    closedir(d);

    return print_result(&data, outfile);
}
