/* -*- mode: c; -*- */

// make the full sysfs path from the dev path, plus an additional path
// return NULL on OOM
static char *vdev_linux_sysfs_fullpath(char const *sysfs_mountpoint,
                                       char const *devpath, char const *attr_path)
{
    char *tmp = NULL;
    char *ret = NULL;

    tmp = vdev_fullpath(sysfs_mountpoint, devpath, NULL);
    if (tmp == NULL)
        return NULL;

    ret = vdev_fullpath(tmp, attr_path, NULL);
    free(tmp);

    return ret;
}


// parse a device number pair
// return 0 on success, and set *major and *minor
// return -EINVAL if we failed to parse
static int vdev_linux_sysfs_parse_device_nums(char const *devbuf,
                                              unsigned int *major,
                                              unsigned int *minor)
{
    int rc = 0;

    // parse devpath
    rc = sscanf(devbuf, "%u:%u", major, minor);
    if (rc != 2)  {
        vdev_error("sscanf('%s') for major:minor rc = %d\n", devbuf, rc);
        rc = -EINVAL;
    } else {
        rc = 0;
    }

    return rc;
}

// read the device major and minor number, using the devpath
// return 0 on success, and set *major and *minor
// return -ENOMEM on OOM
// return -errno on failure to open or read
static int vdev_linux_sysfs_read_dev_nums(struct vdev_linux_context *ctx,
                                          char const *devpath, unsigned int *major,
                                          unsigned int *minor)
{
    int rc = 0;
    int fd = 0;
    ssize_t nr = 0;
    char devbuf[101];

    memset(devbuf, 0, 101);

    char *full_devpath = vdev_linux_sysfs_fullpath(ctx->sysfs_mountpoint,
                                                   devpath, "dev");
    if (full_devpath == NULL)
        return -ENOMEM;

    // open device path
    fd = open(full_devpath, O_RDONLY);
    if (fd < 0) {
        rc = -errno;
        if (rc != -ENOENT)
            vdev_error("open('%s') rc = %d\n", full_devpath, rc);
        free(full_devpath);
        return rc;
    }

    nr = vdev_read_uninterrupted(fd, devbuf, 100);
    if (nr < 0) {
        rc = nr;
        vdev_error("read('%s') rc = %d\n", full_devpath, rc);
        free(full_devpath);
        close(fd);
        return rc;
    }

    close(fd);
    free(full_devpath);

    rc = vdev_linux_sysfs_parse_device_nums(devbuf, major, minor);

    if (rc != 0) {
        vdev_error("Failed to parse '%s'\n", devbuf);
        rc = -EIO;
    }

    return rc;
}

// find sysfs mountpoint in /proc/mounts
// this is apparently superfluous (it *should* be mounted at /sys), but you never know.
// mountpoint must be big enough (PATH_MAX will do)
// return 0 on success
// return -ENOSYS if sysfs is not mounted
// return -ENOMEM if the buffer isn't big enough
// return -EINVAL if somehow we failed to parse a mount entry
// return negative for some other errors (like access permission failures, or /proc not mounted)
static int vdev_linux_find_sysfs_mountpoint(char *mountpoint, size_t mountpoint_len)
{
    FILE *f = NULL;
    int rc = 0;

    char mntbuf[4096];
    struct mntent ment_buf;
    struct mntent *ment_ptr = NULL;
    int ent_count = 1;
    bool found = false;

    f = fopen("/proc/mounts", "r");
    if (!f) {
        rc = -errno;
        fprintf(stderr, "Failed to open /proc/mounts, rc = %d\n", rc);
        return rc;
    }

    // scan for sysfs mount type
    while (1) {
        ment_ptr = getmntent_r(f, &ment_buf, mntbuf, 4096);
        if (!ment_ptr) {
            vdev_error("Failed on processing entry #%d of /proc/mounts\n", ent_count);
            rc = -EINVAL;
            break;
        }

        if (strcmp(ment_ptr->mnt_type, "sysfs") == 0) {
            // found!
            strncpy(mountpoint, ment_ptr->mnt_dir, mountpoint_len - 1);
            found = true;
            rc = 0;
            break;
        }
        ent_count++;
    }

    fclose(f);

    if (rc == 0 && !found) {
        fprintf(stderr, "Failed to find mounted sysfs filesystem\n");
        return -ENOSYS;
    }

    return rc;
}

// get a uevent from a uevent file
// replace newlines with '\0', making the uevent look like it came from the netlink socket
// (i.e. so it can be parsed by vdev_linux_parse_request)
// return 0 on success
// return -ENOMEM on OOM
// return -errno on failure to stat or read
static int vdev_linux_sysfs_read_uevent(char const *fp_uevent,
                                        char **ret_uevent_buf,
                                        size_t *ret_uevent_len)
{
    int rc = 0;
    struct stat sb;
    char *uevent_buf = NULL;
    size_t uevent_buf_len = 0;
    size_t uevent_len = 0;

    // get uevent size
    rc = stat(fp_uevent, &sb);
    if (rc != 0) {
        rc = -errno;
        vdev_error("stat('%s') rc = %d\n", fp_uevent, rc);
        return rc;
    } else {
        uevent_buf_len = sb.st_size;
    }

    // read the uevent
    if (fp_uevent != NULL) {
        uevent_buf = VDEV_CALLOC(char, uevent_buf_len);
        if (!uevent_buf)
            return -ENOMEM;

        rc = vdev_read_file(fp_uevent, uevent_buf, uevent_buf_len);
        if (rc != 0) {
            // failed in this
            vdev_error("vdev_read_file('%s') rc = %d\n", fp_uevent, rc);
            free(uevent_buf);
        } else {
            for (unsigned int i = 0; i < uevent_buf_len; i++) {
                if (uevent_buf[i] == '\n')
                    uevent_buf[i] = '\0';
            }

            /* NOTE: the stat size is an upper-bound.
             * Find the exact number of bytes.
             */
            for (uevent_len = 0; uevent_len < uevent_buf_len;) {
                if (*(uevent_buf + uevent_len) == '\0')
                    break;
                uevent_len += strlen(uevent_buf + uevent_len) + 1;
            }

            *ret_uevent_buf = uevent_buf;
            *ret_uevent_len = uevent_len;
        }
    }

    return rc;
}


// append a key/value pair to a uevent buffer
// return 0 on success
// return -ENOMEM on OOM
static int vdev_linux_uevent_append(char **ret_uevent_buf,
                                    size_t *ret_uevent_buf_len,
                                    char const *key, char const *value)
{
    char *tmp = NULL;
    char *uevent_buf = *ret_uevent_buf;
    size_t uevent_buf_len = *ret_uevent_buf_len;

    // add it to the uevent buffer, so we can parse it like a normal uevent
    tmp = (char*)realloc(uevent_buf, uevent_buf_len + 1 +
                                     strlen(key) + 1 + strlen(value) + 1);
    if (!tmp)
        return -ENOMEM;

    uevent_buf = tmp;

    // add key
    memcpy(uevent_buf + uevent_buf_len, key, strlen(key));
    uevent_buf_len += strlen(key);

    // add '='
    *(uevent_buf + uevent_buf_len) = '=';
    uevent_buf_len ++;

    // add value
    memcpy(uevent_buf + uevent_buf_len, value, strlen(value));
    uevent_buf_len += strlen(value);

    // NULL-terminate
    *(uevent_buf + uevent_buf_len) = '\0';
    uevent_buf_len ++;

    *ret_uevent_buf = uevent_buf;
    *ret_uevent_buf_len = uevent_buf_len;

    return 0;
}


// register a device from sysfs, given the path to its uevent file.
// that is, read the uevent file, generate a device request, and add it to the initial_requests list in the ctx.
// NOTE: it is assumed that fp_uevent--the full path to the uevent file--lives in /sys/devices
// return 0 on success
// return -ENOMEM on OOM
// return negative on error.
static int vdev_linux_sysfs_register_device(struct vdev_linux_context *ctx,
                                            char const *fp_uevent, int tracking_flag)
{
    int rc = 0;
    struct stat sb;
    char *uevent_buf = NULL;
    size_t uevent_buf_len = 0;
    char *full_devpath = NULL;
    char *devpath = NULL;
    char *devname = NULL;
    char *delim = NULL;
    char flagbuf[2];
    flagbuf[1] = 0;

    itoa10_safe(tracking_flag, flagbuf);

    /* extract the devpath from the uevent path */
    full_devpath = vdev_dirname(fp_uevent, NULL);
    if (!full_devpath)
        return -ENOMEM;

    /* get uevent */
    rc = vdev_linux_sysfs_read_uevent(fp_uevent, &uevent_buf, &uevent_buf_len);
    if (rc != 0) {
        vdev_error("vdev_linux_sysfs_read_uevent('%s') rc = %d\n", fp_uevent, rc);
        free(full_devpath);
        return rc;
    }

    if (uevent_buf_len == 0) {
        /* no messages to be had */
        vdev_debug("Empty uevent file at '%s'\n", fp_uevent);
        free(full_devpath);
        free(uevent_buf);
        return -1;
    }

    // truncate to /devices
    devpath = full_devpath + strlen(ctx->sysfs_mountpoint);

    /* we're adding this, so make ACTION=add */
    rc = vdev_linux_uevent_append(&uevent_buf, &uevent_buf_len, "ACTION", "add");
    if (rc != 0) {
        vdev_error("vdev_linux_uevent_append('%s=%s') rc = %d\n", "ACTION", "add", rc);
        free(uevent_buf);
        free(full_devpath);
        return rc;
    }

    /* see if the uevent has a devname.  Use that over our devname, if need be */
    devname = strstr(uevent_buf, "DEVNAME=");
    if (devname) {

        // have a devname!
        devname += strlen("DEVNAME=") + 1;

        size_t devname_len = strcspn(devname, "\n\0");
        if (devname_len > 0) {

            char *tmp = VDEV_CALLOC(char, devname_len + 1);
            if (tmp == NULL) {

                free(uevent_buf);
                free(full_devpath);

                return -ENOMEM;
            }

            strncpy(tmp, devname, devname_len);

            devname = tmp;
        }
    }

    // include the device path
    rc = vdev_linux_uevent_append(&uevent_buf, &uevent_buf_len,
                                  "DEVPATH", devpath);
    if (rc != 0) {
        vdev_error("vdev_linux_uevent_append('%s=%s') rc = %d\n", "DEVPATH",
                   devpath, rc);
        free(uevent_buf);
        free(full_devpath);
        free(devname);
        return rc;
    }

    rc = vdev_linux_uevent_append(&uevent_buf, &uevent_buf_len,
                                  "QUEUE_TRACKING_FLAG", flagbuf);
    if (rc != 0) {
        vdev_error("vdev_linux_uevent_append('%s=%s') rc = %d\n",
                   "QUEUE_TRACKING_FLAG", flagbuf, rc);
        free(uevent_buf);
        free(full_devpath);
        free(devname);
        return rc;
    }

    /* make the device request */
    struct vdev_device_request *vreq = VDEV_CALLOC(struct vdev_device_request, 1);
    if (!vreq) {
        free(full_devpath);
        free(uevent_buf);
        return -ENOMEM;
    }

    /* build up the request */
    vdev_device_request_init(vreq, ctx->os_ctx->state, VDEV_DEVICE_INVALID, devname);

    /* parse from our uevent */
    rc = vdev_linux_parse_request(ctx, vreq, uevent_buf, uevent_buf_len);

    free(uevent_buf);
    uevent_buf = NULL;

    if (rc != 0) {
        vdev_error("vdev_linux_parse_request('%s') rc = %d\n", fp_uevent, rc);
        free(full_devpath);
        free(devname);
        return rc;
    }

    free(full_devpath);
    free(devname);

    pthread_mutex_lock(&ctx->initial_requests_lock);

    /* append */
    if (!ctx->initial_requests) {
        ctx->initial_requests = vreq;
        ctx->initial_requests_tail = vreq;
    } else {
        ctx->initial_requests_tail->next = vreq;
        ctx->initial_requests_tail = vreq;
    }

    vreq->next = NULL;

    pthread_mutex_unlock(&ctx->initial_requests_lock);

    return rc;
}


// scan structure for finding new device directories, and signalling whether or not
// the given directory has a uevent.
struct vdev_linux_sysfs_scan_context {
    char *uevent_path;
    struct sglib_cstr_vector *device_frontier;
};

// scan a directory in /sys/devices directory, to find its child directories
// return 0 on success
// return -ENOMEM on OOM
// return -errno on failure to stat
static int vdev_linux_sysfs_scan_device_directory(char const *fp, void *cls)
{
    struct vdev_linux_sysfs_scan_context *scan_ctx = (struct vdev_linux_sysfs_scan_context *)cls;
    struct sglib_cstr_vector *device_frontier = scan_ctx->device_frontier;

    struct stat sb;
    int rc = 0;
    char *fp_base = NULL;
    char *fp_dup = NULL;

    fp_base = strrchr(fp, '/');

    if (!fp_base)
        return 0;

    // skip . and ..
    if (strcmp(fp_base, "/.") == 0 || strcmp(fp_base, "/..") == 0)
        return 0;

    // add directories
    rc = lstat(fp, &sb);
    if (rc != 0) {
        rc = -errno;
        vdev_error("lstat('%s') rc = %d\n", fp, rc);
        return rc;
    }

    if (!S_ISDIR(sb.st_mode) && strcmp(fp_base, "/uevent") != 0)
        /* not a directory, and not a uevent */
        return 0;

    fp_dup = vdev_strdup_or_null(fp);
    if (!fp_dup)
        return -ENOMEM;

    if (S_ISDIR(sb.st_mode)) {
        ///vdev_debug("Search '%s'\n", fp_dup);
        rc = sglib_cstr_vector_push_back(device_frontier, fp_dup);
        if (rc != 0) {
            free(fp_dup);
            return rc;
        }
    } else {
        /* this is a uevent; this directory is a device */
        ///vdev_debug("Uevent '%s'\n", fp_dup);
        scan_ctx->uevent_path = fp_dup;
    }

    return 0;
}

/**
 * Determines the size of readable data in a /sys or /proc file.
 * Return:
 *   TRUE  (Number of bytes read during the test, success)
 *   FALSE (Empty file / EOF) or
 *         (Error: it doesn't exist, broken symlink or without permissions)
 */
static bool readable_data(const char *path)
{
    struct stat st;
    char dummy; /* We just have to prove that the data exists */
    ssize_t nr;
    int fd;

    /* We use lstat to see what the object is without tracking it */
    if (lstat(path, &st) != 0)
        /* It doesn't exist or error */
        return false;

    /* If it's a symbolic link, check to see whether it's broken */
    if (S_ISLNK(st.st_mode)) {
        /* Is it broken? */
        if (access(path, F_OK) != 0)
            return false;
    }

    /* Real read test (required in /sys/devices/.../uevent) */
    fd = open(path, O_RDONLY | O_NONBLOCK);
    if (fd < 0)
        return false;
        
    nr = read(fd, &dummy, 1);
    close(fd);

    return (nr > 0);
}


// read all devices from sysfs, and put their uevent paths into the given uevent_paths.
// scan in breadth-first order, so we find and process parent devices before their child devices.
// return 0 on success
// return negative on error
static int vdev_linux_sysfs_find_devices(struct vdev_linux_context *ctx,
                                         struct sglib_cstr_vector *uevent_paths)
{
    int rc = 0;

    /* find all directories that have a 'uevent' file in them */
    struct sglib_cstr_vector device_frontier;
    sglib_cstr_vector_init(&device_frontier);

    struct vdev_linux_sysfs_scan_context scan_context;
    memset(&scan_context, 0, sizeof(struct vdev_linux_sysfs_scan_context));

    scan_context.device_frontier = &device_frontier;

    char *device_root = NULL;

    /* scan /sys/devices */
    device_root = vdev_fullpath(ctx->sysfs_mountpoint, "/devices", NULL);
    if (!device_root)
        return -ENOMEM;

    rc = vdev_load_all(device_root,
                       vdev_linux_sysfs_scan_device_directory, &scan_context);
    if (rc != 0) {
        vdev_error("vdev_load_all('%s') rc = %d\n", device_root, rc);
        free(device_root);
        sglib_cstr_vector_free(&device_frontier);
        return rc;
    }

    free(device_root);

    while (1) {
        unsigned long len = sglib_cstr_vector_size(&device_frontier);
        if (len == 0)
            break;

        device_root = sglib_cstr_vector_at(&device_frontier, len - 1);
        sglib_cstr_vector_set(&device_frontier, NULL, len - 1);
        sglib_cstr_vector_pop_back(&device_frontier);

        if (access (device_root, F_OK) == -1)
            continue;

        /* scan for more devices */
        rc = vdev_load_all(device_root,
                           vdev_linux_sysfs_scan_device_directory, &scan_context);
        if (rc != 0) {
            vdev_error("vdev_load_all('%s') rc = %d\n", device_root, rc);
            free(device_root);
            break;
        }

        /* Is one of them a uevent? If so, does it contain readable data? */
        if (scan_context.uevent_path && readable_data(scan_context.uevent_path) > 0) {
            // yup--remember it
            char *uevent_path = vdev_strdup_or_null(scan_context.uevent_path);
            if (!uevent_path) {
                free(device_root);
                free(scan_context.uevent_path);
                scan_context.uevent_path = NULL;
                rc = -ENOMEM;
                break;
            }
            rc = sglib_cstr_vector_push_back(uevent_paths, uevent_path);
            if (rc < 0) {
                free(uevent_path);
                free(device_root);
                free(scan_context.uevent_path);
                scan_context.uevent_path = NULL;
                break;
            }
            free(scan_context.uevent_path);
            scan_context.uevent_path = NULL;
        }
        free(device_root);
    }

    sglib_cstr_vector_free(&device_frontier);

    return rc;
}

/*
 * Coldplug-only: register every sysfs uevent. When queue_tracking_flag > 0,
 * put QUEUE_TRACKING_FLAG on the last real device (or a stable carrier if the
 * walk is empty). Subsystem filtering belongs to vdevadm / init scripts.
 */
static int vdev_linux_sysfs_register_devices(struct vdev_linux_context* ctx)
{
    int rc;
    int flag = 0;
    unsigned items = 0;
    unsigned last_idx = (unsigned)-1;
    bool keep_track;
    struct sglib_cstr_vector uevents;

    sglib_cstr_vector_init(&uevents);

    rc = vdev_linux_sysfs_find_devices(ctx, &uevents);
    if (rc != 0) {
        vdev_error("vdev_linux_sysfs_find_devices() rc = %d\n", rc);
        vdev_cstr_vector_free_all(&uevents);
        sglib_cstr_vector_free(&uevents);
        return rc;
    }

    items = sglib_cstr_vector_size(&uevents);
    keep_track = ctx->os_ctx->state->coldplug_only &&
                 ctx->os_ctx->state->config->queue_tracking_flag > 0;

    for (unsigned i = 0; i < items; i++) {
        if (sglib_cstr_vector_at(&uevents, i) != NULL)
            last_idx = i;
    }

    if (items == 0 && keep_track) {
        char fb[PATH_MAX];
        struct stat stb;
        char const *carrier = NULL;
        int tracking_flag_last = ctx->os_ctx->state->config->queue_tracking_flag;
        int nw;

        nw = snprintf(fb, sizeof(fb), "%s/devices/virtual/mem/null/uevent",
                      ctx->sysfs_mountpoint);
        if (nw > 0 && (size_t)nw < sizeof(fb) && stat(fb, &stb) == 0)
            carrier = fb;
        if (carrier == NULL) {
            nw = snprintf(fb, sizeof(fb), "%s/devices/system/cpu/cpu0/uevent",
                          ctx->sysfs_mountpoint);
            if (nw > 0 && (size_t)nw < sizeof(fb) && stat(fb, &stb) == 0)
                carrier = fb;
        }
        if (carrier != NULL) {
            int trc = vdev_linux_sysfs_register_device(ctx, carrier,
                                                       tracking_flag_last);

            if (trc != 0) {
                vdev_warn("carrier '%s' QUEUE_TRACKING_FLAG=%d rc=%d\n",
                          carrier, tracking_flag_last, trc);
                rc = trc;
            }
        } else {
            vdev_warn("%s",
                      "empty sysfs walk; no uevent carrier for "
                      "QUEUE_TRACKING — stamp may stall\n");
        }
    } else {
        for (unsigned i = 0; i < items; i++) {
            if (sglib_cstr_vector_at(&uevents, i) == NULL)
                continue;
            flag = 0;
            if (keep_track && last_idx != (unsigned)-1 && i == last_idx)
                flag = ctx->os_ctx->state->config->queue_tracking_flag;
            rc = vdev_linux_sysfs_register_device(
                ctx, sglib_cstr_vector_at(&uevents, i), flag);
            if (rc != 0 && flag != 0)
                vdev_error("vdev_linux_sysfs_register_device '%s': '%s'\n",
                           sglib_cstr_vector_at(&uevents, i), strerror(errno));
        }
    }

    vdev_cstr_vector_free_all(&uevents);
    sglib_cstr_vector_free(&uevents);

    return rc;
}
