#include "jemalloc/internal/jemalloc_preamble.h"
#include "jemalloc/internal/jemalloc_internal_includes.h"

#include "jemalloc/internal/malloc_io.h"
#include "jemalloc/internal/prof_sys.h"

#if defined(__linux__) && defined(JEMALLOC_HAVE_GETTID)

#    include <errno.h>
#    include <fcntl.h>
#    include <stdio.h>
#    include <stdlib.h>  // strtoul
#    include <string.h>
#    include <unistd.h>

/*
 * Converts a string representing a hexadecimal number to an unsigned long long
 * integer. Functionally equivalent to strtoull() (for base 16) but faster for
 * that case.
 *
 * @param nptr Pointer to the string to be converted.
 * @param endptr Pointer to a pointer to character, which will be set to the
 * character in `nptr` where parsing stopped. Can be NULL.
 * @return The converted unsigned long long integer value.
 */
static inline unsigned long long int
strtoull_hex(const char *nptr, char **endptr) {
    unsigned long long int val = 0;
    int ii = 0;
    for (; ii < 16; ++ii) {
        char c = nptr[ii];
        if (c >= '0' && c <= '9') {
            val = (val << 4) + (c - '0');
        } else if (c >= 'a' && c <= 'f') {
            val = (val << 4) + (c - 'a' + 10);
        } else {
            break;
        }
    }
    if (endptr) {
        *endptr = (char *)(nptr + ii);
    }
    return val;
}

static int
prof_mapping_containing_addr(uintptr_t addr, const char *maps_path,
  uintptr_t *mm_start, uintptr_t *mm_end) {
    int ret = ENOENT; /* not found */
    *mm_start = *mm_end = 0;

    /*
     * Each line of /proc/<pid>/maps is:
     * <start>-<end> <perms> <offset> <dev> <inode> <pathname>
     *
     * The fields we care about are always within the first 34 characters so
     * as long as `buf` contains the start of a mapping line it can always be
     * parsed.
     */
    static const int kMappingFieldsWidth = 34;

    int fd = -1;
    char buf[4096];
    ssize_t remaining = 0; /* actual number of bytes read to buf */
    char *line = NULL;

    while (1) {
        if (fd < 0) {
            /* case 0: initial open of maps file */
            fd = malloc_open(maps_path, O_RDONLY);
            if (fd < 0) {
                return errno;
            }

            remaining = malloc_read_fd(fd, buf, sizeof(buf));
            if (remaining <= 0) {
                ret = errno;
                break;
            }
            line = buf;
        } else if (line == NULL) {
            /* case 1: no newline found in buf */
            remaining = malloc_read_fd(fd, buf, sizeof(buf));
            if (remaining <= 0) {
                ret = errno;
                break;
            }
            line = memchr(buf, '\n', remaining);
            if (line != NULL) {
                line++;  /* advance to character after newline */
                remaining -= (line - buf);
            }
        } else if (line != NULL && remaining < kMappingFieldsWidth) {
            /*
             * case 2: found newline but insufficient characters remaining in
             * buf
             */
            memcpy(buf, line,
              remaining);  /* copy remaining characters to start of buf */
            line = buf;

            size_t count =
              malloc_read_fd(fd, buf + remaining, sizeof(buf) - remaining);
            if (count <= 0) {
                ret = errno;
                break;
            }

            remaining += count;  /* actual number of bytes read to buf */
        } else {
            /* case 3: found newline and sufficient characters to parse */

            /* parse <start>-<end> */
            char *tmp = line;
            uintptr_t start_addr = (uintptr_t)strtoull_hex(tmp, &tmp);
            if (addr >= start_addr) {
                tmp++;  /* advance to character after '-' */
                uintptr_t end_addr = (uintptr_t)strtoull_hex(tmp, NULL);
                if (addr < end_addr) {
                    *mm_start = start_addr;
                    *mm_end = end_addr;
                    ret = 0;
                    break;
                }
            }

            /* Advance to character after next newline in the current buf. */
            char *prev_line = line;
            line = memchr(line, '\n', remaining);
            if (line != NULL) {
                line++;  /* advance to character after newline */
                remaining -= (line - prev_line);
            }
        }
    }

    malloc_close(fd);
    return ret;
}

int
prof_thread_stack_range(uintptr_t fp, uintptr_t *low, uintptr_t *high) {
    /*
     * NOTE: Prior to kernel 4.5 an entry for every thread stack was included in
     * /proc/<pid>/maps as [STACK:<tid>]. Starting with kernel 4.5 only the main
     * thread stack remains as the [stack] mapping. For other thread stacks the
     * mapping is still visible in /proc/<pid>/task/<tid>/maps (though not
     * labeled as [STACK:tid]).
     * https://lists.ubuntu.com/archives/kernel-team/2016-March/074681.html
    */
    char maps_path[64];  // "/proc/<pid>/task/<tid>/maps"
    malloc_snprintf(maps_path, sizeof(maps_path), "/proc/%d/task/%d/maps",
      getpid(), gettid());
    return prof_mapping_containing_addr(fp, maps_path, low, high);
}

#else

int
prof_thread_stack_range(
  UNUSED uintptr_t addr, uintptr_t *stack_start, uintptr_t *stack_end) {
    *stack_start = *stack_end = 0;
    return ENOENT;
}

#endif  // __linux__
