/*
 * Ouroboros - Copyright (C) 2016
 *
 * Shared memory map for data units
 *
 *    Dimitri Staessens <dimitri.staessens@intec.ugent.be>
 *    Sander Vrijders   <sander.vrijders@intec.ugent.be>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <ouroboros/config.h>
#include <ouroboros/errno.h>
#include <ouroboros/shm_du_map.h>
#include <ouroboros/shm_ap_rbuff.h>
#include <ouroboros/time_utils.h>

#include <pthread.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/stat.h>

#define OUROBOROS_PREFIX "shm_du_map"

#include <ouroboros/logs.h>

#define SHM_BLOCKS_SIZE (SHM_BUFFER_SIZE * SHM_DU_BUFF_BLOCK_SIZE)
#define SHM_FILE_SIZE (SHM_BLOCKS_SIZE + 3 * sizeof (size_t)                   \
                       + sizeof(pthread_mutex_t) + 2 * sizeof(pthread_cond_t)  \
                       + sizeof(pid_t))

#define get_head_ptr(dum)                                                      \
((struct shm_du_buff *)(dum->shm_base + (*dum->ptr_head *                      \
                                         SHM_DU_BUFF_BLOCK_SIZE)))

#define get_tail_ptr(dum)                                                      \
((struct shm_du_buff *)(dum->shm_base + (*dum->ptr_tail *                      \
                                         SHM_DU_BUFF_BLOCK_SIZE)))

#define idx_to_du_buff_ptr(dum, idx)                                           \
        ((struct shm_du_buff *)(dum->shm_base + (idx * SHM_DU_BUFF_BLOCK_SIZE)))

#define block_ptr_to_idx(dum, sdb)                                             \
        (((uint8_t *)sdb - dum->shm_base) / SHM_DU_BUFF_BLOCK_SIZE)

#define shm_map_used(dum)((*dum->ptr_head + SHM_BUFFER_SIZE - *dum->ptr_tail)\
                          & (SHM_BUFFER_SIZE - 1))
#define shm_map_free(dum, i)(shm_map_used(dum) + i < SHM_BUFFER_SIZE)

#define shm_map_empty(dum) (*dum->ptr_tail == *dum->ptr_head)

struct shm_du_buff {
        size_t size;
#ifdef SHM_DU_MAP_MULTI_BLOCK
        size_t blocks;
#endif
        size_t du_head;
        size_t du_tail;
        pid_t  dst_api;
};

struct shm_du_map {
        uint8_t *         shm_base;    /* start of blocks */
        size_t *          ptr_head;    /* start of ringbuffer head */
        size_t *          ptr_tail;    /* start of ringbuffer tail */
        pthread_mutex_t * lock;   /* lock all free space in shm */
        size_t *          choked;      /* stale sdu detection */
        pthread_cond_t *  healthy;     /* du map is healthy */
        pthread_cond_t *  full;        /* run sanitizer when buffer full */
        pid_t *           api;         /* api of the irmd owner */
        int               fd;
};

static void garbage_collect(struct shm_du_map * dum)
{
#ifdef SHM_DU_MAP_MULTI_BLOCK
        struct shm_du_buff * sdb;
        while (!shm_map_empty(dum) && (sdb = get_tail_ptr(dum))->dst_api == -1)
                *dum->ptr_tail = (*dum->ptr_tail + sdb->blocks)
                        & (SHM_BUFFER_SIZE - 1);
#else
        while (!shm_map_empty(dum) && get_tail_ptr(dum)->dst_api == -1)
                *dum->ptr_tail =
                        (*dum->ptr_tail + 1) & (SHM_BUFFER_SIZE - 1);

#endif
}

static void clean_sdus(struct shm_du_map * dum, pid_t api, bool exit)
{
        size_t idx = *dum->ptr_tail;
        struct shm_du_buff * buf;

        while (idx != *dum->ptr_head) {
                buf = idx_to_du_buff_ptr(dum, idx);
                if (buf->dst_api == api)
                        buf->dst_api = -1;
#ifdef SHM_DU_MAP_MULTI_BLOCK
                idx = (idx + buf->blocks) & (SHM_BUFFER_SIZE - 1);
#else
                idx = (idx + 1) & (SHM_BUFFER_SIZE - 1);
#endif
        }

        garbage_collect(dum);

        if (!exit && kill(api, 0) == 0) {
                struct shm_ap_rbuff * rb;
                rb = shm_ap_rbuff_open(api);
                if (rb != NULL) {
                        shm_ap_rbuff_reset(rb);
                        shm_ap_rbuff_close(rb);
                }
        }

        *dum->choked = 0;
}

struct shm_du_map * shm_du_map_create()
{
        struct shm_du_map * dum;
        int                 shm_fd;
        uint8_t *           shm_base;
        pthread_mutexattr_t mattr;
        pthread_condattr_t  cattr;

        dum = malloc(sizeof *dum);
        if (dum == NULL) {
                LOG_DBGF("Could not allocate struct.");
                return NULL;
        }

        shm_fd = shm_open(SHM_DU_MAP_FILENAME, O_CREAT | O_EXCL | O_RDWR, 0666);
        if (shm_fd == -1) {
                LOG_DBGF("Failed creating shared memory map.");
                free(dum);
                return NULL;
        }

        if (fchmod(shm_fd, 0666)) {
                LOG_DBGF("Failed to chmod shared memory map.");
                free(dum);
                return NULL;
        }

        if (ftruncate(shm_fd, SHM_FILE_SIZE - 1) < 0) {
                LOG_DBGF("Failed to extend shared memory map.");
                free(dum);
                return NULL;
        }

        if (write(shm_fd, "", 1) != 1) {
                LOG_DBGF("Failed to finalise extension of shared memory map.");
                free(dum);
                return NULL;
        }

        shm_base = mmap(NULL,
                        SHM_FILE_SIZE,
                        PROT_READ | PROT_WRITE,
                        MAP_SHARED,
                        shm_fd,
                        0);

        if (shm_base == MAP_FAILED) {
                LOG_DBGF("Failed to map shared memory.");
                if (shm_unlink(SHM_DU_MAP_FILENAME) == -1)
                        LOG_DBGF("Failed to remove invalid shm.");

                free(dum);
                return NULL;
        }

        dum->shm_base = shm_base;
        dum->ptr_head = (size_t *)
                ((uint8_t *) dum->shm_base + SHM_BLOCKS_SIZE);
        dum->ptr_tail = dum->ptr_head + 1;
        dum->lock = (pthread_mutex_t *) (dum->ptr_tail + 1);
        dum->choked = (size_t *) (dum->lock + 1);
        dum->healthy = (pthread_cond_t *) (dum->choked + 1);
        dum->full = dum->healthy + 1;
        dum->api = (pid_t *) (dum->full + 1);

        pthread_mutexattr_init(&mattr);
        pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);
        pthread_mutexattr_setrobust(&mattr, PTHREAD_MUTEX_ROBUST);
        pthread_mutex_init(dum->lock, &mattr);

        pthread_condattr_init(&cattr);
        pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED);
        pthread_cond_init(dum->full, &cattr);
        pthread_cond_init(dum->healthy, &cattr);

        *dum->ptr_head = 0;
        *dum->ptr_tail = 0;

        *dum->choked = 0;

        *dum->api = getpid();

        dum->fd = shm_fd;

        return dum;
}

struct shm_du_map * shm_du_map_open()
{
        struct shm_du_map * dum;
        int                 shm_fd;
        uint8_t *           shm_base;

        dum = malloc(sizeof *dum);
        if (dum == NULL) {
                LOG_DBGF("Could not allocate struct.");
                return NULL;
        }

        shm_fd = shm_open(SHM_DU_MAP_FILENAME, O_RDWR, 0666);
        if (shm_fd < 0) {
                LOG_DBGF("Failed opening shared memory.");
                free(dum);
                return NULL;
        }

        shm_base = mmap(NULL,
                        SHM_FILE_SIZE,
                        PROT_READ | PROT_WRITE,
                        MAP_SHARED,
                        shm_fd,
                        0);
        if (shm_base == MAP_FAILED) {
                LOG_DBGF("Failed to map shared memory.");
                if (close(shm_fd) == -1)
                        LOG_DBGF("Failed to close invalid shm.");
                if (shm_unlink(SHM_DU_MAP_FILENAME) == -1)
                        LOG_DBGF("Failed to unlink invalid shm.");
                free(dum);
                return NULL;
        }

        dum->shm_base = shm_base;
        dum->ptr_head = (size_t *)
                ((uint8_t *) dum->shm_base + SHM_BLOCKS_SIZE);
        dum->ptr_tail = dum->ptr_head + 1;
        dum->lock = (pthread_mutex_t *) (dum->ptr_tail + 1);
        dum->choked = (size_t *) (dum->lock + 1);
        dum->healthy = (pthread_cond_t *) (dum->choked + 1);
        dum->full = dum->healthy + 1;
        dum->api = (pid_t *) (dum->full + 1);

        dum->fd = shm_fd;

        return dum;
}

void * shm_du_map_sanitize(void * o)
{
        struct shm_du_map * dum = (struct shm_du_map *) o;
        struct timespec intv
                = {SHM_DU_TIMEOUT_MICROS / MILLION,
                   (SHM_DU_TIMEOUT_MICROS % MILLION) * 1000};

        pid_t   api;

        if (dum == NULL)
                return (void *) -1;
        if (pthread_mutex_lock(dum->lock) == EOWNERDEAD) {
                LOG_WARN("Recovering dead mutex.");
                pthread_mutex_consistent(dum->lock);
        }

        pthread_cleanup_push((void (*)(void *)) pthread_mutex_unlock,
                             (void *) dum->lock);

        while (true) {
                int ret = 0;
                struct timespec now;
                struct timespec dl;

                if (pthread_cond_wait(dum->full, dum->lock) == EOWNERDEAD) {
                        LOG_WARN("Recovering dead mutex.");
                        pthread_mutex_consistent(dum->lock);
                }

                *dum->choked = 1;

                garbage_collect(dum);

                if (shm_map_empty(dum))
                        continue;

                api = get_tail_ptr(dum)->dst_api;

                if (kill(api, 0)) {
                        LOG_DBGF("Dead process %d left stale sdu.", api);
                        clean_sdus(dum, api, false);
                        continue;
                }

                clock_gettime(CLOCK_REALTIME, &now);
                ts_add(&now, &intv, &dl);
                while (*dum->choked) {
                        ret = pthread_cond_timedwait(dum->healthy,
                                                     dum->lock,
                                                     &dl);
                        if (!ret)
                                continue;

                        if (ret == EOWNERDEAD) {
                                LOG_WARN("Recovering dead mutex.");
                                pthread_mutex_consistent(dum->lock);
                        }

                        if (ret == ETIMEDOUT) {
                                LOG_DBGF("SDU timed out (dst: %d).", api);
                                clean_sdus(dum, api, false);
                        }
                }
        }

        pthread_cleanup_pop(true);

        return (void *) 0;
}

void shm_du_map_close_on_exit(struct shm_du_map * dum)
{
        if (dum == NULL) {
                LOG_DBGF("Bogus input. Bugging out.");
                return;
        }

        clean_sdus(dum, getpid(), true);

        if (close(dum->fd) < 0)
                LOG_DBGF("Couldn't close shared memory.");

        if (munmap(dum->shm_base, SHM_FILE_SIZE) == -1)
                LOG_DBGF("Couldn't unmap shared memory.");

        free(dum);
}

void shm_du_map_close(struct shm_du_map * dum)
{
        if (dum == NULL) {
                LOG_DBGF("Bogus input. Bugging out.");
                return;
        }

        if (close(dum->fd) < 0)
                LOG_DBGF("Couldn't close shared memory.");

        if (munmap(dum->shm_base, SHM_FILE_SIZE) == -1)
                LOG_DBGF("Couldn't unmap shared memory.");

        free(dum);
}

void shm_du_map_destroy(struct shm_du_map * dum)
{
        if (dum == NULL) {
                LOG_DBGF("Bogus input. Bugging out.");
                return;
        }

        if (getpid() != *dum->api && kill(*dum->api, 0) == 0) {
                LOG_DBGF("Only IRMd can destroy %s.", SHM_DU_MAP_FILENAME);
                return;
        }

        if (close(dum->fd) < 0)
                LOG_DBGF("Couldn't close shared memory.");

        if (munmap(dum->shm_base, SHM_FILE_SIZE) == -1)
                LOG_DBGF("Couldn't unmap shared memory.");

        if (shm_unlink(SHM_DU_MAP_FILENAME) == -1)
                LOG_DBGF("Failed to unlink shm.");

        free(dum);
}

ssize_t shm_du_map_write(struct shm_du_map * dum,
                         pid_t               dst_api,
                         size_t              headspace,
                         size_t              tailspace,
                         uint8_t *           data,
                         size_t              len)
{
        struct shm_du_buff * sdb;
        size_t               size = headspace + len + tailspace;
#ifdef SHM_DU_MAP_MULTI_BLOCK
        long                 blocks = 0;
        long                 padblocks = 0;
#endif
        int                  sz = size + sizeof *sdb;
        uint8_t *            write_pos;
        ssize_t              idx = -1;

        if (dum == NULL || data == NULL) {
                LOG_DBGF("Bogus input, bugging out.");
                return -1;
        }

#ifndef SHM_DU_MAP_MULTI_BLOCK
        if (sz > SHM_DU_BUFF_BLOCK_SIZE) {
                LOG_DBGF("Multi-block SDU's disabled. Dropping.");
                return -1;
        }
#endif
        if (pthread_mutex_lock(dum->lock) == EOWNERDEAD) {
                LOG_DBGF("Recovering dead mutex.");
                pthread_mutex_consistent(dum->lock);
        }
#ifdef SHM_DU_MAP_MULTI_BLOCK
        while (sz > 0) {
                sz -= SHM_DU_BUFF_BLOCK_SIZE;
                ++blocks;
        }

        if (blocks + *dum->ptr_head > SHM_BUFFER_SIZE)
                padblocks = SHM_BUFFER_SIZE - *dum->ptr_head;

        if (!shm_map_free(dum, (blocks + padblocks))) {
#else
        if (!shm_map_free(dum, 1)) {
#endif
                pthread_cond_signal(dum->full);
                pthread_mutex_unlock(dum->lock);
                return -1;
        }

#ifdef SHM_DU_MAP_MULTI_BLOCK
        if (padblocks) {
                sdb = get_head_ptr(dum);
                sdb->size    = 0;
                sdb->blocks  = padblocks;
                sdb->dst_api = -1;
                sdb->du_head = 0;
                sdb->du_tail = 0;

                *dum->ptr_head = 0;
        }
#endif
        sdb          = get_head_ptr(dum);
        sdb->size    = size;
        sdb->dst_api = dst_api;
        sdb->du_head = headspace;
        sdb->du_tail = sdb->du_head + len;
#ifdef  SHM_DU_MAP_MULTI_BLOCK
        sdb->blocks  = blocks;
#endif
        write_pos = ((uint8_t *) (sdb + 1)) + headspace;

        memcpy(write_pos, data, len);

        idx = *dum->ptr_head;
#ifdef SHM_DU_MAP_MULTI_BLOCK
        *dum->ptr_head = (*dum->ptr_head + blocks) & (SHM_BUFFER_SIZE - 1);
#else
        *dum->ptr_head = (*dum->ptr_head + 1) & (SHM_BUFFER_SIZE - 1);
#endif
        pthread_mutex_unlock(dum->lock);

        return idx;
}

ssize_t shm_du_map_write_b(struct shm_du_map * dum,
                           pid_t               dst_api,
                           size_t              headspace,
                           size_t              tailspace,
                           uint8_t *           data,
                           size_t              len)
{
        struct shm_du_buff * sdb;
        size_t               size = headspace + len + tailspace;
#ifdef SHM_DU_MAP_MULTI_BLOCK
        long                 blocks = 0;
        long                 padblocks = 0;
#endif
        int                  sz = size + sizeof *sdb;
        uint8_t *            write_pos;
        ssize_t              idx = -1;

        if (dum == NULL || data == NULL) {
                LOG_DBGF("Bogus input, bugging out.");
                return -1;
        }

#ifndef SHM_DU_MAP_MULTI_BLOCK
        if (sz > SHM_DU_BUFF_BLOCK_SIZE) {
                LOG_DBGF("Multi-block SDU's disabled. Dropping.");
                return -1;
        }
#endif
        if (pthread_mutex_lock(dum->lock) == EOWNERDEAD) {
                LOG_DBGF("Recovering dead mutex.");
                pthread_mutex_consistent(dum->lock);
        }

        pthread_cleanup_push((void(*)(void *))pthread_mutex_unlock,
                             (void *) dum->lock);

#ifdef SHM_DU_MAP_MULTI_BLOCK
        while (sz > 0) {
                sz -= SHM_DU_BUFF_BLOCK_SIZE;
                ++blocks;
        }

        if (blocks + *dum->ptr_head > SHM_BUFFER_SIZE)
                padblocks = SHM_BUFFER_SIZE - *dum->ptr_head;

        while (!shm_map_free(dum, (blocks + padblocks))) {
#else
        while (!shm_map_free(dum, 1)) {
#endif
                pthread_cond_signal(dum->full);
                pthread_cond_wait(dum->healthy, dum->lock);
        }

#ifdef SHM_DU_MAP_MULTI_BLOCK
        if (padblocks) {
                sdb = get_head_ptr(dum);
                sdb->size    = 0;
                sdb->blocks  = padblocks;
                sdb->dst_api = -1;
                sdb->du_head = 0;
                sdb->du_tail = 0;

                *dum->ptr_head = 0;
        }
#endif
        sdb          = get_head_ptr(dum);
        sdb->size    = size;
        sdb->dst_api = dst_api;
        sdb->du_head = headspace;
        sdb->du_tail = sdb->du_head + len;
#ifdef  SHM_DU_MAP_MULTI_BLOCK
        sdb->blocks  = blocks;
#endif
        write_pos = ((uint8_t *) (sdb + 1)) + headspace;

        memcpy(write_pos, data, len);

        idx = *dum->ptr_head;
#ifdef SHM_DU_MAP_MULTI_BLOCK
        *dum->ptr_head = (*dum->ptr_head + blocks) & (SHM_BUFFER_SIZE - 1);
#else
        *dum->ptr_head = (*dum->ptr_head + 1) & (SHM_BUFFER_SIZE - 1);
#endif
        pthread_cleanup_pop(true);

        return idx;
}

int shm_du_map_read(uint8_t **          dst,
                    struct shm_du_map * dum,
                    ssize_t             idx)
{
        size_t len = 0;
        struct shm_du_buff * sdb;

        if (idx > SHM_BUFFER_SIZE)
                return -1;

        if (pthread_mutex_lock(dum->lock) == EOWNERDEAD) {
                LOG_DBGF("Recovering dead mutex.");
                pthread_mutex_consistent(dum->lock);
        }

        if (shm_map_empty(dum)) {
                pthread_mutex_unlock(dum->lock);
                return -1;
        }

        sdb = idx_to_du_buff_ptr(dum, idx);
        len = sdb->du_tail - sdb->du_head;
        *dst = ((uint8_t *) (sdb + 1)) + sdb->du_head;

        pthread_mutex_unlock(dum->lock);

        return len;
}

int shm_du_map_remove(struct shm_du_map * dum, ssize_t idx)
{
        if (idx > SHM_BUFFER_SIZE)
                return -1;

        if (pthread_mutex_lock(dum->lock) == EOWNERDEAD) {
                LOG_DBGF("Recovering dead mutex.");
                pthread_mutex_consistent(dum->lock);
        }

        if (shm_map_empty(dum)) {
                pthread_mutex_unlock(dum->lock);
                return -1;
        }

        idx_to_du_buff_ptr(dum, idx)->dst_api = -1;

        if (idx != *dum->ptr_tail) {
                pthread_mutex_unlock(dum->lock);
                return 0;
        }

        garbage_collect(dum);

        *dum->choked = 0;

        pthread_cond_broadcast(dum->healthy);

        pthread_mutex_unlock(dum->lock);

        return 0;
}

uint8_t * shm_du_buff_head_alloc(struct shm_du_map * dum,
                                 int                 idx,
                                 ssize_t             size)
{
        struct shm_du_buff * sdb;
        uint8_t * buf;

        if (dum  == NULL)
                return NULL;

        if (idx < 0 || idx > SHM_BUFFER_SIZE)
                return NULL;

        if (pthread_mutex_lock(dum->lock) == EOWNERDEAD) {
                LOG_DBGF("Recovering dead mutex.");
                pthread_mutex_consistent(dum->lock);
        }

        sdb = idx_to_du_buff_ptr(dum, idx);

        if ((long) (sdb->du_head - size) < 0) {
                pthread_mutex_unlock(dum->lock);
                LOG_DBGF("Failed to allocate PCI headspace.");
                return NULL;
        }

        sdb->du_head -= size;

        buf = (uint8_t *) (sdb + 1) + sdb->du_head;

        pthread_mutex_unlock(dum->lock);

        return buf;
}

uint8_t * shm_du_buff_tail_alloc(struct shm_du_map * dum,
                                 int                 idx,
                                 ssize_t             size)
{
        struct shm_du_buff * sdb;
        uint8_t * buf;

        if (dum  == NULL)
                return NULL;

        if (idx < 0 || idx > SHM_BUFFER_SIZE)
                return NULL;

        if (pthread_mutex_lock(dum->lock) == EOWNERDEAD) {
                LOG_DBGF("Recovering dead mutex.");
                pthread_mutex_consistent(dum->lock);
        }

        sdb = idx_to_du_buff_ptr(dum, idx);

        if (sdb->du_tail + size >= sdb->size) {
                pthread_mutex_unlock(dum->lock);
                LOG_DBGF("Failed to allocate PCI tailspace.");
                return NULL;
        }

        buf = (uint8_t *) (sdb + 1) + sdb->du_tail;

        sdb->du_tail += size;

        pthread_mutex_unlock(dum->lock);

        return buf;
}

int shm_du_buff_head_release(struct shm_du_map * dum,
                             int                 idx,
                             ssize_t             size)
{
        struct shm_du_buff * sdb;

        if (dum  == NULL)
                return -1;

        if (idx < 0 || idx > SHM_BUFFER_SIZE)
                return -1;

        if (pthread_mutex_lock(dum->lock) == EOWNERDEAD) {
                LOG_DBGF("Recovering dead mutex.");
                pthread_mutex_consistent(dum->lock);
        }

        sdb = idx_to_du_buff_ptr(dum, idx);

        if (size > sdb->du_tail - sdb->du_head) {
                pthread_mutex_unlock(dum->lock);
                LOG_DBGF("Tried to release beyond sdu boundary.");
                return -EOVERFLOW;
        }

        sdb->du_head += size;

        pthread_mutex_unlock(dum->lock);

        return 0;
}

int shm_du_buff_tail_release(struct shm_du_map * dum,
                             int                 idx,
                             ssize_t             size)
{
        struct shm_du_buff * sdb;

        if (dum  == NULL)
                return -1;

        if (idx < 0 || idx > SHM_BUFFER_SIZE)
                return -1;

        if (pthread_mutex_lock(dum->lock) == EOWNERDEAD) {
                LOG_DBGF("Recovering dead mutex.");
                pthread_mutex_consistent(dum->lock);
        }

        sdb = idx_to_du_buff_ptr(dum, idx);

        if (size > sdb->du_tail - sdb->du_head) {
                pthread_mutex_unlock(dum->lock);
                LOG_DBGF("Tried to release beyond sdu boundary.");
                return -EOVERFLOW;
        }

        sdb->du_tail -= size;

        pthread_mutex_unlock(dum->lock);

        return 0;
}
